Skip to content

Commit 48f011c

Browse files
authored
ci(docs): switch dispatch trigger from GitHub App to fine-grained PAT (with skip-when-unconfigured guard) (sbpp#1347)
* ci(docs): skip dispatch when GitHub App not configured Adds a job-level `if: vars.DOCS_DEPLOY_APP_ID != ''` guard to `docs-deploy-trigger.yml` so the workflow no-ops cleanly until the cutover steps in sbpp#1333 step 2 land (App creation + `DOCS_DEPLOY_APP_ID` var + `DOCS_DEPLOY_APP_KEY` secret). Pre-fix, every push to `docs/**` after sbpp#1339 merged ran the dispatch job, hit `actions/create-github-app-token@v1` with an empty `app-id` input, and exited red with `Input required and not supplied: app-id` (run 25646867498). Operators landing unrelated docs PRs would now see a stream of red runs that look like real CI failures but are actually the expected "cutover not done yet" state — exactly the friction the "Required repo configuration" comment block was meant to warn about. The job-level `if:` is the canonical "feature-flag a job" shape: unset repo variables resolve to empty string in expressions, so the guard is transparent once the var lands and the job runs every push as designed. Skipped runs show up as a grey "Skipped" badge in the Actions tab — the right visual signal for "intentionally inert until configured". Comment block above the workflow expanded to call this out explicitly so future readers don't try to "fix" the guard by removing it. * ci(docs): switch dispatch trigger from GitHub App to fine-grained PAT Replaces the `actions/create-github-app-token@v1` round-trip and the App-side `vars.DOCS_DEPLOY_APP_ID` + `secrets.DOCS_DEPLOY_APP_KEY` pair with a direct `secrets.DOCS_DEPLOY_PAT` (a fine-grained PAT scoped to `Actions: Read and write` on `sbpp.github.io` only). The previous fix in this PR's first commit (`0bf26262`) gated the job on `vars.DOCS_DEPLOY_APP_ID`. Renamed the gate variable to `vars.DOCS_DEPLOY_ENABLED` since "App ID" is no longer the right mental model — the variable is now a pure feature-flag the operator sets to opt in once the PAT is configured. (We can't reference `secrets.*` directly in a job-level `if:` per GitHub Actions' context-availability rules, so the paired VARIABLE is the canonical "feature-flag a job" shape.) Why PAT over App: a fine-grained PAT scoped to one repo + one permission carries the same blast radius as the org-owned App but takes ~2 minutes to provision instead of ~10. Tradeoff is the PAT is tied to a personal account and has a max one-year expiry that needs rotation; for a project of this size that's a fair deal. Operator instructions live in the workflow's top-of-file comment block + `docs/README.md`'s workflow table. The job-level `if:` guard pattern is unchanged in shape — every push to `docs/**` shows up as "Skipped" (grey badge) until the operator sets BOTH `secrets.DOCS_DEPLOY_PAT` and `vars.DOCS_DEPLOY_ENABLED`. The deploy shell in `sbpp.github.io` also has a `workflow_dispatch` trigger as a manual fallback while the PAT is pending. * ci(docs): drop redundant DOCS_DEPLOY_ENABLED variable The previous commit on this branch added a paired `vars.DOCS_DEPLOY_ENABLED` feature flag because I'd convinced myself `secrets.*` was unavailable in any `if:` context. That's half-true: it's unavailable in JOB-level `if:` (which is what I was trying to use), but it IS available in STEP-level `if:`. Since the trigger job has only one step (the dispatch), gating that single step on `if: secrets.DOCS_DEPLOY_PAT != ''` is behaviorally equivalent to a job-level guard — the runner spins up briefly to evaluate the step, sees no credential, marks the step Skipped, and the run finishes green-with-skipped instead of red-failing. The "wasted runner spin" cost I cited as justification for the variable is real but tiny (a few seconds of CI minutes vs ~30s of job-startup overhead either way), and the green-with-skipped status is arguably MORE informative than a fully-skipped grey job — it tells you "I ran, had no work to do" rather than "I didn't run". The variable was the operator's "I've done the cutover" signal, but the secret being set IS that signal. One source of truth, one less thing to forget. Cutover is now: create PAT, paste into `secrets.DOCS_DEPLOY_PAT`, done. Comment block above the workflow + `docs/README.md` workflow table updated.
1 parent 40e6f54 commit 48f011c

2 files changed

Lines changed: 30 additions & 23 deletions

File tree

.github/workflows/docs-deploy-trigger.yml

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,26 @@
55
# Cadence: only on push to main with a docs/** path filter. PRs use
66
# docs-build.yml to validate; this workflow is the production trigger.
77
#
8-
# Required repo configuration BEFORE this workflow can succeed (one-time
9-
# cutover steps documented in #1333 cutover steps 1-2):
8+
# Required repo configuration BEFORE this workflow does anything (one-time
9+
# cutover step):
1010
#
11-
# - Create the `sbpp-docs-deploy` GitHub App (org-owned), scope
12-
# `Actions: write` on `sbpp.github.io` only, install on
13-
# `sbpp/sbpp.github.io`.
14-
# - Repo VARIABLE `DOCS_DEPLOY_APP_ID` = the App's numeric ID.
15-
# - Repo SECRET `DOCS_DEPLOY_APP_KEY` = the App's PEM private key.
11+
# - Create a fine-grained PAT scoped to `sbpp/sbpp.github.io` only,
12+
# with the `Actions: Read and write` repository permission. (Classic
13+
# PATs work too, but the fine-grained variant is strictly narrower
14+
# and the right default.) Max expiry is one year — set a calendar
15+
# reminder to rotate.
16+
# - Repo SECRET `DOCS_DEPLOY_PAT` = the token value.
1617
#
17-
# Until those land, this workflow will fail on the first run with an
18-
# auth error. That's expected; the deploy shell in sbpp.github.io
19-
# also has a workflow_dispatch trigger as a manual fallback.
18+
# Until `DOCS_DEPLOY_PAT` is set, the dispatch step below is skipped via
19+
# its `if: secrets.DOCS_DEPLOY_PAT != ''` guard — every push to `docs/**`
20+
# shows up as a green run with the dispatch step marked "Skipped",
21+
# instead of red-failing on a missing credential. This stops the
22+
# original anti-pattern (#1339-followup) where the dispatch hard-erred
23+
# and an operator who hasn't done the cutover yet sees a stream of
24+
# confusing failures.
25+
#
26+
# The deploy shell in sbpp.github.io also has a `workflow_dispatch`
27+
# trigger as a manual fallback while the PAT is pending.
2028

2129
name: docs-deploy-trigger
2230

@@ -44,22 +52,21 @@ jobs:
4452
permissions: {}
4553

4654
steps:
47-
- name: Mint installation token via GitHub App
48-
id: mint-token
49-
uses: actions/create-github-app-token@v1
50-
with:
51-
app-id: ${{ vars.DOCS_DEPLOY_APP_ID }}
52-
private-key: ${{ secrets.DOCS_DEPLOY_APP_KEY }}
53-
owner: sbpp
54-
repositories: sbpp.github.io
55-
5655
# The dispatched workflow in sbpp.github.io listens for
5756
# `event_type: docs-changed`. The client_payload carries the
58-
# commit SHA and ref so the deploy job can stamp it into the
59-
# site footer / build manifest if it wants.
57+
# commit SHA and ref so the deploy job can pin its sourcebans-pp
58+
# checkout to the exact commit that fired the dispatch (race
59+
# guard for back-to-back pushes).
60+
#
61+
# Step-level `if:` evaluates against `secrets.*` (job-level `if:`
62+
# does not), so we gate the dispatch directly on the PAT being
63+
# configured — no separate feature-flag variable needed. When
64+
# `DOCS_DEPLOY_PAT` is unset, the step is skipped and the run is
65+
# green-with-skipped instead of red-failing.
6066
- name: Dispatch repository_dispatch into sbpp.github.io
67+
if: secrets.DOCS_DEPLOY_PAT != ''
6168
env:
62-
GH_TOKEN: ${{ steps.mint-token.outputs.token }}
69+
GH_TOKEN: ${{ secrets.DOCS_DEPLOY_PAT }}
6370
run: |
6471
gh api repos/sbpp/sbpp.github.io/dispatches \
6572
--method POST \

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Four workflows under `.github/workflows/` cover the docs site:
8787
| Workflow | Trigger | What it does |
8888
| -------- | ------- | ------------ |
8989
| `docs-build.yml` | PRs + main pushes touching `docs/**` | Runs `npm run build`. Uploads the built `dist/` as an artifact. |
90-
| `docs-deploy-trigger.yml` | main pushes touching `docs/**` | Fires a `repository_dispatch` (event_type=`docs-changed`) into `sbpp/sbpp.github.io`, which kicks the actual GitHub Pages deploy. Requires the `DOCS_DEPLOY_APP_ID` repo variable + `DOCS_DEPLOY_APP_KEY` repo secret to be configured (one-time cutover step). |
90+
| `docs-deploy-trigger.yml` | main pushes touching `docs/**` | Fires a `repository_dispatch` (event_type=`docs-changed`) into `sbpp/sbpp.github.io`, which kicks the actual GitHub Pages deploy. Requires the `DOCS_DEPLOY_PAT` repo secret (fine-grained PAT, `Actions: Read and write` on `sbpp.github.io` only). Until the secret is set, the dispatch step is skipped on every run (the run is green-with-skipped, not red-failing); the deploy shell in `sbpp.github.io` still has a `workflow_dispatch` button as a manual fallback. |
9191
| `docs-screenshots-build.yml` | PRs touching `docs/scripts/capture.mjs` or `docs/package*.json` | Sandboxed verification: `npm ci` + `node --check scripts/capture.mjs`. No secrets, no write permissions; runs the standard `pull_request` token. Catches "did the capture script still parse" on every PR. |
9292
| `docs-screenshots-capture.yml` | PRs labelled `safe-to-screenshot` (same-repo only) + `workflow_dispatch` | Boots the dev stack, seeds the DB, runs `npm run capture` from a TRUSTED-FROM-MAIN checkout, commits PNG deltas back to the PR branch. |
9393

0 commit comments

Comments
 (0)