ci: guard releases against version/changelog drift#405
Conversation
📝 WalkthroughWalkthroughThis PR strengthens the release process by adding pre-flight validation to ensure Cargo.toml version, CHANGELOG.md entries, and release tags stay synchronized. The mise task implementation enforces these checks before release actions; the workflow gates CI on verification; documentation describes the structured two-step release flow. ChangesRelease Validation Workflow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds release-time guards (CI + local tooling) to prevent publishing a GitHub release when the git tag version does not match the Rust workspace version and/or when the changelog is missing the corresponding release section, and documents the intended prepare-release → tag workflow.
Changes:
- Introduces a
verify-releaseGitHub Actions job to validate tag ↔Cargo.tomlversion and presence of a matchingCHANGELOG.mdsection before any build/publish runs. - Strengthens
mise run releasewith preflight checks (tag format, clean/in-syncmain, version + changelog invariants, tag non-existence) before tagging and creating the GitHub release. - Updates release documentation to reflect the new prepare-release PR flow and the additional safeguards.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
mise.toml |
Adds client-side preflight checks before tagging/releases to prevent version/changelog drift. |
DEVELOPMENT.md |
Documents the prepare-release PR → tag flow and the CI/tooling guards. |
.github/workflows/release.yml |
Adds a verify-release gate job and wires it into the build dependency chain for release events. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Adds a verify-release job to the release workflow that fails a numbered release unless the git tag matches the [workspace.package] version in Cargo.toml and CHANGELOG.md has a matching section. The build matrix now depends on it, so a mismatched tag never builds or publishes. Reworks 'mise run release' to verify the same invariants client-side (clean, in-sync main; version and changelog match the tag; tag is new) before tagging, instead of blindly tagging whatever is checked out. Documents the prepare-release-PR -> tag flow in DEVELOPMENT.md. This prevents the drift that left v2.2.1 shipping with Cargo.toml still at 2.2.0-alpha.1 and no [2.2.1] changelog entry.
19f0891 to
d22ad0c
Compare
|
Rebased onto
Re-validated: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 31-33: Validate the incoming tag value before using it: before
computing version from tag (the variable named tag and the expansion
version="${tag#v}"), add a check that tag matches the strict vX.Y.Z pattern
(e.g., starting with "v" followed by numeric major.minor.patch) and fail the job
with a clear error if it does not; only strip the leading "v" and set version
when the regex check passes to avoid using untrusted content in the shell
context.
- Line 24: Update the workflow step that uses actions/checkout@v4 to reference
an immutable commit SHA instead of the mutable tag and add persist-credentials:
false to the checkout step; specifically locate the checkout usage (the line
containing actions/checkout@v4) and replace the tag with the pinned commit SHA
(actions/checkout@<commit-sha>) and add the persist-credentials: false entry
under that step to ensure credentials are not left in .git/config.
- Around line 19-46: The verify-release job currently inherits default
permissions; add an explicit permissions block for the job named verify-release
that restricts permissions to only what’s needed to read the repository (e.g.,
set contents: read) so the workflow follows least-privilege; place the
permissions: block directly under the verify-release job definition to limit
access for the steps that check Cargo.toml and CHANGELOG.md.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: be48f311-9bf9-4f2d-8973-abc6dec28ae6
📒 Files selected for processing (3)
.github/workflows/release.ymlDEVELOPMENT.mdmise.toml
✅ Files skipped from review due to trivial changes (1)
- DEVELOPMENT.md
🚧 Files skipped from review as they are similar to previous changes (1)
- mise.toml
| verify-release: | ||
| name: Verify release metadata | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| # Only enforced for numbered releases. On push/PR/workflow_dispatch this | ||
| # job is a no-op so it can still gate the build matrix below. | ||
| - name: Check version + changelog match the release tag | ||
| if: github.event_name == 'release' | ||
| run: | | ||
| tag='${{ github.event.release.tag_name }}' | ||
| version="${tag#v}" | ||
|
|
||
| cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)" | ||
| if [ "$cargo_version" != "$version" ]; then | ||
| echo "::error::Cargo.toml workspace version ($cargo_version) does not match release tag $tag. Bump the version in a prepare-release PR before tagging." | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Fixed-string match so dots in the version aren't treated as regex wildcards. | ||
| if ! grep -qF "## [$version]" CHANGELOG.md; then | ||
| echo "::error::CHANGELOG.md has no '## [$version]' section. Add release notes in a prepare-release PR before tagging." | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "OK: tag $tag matches Cargo.toml version and CHANGELOG has a [$version] section." |
There was a problem hiding this comment.
Add explicit permissions block to follow least-privilege principle.
The verify-release job inherits default permissions, granting broader access than necessary. Limit permissions to only what's required: reading repository contents.
🔒 Proposed fix to restrict permissions
verify-release:
name: Verify release metadata
runs-on: ubuntu-latest
timeout-minutes: 5
+ permissions:
+ contents: read
steps:🧰 Tools
🪛 zizmor (1.25.2)
[warning] 24-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[warning] 19-46: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block
(excessive-permissions)
[error] 31-31: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 19 - 46, The verify-release job
currently inherits default permissions; add an explicit permissions block for
the job named verify-release that restricts permissions to only what’s needed to
read the repository (e.g., set contents: read) so the workflow follows
least-privilege; place the permissions: block directly under the verify-release
job definition to limit access for the steps that check Cargo.toml and
CHANGELOG.md.
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
Pin action to commit SHA and disable credential persistence.
Two security concerns:
- The action reference
@v4is not pinned to an immutable commit SHA, violating the security policy flagged by static analysis. Mutable tags can be updated maliciously. - Missing
persist-credentials: falsemeans the GitHub token persists in.git/configand could leak through uploaded artifacts.
🔒 Proposed fix to pin action and disable credential persistence
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - uses: actions/checkout@v4 | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 24-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml at line 24, Update the workflow step that uses
actions/checkout@v4 to reference an immutable commit SHA instead of the mutable
tag and add persist-credentials: false to the checkout step; specifically locate
the checkout usage (the line containing actions/checkout@v4) and replace the tag
with the pinned commit SHA (actions/checkout@<commit-sha>) and add the
persist-credentials: false entry under that step to ensure credentials are not
left in .git/config.
| tag='${{ github.event.release.tag_name }}' | ||
| version="${tag#v}" | ||
|
|
There was a problem hiding this comment.
Validate tag format before use to prevent potential code injection.
Directly interpolating github.event.release.tag_name into shell context without validation could allow code injection if the tag contains shell metacharacters (e.g., quotes, semicolons). While GitHub's tag restrictions reduce this risk, defense-in-depth suggests validating the expected vX.Y.Z format before using the value.
🛡️ Proposed fix to validate tag format
run: |
tag='${{ github.event.release.tag_name }}'
+ # Validate tag format before using it
+ if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
+ echo "::error::Release tag must match vX.Y.Z format (with optional pre-release suffix), got: $tag"
+ exit 1
+ fi
version="${tag#v}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| tag='${{ github.event.release.tag_name }}' | |
| version="${tag#v}" | |
| tag='${{ github.event.release.tag_name }}' | |
| # Validate tag format before using it | |
| if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then | |
| echo "::error::Release tag must match vX.Y.Z format (with optional pre-release suffix), got: $tag" | |
| exit 1 | |
| fi | |
| version="${tag#v}" | |
🧰 Tools
🪛 zizmor (1.25.2)
[error] 31-31: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 31 - 33, Validate the incoming
tag value before using it: before computing version from tag (the variable named
tag and the expansion version="${tag#v}"), add a check that tag matches the
strict vX.Y.Z pattern (e.g., starting with "v" followed by numeric
major.minor.patch) and fail the job with a clear error if it does not; only
strip the leading "v" and set version when the regex check passes to avoid using
untrusted content in the shell context.
Summary
Stops releases from going out with a stale crate version or missing changelog entry — the drift that left v2.2.1 shipping with
Cargo.tomlstill at2.2.0-alpha.1and no[2.2.1]changelog section.Two layers, plus docs:
1. CI guard (
.github/workflows/release.yml)New
verify-releasejob that, on a numbered release, fails unless:v) equals the[workspace.package]version inCargo.toml, andCHANGELOG.mdcontains a matching## [X.Y.Z]section.The
buildmatrix nowneeds: verify-release, so a mismatched tag never builds or publishes. On push/PR/workflow_dispatchthe check is a no-op (gated byif: github.event_name == 'release'), so existingmain-tag builds are unaffected.2.
mise run release(mise.toml)Now verifies the same invariants client-side before tagging:
vX.Y.Z(optional pre-release suffix),mainthat's in sync withorigin/main,Cargo.tomlversion andCHANGELOG.mdsection already describe the version,Previously it blindly tagged whatever was checked out — which is how the version bump got skipped.
3. Docs (
DEVELOPMENT.md)Documents the prepare-release PR → tag flow so the version bump + changelog entry are a reviewed step, not something remembered at tag time.
Design note
The release stays PR-prepared, not task-mutated: the version bump and changelog land in a reviewable PR (e.g. #404 for v2.2.2), and
mise run releaseonly gates and tags. This fits branch protection onmainand keeps the changelog honest between releases.Validation
mise.tomlparses (tomllib); extracted script passesbash -n; no-arg run fails fast; tag regex acceptsv2.2.2/v2.2.0-alpha.1, rejects2.2.2/v2.2/vfoo.release.ymlparses (yaml.safe_load); job graph isverify-release → build → merge.Stacks logically after #404 (which adds the v2.2.2 version bump + changelog the guard checks for).
Summary by CodeRabbit
Release Notes
Chores
Documentation