Skip to content

ci: guard releases against version/changelog drift#405

Merged
coderdan merged 1 commit into
mainfrom
dan/release-guard
Jun 1, 2026
Merged

ci: guard releases against version/changelog drift#405
coderdan merged 1 commit into
mainfrom
dan/release-guard

Conversation

@coderdan
Copy link
Copy Markdown
Contributor

@coderdan coderdan commented Jun 1, 2026

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.toml still at 2.2.0-alpha.1 and no [2.2.1] changelog section.

Two layers, plus docs:

1. CI guard (.github/workflows/release.yml)

New verify-release job that, on a numbered release, fails unless:

  • the tag (minus v) equals the [workspace.package] version in Cargo.toml, and
  • CHANGELOG.md contains a matching ## [X.Y.Z] section.

The build matrix now needs: verify-release, so a mismatched tag never builds or publishes. On push/PR/workflow_dispatch the check is a no-op (gated by if: github.event_name == 'release'), so existing main-tag builds are unaffected.

2. mise run release (mise.toml)

Now verifies the same invariants client-side before tagging:

  • tag matches vX.Y.Z (optional pre-release suffix),
  • on a clean main that's in sync with origin/main,
  • Cargo.toml version and CHANGELOG.md section already describe the version,
  • tag doesn't already exist.

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 release only gates and tags. This fits branch protection on main and keeps the changelog honest between releases.

Validation

  • mise.toml parses (tomllib); extracted script passes bash -n; no-arg run fails fast; tag regex accepts v2.2.2/v2.2.0-alpha.1, rejects 2.2.2/v2.2/vfoo.
  • release.yml parses (yaml.safe_load); job graph is verify-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

    • Added a release validation step that gates builds during releases and enforces matching release tag, workspace version, and corresponding changelog entry
    • Tightened the release task with pre-flight checks: version format, main-branch requirement, clean working tree, local/remote sync, and existing-tag detection
  • Documentation

    • Updated releasing docs to a two-step process with version placeholders and clearer release instructions

Copilot AI review requested due to automatic review settings June 1, 2026 04:03
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Release Validation Workflow

Layer / File(s) Summary
Release task pre-flight validations
mise.toml
The tasks.release script expanded with checks for version format, main branch, clean working tree, synchronized origin/main, Cargo.toml workspace version match, CHANGELOG.md ## [<version>] presence, and git tag existence, then prints verified message before tagging/pushing and creating the GitHub release.
CI workflow release verification and gating
.github/workflows/release.yml
New verify-release job validates the release tag against Cargo.toml workspace version and enforces a corresponding CHANGELOG.md section; the build job now depends on verify-release.
Release process documentation
DEVELOPMENT.md
Rewrote the Releasing section to a two-step flow: prepare-release PR (bump Cargo.toml and add changelog section) then cut the release with mise run release vX.Y.Z; manual release instructions updated to use placeholders and expanded steps.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • cipherstash/proxy#365: Updates Cargo.toml and CHANGELOG.md to a release section that the new checks validate.
  • cipherstash/proxy#404: Adjusts Cargo.toml/CHANGELOG.md to match release format required by the preflight checks.

Suggested reviewers

  • tobyhede

Poem

🐰 I hopped through tags and changelog lines,
Checked versions, branches, spotless signs,
A verified hop, then push and cheer,
Releases safe — the carrots near!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'ci: guard releases against version/changelog drift' accurately and specifically describes the main change—adding validation to prevent releases with mismatched versions or missing changelog entries.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dan/release-guard

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-release GitHub Actions job to validate tag ↔ Cargo.toml version and presence of a matching CHANGELOG.md section before any build/publish runs.
  • Strengthens mise run release with preflight checks (tag format, clean/in-sync main, 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.

Comment thread mise.toml Outdated
Comment thread mise.toml Outdated
Comment thread .github/workflows/release.yml Outdated
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.
@coderdan coderdan force-pushed the dan/release-guard branch from 19f0891 to d22ad0c Compare June 1, 2026 04:25
@coderdan
Copy link
Copy Markdown
Contributor Author

coderdan commented Jun 1, 2026

Rebased onto main (now includes #404) and addressed the Copilot review:

  • Regex wildcard in changelog match (mise.toml + release.yml): switched grep -q "^## \[$VER\]"grep -qF "## [$VER]" so dots in the version are literal. Verified 2.2.2 no longer false-matches ## [2x2x2].
  • git rev-parse "$VERSION" too permissive (mise.toml): replaced with an explicit local check (git rev-parse -q --verify "refs/tags/$VERSION") plus a remote check (git ls-remote --exit-code --tags origin "refs/tags/$VERSION"), so it only blocks on an actual existing tag and also catches remote-only tags.

Re-validated: mise.toml parses (tomllib), release task passes bash -n; release.yml parses and the job graph is still verify-release → build → merge.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 19f0891 and d22ad0c.

📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • DEVELOPMENT.md
  • mise.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

Comment on lines +19 to +46
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."
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin action to commit SHA and disable credential persistence.

Two security concerns:

  1. The action reference @v4 is not pinned to an immutable commit SHA, violating the security policy flagged by static analysis. Mutable tags can be updated maliciously.
  2. Missing persist-credentials: false means the GitHub token persists in .git/config and 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.

Suggested change
- 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.

Comment on lines +31 to +33
tag='${{ github.event.release.tag_name }}'
version="${tag#v}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

@coderdan coderdan merged commit bdeead3 into main Jun 1, 2026
10 checks passed
@coderdan coderdan deleted the dan/release-guard branch June 1, 2026 04:47
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.

3 participants