ci: migrate AUTOFIX_BOT_PAT to GitHub App token

Replace the long-lived fine-grained PAT (AUTOFIX_BOT_PAT) with short-lived
(1-hour) installation access tokens minted via a new get-app-token composite
action wrapping actions/create-github-app-token@v3.2.0.

The PAT was used in 13 spots across 8 workflow files for gh CLI / GitHub API
calls. The per-repo GITHUB_TOKEN (1,000 req/hr) was getting rate-limited when
multiple workflows fire concurrently (deploy-site, skills-index, ci-timings,
supply-chain-audit, js-autofix). App installation tokens get 5,000 req/hr
per installation and are scoped to the App's permissions, not a user account.

New composite action: .github/actions/get-app-token/
  - Wraps actions/create-github-app-token@bcd2ba49 (v3.2.0, SHA-pinned)
  - Reads APP_ID + APP_PRIVATE_KEY repo secrets
  - Outputs a 1hr installation token via steps.app-token.outputs.token

Requires two new repo secrets (set after creating the GitHub App):
  - APP_ID: the App's numeric ID
  - APP_PRIVATE_KEY: the PEM private key

App installation permissions needed:
  contents: write    (js-autofix push, pypi release upload)
  pull-requests: write (js-autofix PR create/merge, supply-chain comment)
  issues: write       (skills-index-freshness issue creation)
  actions: write     (skills-index workflow trigger)
  workflows: write   (skills-index triggers deploy-site.yml)

The AUTOFIX_BOT_PAT secret can be deleted once CI passes on this PR.
The comment in js-autofix.yml noting that PAT pushes trigger downstream
workflows is updated — App tokens have the same property (they are not
GITHUB_TOKEN), so the concurrency-cancel loop logic is unchanged.
This commit is contained in:
ethernet 2026-07-20 13:33:47 -04:00
parent b9f82ed39f
commit 7a69b82ad4
9 changed files with 112 additions and 16 deletions

View file

@ -7,7 +7,7 @@ description: >-
inputs:
github-token:
description: Token for the GitHub API (gh CLI). Pass secrets.AUTOFIX_BOT_PAT from the calling workflow.
description: Token for the GitHub API (gh CLI). Pass steps.app-token.outputs.token from the calling workflow.
required: false
default: ${{ github.token }}

View file

@ -0,0 +1,59 @@
name: Get GitHub App Token
description: >-
Mint a short-lived (1-hour) installation access token from the repo's
GitHub App, replacing the long-lived AUTOFIX_BOT_PAT. App tokens get
5,000 req/hr per installation (vs 1,000 for the default GITHUB_TOKEN)
and are scoped to the App's installation permissions, not a user account.
Falls back to the built-in GITHUB_TOKEN when APP_ID is not set — this
happens on fork PRs where repo secrets are unavailable. The fallback
ensures classification, timings, and review comments still work on
forks (with the lower GITHUB_TOKEN rate limit).
Requires two repo secrets (store when creating the App):
- APP_ID — the App's numeric ID (Settings → General)
- APP_PRIVATE_KEY — the PEM private key (Settings → Private keys)
The App must be installed on the repository (or org) with the
permissions the calling workflow needs.
inputs:
owner:
description: Repository owner (for cross-org tokens). Defaults to the current repo's owner.
required: false
default: ${{ github.repository_owner }}
outputs:
token:
description: A GitHub App installation access token (1-hour TTL), or GITHUB_TOKEN on forks.
value: ${{ steps.app-token.outputs.token || steps.fallback.outputs.token }}
runs:
using: composite
steps:
- name: Check if App credentials exist
id: check
shell: bash
env:
APP_ID: ${{ secrets.APP_ID }}
run: |
if [ -n "$APP_ID" ]; then
echo "has_app=true" >> "$GITHUB_OUTPUT"
else
echo "has_app=false" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub App token
id: app-token
if: steps.check.outputs.has_app == 'true'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ inputs.owner }}
- name: Fall back to GITHUB_TOKEN
id: fallback
if: steps.check.outputs.has_app != 'true'
shell: bash
run: echo "token=${{ github.token }}" >> "$GITHUB_OUTPUT"

View file

@ -49,13 +49,16 @@ jobs:
event_name: ${{ github.event_name }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- name: Detect affected areas
id: classify
uses: ./.github/actions/detect-changes
with:
# Forks get no repo secrets (AUTOFIX_BOT_PAT is empty); fall back to
# the built-in read-only token so classification still works there.
github-token: ${{ secrets.AUTOFIX_BOT_PAT || github.token }}
# The get-app-token composite action falls back to GITHUB_TOKEN
# on fork PRs where APP_ID is unavailable.
github-token: ${{ steps.app-token.outputs.token }}
# ─────────────────────────────────────────────────────────────────────
# Lane-gated sub-workflows. Each runs in parallel after detect finishes.
@ -318,6 +321,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- name: Restore baseline cache (PR only)
if: github.event_name == 'pull_request'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@ -334,7 +341,9 @@ jobs:
# Forks get no repo secrets (AUTOFIX_BOT_PAT is empty); fall back to
# the built-in read-only token so the timings API read still works
# there instead of hard-failing this advisory job on every fork PR.
GITHUB_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT || github.token }}
# The get-app-token composite action falls back to GITHUB_TOKEN
# on fork PRs where APP_ID is unavailable.
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
python3 scripts/ci/timings_report.py \
--baseline ci-timings-baseline.json \

View file

@ -56,6 +56,10 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
@ -73,8 +77,8 @@ jobs:
- name: Prepare skills index (unified multi-source catalog)
env:
GH_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GITHUB_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
SKILLS_INDEX_RUN_ID: ${{ github.event.inputs.skills_index_run_id || '' }}
REBUILD_SKILLS_INDEX: ${{ github.event.inputs.rebuild_skills_index || 'false' }}
run: |

View file

@ -7,7 +7,7 @@ name: auto-fix lint issues & formatting
# auto-corrected on merge so PRs aren't blocked by them. The PR-time eslint
# check in typecheck.yml fails only when un-fixable errors remain.
#
# NOTE: AUTOFIX_BOT_PAT pushes DO trigger further workflow runs (unlike
# NOTE: App token pushes DO trigger further workflow runs (unlike
# secrets.GITHUB_TOKEN). The concurrency group (ts-autofix-${{ github.ref }})
# with cancel-in-progress: true prevents an infinite loop — a re-triggered
# run cancels the in-flight one, and since the second run finds no new fixes
@ -128,6 +128,10 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- name: Download patch
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
@ -170,7 +174,7 @@ jobs:
- name: Create/update PR and enable auto-merge
env:
GH_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
BOT_BRANCH: bot/js-autofix
run: |
set -euo pipefail
@ -193,7 +197,7 @@ jobs:
- name: Wait for merge, auto-close on failure or stale
env:
GH_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
START_SHA: ${{ github.sha }}
run: |
set -euo pipefail

View file

@ -108,10 +108,15 @@ jobs:
echo "Summary: ${{ steps.probe.outputs.summary }}"
fi
- name: Get GitHub App token
if: steps.probe.outputs.status != 'ok'
id: app-token
uses: ./.github/actions/get-app-token
- name: Open issue on degraded / failed probe
if: steps.probe.outputs.status != 'ok'
env:
GH_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
STATUS: ${{ steps.probe.outputs.status }}
DETAIL: ${{ steps.probe.outputs.detail }}
run: |

View file

@ -24,6 +24,10 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
@ -35,7 +39,7 @@ jobs:
- name: Build skills index
env:
GITHUB_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
run: python scripts/build_skills_index.py
- name: Upload index artifact
@ -54,7 +58,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- name: Trigger Deploy Site workflow
env:
GH_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: gh workflow run deploy-site.yml --repo ${{ github.repository }} -f skills_index_run_id=${{ github.run_id }}

View file

@ -60,10 +60,14 @@ jobs:
with:
fetch-depth: 0
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- name: Scan diff for critical patterns
id: scan
env:
GH_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail

View file

@ -143,9 +143,13 @@ jobs:
name: python-package-distributions
path: dist/
- name: Get GitHub App token
id: app-token
uses: ./.github/actions/get-app-token
- name: Wait for GitHub Release to exist
env:
GITHUB_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# release.py creates the GitHub Release after pushing the tag,
# but this workflow starts from the tag push — wait for it.
run: |
@ -171,7 +175,7 @@ jobs:
- name: Attach signed artifacts to GitHub Release
if: env.skip_sign != 'true'
env:
GITHUB_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# release.py already created the GitHub Release — just upload
# the Sigstore signatures alongside the existing assets.
run: >-