Skip to content

[COVAL-7697] Tag releases without running checked-out code under a write token - #137

Merged
callumreid merged 1 commit into
mainfrom
fix/coval-7697-release-tag-job
Oct 15, 2026
Merged

callumreid merged 1 commit into
mainfrom
fix/coval-7697-release-tag-job

Conversation

@callumreid

Copy link
Copy Markdown
Contributor

Problem

CodeQL alert #2 (actions/untrusted-checkout/critical, open on main since 2026-10-01) flags the prepare job in release-on-version-bump.yml. Triggered by workflow_run, it:

  1. checks out workflow_run.head_sha, keeping the contents: write token in .git/config;
  2. runs scripts/release_version.py from that checkout;
  3. pushes the release tag with the same token.

In practice the commit is trusted: prepare runs only after a successful push CI run on main, or on a dispatch from main. CodeQL can't see that if: guard, and the job still runs repository code next to a write token it doesn't need for that step.

Change

  • prepare is read-only. It checks out the commit with persist-credentials: false and a contents: read token, runs release_version.py, and outputs the commit SHA, tag and release_candidate. It no longer needs fetch-depth: 0, because tags are looked up through the API.

  • A new tag job holds the write token and runs no repository code. It has contents: write and no checkout. It keeps the old tag step's decisions:

    • Not a stable version: nothing to do.
    • Tag already exists: a CI-triggered run does nothing, and a manual dispatch reruns the release at the tagged commit.
    • Otherwise: it creates the annotated tag through POST /git/tags and POST /git/refs.

    Existing tags are found by exact ref, because matching-refs is a prefix search, and resolved to their commit with GET /commits/refs/tags/<tag>.

  • Least-privilege defaults. The workflow-level token drops from contents: write + issues: write to contents: read. Only tag and the reusable release call get write access, and the call passes on the contents: write + issues: write that release.yml's jobs ask for.

Release tags stay annotated, with message vX.Y.Z and tagger github-actions[bot], the same as v0.9.1. As before, a tag created with GITHUB_TOKEN does not trigger another workflow, so release.yml is still called directly.

Related

  • Alert #1 (validate in release.yml) and alert #3 (update-homebrew) are fixed in [COVAL-7631] Open a Homebrew tap pull request instead of pushing to main #136. That PR already rewrites those steps, so fixing them there avoids a merge conflict. The two PRs touch different files and can merge in either order.
  • Out of scope: alert #4 (actions/cache-poisoning/poisonable-step on the build job) and the three Rust cleartext-logging alerts.

Validation

  • actionlint (with shellcheck) is clean.
  • I ran the tag job's script locally with a stubbed gh in four cases, and each produced the same outputs as the old step:
    • not a release candidate: release=false;
    • tag already exists, CI-triggered run: release=false, with release_ref set to the tagged commit;
    • tag already exists, manual dispatch: release=true, rerunning that commit;
    • new tag: creates the tag object, then the ref, and outputs release=true with release_ref set to the CI-tested SHA.
  • I checked the lookups against the live repo with read-only calls:
    • The exact-ref filter finds v0.9.1 and returns 0 for v0.9.10. A plain matching-refs/tags/v0.9 prefix search returns 2 refs.
    • commits/refs/tags/v0.9.1 peels the annotated tag to 21eb3d8.
  • Not exercised: the two POST calls that create a real tag. They run on the next version bump. If they fail, the run fails before release.yml starts, and a manual Release on version bump dispatch from main retries cleanly.
  • CodeQL on this PR should show alert docs: update homebrew tap name #2 as fixed.

No database queries change.

🤖 Generated with Claude Code

…ite token

prepare checked out the CI-tested commit with a persisted contents: write token,
ran release_version.py from it, then pushed the tag (CodeQL alert #2). The
version is now read in a read-only job with no persisted credentials, and a
separate contents: write job creates the annotated tag through the API without
a checkout. The workflow token defaults to contents: read; only the tag job and
the release call get write access.
@callumreid
callumreid marked this pull request as ready for review October 15, 2026 13:42
@callumreid
callumreid merged commit a72e35d into main Oct 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants