Skip to content

GITHUB_DISPATCH_TOKEN Rotation Runbook ​

What ​

Fine-grained GitHub Personal Access Token used by srv/lib/rebuild-trigger.js to fire workflow_dispatch on rebuild-content.yml after admin writes. Keeps /browse/ SSR'd content fresh within minutes of admin saves to mission/group/featured-flag entities.

Set on the deployed tutorials-srv app as the env var GITHUB_DISPATCH_TOKEN. When unset, rebuild-trigger.js no-ops gracefully (logs one boot warning, falls back to the existing push-trigger cadence on the rebuild-content.yml workflow).

Environment-aware dispatch (REBUILD_TARGET_ENV) ​

The dispatch payload includes an environment input (dev / qa / prod) that the rebuild-content.yml workflow uses to target the right Cloud Foundry approuter. Set this env var alongside GITHUB_DISPATCH_TOKEN on each CF environment so admin writes on QA dispatch a QA rebuild (not a DEV one):

CF spaceREBUILD_TARGET_ENV
devdev (or unset — defaults to dev)
qaqa
prodprod

Validate via the boot log line: [rebuild-trigger] active — admin writes will dispatch with environment='<env>'. If the line shows the wrong env, fix the CF env var and restart.

When to rotate ​

  • Every 90 days (token expiry default).
  • Immediately if the token is suspected leaked (see "Emergency revocation" below).
  • When the PAT-owning user leaves SAP or changes role.

How to rotate ​

  1. Generate the new token. GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.

    • Resource owner: sap-tutorials
    • Repository access: Only select repositories → tutorials-ims only
    • Repository permissions: Actions → Read and write (sole permission needed)
    • Expiration: 90 days
  2. Test the new token before deploying it:

    bash
    curl -X POST -H "Accept: application/vnd.github+json" \
      -H "Authorization: Bearer <NEW_TOKEN>" \
      -H "X-GitHub-Api-Version: 2022-11-28" \
      https://api.github.com/repos/sap-tutorials/tutorials-ims/actions/workflows/rebuild-content.yml/dispatches \
      -d '{"ref":"main","inputs":{"trigger-source":"manual","environment":"dev","slug":""}}'

    Expect: HTTP 204 (no body). A new run should appear in the Actions tab within seconds.

  3. Update the credential store + redeploy each environment. The token lives as the GITHUB_DISPATCH_TOKEN alias in the BTP Credential Store (managed via /admin-ui/#secrets on each env's approuter). Update the alias directly rather than the old GitHub Actions DISPATCH_TOKEN secret — the deploy workflow no longer injects the token via envsubst; the runtime reads it exclusively from credstore on startup. The runtime env var on tutorials-srv is still GITHUB_DISPATCH_TOKEN (that's what srv/lib/rebuild-trigger.js reads). REBUILD_TARGET_ENV is a literal in each deploy/<env>.mtaext and does not need rotation.

    bash
    # Rotate the token in the BTP Credential Store for each env's approuter.
    # This requires XSUAA login to the target env's admin UI — there is no
    # `gh` / `cf` CLI shortcut. Open in a browser:
    #   /admin-ui/#secrets  → find GITHUB_DISPATCH_TOKEN → Rotate → paste <NEW_TOKEN>
    #
    # The runtime picks up the new value on the next resolveSecret() call
    # (5-minute TTL cache). To force immediate propagation, use the
    # "Invalidate cache" action on the same admin UI row — this calls
    # invalidateSecret('GITHUB_DISPATCH_TOKEN') server-side.
    #
    # No cf deploy needed — credstore rotation is a runtime-only change.

    Validate after each deploy: cf logs tutorials-srv --recent | grep rebuild-trigger should show [rebuild-trigger] active — admin writes will dispatch with environment='<env>' with the right env, and the unset-token warning should NOT appear.

    Local rotation validation (optional — verify the new token before merging the secret bump to all envs):

    Rotate through the admin UI at /admin-ui/#secrets on the target env's approuter (alias GITHUB_DISPATCH_TOKEN). The runtime picks up the new value on the next resolveSecret call — the 5-minute TTL means propagation is near-immediate. Confirm via:

    bash
    cf logs tutorials-srv --recent | grep 'rebuild-trigger'
    # Expected: [rebuild-trigger] active — admin writes will dispatch with environment='<env>'
    # NOT expected: [rebuild-trigger] unset-token warning

    If you need to invalidate the resolver cache before the 5-minute TTL, use the admin UI's "invalidate" action on the alias — this calls invalidateSecret('GITHUB_DISPATCH_TOKEN') server-side.

  4. Revoke the old token. GitHub → Settings → Developer settings → Personal access tokens → click old token → Revoke.

  5. Update the rotation calendar reminder for +90 days.

Emergency revocation ​

If the token is suspected leaked (committed to a repo, posted in a chat, posted in a screenshot, etc.):

  1. Revoke immediately via the GitHub UI. This stops further dispatches even before CF env is updated.
  2. Generate a replacement and update CF env per "How to rotate" above.
  3. Audit the tutorials-ims Actions tab for unexpected workflow runs in the leak window. Any unauthorized dispatch is a possible incident — file per the project's security incident process.

Failure modes ​

ModeSymptomAction
Token unset[rebuild-trigger] boot warning; admin writes don't trigger rebuildsAcceptable degraded mode — content stays fresh via the existing push trigger only. Confirm gh secret list --repo sap-tutorials/tutorials-ims shows DISPATCH_TOKEN (note: secret is named DISPATCH_TOKEN, not GITHUB_DISPATCH_TOKEN, because GitHub reserves the GITHUB_ prefix); if missing, add it and trigger a redeploy. If present, check the most recent deploy run's Resolve mtaext placeholders step for the GITHUB_DISPATCH_TOKEN env-var declaration and confirm the next step's cf deploy consumed deploy/<env>.resolved.mtaext.
Token expired / revokedGitHub returns 401; admin save logs [rebuild-trigger] dispatch failed: GitHub dispatch 401 ...Rotate per the steps above. Admin saves still succeed; only the auto-rebuild dispatch is broken.
REBUILD_TARGET_ENV mismatchAdmin save on QA srv triggers a DEV rebuild (or vice versa); boot log shows environment='dev' on a non-DEV spaceSet REBUILD_TARGET_ENV to match the space (qa/prod) and cf restart. Until then content stays fresh on the wrong env.
Token over-permissionedToken has scopes beyond actions:write (e.g. contents:write, metadata:read, etc.)Defense-in-depth violation, not an outage. Re-issue with actions:write only on next rotation cycle.
GitHub rate-limitedSporadic 429 in logsThe 60s debounce already collapses bulk admin edits. If 429 recurs, investigate whether some other CI workflow is sharing this PAT — fine-grained PATs should not be shared across services.

Why fine-grained PAT (not GITHUB_APP / OIDC) ​

The simpler alternatives were considered and rejected for this scope:

  • GitHub App — overkill for a single workflow_dispatch trigger; requires app installation + private-key secret rotation.
  • OIDC from CF — Cloud Foundry doesn't expose OIDC tokens to apps in a way GitHub's actions:write API consumes. Would need a token-broker.
  • Repository secret + workflow — works for CI-triggered rebuilds but doesn't help the admin-write hook (which fires from a deployed CAP, not from CI).

The fine-grained PAT is the simplest fit: scoped, expirable, revocable, and the failure mode (no rebuild dispatch) is graceful.

Where this is documented ​

There is currently no .env.example file in this repo's root. If one is added in the future, include GITHUB_DISPATCH_TOKEN= (empty) with a pointer to this runbook.

References ​