Releasing pgevolve
This runbook applies the Constitution §9
release discipline + the CLAUDE.md §11 "never cargo publish until CI is green" rule (added 2026-05-28 after the v0.3.8
disaster).
Canonical executable form
For a normal release, run:
scripts/release.sh X.Y.Z
The script walks every step below, gates on the pre-flight verify, waits for CI green on both the push and the tag, and prompts before each irreversible action (publish, push, tag). If you bail at any prompt, nothing irrecoverable has happened.
The prose runbook below documents what the script is doing, for when something goes wrong and you need to step through manually.
Pre-flight
Before starting the release commit, the following must be true:
- All open PRs targeting the release have merged.
-
cargo test --workspace --all-targetsis green locally. -
cargo clippy --workspace --all-targets -- -D warningsis green. -
cargo fmt --all -- --checkis clean. -
cargo doc --workspace --no-depsbuilds with zero warnings (run withRUSTDOCFLAGS=-D warnings). -
cargo deny checkpasses. Install if missing:cargo install cargo-deny. -
The
[Unreleased]section ofCHANGELOG.mdaccurately describes everything in the release. -
The previous release tag's signature verifies (
git verify-tag <prev-tag>) — proves your signing key is configured.
Release commit
-
Update versions — three locations must agree:
Cargo.toml→[workspace.package].versioncrates/pgevolve-core-macros/Cargo.toml→[package].version(not workspace-inherited because it's a proc-macro crate with its own version literal)- The next CHANGELOG section header
-
Date-stamp the CHANGELOG entry:
## [X.Y.Z] — YYYY-MM-DDThe CI
changelogjob (in.github/workflows/ci.yml) verifies that theCargo.tomlversion has a matching## [X.Y.Z] — DATEline. -
Rebuild so
Cargo.lockpicks up the new version:cargo build --workspace -
Commit:
git add Cargo.toml Cargo.lock CHANGELOG.md crates/pgevolve-core-macros/Cargo.toml git commit -m "release: vX.Y.Z"
Push main + WAIT FOR CI GREEN
Push the release commit:
git push origin main
Then wait for the per-push CI run to finish green across all 5 PG majors. Per CLAUDE.md §11, this is non-negotiable — today's v0.3.8 disaster happened because the maintainer published immediately after the tag push while CI was still running. CI then failed on PG 15 + 16 (broken ICU SQL); the buggy v0.3.8 was already on crates.io.
COMMIT=$(git rev-parse HEAD)
RUN_ID=$(gh run list --branch main --commit "$COMMIT" --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$RUN_ID" --exit-status
--exit-status makes the command return non-zero if any job fails;
do not proceed past this step until it returns 0.
Tag
Tags must be signed. Per Constitution §9, an unsigned release tag is not a valid release.
git tag -s vX.Y.Z -m "pgevolve vX.Y.Z
<short release summary, 2-3 lines>"
Verify locally:
git verify-tag vX.Y.Z
If git tag -s complains, your signing key isn't configured. See:
git config user.signingkey <KEY-ID>git config gpg.format ssh(for SSH signing — recommended in 2026)git config gpg.ssh.allowedSignersFile <path>(for verification)
The first time you sign, also enable signing-by-default for safety:
git config commit.gpgsign true
git config tag.gpgsign true
Push tag
git push origin vX.Y.Z
The tag push does not trigger CI by itself in the current workflow setup — CI already ran on the underlying commit when you pushed main above. If for some reason the tag is on a different commit than main's HEAD, wait for CI green on that commit too before publishing.
Publish to crates.io
When the release is ready for crates.io, publish in dependency order:
cargo publish -p pgevolve-core
# Wait ~30 seconds for the index to sync, then:
cargo publish -p pgevolve
pgevolve-core-macros is a proc-macro crate that's only published when
its own version actually changes (it has its own [package].version
literal, not workspace-inherited). In practice that's rare: the macros
crate has stayed at 0.2.1 since v0.2.x while the rest of the workspace
has bumped through v0.3.x. Only publish it when the literal version in
crates/pgevolve-core-macros/Cargo.toml changed in the release commit:
# Only if pgevolve-core-macros version was bumped this release:
cargo publish -p pgevolve-core-macros
# Then wait ~30s for the index, then publish pgevolve-core, then pgevolve.
scripts/release.sh deliberately omits the macros publish — running it
after a macros bump means manually publishing macros first, before
re-running the script's publish step. (If macros bumps become more
frequent, the script should grow a conditional check.)
pgevolve-conformance, pgevolve-testkit, and xtask are all
publish = false and stay local.
For pre-publish sanity:
cargo publish --dry-run -p pgevolve-core
cargo publish --dry-run -p pgevolve
# Add macros to the dry-run set if its version was bumped this release.
Yank a prior version (if shipping a fix)
If the version you just published replaces a buggy prior version (v0.3.9 replacing the broken v0.3.8 is the canonical example), yank the prior so cargo prefers the new version:
cargo yank --version <prior> pgevolve-core
cargo yank --version <prior> pgevolve
The CHANGELOG entry for the fix release should call out the yank
explicitly in a ### Yanked section. Yanking does not remove the
prior version from crates.io — cargo can still resolve it if pinned —
but it stops new installs from picking it up.
Post-release
- Push the tag (already done above; this is the reminder bullet).
- Verify the badge updates: README's
[![crates.io]and[![Soak]badges refresh within a few minutes. - Open a new
[Unreleased]section at the top ofCHANGELOG.mdso future commits have a place to land. Single line:## [Unreleased]. Stays empty until the next release lands work in it. - Optionally bump the workspace version to
X.(Y+1).0-devto make accidental crates.io uploads of a staleX.Y.Zversion impossible. (Cargo rejects publishes with-devpre-release tags by default without--allow-dirty-style overrides.) - Create a GitHub release from the new tag with the CHANGELOG section as the body. Surfaces the release in GitHub's release feed + RSS + API.
- If this release closes a v1.0-checklist row (per
v1.md§4), flip the row's status inspec/objects.mdand remove it fromspec/roadmap.md's Active matrix in a follow-up docs commit.
Historical notes
The v0.1.0 (commit adb0177) and v0.2.0 (commit 3087a5b) tags
predate the Constitution §9 "release tags are signed" mandate and
are annotated-but-unsigned. The 2026-05-21 constitution audit flagged
them; the maintainer decided NOT to re-sign retroactively because
rewriting historical tags would break consumers who reference them
(e.g., Cargo.lock git deps). Every tag from v0.2.1 onward is signed.
Future audits: for t in $(git tag); do git verify-tag "$t" 2>&1 | head -1; done should show every tag from v0.2.1 forward returning
Good "git" signature.