Skip to content

Commit bdeead3

Browse files
authored
Merge pull request #405 from cipherstash/dan/release-guard
2 parents d36bea7 + d22ad0c commit bdeead3

3 files changed

Lines changed: 107 additions & 11 deletions

File tree

.github/workflows/release.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,38 @@ env:
1616
REGISTRY_IMAGE: cipherstash/proxy
1717

1818
jobs:
19+
verify-release:
20+
name: Verify release metadata
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 5
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
# Only enforced for numbered releases. On push/PR/workflow_dispatch this
27+
# job is a no-op so it can still gate the build matrix below.
28+
- name: Check version + changelog match the release tag
29+
if: github.event_name == 'release'
30+
run: |
31+
tag='${{ github.event.release.tag_name }}'
32+
version="${tag#v}"
33+
34+
cargo_version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
35+
if [ "$cargo_version" != "$version" ]; then
36+
echo "::error::Cargo.toml workspace version ($cargo_version) does not match release tag $tag. Bump the version in a prepare-release PR before tagging."
37+
exit 1
38+
fi
39+
40+
# Fixed-string match so dots in the version aren't treated as regex wildcards.
41+
if ! grep -qF "## [$version]" CHANGELOG.md; then
42+
echo "::error::CHANGELOG.md has no '## [$version]' section. Add release notes in a prepare-release PR before tagging."
43+
exit 1
44+
fi
45+
46+
echo "OK: tag $tag matches Cargo.toml version and CHANGELOG has a [$version] section."
47+
1948
build:
2049
name: Build binaries + Docker images
50+
needs: verify-release
2151
strategy:
2252
fail-fast: false
2353
matrix:

DEVELOPMENT.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -735,29 +735,51 @@ Note: not all errors do this at the moment, and we will change over time.
735735

736736
Releases are published via GitHub Actions when a GitHub release is created.
737737

738-
### Using mise (recommended)
738+
A release happens in two steps: first a **prepare-release PR** that records the
739+
version and notes, then **tagging** that merged commit.
740+
741+
### 1. Prepare-release PR
742+
743+
Open a PR against `main` that:
744+
745+
1. Bumps `version` under `[workspace.package]` in the root `Cargo.toml` (and runs
746+
`cargo update --workspace` so `Cargo.lock` matches).
747+
2. Adds a `## [X.Y.Z]` section to `CHANGELOG.md` describing the user-facing changes.
748+
749+
Both are required: the release tooling (and the release workflow) will refuse to
750+
publish a tag whose version isn't reflected in `Cargo.toml` and `CHANGELOG.md`.
751+
752+
### 2. Cut the release (recommended)
753+
754+
Once the prepare-release PR is merged, from an up-to-date `main`:
739755

740756
```bash
741-
mise run release v2.1.9
757+
mise run release vX.Y.Z
742758
```
743759

744-
This will:
760+
This verifies you're on a clean, in-sync `main`, that `Cargo.toml` and
761+
`CHANGELOG.md` already describe `X.Y.Z`, and that the tag doesn't already exist.
762+
If those checks pass it will:
745763
1. Create a git tag for the version
746764
2. Push the tag to origin
747765
3. Create a GitHub release with auto-generated notes
748766
4. Trigger the release workflow which builds and publishes Docker images
749767

768+
The release workflow re-runs the same version/changelog check (the
769+
`verify-release` job) before building, so a mismatched tag fails fast without
770+
publishing anything.
771+
750772
### Manual release
751773

752-
If you need more control over the release process:
774+
If you need more control, the steps the task automates are:
753775

754776
```bash
755-
# Create and push the tag
756-
git tag v2.1.9
757-
git push origin v2.1.9
777+
# Create and push the tag (from the merged prepare-release commit on main)
778+
git tag vX.Y.Z
779+
git push origin vX.Y.Z
758780

759781
# Create the GitHub release
760-
gh release create v2.1.9 --generate-notes
782+
gh release create vX.Y.Z --generate-notes
761783
```
762784

763785
### Re-releasing a version

mise.toml

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -700,15 +700,59 @@ mise --env tls run proxy:down
700700
"""
701701

702702
[tasks.release]
703-
description = "Create a GitHub release"
703+
description = "Create a GitHub release (run after the prepare-release PR is merged)"
704704
run = """
705705
#!/usr/bin/env bash
706706
set -euo pipefail
707707
708-
VERSION="${1:?Version required, e.g., mise run release v2.1.9}"
708+
VERSION="${1:?Version required, e.g., mise run release v2.2.2}"
709709
710-
echo "Creating release $VERSION..."
710+
# Tag must be vMAJOR.MINOR.PATCH with an optional pre-release suffix.
711+
if [[ ! "$VERSION" =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
712+
echo "error: version must look like v2.2.2 (got '$VERSION')" >&2
713+
exit 1
714+
fi
715+
BARE="${VERSION#v}"
716+
717+
# Releases are cut from a clean, up-to-date main.
718+
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
719+
if [ "$BRANCH" != "main" ]; then
720+
echo "error: releases must be cut from main (currently on '$BRANCH')" >&2
721+
exit 1
722+
fi
723+
if [ -n "$(git status --porcelain)" ]; then
724+
echo "error: working tree is not clean; commit or stash changes first" >&2
725+
exit 1
726+
fi
727+
git fetch origin main --quiet
728+
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
729+
echo "error: local main is not in sync with origin/main; pull first" >&2
730+
exit 1
731+
fi
732+
733+
# The prepare-release PR must have bumped the version and added release notes.
734+
CARGO_VERSION="$(sed -n 's/^version = "\\(.*\\)"/\\1/p' Cargo.toml | head -1)"
735+
if [ "$CARGO_VERSION" != "$BARE" ]; then
736+
echo "error: Cargo.toml workspace version ($CARGO_VERSION) does not match $VERSION" >&2
737+
echo " bump the version in a prepare-release PR and merge it before releasing" >&2
738+
exit 1
739+
fi
740+
# Fixed-string match so dots in the version aren't treated as regex wildcards.
741+
if ! grep -qF "## [$BARE]" CHANGELOG.md; then
742+
echo "error: CHANGELOG.md has no '## [$BARE]' section" >&2
743+
echo " add release notes in a prepare-release PR and merge it before releasing" >&2
744+
exit 1
745+
fi
746+
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
747+
echo "error: tag $VERSION already exists locally" >&2
748+
exit 1
749+
fi
750+
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
751+
echo "error: tag $VERSION already exists on origin" >&2
752+
exit 1
753+
fi
711754
755+
echo "Releasing $VERSION (verified version + changelog)..."
712756
git tag "$VERSION"
713757
git push origin "$VERSION"
714758
gh release create "$VERSION" --generate-notes

0 commit comments

Comments
 (0)