Repository navigation
PAT rotation reminder #73
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PAT rotation reminder | |
| on: | |
| schedule: | |
| # 05:00 UTC daily — after the traffic snapshot has finished. | |
| - cron: "0 5 * * *" | |
| workflow_dispatch: {} | |
| permissions: | |
| issues: write | |
| contents: write | |
| jobs: | |
| check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Read the secret's real updated_at from the API rather than trusting a | |
| # hand-maintained date. The ledger drifted by 16 days in Aug 2026 because | |
| # a rotation was done but the file was never bumped, which made the | |
| # reminder fire against a stale date and left an issue open for 10 days. | |
| - name: Resolve last rotation date | |
| id: resolve | |
| env: | |
| GH_TOKEN: ${{ secrets.TRAFFIC_PAT }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| set -uo pipefail | |
| LEDGER=".github/data/pat-rotation.json" | |
| LEDGER_DATE=$(python -c "import json;print(json.load(open('$LEDGER'))['traffic_pat_last_rotated'])") | |
| # TRAFFIC_PAT carries Administration:Read, so it can read its own | |
| # secret metadata. If the call fails (token expired or scope changed) | |
| # fall back to the ledger — a stale date is better than no check. | |
| API_DATE=$(gh api "repos/$GH_REPO/actions/secrets/TRAFFIC_PAT" --jq '.updated_at[0:10]' 2>/dev/null || true) | |
| # Validate the shape, do not just test for non-empty. On a 401 the | |
| # CLI writes its error body to stdout, so API_DATE ends up holding a | |
| # multi-line JSON blob rather than a date. That is non-empty, so the | |
| # old check took the "api" branch with garbage, and writing a | |
| # multi-line value to GITHUB_OUTPUT then failed the whole step. The | |
| # net effect was that this reminder died exactly when the token had | |
| # expired, which is the one moment it needs to fire. | |
| if ! printf '%s' "$API_DATE" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then | |
| API_DATE="" | |
| fi | |
| if [ -n "$API_DATE" ]; then | |
| SOURCE="api" | |
| LAST_ROTATED="$API_DATE" | |
| else | |
| SOURCE="ledger" | |
| LAST_ROTATED="$LEDGER_DATE" | |
| echo "::warning::Could not read secret metadata — TRAFFIC_PAT may be expired. Falling back to the ledger date." | |
| fi | |
| echo "last_rotated=$LAST_ROTATED" >> "$GITHUB_OUTPUT" | |
| echo "ledger_date=$LEDGER_DATE" >> "$GITHUB_OUTPUT" | |
| echo "source=$SOURCE" >> "$GITHUB_OUTPUT" | |
| echo "Last rotated: $LAST_ROTATED (source: $SOURCE)" | |
| # Keep the human-readable ledger in sync automatically. | |
| - name: Sync ledger if it drifted | |
| if: steps.resolve.outputs.source == 'api' && steps.resolve.outputs.ledger_date != steps.resolve.outputs.last_rotated | |
| env: | |
| NEW_DATE: ${{ steps.resolve.outputs.last_rotated }} | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import json, os | |
| p = ".github/data/pat-rotation.json" | |
| d = json.load(open(p)) | |
| d["traffic_pat_last_rotated"] = os.environ["NEW_DATE"] | |
| with open(p, "w") as f: | |
| json.dump(d, f, indent=2) | |
| f.write("\n") | |
| PY | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add .github/data/pat-rotation.json | |
| if ! git diff --cached --quiet; then | |
| git commit -m "chore: sync PAT rotation ledger to ${NEW_DATE}" | |
| git push | |
| fi | |
| - name: Open issue if approaching expiry | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| LAST_ROTATED: ${{ steps.resolve.outputs.last_rotated }} | |
| run: | | |
| set -euo pipefail | |
| LEDGER=".github/data/pat-rotation.json" | |
| EXPIRY_DAYS=$(python -c "import json;print(json.load(open('$LEDGER'))['traffic_pat_expiry_days'])") | |
| WARN_DAYS=$(python -c "import json;print(json.load(open('$LEDGER'))['warn_days_before_expiry'])") | |
| AGE=$(python -c " | |
| from datetime import date | |
| import os | |
| last = date.fromisoformat(os.environ['LAST_ROTATED']) | |
| print((date.today() - last).days) | |
| ") | |
| REMAINING=$((EXPIRY_DAYS - AGE)) | |
| echo "TRAFFIC_PAT age ${AGE}d — ${REMAINING}d until expiry." | |
| if [ "$AGE" -lt $((EXPIRY_DAYS - WARN_DAYS)) ]; then | |
| echo "Still fresh. No action needed." | |
| exit 0 | |
| fi | |
| # Close any stale reminder that predates the current rotation — | |
| # otherwise an old issue suppresses the new one indefinitely. | |
| STALE=$(gh issue list --repo "$GH_REPO" --state open --label pat-rotation-due \ | |
| --json number,createdAt \ | |
| --jq ".[] | select(.createdAt[0:10] < \"$LAST_ROTATED\") | .number" || true) | |
| for N in $STALE; do | |
| echo "Closing stale reminder #$N (predates rotation on $LAST_ROTATED)" | |
| gh issue close "$N" --repo "$GH_REPO" \ | |
| --comment "Superseded — TRAFFIC_PAT was rotated on ${LAST_ROTATED}." | |
| done | |
| EXISTING=$(gh issue list --repo "$GH_REPO" --state open --label pat-rotation-due \ | |
| --json number --jq '.[0].number // empty' || true) | |
| if [ -n "$EXISTING" ]; then | |
| echo "Open reminder already exists: #$EXISTING. Skipping." | |
| exit 0 | |
| fi | |
| BODY="TRAFFIC_PAT was last rotated on **${LAST_ROTATED}** (${AGE} days ago). It expires in **${REMAINING} day(s)**. | |
| ## Rotate it | |
| 1. Go to https://github.com/settings/tokens?type=beta and regenerate \`TRAFFIC_PAT (8-day rotating)\`. Keep all repo access plus **Administration: Read** and **Metadata: Read**. Set expiry to 8 days. | |
| 2. **Authorize SSO on the token.** Skipping this is the most common cause of a silent failure. | |
| 3. \`gh secret set TRAFFIC_PAT --repo $GH_REPO\` | |
| 4. \`gh workflow run traffic-snapshot.yml --repo $GH_REPO\` to confirm it works. | |
| The ledger now syncs itself from the secret's \`updated_at\`, so there is no file to bump by hand. | |
| ## If this lapses | |
| The nightly \`traffic-snapshot\` workflow fails with HTTP 403 across every microsoft/* repo. Data captured before the failure is still committed (fixed Aug 2026), but **repo traffic for the missed days is lost** — GitHub serves only a rolling 14-day window, so a gap longer than that is unrecoverable. | |
| _Auto-opened by \`.github/workflows/pat-rotation-reminder.yml\`._" | |
| gh issue create --repo "$GH_REPO" \ | |
| --title "🔑 Rotate TRAFFIC_PAT — ${REMAINING} day(s) until expiry" \ | |
| --label pat-rotation-due \ | |
| --body "$BODY" |