Mode: operator-assisted (scripted propagation available — see Step 5) Last validated: 2026-05-05 UTC Validation method: read-only-docs Average duration: 5m (scripted) / 7m (manual) Required role: ops
Applies to: GITHUB_API_DISPATCH_TOKEN, GITHUB_API_READONLY_TOKEN, and any other classic or fine-grained PATs used by Raxx infrastructure for GitHub API access (CI workflows, console build status integration, repo automation).
Bridge script available. Steps 5–6 can be run in one command once the new token is stored in vault. See Step 5 for usage. This script is an interim bridge until Velvet's GitHub adapter (#947) absorbs the responsibility natively.
Recommendation: migrate to fine-grained PATs. GitHub recommends fine-grained tokens over classic PATs whenever possible. Fine-grained tokens have better scoping, mandatory expiry, per-repo permissions, and clearer audit trails. New tokens should be fine-grained unless a needed feature is classic-only. Track legacy classic PATs in the routing matrix and migrate on the next rotation cycle.
# Confirm current token works
curl -sS -H "Authorization: Bearer $CURRENT_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/user | jq '.login'
# Expect: the GitHub username (or app/bot login)
For fine-grained tokens, also verify scope:
# List the resources the token can see
curl -sS -H "Authorization: Bearer $CURRENT_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/user/repos | jq '. | length'
Fine-grained PAT (preferred):
1. Navigate to https://github.com/settings/personal-access-tokens/new.
2. Token name: descriptive (e.g., raxx-console-readonly-2026-04-24).
3. Expiration: select 90 days (or shortest practical for the use case).
4. Resource owner: select MooseQuest org (or personal account).
5. Repository access: select specific repos this token needs (least-privilege).
6. Permissions: select the minimal set:
- For GITHUB_API_READONLY_TOKEN: Repository → Actions: Read-only, Repository → Metadata: Read-only, Repository → Contents: Read-only.
7. Click Generate token.
8. Copy the token value immediately — shown once. Format: github_pat_....
Classic PAT (only if fine-grained cannot satisfy the use case):
1. Navigate to https://github.com/settings/tokens/new.
2. Note: descriptive (e.g., raxx-classic-2026-04-24).
3. Expiration: 90 days.
4. Scopes: minimal set (e.g., read:org, repo:status, actions:read).
5. Click Generate token.
6. Copy. Format: ghp_....
NEW_TOKEN="..."
curl -sS -H "Authorization: Bearer $NEW_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/user | jq '.login'
# Expect: same username/login as step 1
Validate against the consumer's actual workload:
# Example: console reads recent CI runs
curl -sS -H "Authorization: Bearer $NEW_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/raxx-app/TradeMasterAPI/actions/runs?per_page=1" | jq '.workflow_runs[0].status'
# Expect: a run status string ("completed", etc.)
infisical secrets set GITHUB_API_READONLY_TOKEN="$NEW_TOKEN" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
Run the bridge propagation script. It reads the new value from vault, sets it on all Heroku apps and the GitHub Actions secret, then verifies via the GitHub API:
# Required env vars (set before running):
export INFISICAL_CLIENT_ID="..."
export INFISICAL_CLIENT_SECRET="..."
export INFISICAL_PROJECT_ID="..."
export HEROKU_API_KEY="..."
export GITHUB_TOKEN="..." # a token with secrets:write on raxx-app/TradeMasterAPI
# Propagate GITHUB_API_DISPATCH_TOKEN (defaults: both console apps + repo secret):
scripts/ops/rotate-github-pat.sh GITHUB_API_DISPATCH_TOKEN
# Propagate GITHUB_API_READONLY_TOKEN:
scripts/ops/rotate-github-pat.sh GITHUB_API_READONLY_TOKEN
# Dry-run to preview what would happen without touching anything:
scripts/ops/rotate-github-pat.sh GITHUB_API_DISPATCH_TOKEN --dry-run
Override defaults if needed:
scripts/ops/rotate-github-pat.sh GITHUB_API_DISPATCH_TOKEN \
--vault-path /MooseQuest/github/ \
--apps raxx-console-prod,raxx-console-staging \
--gh-secret-repo raxx-app/TradeMasterAPI
The script prints a SUMMARY with the status of each destination and next-steps for revoking the old token.
Note: This script is a bridge artifact until Velvet's Heroku + GitHub adapters (#947) ship. When #947 merges, this manual step is replaced by the Velvet rotation flow.
| Consumer | How |
|---|---|
| Console (raxx-console-prod) | heroku config:set GITHUB_API_DISPATCH_TOKEN="$NEW_TOKEN" -a raxx-console-prod >/dev/null 2>&1 |
| Console (raxx-console-staging) | heroku config:set GITHUB_API_DISPATCH_TOKEN="$NEW_TOKEN" -a raxx-console-staging >/dev/null 2>&1 |
| Raptor (if it makes GitHub API calls) | heroku config:set GITHUB_API_DISPATCH_TOKEN=... -a raxx-api-prod >/dev/null 2>&1 |
GitHub Actions (a workflow that calls gh api outside default GITHUB_TOKEN) |
gh secret set GITHUB_API_DISPATCH_TOKEN -b "$NEW_TOKEN" -R raxx-app/TradeMasterAPI |
| Operator local zshrc | DM via Slack D0AJ7K184TV |
Safety: always append
>/dev/null 2>&1to everyheroku config:setcall. The Heroku CLI echoes the entire config-var payload to stdout on success, exposing the secret value in terminal history and CI logs. Seefeedback_heroku_config_set_echoes_secrets.md.
After dyno restart:
# Console build-status panel should populate
curl -sS https://console.raxx.app/api/status/builds \
-H "Cookie: <ops session>" | jq '.builds.api[0].conclusion'
# Expect: a string (not null), no rate-limit errors
# Tail logs for any GitHub API auth errors
heroku logs --tail -a raxx-console-prod | grep -iE 'github|401|403'
# Expect: no auth failures.
GitHub does not support "regenerate in place" — you must revoke the old token explicitly:
https://github.com/settings/tokens (classic) or https://github.com/settings/personal-access-tokens (fine-grained).Verify:
curl -sS -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $OLD_TOKEN" \
https://api.github.com/user
# Expect: 401
action: secret.rotate.completed
actor: <admin_id>
context: {
"secret_name": "GITHUB_API_READONLY_TOKEN",
"token_type": "fine-grained" or "classic",
"method": "operator-assisted-settings"
}
Until step 7, both tokens are valid. To roll back: 1. Revert Heroku config vars to the OLD token from Infisical history. 2. Restart consumers. 3. Skip step 7. Investigate.
After step 7 (revoke), the old token is dead. Generate a brand-new fresh token and redo from step 2.
POST /credentials/revoke (for tokens belonging to others)GITHUB_TOKEN that is auto-rotated per workflow run. Don't conflate that with PATs — GITHUB_TOKEN is not in scope for this SOP.ghp_, fine-grained start github_pat_. GitHub's secret scanner uses these prefixes.