name: release description: Use when cutting a new release of the jdwp-debugging plugin — bumping the user-facing version, writing a CHANGELOG entry, committing, tagging, and pushing. Triggers — "do a release", "ship X.Y.Z", "release the last commits", "bump the version", "tag a release".
Plugin Release
End-to-end workflow for releasing the jdwp-debugging plugin from this repo.
Never push, force-push, tag-delete-on-remote, or merge without explicit user permission. Local commits and local tags are reversible; remote pushes are not.
What gets released (vs not)
| User-facing artifact | Bump on release? |
|---|---|
.claude-plugin/marketplace.json version |
YES — both occurrences |
CHANGELOG.md new section at top |
YES |
Annotated git tag vX.Y.Z |
YES |
pom.xml <version> (Maven artifact) |
NO — internal build id only |
jdwp-mcp-server/pom.xml <version> |
NO |
.mcp.json |
NO |
README.md |
NO (no version reference there) |
The plugin version (in marketplace.json) is decoupled from the Maven version. Don't try to keep them in sync.
Pre-flight checks
Run all of these before touching anything.
Identify the last release.
git for-each-ref --sort=-taggerdate --format='%(refname:short) %(taggerdate:short) %(subject)' refs/tags/ | head -5. The most recentvX.Y.Ztag is the baseline. Sanity-check: the latest[X.Y.Z]entry at the top ofCHANGELOG.mdmust equal theversionin.claude-plugin/marketplace.jsonand equal the most recent tag. If any of those three disagree, fix that drift before bumping. (We hit this in 2.2.0 —v2.1.2tag was missing and had to be back-filled.)Enumerate unreleased commits.
git log <last-tag>..HEAD --oneline --no-merges. Read the full bodies for anything ambiguous — the feature/fix/breaking signal lives in the commit body, not the subject.Pick the SemVer bump from the commit set:
- Any
feat:(new MCP tool, new behavior) → minor (X.Y+1.0). - Only
fix:/chore:/docs:/review:→ patch (X.Y.Z+1). - Any breaking change (removed/renamed MCP tool, changed default that breaks user scripts) → major (
X+1.0.0). - When in doubt between minor and patch, ask the user. The release plays out in public — over-bumping is fine, under-bumping is awkward.
- Any
Check branch topology.
git log --all --decorate --oneline --graph -10. The release commit must land onmain. Three possible starting states:- On
mainwith clean unreleased commits ahead → simplest case, commit there. - On a feature branch that's NOT yet merged → commit on the branch, PR will carry the release in. Bump before merging so the PR review sees the version change.
- On a feature branch that's ALREADY merged via PR → you're on an orphan tip. Cherry-pick onto
main(see "Cherry-pick recovery" below). This is the trap we hit in 2.2.0.
- On
Confirm the working tree.
git status --short. Untracked junk likeFIX_*.md,core/,.factorypath, IDE files must stay out of the release commit. Stage explicitly (git add <file> <file>), nevergit add -A/git add ..
Procedure
1. Bump .claude-plugin/marketplace.json
Two "version" strings to change — top-level marketplace version AND the inner plugins[0].version. Both must move to the new X.Y.Z together.
2. Write the CHANGELOG.md entry
New section at the top, above the previous version. Format is Keep a Changelog, already established in the file:
## [X.Y.Z] — YYYY-MM-DD
### New — <one-line headline>
<2-4 sentence paragraph explaining the motivation, what was broken or
missing before, and what the change does. User-perspective, not
implementation-perspective.>
- **Bold lead-in** — body sentence explaining one bullet.
- **Another bullet** — …
### Fixed — <one-line headline>
<…>
Section headings to choose from (use the ones that fit): ### New, ### Fixed, ### Breaking, ### Output token savings, ### Docs. Mirror the past entries ([2.1.0], [2.2.0]) for tone — paragraphs first, bulleted detail second. Resolve any #N GitHub issue references in the text (resolves #4) — they're useful for cross-linking.
Date: today's date in YYYY-MM-DD. The em-dash separator — is part of the convention.
3. The release commit
git add .claude-plugin/marketplace.json CHANGELOG.md
git commit -m "chore: release X.Y.Z — <short headline>"
Use a HEREDOC for the body. Three paragraphs is the established length (see 7b95e87, 1c40a71):
- Headline feature, what it does, resolves which issue.
- Concurrency / behavior fixes that ship alongside.
- Review polish summary.
Stage only those two files. Never include untracked files or unrelated edits.
4. Annotated tag
git tag -a vX.Y.Z -m "vX.Y.Z — <same short headline as commit>"
All past release tags are annotated (not lightweight) — keep it that way. Subject convention: vX.Y.Z — short description, em-dash separator. Verify with git for-each-ref --format='%(refname:short) %(objecttype) %(subject)' refs/tags/vX.Y.Z — objecttype must read tag, not commit.
5. Push (requires explicit user permission)
git push origin main
git push origin vX.Y.Z
Confirm with the user before each push. Never --force to main.
Cherry-pick recovery (when the release commit is orphaned)
If you bumped the version on a feature branch and the PR was already merged into main before you committed, the release commit sits on an orphan tip. Recovery:
git checkout main
git merge --ff-only origin/main # catch local main up
git cherry-pick <release-commit-sha> # replays onto main, new SHA
git tag -d vX.Y.Z # if the tag was made on the orphan
git tag -a vX.Y.Z <new-sha> -m "vX.Y.Z — …"
The cherry-pick produces a new SHA. The old tag (if any) still points at the orphan commit — recreate it at the new SHA before pushing. Don't push the orphan; let the feature branch die naturally.
Back-filling a missing tag
A past release commit without a tag (we found v2.1.2 was never tagged):
git tag -a vX.Y.Z <release-commit-sha> -m "vX.Y.Z — <copy headline from commit>"
git push origin vX.Y.Z
Tag the release commit itself (the one with chore: release X.Y.Z in the subject), not the merge that pulled it in.
Red flags
marketplace.jsonandCHANGELOG.mdshow different versions after your edits → you forgot one of the twoversionstrings in JSON, or skipped the changelog.git log <last-tag>..HEADlists merge commits but no feature commits → you're tagging from the wrong baseline; find the actual last release commit.- The release commit's parent is not the current
origin/maintip → either rebase or cherry-pick before pushing. git for-each-ref refs/tags/vX.Y.Zshowsobjecttype commitnottag→ lightweight tag by accident, redo with-a.- Working tree has untracked files when you stage → audit them;
git add -Awill swallow them.
Verification after push
git fetch origin
git log HEAD..origin/main --oneline # should be empty
git log origin/main..HEAD --oneline # should be empty
git ls-remote --tags origin | grep vX.Y.Z # tag exists on remote