ci: tag the release from the release PR merge - #238
Merged
Conversation
Releasing took two steps: merge the version-bump PR, then remember to push
the matching vX.Y.Z tag by hand. That manual tag is the weak point — it is
the actual release trigger, it can be forgotten, mistyped or pushed from a
dirty checkout, and it sits outside the reviewed, gated PR flow.
release-tag.yml closes the gap: a version bump landing on main tags vX.Y.Z,
so review + merge is the whole release.
Security-relevant choices:
- It triggers on `push`, never `pull_request`. A push run always takes the
workflow definition from main, whereas a pull_request run uses the PR
branch's copy — which would let a PR rewrite the very file that holds the
tagging token.
- The tag is pushed with a maintainer PAT rather than GITHUB_TOKEN: a
GITHUB_TOKEN push does not trigger further workflow runs (release.yml
would never fire), tag creation is admin-only per the release-tags
ruleset, and the owner's PAT keeps github.actor on the tag push equal to
the owner, so publish-crates.yml's owner guard still holds.
- Guards: the pusher must be the repository owner, the commit must belong to
a merged PR labelled `release` (the same opt-in version-guard already
requires for a bump, so one label drives the whole release), the version
must actually have changed (`paths: Cargo.toml` also fires for dependency
edits), and an existing tag is never moved. The workflow's own
GITHUB_TOKEN stays `contents: read`; it publishes nothing itself.
Two new tests in release_security.rs pin the pull_request-free trigger and
the owner/label/environment guards; both were verified to fail when the
trigger or the guard is weakened. The decision script was also exercised
against a scratch repo with gh and ls-remote stubbed, covering a legitimate
bump, a missing label, an existing tag, a dependency-only Cargo.toml edit,
and both github.event.before fallbacks.
Needs one-time repo setup: the `release-tag` environment and its
RELEASE_TAG_TOKEN secret, documented in CONTRIBUTING.md ("Release
security").
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Matheus T. dos Santos <[email protected]>
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.
Releasing took two steps: merge the version-bump PR, then remember to push the matching
vX.Y.Ztag by hand. That manual tag is the weak point — it is the actual release trigger, it can be forgotten, mistyped or pushed from a dirty checkout, and it sits outside the reviewed, gated PR flow.release-tag.ymlcloses the gap: a version bump landing onmaincuts the tag, so review + merge is the whole release. No manual tagging step.The two constraints that shaped this
Both are non-obvious and are the reason the workflow looks the way it does:
GITHUB_TOKENpush does not trigger further workflow runs. If the tag were pushed with the built-in token,release.ymlandpublish-crates.ymlwould never fire and the automation would be pointless. So the tag is pushed with a maintainer PAT (RELEASE_TAG_TOKEN). Tag creation is also admin-only per therelease-tagsruleset, which the owner's PAT satisfies — and it keepsgithub.actoron the tag push equal to the owner, sopublish-crates.yml's owner guard needed no loosening.push, neverpull_request. For apush, GitHub always takes the workflow definition from the pushed branch (main, already behind code-owner review). Apull_requestrun uses the PR branch's copy of the definition — which would let a pull request rewrite the very file that holds the tagging token. This one is pinned by a test.Guards
Nothing here weakens the existing model; it adds gates:
github.actorguard);releaselabel — the same opt-inversion-guardalready demands before allowing a bump, so a single label now drives the entire release;paths: Cargo.tomlalso fires for dependency edits);GITHUB_TOKENstayscontents: read— it publishes nothing itself, it only creates the tag;Verification
tests/release_security.rsgrows from 6 to 8 tests, pinning thepull_request-free trigger and the owner/label/environment guards. Both new tests were verified to fail when the trigger or the guard is weakened — they are not decorative.ghandgit ls-remotestubbed, covering all six paths: legitimate bump (cut=true), missingreleaselabel, pre-existing tag, dependency-onlyCargo.tomledit, and bothgithub.event.beforefallbacks (force-push and an unknown SHA). All behave as intended.gh apilabel lookup errors,pipefailroutes it to the skip path — a failure never tags.cargo fmt --checkclean.This needs a
release-tagenvironment holding aRELEASE_TAG_TOKENsecret (a fine-grained PAT, this repo only,Contents: read and write, deployment branches limited tomain). Full steps are in CONTRIBUTING.md → Release security → One-time setup for automatic tagging. Until it exists, the tagging job fails with an explicit error instead of skipping quietly.The trade-off is stated plainly: a long-lived PAT becomes a sensitive asset in the repo. That is unavoidable given constraint (1) above; it is mitigated by minimal scope, a dedicated environment, and a rotation note. The alternative — a GitHub App token — would make
github.actorapp[bot]and force changes topublish-crates.yml's owner guard and the test protecting it: more churn on the security surface, not less.Docs updated alongside: CONTRIBUTING.md, CLAUDE.md, and the header comments of
version-guard.ymlandpublish-crates.yml, which still described the manual-tag flow.🤖 Generated with Claude Code