ci: move coverage comment out of the reusable ci workflow#293
Merged
Conversation
release.yml -> deploy-test.yml -> ci.yml failed workflow validation: the nested `coverage` job requested `pull-requests: write`, but permissions only ever narrow down a reusable-workflow chain. release.yml grants `contents: write` + `id-token: write` at the top level, which implicitly sets every unlisted scope — including pull-requests — to `none`, and its deploy-test job narrows further to `contents: read`. Job-level `permissions:` is evaluated statically at parse time, so the `if: github.event_name == 'pull_request'` guard on the posting step did not save it: the declaration alone was enough to reject the file and fail the whole release run before any job started. Keep ci.yml at `contents: read` so it stays callable from anywhere. The coverage job now uploads the summary table and PR number as an artifact; a new workflow_run listener, coverage-comment.yml, downloads it and posts the sticky comment. That listener is the only holder of `pull-requests: write` and is never in the release call graph. Side effect: fork PRs now get a coverage comment. workflow_run runs in the base-repo context with a writable token, which the in-line step could not obtain. The listener never checks out or executes PR code — it reads the uploaded markdown and validates the PR number is numeric.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The v0.9.0 release run (29030687121) failed workflow validation before a single job started:
Root cause
Permissions only ever narrow down a reusable-workflow chain —
release.yml→deploy-test.yml→ci.yml. Walk it and watch the scope collapse:release.ymldeclares a top-levelpermissions:block listing onlycontents: writeandid-token: write. Writing an explicit block sets every unlisted scope tonone— not to its default. Sopull-requests: nonefrom the top.deploy-testjob narrows further tocontents: read,id-token: write.deploy-test.yml'scijob has nopermissions:block, so it inherits that ceiling verbatim.ci.yml'scoveragejob asks forpull-requests: write→ exceeds the ceiling → the file is rejected.The trap: job-level
permissions:is static, read at parse time. The posting step was already gated onif: github.event_name == 'pull_request'and could never have run on a tag push — but the declaration alone fails validation. It's a parse-time error, not a runtime one, so the entire release run dies before anything executes.Fix
Split the write scope out of the reusable workflow rather than propagating it down the chain. Granting
pull-requests: writeto a release run that provably never posts a comment would have worked, but it's the wrong direction for least privilege.ci.yml—coveragejob dropspull-requests: writeand the sticky-comment step. Onpull_requestevents it uploadscoverage-summary.md+ the PR number as acoverage-summaryartifact. The$GITHUB_STEP_SUMMARYoutput is unchanged, so the table still lands on the Actions run page. The workflow now holdscontents: readonly, and is safe to call from anywhere.coverage-comment.yml(new) — aworkflow_runlistener on CI that downloads the artifact and posts the sticky comment (still keyedheader: coverage, so it updates in place). Sole holder ofpull-requests: write, and never in the release call graph.docs/ci.md— pipeline diagram + Coverage reporting section updated, recording why the split exists so the comment step doesn't get folded back intoci.ymland re-break release.Side effect: fork PRs now get a coverage comment
docs/ci.mdpreviously called this out as an accepted limitation — fork PRs get a read-onlyGITHUB_TOKEN, so the in-line step was skipped.workflow_runruns in the base-repo context with a writable token, so it works there now.The usual
workflow_runprivilege-escalation path stays closed: the listener never checks out or executes PR code. It reads the uploaded markdown and the PR number, and validates the number is numeric before passing it to the comment action.Verification
ci.ymlcontains nowritescope outside a comment.npm run lintandnpm run format:checkclean.main(picks up3521cb2 ci: pin npm 11, which also touchedci.yml).Not verifiable from here: the release chain's validity can only be confirmed by GitHub on the next tag push. Note also that
workflow_runalways dispatches the default-branch copy of a listener —coverage-comment.ymlwill not fire until this merges, so this PR will not post a coverage comment of its own.🤖 Generated with Claude Code