Skip to content

ci(docs): switch dispatch trigger from GitHub App to fine-grained PAT (with skip-when-unconfigured guard)#1347

Merged
rumblefrog merged 3 commits into
mainfrom
fix/docs-deploy-trigger-app-not-configured
May 11, 2026
Merged

ci(docs): switch dispatch trigger from GitHub App to fine-grained PAT (with skip-when-unconfigured guard)#1347
rumblefrog merged 3 commits into
mainfrom
fix/docs-deploy-trigger-app-not-configured

Conversation

@rumblefrog

@rumblefrog rumblefrog commented May 11, 2026

Copy link
Copy Markdown
Member

Followup to #1339 (issue #1333 cutover).

What broke

Failing run: https://github.com/sbpp/sourcebans-pp/actions/runs/25646867498/job/75277366841

After #1339 merged, the first push to main under docs/** (the merge commit itself) fired docs-deploy-trigger.yml, which immediately errored:

Error: Input required and not supplied: app-id

vars.DOCS_DEPLOY_APP_ID resolved to empty string because the cutover step that creates the GitHub App and registers its credentials hadn't been done yet — and per discussion the App was overkill for a single-maintainer setup.

What this PR changes

Two things:

  1. Drop the GitHub App entirely; use a fine-grained PAT instead. The actions/create-github-app-token@v1 step is gone. gh api ... consumes secrets.DOCS_DEPLOY_PAT directly via GH_TOKEN. Credential setup goes from "create + install + register App" (~10+ minutes) to "create + paste fine-grained PAT" (~2 minutes).

  2. Step-level if: secrets.DOCS_DEPLOY_PAT != '' guard so the dispatch is skipped cleanly when the credential isn't set yet. Until the operator pastes the PAT, every push to docs/** shows up as a green run with the dispatch step marked "Skipped", instead of red-failing on a missing credential. Once the PAT lands, the guard is transparent and the dispatch runs every push.

The secret being set IS the operator's "I've done the cutover" signal; no separate feature-flag variable needed. (Earlier iteration of this PR used a paired vars.DOCS_DEPLOY_ENABLED because I'd misremembered secrets.* as unavailable in any if: context — it's only unavailable at job level. Step-level if: sees secrets fine.)

Operator setup (one-time, when you want auto-deploy on PR merge)

This is now optional — workflow_dispatch (manual button on sbpp.github.io's Actions tab) and the weekly cron in the deploy shell still cover the bases without any credential at all.

  1. Create a fine-grained PAT at https://github.com/settings/personal-access-tokens/new:
    • Resource owner: sbpp (or your account if sbpp doesn't show up — fine-grained PATs work either way)
    • Repository access: Only select repositoriessbpp/sbpp.github.io
    • Repository permissions: Actions: Read and write (everything else stays "No access")
    • Expiration: max one year — set a calendar reminder to rotate
  2. In sbpp/sourcebans-pp settings → Secrets and variables → Actions → Secrets → New repository secret:
    • Name: DOCS_DEPLOY_PAT
    • Value: the token

That's it. The dispatch starts firing on the next push to docs/**.

Classic PATs work too, but the fine-grained variant is strictly narrower (per-repo + per-permission scoping) and the right default.

Tradeoff vs the App

PATs are tied to a personal account and expire after at most a year. For a single-maintainer org this is acceptable; the rotation reminder is the only maintenance cost. If the project ever grows enough team members that a personal-account-tied credential becomes awkward, swapping back to the App is a small follow-up — just rebind the credential name in the workflow.

Validation

  • python3 -c 'import yaml; yaml.safe_load(open(".github/workflows/docs-deploy-trigger.yml"))' — YAML OK.
  • After merge, the next push to docs/** should show "Skipped" on the dispatch step (green run overall) until you set DOCS_DEPLOY_PAT.

Paired sibling PR

docs/README.md's workflow table is updated in this PR. The deploy workflow in sbpp/sbpp.github.io mentions the App in its comment block; that's updated in sbpp/sbpp.github.io#55 (no behavior change there — repository_dispatch listens for events from anywhere; the credential the dispatcher uses is invisible to the receiver).

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 #1333 step 2 land (App creation +
`DOCS_DEPLOY_APP_ID` var + `DOCS_DEPLOY_APP_KEY` secret).

Pre-fix, every push to `docs/**` after #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.
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.
@rumblefrog rumblefrog changed the title ci(docs): skip docs-deploy-trigger when GitHub App not yet configured ci(docs): switch dispatch trigger from GitHub App to fine-grained PAT (with skip-when-unconfigured guard) May 11, 2026
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.
@rumblefrog rumblefrog added this pull request to the merge queue May 11, 2026
Merged via the queue into main with commit 48f011c May 11, 2026
3 checks passed
@rumblefrog rumblefrog deleted the fix/docs-deploy-trigger-app-not-configured branch May 11, 2026 03:41
Rushaway pushed a commit to srcdslab/sourcebans-pp that referenced this pull request May 15, 2026
…in `if:` (sbpp#1350)

sbpp#1347's skip-when-unconfigured guard used `if: secrets.DOCS_DEPLOY_PAT != ''`
on the dispatch step. `secrets.*` isn't available in `if:` at any scope
(workflow / job / step) per the Actions context-availability table — the
parser rejects the file with "Unrecognized named-value: 'secrets'" before
any job runs, so every push records a red workflow-file-issue run with no
jobs (visible on the sbpp#1346 dependabot push, run 25652463088). Exactly the
failure mode the guard was meant to prevent.

Fix: read the secret into a precheck step's `env:` (where `secrets.*` IS
allowed), shell-test for presence, emit a `configured=true|false` step
output, gate the dispatch step on `steps.pat.outputs.configured == 'true'`
(`steps.*` IS available in step `if:`). Operator-facing UX unchanged —
dispatch step still shows as Skipped until the PAT is set, run is green.

Both the file-level comment block and the inline step comment now spell
out the actual context-availability rule so a future reader doesn't reach
for the same broken shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant