Bump Anvil's pin with the job token, not a provisioned secret; stop the release loop (#81) #52

open colechristensen cole.christensen@gmail.com wants to merge feat/cross-repo-grant-pin-bump into main

Depends on fangorn/anvil#245. Draft until that merges AND deploys.

Merging this early would break CI on every run in this repository: the currently deployed parser rejects an unknown top-level permissions: key. Verified directly against origin/main’s parser rather than assumed —

PARSE FAILED
["top-level: unknown key 'permissions'"]

— and against the parser in anvil#245:

PARSED OK
permissions: %{"fangorn/anvil" => %{"contents" => "write", "pull_requests" => "write"}}
step count: 7
warnings: []

What changes

bump-anvil-pin needs write access to fangorn/anvil, which an Anvil job token could not have — it is scoped to the repository that dispatched it. The stopgap was ANVIL_PIN_BUMP_TOKEN, a separately provisioned CI secret: a long-lived credential carrying a human’s full authority across every repo they can reach, with nothing recording what it was for. It was never provisioned, so the step has been failing loudly since it landed in #51.

With cross-repo grants, the top-level block asks for what it needs:

permissions:
fangorn/anvil:
contents: write
pull_requests: write

An admin of fangorn/anvil approves the request once (Settings → Cross-repo Grants), and the ordinary injected ANVIL_TOKEN carries those scopes for the life of the job. Nothing to provision, nothing to rotate, and the credential dies with the job that used it.

Asking is not receiving. The block above is a request that can only narrow what the grant allows; with no grant it yields nothing. Anyone who can open a pull request against this repo can edit that file, so it cannot be the authority — the grant on the other side is.

The refusal path

Anvil reports every cross-repo refusal identically and without a reason, deliberately, so a token cannot enumerate which repos have grants or what they cover. That makes an unapproved request indistinguishable from an ordinary auth failure at the call site. Rather than guess which it was, abort/1 appends the approval instructions whenever a failure looks like one:

fangorn/anvil refused this token's cross-repo request.
...
fangorn/anvil → Settings → Cross-repo Grants → Approve

The step still fails hard in both cases. A silent skip is the exact bug #81 exists to fix.

Also fixes a release loop

Noticed while verifying this branch, in the same steps this PR already touches.

release ends by pushing a tag. That push starts a CI run whose HEAD is still the main commit — so the “am I on main?” SHA gate passes and it releases again, tags again, and starts another run. 2026.7.6, 2026.7.7 and 2026.7.8 are three releases of the identical commit a7e0745, each minted by the run the previous tag push started. The chain stopped only because an unrelated 403 broke it, not because anything caught it.

The SHA gate cannot catch this by construction: a tag pointing at the main commit is indistinguishable from main by SHA. Only the ref tells them apart, so both release and bump-anvil-pin now refuse runs whose ANVIL_BRANCH is under refs/tags/. The guard is needed on bump-anvil-pin too, or every tag would open a second identical pin-bump PR.

ANVIL_BRANCH carries the full ref — the runner injects pipeline_run.branch, which PushConsumer sets to event.ref verbatim.

The three duplicate tags/releases already published are left alone; deleting them is a judgement call for a human.

REQ-CI-003

Updated to match. The property it asserts is unchanged — the permission is explicit, and its absence fails with an actionable message rather than skipping silently. Only the mechanism moved from a provisioned secret to a grant, so the requirement no longer mandates a secret that will not exist.

Verification

mix format --check-formatted, mix compile --warnings-as-errors, mix dialyzer (0 errors) and mix test (1082 passed, 52 excluded) all pass.

Merge order

  1. Merge and deploy fangorn/anvil#245
  2. Approve the request on fangorn/anvil → Settings → Cross-repo Grants (it appears there once this branch’s pipeline has run)
  3. Mark this ready and merge

🤖 Generated with Claude Code

Created Aug 05, 2026 at 02:42 UTC