fix(standards-sync-stuck-automerge-alert): author the tracking issue as the App, not the ambient token - #223
Conversation
…as the App, not the ambient token
The reusable minted a read-only App token for its cross-repo PR scan but
still opened/updated/closed its own tracking issue as the ambient
GITHUB_TOKEN (github-actions[bot]), which only works if the calling job
grants issues:write. That forced every caller into runner-policy's
write-caller-permissions contract shape, which requires an identity-passthrough
secret mapping (`${{ secrets.<name> }}`, no hyphens) that this reusable's
kebab-case app-client-id/app-private-key inputs can never satisfy.
Mint a second App token scoped to only the calling repository's
issues:write, and author the tracking issue with it instead. This is
strictly tighter decoy resistance (the App's identity isn't shared by every
other ambient-token workflow in the repo) and removes the caller's need to
grant issues:write at all, so its runner-policy contract can be secrets-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYvF6bWGqemS9aYFfWJRiW
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Warning Automated review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
|
Warning Automated security review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c18c0ef64d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… SHA melodic-software/ci-workflows#223 merged (43bc8d0), moving the reusable's tracking-issue writes onto a self-minted App token. Flip the caller pin and its policy.json contract key to that SHA now that the blocker is resolved; no other change needed (the caller was already written for this shape: no issues:write, no allowedCallerPermissions waiver). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KYvF6bWGqemS9aYFfWJRiW
…tomerge-alert (#251) No related issue: caller rollout for merged melodic-software/ci-workflows#213 (issue #210 closed) ## Related ci-workflows#210, ci-workflows#213, ci-workflows#223 ## Summary - standards owns the sync trigger point and already holds the App secrets `sync.yml` uses, so it hosts the scheduled caller for the stuck-automerge watchdog reusable added in melodic-software/ci-workflows#213, rather than medley. - **This PR is a draft, blocked on melodic-software/ci-workflows#223.** The pinned SHA (`dd45dacd7b74dd05f3334b78769e57225f7356d8`) is the reusable as merged in #213, which authors its tracking issue with the caller's ambient `GITHUB_TOKEN` and therefore needs a caller-granted `issues: write`. That combination cannot satisfy `runner-policy`'s write-caller-permissions invariant (identity-passthrough-only secret mapping, no hyphens) against this reusable's kebab-case `app-client-id`/`app-private-key` secret inputs — see #223 for the full diagnosis and the fix (the reusable mints its own App token for issue writes instead). - The workflow file and `policy.json` entry here are already written for the **post-#223 shape**: no caller `issues: write`, no `allowedCallerPermissions` waiver, secrets-only contract. Only the pinned SHA (in both the workflow file and the policy.json key) needs to flip to #223's merge SHA once it lands — no other change. ## Test plan - [x] `npm run test:runner-policy` — 228/228 pass - [x] `npm run lint:runner-policy` — Runner policy passed - [x] `actionlint .github/workflows/standards-sync-stuck-automerge-alert.yml` — clean - [x] `zizmor .github/workflows/standards-sync-stuck-automerge-alert.yml` — no findings - [x] `npx biome check components/runner-policy/policy.json` — no fixes needed - [ ] Flip pinned SHA to ci-workflows#223's merge SHA once it merges, mark ready for review 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KYvF6bWGqemS9aYFfWJRiW --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…to a cheap page fetch and a per-candidate merge-state probe with retry (#234) Closes #226 ## Summary - Splits the stuck-automerge scan into two phases so the hourly cron stops dying on GitHub's opaque server-side GraphQL error: phase 1 pages open PRs selecting only `number`/`url`/`author`/`autoMergeRequest { enabledAt }` (no `mergeStateStatus`), phase 2 probes `mergeStateStatus` one PR at a time via `repository.pullRequest(number:)`, and only for armed, past-threshold bot PRs. - Wraps every GraphQL call in `graphqlWithRetry` — exponential backoff (`GRAPHQL_RETRY_ATTEMPTS=4`, `GRAPHQL_RETRY_BASE_MS=1000`, both step-env-tunable), rethrowing the last error on exhaustion so a persistent failure still fails the run loudly. On a throw the `stuck-count` output is never set, so the close-issue branch cannot fire a false all-clear. - Retains the existing manual-cursor pagination and `MAX_PAGES` soundness guard unchanged; "stuck" semantics are preserved (the `BLOCKED` check moved from the bulk page filter to the per-candidate probe). - No changes to the trigger surface, App-authored issue identity, `uses:` SHA pins, or permissions blocks. ## Test plan - `node --test .github/scripts/*.test.cjs` (CI's exact command): 257/257 pass, including 24 in `standards-sync-stuck-automerge-alert.test.cjs` — new coverage asserts the page query never selects `mergeStateStatus`, exactly one phase-2 probe per candidate (no bulk fan-out), transient-error retry on both page fetch and probe, and persistent-error loud failure with no false all-clear. - `actionlint` on the changed workflow: clean. - `biome check` (CI config/scope): clean. - Post-merge follow-through: the phase-2 query shape is mock-validated only — confirm ≥2 consecutive green scheduled runs against the real fleet (the issue's remaining acceptance criterion). ## Related - Refs melodic-software/standards#251 — deployment PR for the failing hourly caller - Refs #224 — comment-events opt-in context named by the issue - Refs #223 — prior fix on the same workflow (App-authored issue identity), deliberately untouched here 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
No related issue: design amendment to merged #213 (runner-policy contract conflict)
Related
#210, #213, melodic-software/standards components/runner-policy
Summary
GITHUB_TOKEN, authored asgithub-actions[bot].issues: write(cross-org reusable workflows cannot elevate their own ambient-token scope beyond what the caller's job permissions declare — GitHub's own reusable-workflow calling example showspermissions:set on the job withuses:, and this repo's ownrunner-policyprecedent forlink-check.yml/pulumi-version-drift-check.ymlalready encodes the same requirement for their issue-writing behavior).issues: writetriggersrunner-policy's invariant that a contract withallowedCallerPermissionsmust map everyallowedSecretsentry as an exact identity passthrough${{ secrets.<name> }}, where<name>must be a valid, non-hyphenated identifier. This reusable's ownworkflow_call.secretsinputs areapp-client-id/app-private-key— kebab-case, so no mapping can ever satisfy that invariant. A caller adopting this reusable for its issue-writing behavior is permanently blocked byrunner-policy, independent of which secret names it picks.runner-policy's write-caller-permissions invariant to allow non-identity secret mappings. Declined — that invariant is a deliberate defense-in-depth boundary (don't let a write-permission grant hide behind an opaque secret remap), and weakening it is a security-relevant call for thestandardsowner, not something to do as a side effect of unblocking one caller.Change
permission-issues: write,owner/repositoriesomitted so it defaults to the calling repository only — verified againstcreate-github-app-token's own input docs) and use it for all three issue-management steps (find/adopt, open-or-update, close).ISSUE_AUTHOR_LOGINfromgithub-actions[bot]tomelodic-standards-sync[bot]— empirically verified viagh api users/melodic-standards-sync%5Bbot%5D(login: "melodic-standards-sync[bot]",type: "Bot").issues: writepermission — no ambient-token issue write remains, so it isn't needed.github-actions[bot]is the shared identity of every ambient-token workflow in a repo, so any other workflow withissues: writecould theoretically author a decoy tracking issue. The App's identity is exclusive to this workflow's own token mint.workflow_callinputs/secrets contract is unchanged, but a caller no longer needs to grantissues: writeat all — itsrunner-policycontract entry can be secrets-only, with noallowedCallerPermissionswaiver.Test plan
actionlint .github/workflows/standards-sync-stuck-automerge-alert.yml— cleanzizmor .github/workflows/standards-sync-stuck-automerge-alert.yml— no findingsmelodic-standards-sync[bot]🤖 Generated with Claude Code
https://claude.ai/code/session_01KYvF6bWGqemS9aYFfWJRiW