Cut a release
Two phases. Phase 1 prepares the release and opens a PR. Then you STOP: the maintainer reviews and merges. Phase 2 tags and publishes, and can only run after the merge because the tag must point at the merge commit.
If $1 is a version (0.9.0, v0.9.0 — strip any leading v), use it. If not,
derive it and confirm before writing anything.
Phase 1 — prepare
1. Check the ground
git switch main && git pull --ff-only
git status --short
jq -r .version .claude-plugin/plugin.json
sed -n '1,40p' CHANGELOG.md
Stop and report if: the tree is dirty, main is not current, or CHANGELOG.md
has no ## [Unreleased] section with real entries under it. An empty Unreleased
section means there is nothing to release — say so rather than cutting an empty
version.
2. Pick the version
SemVer at 0.x, based on what is actually under ## [Unreleased]:
- MINOR (
0.8.0→0.9.0) if there is anything under### Added, or a### Changedentry that alters behaviour users rely on. New style preset, new provider, new env var. - PATCH (
0.8.0→0.8.1) only if every entry is under### Fixed.
Never hide a feature in a patch. State the version and the reason in one line before proceeding.
3. Edit the two files
Only these two carry a version. marketplace.json does not pin one — leave it.
CHANGELOG.md — three edits:
## [Unreleased]→## [<version>] - <today, YYYY-MM-DD>. Get today's date withdate +%F; do not guess it.- Add a comparison link ref in the block at the bottom, directly above the
previous version's line:
[<version>]: https://github.com/gvzdv/claudish-to-english/compare/v<previous>...v<version> - Repoint the Unreleased ref:
[Unreleased]: https://github.com/gvzdv/claudish-to-english/compare/v<version>...HEAD
Leave the [Unreleased] ref in place but do not re-add an empty
## [Unreleased] heading — this repo adds that heading only when there is
something to put under it.
While you are in the file, tidy the prose of the entries if they read like PR descriptions rather than changelog entries, but do not change their meaning or drop anything.
.claude-plugin/plugin.json — bump "version". Edit the string in place so
key order and formatting survive; do not round-trip the file through a JSON
serialiser.
4. Verify before committing
jq empty .claude-plugin/plugin.json && jq -r .version .claude-plugin/plugin.json
for f in *.sh; do bash -n "$f" || echo "FAIL $f"; done
Then confirm every ## [x] heading has a matching [x]: ref and there are no
orphans:
python3 - <<'PY'
import re, io
s = io.open('CHANGELOG.md', encoding='utf-8').read()
heads = re.findall(r'^## \[([^\]]+)\]', s, re.M)
refs = re.findall(r'^\[([^\]]+)\]:', s, re.M)
print("headings without a ref:", [h for h in heads if h not in refs] or "none")
print("orphaned refs:", [r for r in refs if r not in heads and r != 'Unreleased'] or "none")
PY
jq -r .version must equal the new heading. Show the full git diff — it should
touch exactly two files.
5. Branch, commit, PR
git switch -c release/<version> origin/main
git commit -am "chore: v<version> — <one-line summary of what ships>"
git push -u origin release/<version>
gh pr create --base main \
--title "chore: v<version> — <summary>" \
--body-file <a file you write>
The PR body should state why MINOR or PATCH, list the two changed files, and summarise what the release ships (drawn from the changelog section).
6. Stop
Report the PR URL and say the tag comes after the merge, since it must point at the merge commit. Do not merge the PR yourself unless the maintainer explicitly asks.
Phase 2 — after the merge
Only once the release PR is merged.
git switch main && git pull --ff-only
git log --oneline -3
Identify the merge commit (Merge pull request #N from gvzdv/release/<version>).
Tag that, not the chore: bump commit — tagging the bump leaves the release
branch's commits dangling inside compare/v<version>...HEAD next cycle.
git tag v<version> <merge-commit-sha>
git push origin v<version>
Verify the tag lands on the right tree before moving on:
git log -1 --format='%h %s' v<version>
git show v<version>:.claude-plugin/plugin.json | jq -r .version # == <version>
Then create the GitHub release, with notes taken from the CHANGELOG section rather than auto-generated — the hand-written entries are the point:
gh release create v<version> \
--title "v<version> — <summary>" \
--notes-file <the extracted CHANGELOG section>
Extract the section by slicing from ## [<version>] to the next ## [ line.
Report the tag and release URLs.
Notes
- Tags here are lightweight and sit at the release merge commit.
v0.3.0is the exception (it predates the convention and sits on a bump commit); do not "fix" it. - Tags for
v0.4.0–v0.7.1were backfilled, so those releases have tags but no GitHub release objects. That is expected; do not backfill releases unless asked. - If a tag already exists for the target version, stop and report it. Never force-move a published tag.