Release
Cut a release — run the structural test suite, bump the version on develop, open a PR into main, merge it, push the v<version> tag, create the GitHub Release, and back-merge main into developFrom its SKILL.md
npx -y skills add zhaow-de/llm-wiki --skill releaseAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
13.6 KB, as published. Nobody here has run it
Cuts a release PR from develop to main, then pushes the v<version> tag and creates the GitHub Release directly from this skill. After the release is published, the skill back-merges main into develop to keep them in lock-step.
This is a Claude Code plugin. The version lives in five places kept in lock-step by commitizen's version_files: .cz.toml, pyproject.toml, the README Version badge, plugin/.claude-plugin/plugin.json, and .claude-plugin/marketplace.json.
Context
- Current branch: !
git branch --show-current - Current version: !
cz version --project - Prerequisites: !
which cz && which gh && which python3 && which uv && echo "all found" || echo "MISSING tools"
Instructions
-
Verify prerequisites from the context above. If not on
develop, ask the user to switch branches first. If any tool is missing (cz,gh,python3,uv), report and stop.czis installed withbrew install commitizen. Confirmgh auth statussucceeds — if it returns 401, ask the user to rungh auth refresh -h github.comfirst. -
Run the structural test suite as a release gate — these are the deterministic, no-LLM checks. All must pass before bumping:
./tests/test-plugin-validate.sh ./tests/test-structure.sh ./tests/test-local-cli-doctor.shIf any fails, stop and report — do not release. For releases that change command behavior, also run the deeper editorial
/pkb:doctorand a manual/pkb statussmoke check in a fresh Claude Code session, and exercise the changed/pkb:*subcommand before continuing. -
Update develop and verify it is synced with main:
git pull origin develop git fetch origin mainCheck for drift — if
mainhas commits not ondevelop, a release or hotfix landed onmainwithout being back-merged. Releasing now would re-introduce conflicts (version files,CHANGELOG.md, files modified-on-main but deleted-on-develop).git log develop..origin/main --onelineIf this shows any commits, drift exists. Use
AskUserQuestionto ask whether to resolve it now by mergingmainintodevelop, or to abort:- Show the drifting commits (so the user can see what's about to come in).
- Options: "Yes, back-merge main → develop now" / "Abort, I'll handle it manually".
If the user chooses to back-merge:
git merge origin/main --no-edit- If the merge succeeds cleanly, push
git push origin develop, then continue to step 4. - If the merge fails with conflicts, do not push. Investigate each conflicted file, propose a resolution to the user (typical patterns: version files → take whichever is newer / about-to-be-bumped;
CHANGELOG.md→ keep both entries chronologically; modified-on-main but deleted-on-develop → confirm the fix exists in the replacement code, then keep the deletion), apply it, commit the merge, and push. Only then continue.
If the user chooses to abort: stop and report — do not proceed.
-
Create the release branch:
git checkout -b "release/$(date +%Y%m%d-%H%M%S)" -
Bump the version files and generate the raw changelog —
--files-only, so nothing is committed or tagged yet:cz bump --yes --changelog --files-onlyThis updates every
version_filesentry (pyproject.toml, the READMEVersionbadge,plugin/.claude-plugin/plugin.json, and.claude-plugin/marketplace.json), bumps.cz.toml, and writes the raw commit list toCHANGELOG.md. With--files-onlyit does not commit or tag — the skill makes one commit and creates thev<version>tag in step 10, after the changelog is rewritten (step 9) anduv.lockis refreshed (step 6).Prerequisite: commitizen must be configured — this repo's
.cz.tomllists all fiveversion_files. Set it up before running/releaseif missing. -
Refresh
uv.lockto record the new version:uv lockcz bumpdoes not touchuv.lock; without this step the lockfile would silently drift behindpyproject.toml. The change is uncommitted at this point; step 10 commits it together with the version bump and changelog. -
Verify every
version_filesentry was actually bumped:cz bumpsilently skips aversion_filesentry whose line no longer contains the previous version string (drift from a past bad bump). A silent skip would leave one of the five files behind.NEW_VERSION=$(cz version --project) python3 - "$NEW_VERSION" <<'PY' import json, pathlib, re, sys new = sys.argv[1] errs = [] pp = pathlib.Path("pyproject.toml").read_text() m = re.search(r'^version\s*=\s*"([^"]+)"', pp, re.M) if not (m and m.group(1) == new): errs.append(f"pyproject.toml: {m and m.group(1)}") rd = pathlib.Path("README.md").read_text() m = re.search(r'badge/version-v([0-9]+\.[0-9]+\.[0-9]+)', rd) if not (m and m.group(1) == new): errs.append(f"README badge: {m and m.group(1)}") pj = json.loads(pathlib.Path("plugin/.claude-plugin/plugin.json").read_text()) if pj.get("version") != new: errs.append(f"plugin.json: {pj.get('version')}") mp = json.loads(pathlib.Path(".claude-plugin/marketplace.json").read_text()) if mp.get("version") != new: errs.append(f"marketplace.json top: {mp.get('version')}") for p in mp.get("plugins", []): if p.get("version") != new: errs.append(f"marketplace.json plugin {p.get('name')}: {p.get('version')}") if errs: print("ERROR: cz bump did not bump every version file to", new) for e in errs: print(" -", e) print("Stop and investigate — do NOT push. Likely cause: a file's version drifted from .cz.toml in a past release, so cz's find-and-replace cannot locate the old value. Fix the stale line by hand, re-run cz bump, or hand-edit to the new version.") sys.exit(1) print("All version files at", new) PYIf this fails, stop and report — do not proceed.
-
Fetch PR data for the changelog:
python3 ${CLAUDE_SKILL_DIR}/scripts/fetch-pr-data.py ${CLAUDE_SESSION_ID} -
Generate the user-friendly changelog:
Read the PR data from
/tmp/release_pr_data_${CLAUDE_SESSION_ID}.jsonand follow the format and guidelines in changelog-format.md. Replace the raw commit listcz bumpwrote intoCHANGELOG.mdwith the user-friendly section for the new version at the top, and preserve all previous version sections below it.Keep that new-version section text — step 17 passes the same content to
gh release create --notesas the Release description, so it never needs to live in the repo as a separate file. -
Commit the release and create the tag:
VERSION=$(cz version --project) git add .cz.toml pyproject.toml README.md CHANGELOG.md uv.lock plugin/.claude-plugin/plugin.json .claude-plugin/marketplace.json git commit -m "chore(release): bump version to v$VERSION" git tag "v$VERSION"One commit carries the version bump, changelog, and refreshed lockfile; the
v<version>tag points at it. RememberVERSIONfor the remaining steps. -
Push the release branch:
git push origin "$(git branch --show-current)" -
Create the PR with
gh pr createtargetingmain, using the description template from pr-description.md (fill in{version}withVERSION). Include flags--base main,--title "Release v<VERSION>",--assignee @me, and pass the filled template via--body-file(or--body). -
Store the PR number and URL from the output of step 12. The PR URL ends in the number (e.g.
.../pull/63means PR number63). -
Return to develop:
git checkout develop -
Auto-merge the release PR with a merge commit (preserving the tagged bump commit on
main). The skill does not pause to ask the user — releases run end-to-end. Stop only if something is genuinely worth attention: the PR has conflicts, or it was closed without merging. Otherwise poll every 30 seconds and merge as soon as GitHub reports the PR mergeable and not blocked by branch protection:PR_NUMBER=<the PR number from step 13> while true; do pr_state=$(gh pr view "$PR_NUMBER" --json state -q .state) pr_mergeable=$(gh pr view "$PR_NUMBER" --json mergeable -q .mergeable) pr_state_status=$(gh pr view "$PR_NUMBER" --json mergeStateStatus -q .mergeStateStatus) if [ "$pr_state" = "MERGED" ]; then echo "PR merged!" break elif [ "$pr_state" = "CLOSED" ]; then echo "PR was closed without merging — stop" exit 1 elif [ "$pr_mergeable" = "CONFLICTING" ]; then echo "PR has conflicts — stop, do not merge" exit 1 elif [ "$pr_mergeable" = "MERGEABLE" ] && [ "$pr_state_status" != "BLOCKED" ]; then gh pr merge "$PR_NUMBER" --merge --delete-branch && break fi echo "Waiting for PR to be ready... (state: $pr_state, mergeable: $pr_mergeable, status: $pr_state_status)" sleep 30 done -
Push the release tag so it points at the bump commit now on
main. The tag was created in step 10 and persists across the branch switch.git push origin "v<VERSION>" -
Create the GitHub Release directly, passing the new version's changelog section (from step 9, also at the top of
CHANGELOG.md) inline as the description:gh release create "v<VERSION>" --title "v<VERSION>" --notes "<new version's CHANGELOG.md section>" --verify-tag--verify-tagaborts if the tag was not pushed in step 16. The command prints the Release URL — keep it for the final report. -
Back-merge
main→developvia a PR. This repo'sdevelopis protected against direct pushes, so the back-merge cannot be a plaingit push. Cut a dedicatedchore/back-merge-v<VERSION>branch offorigin/main, push it, open the PR, then auto-merge with the same poll-then-merge loop as step 15:git fetch origin main BACKMERGE_BRANCH="chore/back-merge-v<VERSION>" git checkout -b "$BACKMERGE_BRANCH" origin/main git push -u origin "$BACKMERGE_BRANCH" BACKMERGE_URL=$(gh pr create --base develop --head "$BACKMERGE_BRANCH" \ --title "chore(config): back-merge v<VERSION> into develop" \ --body "Back-merge of the \`v<VERSION>\` release commit from \`main\` into \`develop\` so the two branches stay in lock-step per \`.claude/rules/branch-workflow.md\`. Auto-opened by the \`/release\` skill.") BACKMERGE_NUMBER=$(echo "$BACKMERGE_URL" | sed -E 's|.*/pull/([0-9]+)|\1|') while true; do pr_state=$(gh pr view "$BACKMERGE_NUMBER" --json state -q .state) pr_mergeable=$(gh pr view "$BACKMERGE_NUMBER" --json mergeable -q .mergeable) pr_state_status=$(gh pr view "$BACKMERGE_NUMBER" --json mergeStateStatus -q .mergeStateStatus) if [ "$pr_state" = "MERGED" ]; then echo "Back-merge PR merged!" break elif [ "$pr_state" = "CLOSED" ]; then echo "Back-merge PR was closed without merging — stop" exit 1 elif [ "$pr_mergeable" = "CONFLICTING" ]; then echo "Back-merge PR has conflicts — stop, do not merge" exit 1 elif [ "$pr_mergeable" = "MERGEABLE" ] && [ "$pr_state_status" != "BLOCKED" ]; then gh pr merge "$BACKMERGE_NUMBER" --merge --delete-branch && break fi echo "Waiting for back-merge PR... (state: $pr_state, mergeable: $pr_mergeable, status: $pr_state_status)" sleep 30 done -
Local cleanup — fast-forward both branches against their remotes, drop the throwaway release and back-merge branches, and prune stale tracking refs:
git checkout develop git pull --ff-only origin develop git checkout main git pull --ff-only origin main git checkout develop git branch -d release/<timestamp from step 4> git branch -d chore/back-merge-v<VERSION> git fetch --all --pruneBoth
git branch -dcalls succeed because each branch is fully integrated via its merge commit. If either errors "not fully merged" while its PR shows merged and the remote head is gone, the work is integrated —git branch -Dis then safe. -
Report success with the release PR URL, the GitHub Release URL (printed by step 17), and the back-merge PR URL. The Release description is the new version's changelog section; the full history lives in
CHANGELOG.md.
Notes
- If any step fails, stop and report the error to the user.
- The
--yesflag oncz bumpauto-confirms the version bump. - Auto-merge in steps 15 and 18 uses
--merge— never switch to--squashor--rebase, which would lose the bump-commit-tagged invariant onmain. - Do not use composite commands — they always force a permission request from the user.
- You are already in the repo folder — do not
cdfirst (redundant, and can cause a composite command).