Portfolio-GitHub-App¶
Zentral verwaltete GitHub App, die es den wiederverwendbaren Workflows
dieses Repositories erlaubt, Events zu emittieren, die GitHub als
user-initiated betrachtet. Ohne sie scheitern drei Workflow-Ketten im
Portfolio still: release-drafter nach Automerge,
release-cd-refresh-master nach release-publish und
build-static-tests auf dem develop-Tip nach Automerge.
Tracking-Issue: #330.
Spec-Referenz: spec/project/workflow-health/ §Known platform constraints.
Warum die App existiert¶
GitHub Actions hat eine deterministische Plattform-Einschränkung:
Workflow-Events, die unter dem default GITHUB_TOKEN emittiert werden,
triggern keine nachgelagerten Workflow-Runs. Die reusable Workflows in
nolte/gh-plumbing, die nachgelagert relevante Writes ausführen — der
Squash-Merge in reusable-automerge.yaml, das Flippen eines Releases
auf published in reusable-release-publish.yml — brauchen daher ein
Credential, das GitHub als user-initiated wertet.
Die Spec verlangt eine Portfolio-Level-Lösung: eine App, in jedem
Konsument-Repo installiert, mit demselben Wrapper-Pattern in jedem
.github/workflows/. Kein Sammeln von Personal-Access-Tokens (PAT)
pro Repo, kein personengebundenes Credential.
Owner-Modi¶
Die Portfolio-App kann Konsumenten unter einer GitHub-Organisation oder unter einem persönlichen User-Account bedienen. Den Modus wählen, der zum Account passt, der die Konsumenten-Repositories besitzt. Beide Modi liefern dasselbe nachgelagerte Verhalten — App-emittierte Events kaskadieren user-initiated —, nur die Credential-Verkabelung unterscheidet sich.
| Modus | Wann wählen | Credentials liegen als |
|---|---|---|
| Organisations-Modus | Der Account, der die Konsumenten-Repositories besitzt, ist eine GitHub-Organisation. Günstiger im Betrieb ab drei Konsumenten. | Eine Org-Level-Actions-Variable + ein Org-Level-Actions-Secret mit visibility = "selected", beschränkt auf die Konsumenten-Liste. |
| User-Modus | Der Account, der die Konsumenten-Repositories besitzt, ist ein persönlicher GitHub-Account (keine Org). | Eine Repo-Level-Actions-Variable + ein Repo-Level-Actions-Secret in jedem Konsumenten-Repository. |
Beide Modi nutzen dasselbe Wrapper-Pattern in .github/workflows/;
die Wrapper interessiert nicht, welcher Modus ihr
PORTFOLIO_APP_ID / PORTFOLIO_APP_PRIVATE_KEY-Paar gesetzt hat,
sondern nur, dass das Paar zum Job-Start eine gültige App ergibt.
Welche Permissions die App braucht¶
Bei der Registrierung exakt diese Repository permissions vergeben:
| Permission | Scope | Wofür |
|---|---|---|
Contents |
Read and write |
Squash-Merge nach develop, Releases editieren, master fast-forwarden |
Pull requests |
Read and write |
pascalgn/automerge-action liest und merged PRs |
Issues |
Read and write |
Auto-Close von Issues, die im PR-Body via Closes #N / Fixes #N / Resolves #N referenziert sind, wenn die App den Squash-Merge ausführt. GitHub schließt referenzierte Issues nur, wenn der mergende Actor Issues: write hat — fehlt diese Permission, parst GitHub die Autolinks zwar in closingIssuesReferences, aber der Close-Flip feuert nicht (Live-Beobachtung an #357 / #358). |
Actions |
Read-only |
release-publish liest gh run list für den Post-Publish-Cascade-Check |
Metadata |
Read-only |
Pflicht-Basis (setzt GitHub automatisch und lässt sich nicht abwählen) |
Jede andere Repository-Permission bleibt auf No access,
insbesondere die zwei, die verwandt klingen, aber keine sind:
| Permission | Setting | Wieso NICHT |
|---|---|---|
Administration |
No access |
Die Probot Settings App verwaltet Repo-Konfiguration. Diese Permission hier zu setzen erhöht nur die Angriffsfläche, ohne Nutzen. |
Workflows |
No access |
Würde der App erlauben, .github/workflows/*.yaml zu überschreiben. Unsere Use-Case merged Branches und Releases, keine Workflow-Dateien. Später aktivieren, falls jemals nötig. |
Alle Organization permissions und alle Account permissions
bleiben auf No access — die Arbeit der App ist repo-scoped.
Identifying and authorizing users¶
Diese ganze Sektion bleibt aus. Unsere App agiert über Installation-Tokens, die in Workflow-Runs gemintet werden, niemals im Namen eines Endnutzers:
- Callback URL bleibt leer (
Deleteauf den Platzhalter klicken). - Request user authorization (OAuth) during installation bleibt ungesetzt.
- Enable Device Flow bleibt ungesetzt.
- Expire user authorization tokens ist egal; GitHub setzt es vor, aber wir geben keine User-Tokens aus, die ablaufen könnten.
Webhook¶
Webhook komplett deaktivieren (Active abwählen). Die App wird ausschließlich aus Workflow-Runs konsumiert, die zu Beginn eines Jobs ein Installation-Token holen — keine Event-Subscriptions notwendig und kein Endpoint vorhanden, der sie entgegennimmt.
Subscribe to events¶
In der Event-Liste der App keinen einzigen Event abonnieren. Jedes aktivierte Häkchen würde nur tote Webhook-Versuche erzeugen.
Provisionierungs-Checkliste¶
- App registrieren unter
https://github.com/organizations/<org>/settings/apps/new(bzw.https://github.com/settings/apps/newfür einen persönlichen Account). Namensvorschlag:nolte-portfolio-bot. - Permissions wie oben vergeben und jeden Webhook-Event abwählen.
- Private Key im PEM-Format (Privacy-Enhanced Mail) generieren — einmal speichern; GitHub zeigt ihn nicht erneut an.
- App-ID notieren (sichtbar auf der App-Settings-Seite).
- App installieren in jedem Konsument-Repository, das
cascade-korrekte Workflows braucht. Mit
nolte/gh-plumbingselbst beginnen. - Credentials setzen im Scope, der zum Owner-Modus passt:
- Organisations-Modus: eine
Org-Level-Actions-Variable
PORTFOLIO_APP_IDund ein Org-Level-Actions-SecretPORTFOLIO_APP_PRIVATE_KEY, beide mitvisibility = "selected"auf die Konsumenten-Liste eingeschränkt. - User-Modus: eine Repo-Level-Actions-Variable
PORTFOLIO_APP_IDund ein Repo-Level-Actions-SecretPORTFOLIO_APP_PRIVATE_KEYin jedem Konsumenten-Repository. Persönliche Accounts bieten keine Org-Level-Actions-Resourcen, daher trägt jedes Repository sein eigenes Paar.
Die App-ID ist nicht sensibel; als Variable abgelegt kann die
if:-Condition im Wrapper die Adoption erkennen. Der
Private-Key-Inhalt aus Schritt 3 landet im Secret.
Terraform-getriebene Provisionierung
Die Schritte 5 und 6 sind auch als Terraform-Modul unter
terraform/portfolio-app/
verfügbar. Das Modul unterstützt beide Modi (Organisation und
User), legt Variable und Secret in einem terraform apply an und
deklariert optional die App als Branch-Protection-Bypass-Actor.
Schritte 1–4 bleiben manuell, weil GitHub keine API zur
App-Erstellung anbietet. Siehe
examples/basic/
für den Organisations-Modus und
examples/personal-account/
für den User-Modus.
Wrapper-Pattern (für nachgelagerte Konsumenten)¶
Die Cascade-emittierenden Wrapper in nolte/gh-plumbing zeigen das
Pattern. Dieselbe Form in das eigene Repository für jeden Wrapper
übernehmen, der eine reusable-*.yaml aufruft, deren Arbeit
nachgelagerte Workflows triggern soll oder die unter derselben
Release-Audit-Identität bleiben soll.
on:
pull_request:
types: [labeled, unlabeled, synchronize, opened, edited, ready_for_review, reopened, unlocked]
pull_request_review:
types: [submitted]
check_suite:
types: [completed]
status: {}
jobs:
automerge:
uses: nolte/gh-plumbing/.github/workflows/reusable-automerge.yaml@<tag>
with:
app-id: ${{ vars.PORTFOLIO_APP_ID }}
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
app-private-key: ${{ secrets.PORTFOLIO_APP_PRIVATE_KEY }}
Schlüsseleigenschaften:
- Backwards-kompatibel. Wenn
vars.PORTFOLIO_APP_IDungesetzt ist, überspringt der Reusable den Mint-Step und fällt zurück aufsecrets.token(=GITHUB_TOKEN). Das Verhalten für nicht-adoptierte Konsumenten ist identisch zum Pre-App-Zustand. - Mint passiert im Reusable. GitHub Actions maskiert
App-Token-Outputs und blockiert sie an Job-Grenzen — ein separater
Mint-Job im Wrapper kann das Token nicht an einen nachgelagerten
Reusable-Call weitergeben. Der Reusable hält den Mint-Step selbst,
gated auf
inputs.app-id. - Kurze Token-Lebensdauer.
actions/create-github-app-token@v2erzeugt ein 1-Stunden-Installation-Token, begrenzt auf das aufrufende Repository. Es verlässt nie GitHubs Control Plane als langlebiges Credential.
Fünf Wrapper brauchen das App-Credential-Forwarding
Drei Wrapper pushen durch einen geschützten Branch und müssen das App-Token-Pattern verwenden:
automerge.yaml(Squash-Merge nachdevelop).release-publish.yml(Release-Flip — emittiertrelease: publishedund kaskadiert weiter).release-cd-refresh-master.yml(Fast-Forward nachmaster, das nach Phase 2 ebenfalls push-restricted ist).
Zwei weitere Wrapper reichen dasselbe Credential aus Konsistenz- und Audit-Trail-Gründen durch, obwohl ihr Ziel-Branch nicht geschützt ist:
release-drafter.ymlaktualisiert den Release-Entwurf über die GitHub-API. Unter dem Portfolio-App-Token läuft jeder Release-Toolchain-Schritt unter einer einheitlichen Identität.release-cd-deliver-docs.ymlpusht die gerenderte Site nachgh-pages.gh-pageshat keine Protection — das App-Token ist hier rein Audit-Trail-Verbesserung.
Alle fünf Wrapper bleiben backwards-kompatibel: ist
vars.PORTFOLIO_APP_ID ungesetzt, fällt jeder Reusable auf
GITHUB_TOKEN zurück und funktioniert weiter.
Verifikation¶
Nach Provisionierung und Adoption sollten beide Cascade-Ketten von selbst triggern:
| Aktion | Erwarteter Cascade |
|---|---|
automerge-Label an einen grünen PR |
automerge.yaml squash-merged → release-drafter.yml aktualisiert den Entwurf → build-static-tests.yaml läuft auf dem neuen develop-Tip |
release-publish.yml für offenen Entwurf dispatchen |
reusable-release-publish.yml flippt draft=false → release-cd-refresh-master.yml fast-forwarded master → release-cd-deliver-docs.yml baut die MkDocs-Site neu |
Eine dritte Self-Check-Zeile betrifft den Issue-Lifecycle:
| Aktion | Erwartetes Ergebnis |
|---|---|
PR squash-mergen, dessen Body Closes #N enthält |
Issue #N flippt automatisch auf CLOSED; gh issue view N --json state liefert CLOSED. Setzt die App-Permission Issues: Read and write voraus — ohne sie greift der Merge zwar, der Close feuert aber nicht, obwohl gh pr view --json closingIssuesReferences den Autolink korrekt geparst zeigt. |
Wenn ein Cascade immer noch nicht triggert, prüfen:
- Die App ist im Konsument-Repo installiert (
Settings → GitHub Apps). vars.PORTFOLIO_APP_IDist für den Workflow sichtbar. Organisations-Modus: die Org-Variable hatvisibility = "selected"inklusive des Workflow-Repos. User-Modus: das Repository trägt seine eigenePORTFOLIO_APP_ID-Actions-Variable.- Der reusable-Run zeigt den
Mint App installation token-Step alssuccess(nichtskipped). Skipped Step bedeutet, der App-ID-Input war leer und der Fallback-Pfad griff (GITHUB_TOKEN= Cascade-Lücke). Greift der Fallback-Pfad, trägt die Job-Summary des Runs eine explizite "Cascade gap (GITHUB_TOKEN fallback)"-Warnung, die auf den manuellen Fast-Forward-Notausgang hinweist.
Secret- und Key-Rotation¶
| Trigger | Aktion |
|---|---|
| Reguläre jährliche Rotation | Neuen Private Key auf der App-Seite generieren, PORTFOLIO_APP_PRIVATE_KEY in jedem Konsumenten aktualisieren (oder einmal auf Org-Level, falls org-scoped), den alten Key auf der App-Seite löschen. |
| Vermuteter Key-Leak | Geleakten Key sofort in den App-Settings widerrufen, neuen Key generieren, am selben Tag verteilen. App-ID bleibt unverändert. |
| App-ID-Rotation | Niemals nötig — App-IDs sind unveränderlich. |
| Permission-Scope-Änderung | App-Permissions editieren; bestehende Installation-Tokens mit alten Permissions laufen für ihre Gültigkeitsdauer (TTL, ≤ 1 h) weiter, dann werden sie mit neuem Scope erneuert. |
Branch-Protection-Bypass (Phase 2)¶
commons-settings.yml deklariert die App als Push-Restriction-Actor
auf den Branches develop und master. Da Konsumenten-Repositories
die Commons per _extends: einbinden, propagiert der Bypass
automatisch zu jedem Adopter, sobald die App in dessen Repository
installiert ist.
Der konfigurierte Slug ist nolte-portfolio-app:
branches:
- name: develop
protection:
enforce_admins: true
restrictions:
users: []
teams: []
apps:
- nolte-portfolio-app
Derselbe Block gilt für master.
Persönliche Accounts unterstützen restrictions.apps nicht
GitHub beschränkt restrictions.users / .teams / .apps auf
Organisations-Repositories. Die Probot Settings App überspringt
das Feld still, wenn es in einem persönlichen Account landet.
Persönliche-Account-Konsumenten arbeiten entweder ohne
Push-Restriktionen oder überschreiben den Branches-Block in
ihrem per-Repo-.github/settings.yml.
Vorbedingung für Phase 2
Vor dem Merge des Phase-2-PRs muss die App bereits in jedem
Konsumenten-Repository installiert sein, dessen
commons-settings.yml den Bypass erhält. Eine nicht installierte
App kann nichts bypassen; der einzige Effekt einer zu frühen
Phase 2 wäre, direkte Pushes von allen anderen Actors zu
blockieren.
Versionstragende Datei-Ausrichtung (Primärpfad)¶
Sobald die App installiert und als develop-Bypass-Actor benannt
ist (der Phase-2-Block oben), kann ein Aufrufer den
workflow-getriebenen Primärpfad für die Ausrichtung versionstragender
Dateien (spec/project/release-automation/ §Version-bearing file
alignment) aktivieren, indem er auto-align: true an
reusable-release-publish.yml übergibt. Mit gesetztem Opt-in und
vorhandenem App-Token führt das Reusable vor der Verifikation nun aus:
- aktualisiert jede versionstragende Datei auf den Ziel-Tag gemäß ihrer
Transformation—unter Wahrung der bestehenden
v-Präfix-Konvention jeder Datei und mit Anpassung jedes Treffers eines Array-Selektors wie$.plugins[].versionin einer Multi-Plugin-marketplace.json; - committet die Gesamtänderung als
chore(release): <tag>aufdevelop, autorisiert durch die App; - pusht sie über den deklarierten Bypass und richtet das
target_commitishdes Drafts auf den neuen Commit aus, damit der veröffentlichte Tag exakt vom ausgerichteten Baum geschnitten wird.
Das Opt-in zusammen mit dem App-Token ist der Freischalt-Gate: Der
Align-Schritt läuft nur, wenn auto-align: true gesetzt ist und der
Mint-Schritt ein Token erzeugt hat. Ohne das Opt-in, ohne die App oder
bei einem Mint-Fehler wird der Schritt übersprungen und der Workflow
fällt auf den Operator-Pfad zurück—ein Mensch
öffnet einen chore(release): <tag>-PR, squash-merged ihn über die UI
und dispatcht dann release-publish. Beide Pfade landen denselben
chore(release): <tag>-Commit; nur die erzeugende Credential
unterscheidet sich. Ein dry_run-Dispatch richtet lokal auf dem Runner
aus und verifiziert, pusht aber nie und richtet den Draft nicht neu aus.
Der erste echte Lauf validiert den geschützten Push
Ein direkter Push auf develop unter enforce_admins: true mit
erforderlichen Status-Checks wird nur akzeptiert, wenn die App ein
echter Bypass-Actor ist. dry_run kann das nicht ausüben. Wird der
erste echte Push abgelehnt, ist die Abhilfe die Bypass-Actor-
Einstellung der Branch-Protection (der Phase-2-Block, über die
GitHub-UI angewendet, wenn die Probot-App ein Schutzfeld nicht
synchronisiert), keine Workflow-Änderung; der Lauf degradiert auf den
Operator-Fallback, statt das Release zu blockieren.