Prompt file imported from rjungemann/turmeric (
.claude/commands/cut-patch-release.md). Copyright stays with the author.
Cut a new patch release
Run a full patch-version release of Turmeric. Order matters: the commit must contain the bumped version + CHANGELOG + README before the tag is created, the web deploy must succeed before the tag is pushed (so a deploy failure doesn't strand a tag), and the tag push must include the bump commit so the GitHub Actions release workflow sees the right sources.
Preconditions (verify before doing anything destructive)
Run these in parallel and report findings before proceeding:
git status --porcelain-- working tree must be clean. If not, stop and ask the user to commit or stash.git rev-parse --abbrev-ref HEAD-- must bemain. If not, stop and ask the user to switch.git fetch origin mainfollowed bygit rev-list --left-right --count origin/main...HEAD-- local main must not be behind origin. If behind, stop and ask the user to pull.cat VERSION-- current version (the old version).git describe --tags --abbrev=0 --match 'v*'-- the most recent release tag. Should matchv<VERSION>; if not, surface the mismatch to the user before proceeding.git log v<OLD>..HEAD --oneline-- there must be at least one commit since the last tag. If zero, refuse to release.
If any check fails, stop and report. Do not proceed without the user explicitly overriding.
Advisory: experiment expiries (NEVER blocking)
Run ./build/tur experiments (or read EXPERIMENTS[] in
src/runtime/experiments.c) and list any row whose expires_at is at or
before the version being cut.
This is a notice, not a precondition. It NEVER blocks the release. Report
the rows and continue with the cut. The author decides separately whether to
graduate, shelve, or bump expires_at -- in this release or a later one.
expires_at is a deadline, not an earliest date, and there is no registry
check anywhere in this file's preconditions. Do not invent one: treating this
as a gate has stranded two releases.
Step 0: Check for a pending [Unreleased] section
If CHANGELOG.md has a ## [Unreleased] section, read it first. If it says
the next release must be a minor (or major) bump, stop and tell the
user to run /cut-minor-release (or /cut-major-release) instead -- a
behaviour change that is flagged there must not ship under a patch version.
Otherwise fold its bullets into the entry drafted in Step 2 and delete the
[Unreleased] heading; the section never survives a release.
Step 1: Compute the new version
Read VERSION. Parse MAJOR.MINOR.PATCH. Compute NEW = MAJOR.MINOR.(PATCH+1).
Example: 0.14.0 -> 0.14.1.
Step 2: Draft the CHANGELOG entry
Run git log v<OLD>..HEAD --pretty=format:'%h %s' to get the commit
list since the last tag.
Classify each commit into one of:
- Added -- new features, new stdlib modules, new CLI subcommands
- Changed -- behavior changes, renames, semantic shifts in existing features
- Fixed -- bug fixes (commits starting with
fix:, "fix", or referencing a bug) - Removed -- deletions of features, modules, or APIs
- Docs -- documentation-only changes (only include if non-trivial)
- Internal -- skip from changelog (CI, refactors with no user-visible effect, dependency bumps)
Skim each commit's subject line and, when ambiguous, run
git show --stat <sha> to see what files changed. Don't include every
internal commit -- the changelog audience is users, not the git log.
Format the new entry to match the existing CHANGELOG style
(docs/CHANGELOG.md for reference is CHANGELOG.md at repo root):
## [NEW] -- YYYY-MM-DD
### Added
- ...
### Changed
- ...
### Fixed
- ...
Use today's date (date +%Y-%m-%d). Omit empty subsections. Aim for
3-10 bullets total -- consolidate related commits into one bullet rather
than copying every subject line. Lead each bullet with a short bolded
title where appropriate, matching the existing entries' style.
Step 3: Draft the README "Latest release" line
README.md line 5 has:
**Latest release:** `v<OLD>` -- <one-sentence summary>.
Replace with:
**Latest release:** `v<NEW>` -- <one-sentence summary of the most significant
change in this release>.
Pick the one or two most significant items from the changelog and write one sentence. Match the existing voice (terse, action-oriented).
Step 4: Confirm with the user
Show the user:
- The OLD -> NEW version transition
- The full CHANGELOG entry you drafted
- The new README "Latest release" line
- The list of commits that informed the changelog
Use AskUserQuestion with options:
- Proceed: continue with steps 5-9 as drafted
- Edit the changelog: ask the user what to change, then re-show
- Edit the README line: ask the user for the replacement sentence
- Cancel: stop without making any changes
Do not proceed past this step without explicit confirmation.
Step 5: Apply file changes (no git operations yet)
In parallel:
- Write
NEWtoVERSION(no trailing newline beyond the existing format). 1b. WriteNEWtostdlib/VERSIONtoo.turreads this stamp to tell its own stdlib from another release's; a stale one makes a mismatched stdlib look like a match AND makes the correct one warn.ctest -R tur_stdlib_version_stampfails if the two files disagree. - Edit
src/web/wasm_glue.h-- update theTURMERIC_VERSION "<OLD>"define toTURMERIC_VERSION "<NEW>". 2b. Editweb/public/sw.js-- update theCACHE_VERSION = 'tur-try-v1-<OLD>'literal to<NEW>. Vite rewrites this token at build time, so the deployed worker is correct either way, but the in-tree literal is the dev/no-build fallback and silently drifts a release behind if you skip it. - Edit
CHANGELOG.md-- insert the new entry immediately after the# Changelog\n\nAll notable changes...\nheader and before the existing## [<OLD>]entry. Keep one blank line between entries. - Edit
README.mdline 5 -- replace the "Latest release" line.
After applying, run git diff --stat and show the user what changed.
Do not commit yet.
Step 6: Commit locally
git add VERSION stdlib/VERSION src/web/wasm_glue.h web/public/sw.js CHANGELOG.md README.md
git commit -m "$(cat <<'EOF'
chore: release v<NEW>
<one-paragraph summary copied from the README "Latest release" sentence
or the changelog's most significant items>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
Then create the annotated tag pointing at this new commit:
git tag -a "v<NEW>" -m "Release v<NEW>"
The tag is annotated, not signed (-a, not -s). That is a recorded
decision, not an oversight -- C-4 in
docs/upcoming/security-audit-plan.md:
- What a consumer downloads is a release asset, and those carry
build provenance
as of WP7 -- signed through Sigstore with a short-lived certificate minted
from the release job's OIDC token, binding each asset to the workflow, repo,
commit and run that built it.
gh attestation verify <asset> --repo rjungemann/turmericchecks it. That is the signature that protects users, and it needs no key anyone has to hold or rotate. - A signed tag protects something narrower: it proves who cut the release,
to someone reading the git history. It needs a long-lived GPG or SSH key on
the release machine, and a key that is lost, leaked, or simply not present
turns every future
cut-*-releaseinto a hard failure at this step.
If you do want signed tags, set up a signing key first and verify it works before changing anything here:
git config --global user.signingkey <key-id> # or a path, for SSH signing
git config --global gpg.format ssh # SSH signing only
echo test | git tag -s -m test tur-signing-probe && git tag -v tur-signing-probe
git tag -d tur-signing-probe
Only once that probe passes, change the -a above to -s. Do not switch it
speculatively: an unsigned release is recoverable, a cut that dies partway
through is the awkward state this file's step ordering exists to avoid.
The tag exists locally only; nothing is pushed yet. If the web deploy in step 7 fails, you can delete the local tag and try again without having published a broken release.
Step 7: Build and deploy web
just deploy-web
This runs just wasm (which runs just docs), then just web-deps,
then npm run build, then wrangler deploy ... to push the web app
to Cloudflare. The user must already be authenticated with wrangler.
This regenerates web/public/turmeric.{js,wasm} and web/public/doc-names.json.
They are gitignored build outputs -- do NOT commit them. git status stays
clean through this step; if it does not, something else changed and is worth
looking at. There is no follow-up "regenerate web artifacts" commit any more:
that habit is what left every release tag carrying the previous release's
binary, and (until v0.32.3) a doc-name index missing the release's own new
stdlib symbols.
If just deploy-web fails:
- Report the failure to the user.
- Run
git tag -d v<NEW>to remove the local tag. - Do NOT delete the commit -- the user can amend or fix forward.
- Stop. Do not proceed to step 8.
Step 8: Push commit and tag
Only after a successful deploy:
git push origin main
git push origin "v<NEW>"
The tag push triggers .github/workflows/release.yml, which builds
the three platform binaries (linux-x86_64, linux-aarch64, macos-arm64),
packages the rendered documentation as turmeric-docs-v<NEW>.tar.gz
(OD4 -- what tur docs --open reads once unpacked), and publishes the
GitHub Release. The docs job runs from the generators in tools/; it
does not rewrite stdlib/docstrings.tur, so it cannot dirty the tree.
Step 9: Verify
Wait briefly and then check:
gh run list --workflow=release.yml --limit 1
Report the run ID and status to the user. Optionally watch the run
with gh run watch <id> if the user wants real-time progress, or
just tell them to follow it on the Actions page. Do not block
waiting for the release workflow to finish -- it takes 1-2 minutes
per matrix leg and the user can check on it themselves.
End by reporting:
- The new version
- The commit SHA of the bump commit
- The Release workflow run URL
- The Cloudflare deploy URL or "deployed" confirmation
- A reminder that the release page will populate with tarballs once the workflow finishes (link to releases page)
Things to refuse
- Refuse to bypass any precondition without explicit user override.
- Never refuse or delay a release because an experiment's
expires_atis at or past the version being cut. That is advisory (see the Advisory section above); surface it and proceed. - Refuse to push the tag before the deploy succeeds.
- Refuse to skip the changelog/README updates -- they're load-bearing for users discovering the release.
- Refuse to use
git push --forcefor any step here. - Refuse to amend a commit that has already been pushed.
