The release flow is one command. Read this once; then trust the script.
A release is one commit, one push, one publish, one GitHub release — in that order, atomically. Never push the version bump as a separate commit from the change that justifies the version bump.
| File | Changes on | Owner |
|---|---|---|
Cargo.toml version |
every release | release script |
Cargo.lock |
every release (auto) | cargo |
CHANGELOG.md |
every release | you (write the entry beforehand) |
README.md install line localharness = "X.Y" |
breaking minor/major only | you |
LICENSE |
never | upstream |
The release script only stages Cargo.toml + Cargo.lock +
CHANGELOG.md. Anything else that needs to ship with the version
bump — code, templates, docs, RELEASING.md changes, web bundle —
must be committed before invoking the script. Mixing the two
breaks the "one commit per release" invariant.
-
Land all the feature work as normal commits on
main. Bump thetemplates.rsversion tag in the same commit as the feature work. -
Confirm
mainis green:cargo test && cargo clippy --all-targets, pluscargo check --no-default-features --features browser-app --target wasm32-unknown-unknownif anything insrc/app/changed. -
Decide the next version per SemVer:
- patch (
0.10.0 → 0.10.1): bug fixes, internal refactors, docs, UX polish that doesn't move the public API. - minor (
0.10.x → 0.11.0): backward-compatible additions. - major (
0.x.y → 1.0.0): breaking changes. Before 1.0, breaking changes go in a minor bump per cargo convention.
- patch (
-
Add a
CHANGELOG.mdentry under a new heading (no date — the script stamps today's date in):## [0.10.1] ### Added - … ### Changed - … ### Fixed - …
The release script extracts this section verbatim into the GitHub release notes.
# Linux / macOS / git-bash
./scripts/release.sh 0.10.1
# Windows PowerShell
./scripts/release.ps1 -Version 0.10.1The script performs:
- Pre-flight — clean working tree, on
main,CHANGELOG.mdhas a## [<version>]entry,gh+cargoare authenticated. - Bump —
Cargo.tomlversion = "<version>", refreshCargo.lock. - Verify —
cargo test,cargo clippy --all-targets -D warnings,cargo packagedry-run. - Commit —
release v<version>with the two version files. - Tag — annotated
v<version>. - Push —
git push --atomic origin main v<version>(main + tag in one push). - Publish —
cargo publish. - Release —
gh release create v<version>with notes lifted fromCHANGELOG.md.
Each step exits non-zero on failure. The script never proceeds with a half-finished release.
| Failure point | Recovery |
|---|---|
| Pre-flight | Fix the issue and re-run. No state changed. |
| Bump / verify | git checkout Cargo.toml Cargo.lock and re-run. |
| Commit / tag | git reset --hard HEAD~1 && git tag -d v<version> then re-run. |
| Push | Tag may or may not be on the remote. git fetch && git push --tags --force-with-lease to reconcile. |
cargo publish |
Version is consumed permanently. Bump to <version+1> and re-run. Yank the old tag if needed: gh release delete v<version> + git push --delete origin v<version>. |
gh release |
Run gh release create v<version> --notes-file … manually. The crate is already live; only the GH release is missing. |
cargo yank --version <version> # crates.io: hide from new deps
gh release delete v<version> # remove GH release
git push --delete origin v<version> # remove tag from origin
git tag -d v<version> # remove tag locallyA yanked crate is still downloadable for anyone with <version> in
their Cargo.lock — yanking is "discourage", not "delete".