Release
Release a new version of the mumei repository. Invoke when the user gives an explicit release instruction ("release it", "/release", "patch release", "ship 0.2.0"). Takes no argument or "patch" / "minor" / "major" for a SemVer bump, or a direct version such as "0.2.0". Wraps any uncommitted changes into a single commit, pushes to main, watches the main CI run, and on green creates an annotated tag, pushes it, and creates the GitHub Release page (--generate-notes) in one pass. If CI fails, aborts, reports to the user, and prompts a rerun after the fix.From its SKILL.md
npx -y skills add iroha924/mumei --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
- 2 stars2 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
15.2 KB, as published. Nobody here has run it
Release (mumei release skill)
Releases a new version of the mumei repo. Starts only when the user gives an explicit release instruction (matched via the description; side effects are large, so suppress accidental invocation).
Argument interpretation
| Argument | Meaning |
|---|---|
| (omitted) | patch bump |
patch | patch bump (0.1.0 → 0.1.1) |
minor | minor bump (0.1.0 → 0.2.0) |
major | major bump (0.1.0 → 1.0.0) |
0.2.0 / v0.2.0 | direct version |
A direct version that matches an existing tag is treated as a retag (re-cut).
No-CHANGELOG policy
mumei does not maintain a CHANGELOG.md. When release notes are needed:
git log v<prev>..v<curr> --oneline
or use the diff auto-generated by the GitHub release UI (Conventional Commits subjects become the release notes as-is). This means:
- less work in the release skill (no CHANGELOG editing step)
- zero risk of "implementation and CHANGELOG drift apart"
- git tags are the single source of truth for version history
The release skill therefore never edits a CHANGELOG. The only version-bump target is .claude-plugin/plugin.json.
Overall flow
main is protected (required status checks, required conversation resolution, linear history, no force push), but enforce_admins: false: the maintainer is a repo admin and can push directly to main. The release flow uses that path — one commit, pushed straight to main, gated by the commit → push → CI watch sequence below. A push to main runs only the codeql job of ci.yml (lint / lint-extra / bats are PR-only), so the task release:check pre-flight gate is the lint/test evidence for the release commit.
1. Parse the argument → determine the new version
2. Pre-flight checks (incl. task release:check)
3. Bump the version in plugin.json
4. Show the user the diff and confirm (approval covers the direct push to main)
5. Commit on main → push to origin main
6. Watch the main CI run (gh run watch --exit-status)
7. CI green → create annotated tag + push (the tag push triggers release.yml)
8. Create the GitHub Release page (gh release create --generate-notes)
8.5. Watch the release.yml workflow (tag-push trigger; SLSA / Sigstore / asset upload)
9. Final report
CI failure → abort, ask the user to rerun after the fix
Step 1: Parse the argument
arg="${1:-patch}"
current="$(jq -r '.version' .claude-plugin/plugin.json)"
IFS='.' read -r major minor patch <<< "$current"
case "$arg" in
patch) new="${major}.${minor}.$((patch + 1))" ;;
minor) new="${major}.$((minor + 1)).0" ;;
major) new="$((major + 1)).0.0" ;;
v*) new="${arg#v}" ;;
[0-9]*.[0-9]*.[0-9]*) new="$arg" ;;
*) echo "Invalid argument: $arg"; exit 1 ;;
esac
echo "Current: $current"
echo "New: $new"
Step 2: Pre-flight checks
Run in order. Abort on the first failure:
-
Branch check:
git rev-parse --abbrev-ref HEADismain. -
origin sync:
git fetch origin main && git rev-list --count HEAD..origin/mainis 0 (not behind). -
gh CLI available:
command -v gh. Required for CI watching. If missing, ask the user to install it and abort. -
Local sanity gate:
task release:check(lint + test + cron alignment) exits clean. A push tomaindoes not run thelint/batsCI jobs, so this local gate is mandatory before the release commit. -
Existing tag check:
if git rev-parse "v$new" >/dev/null 2>&1; then existing_sha="$(git rev-parse "v$new")" head_sha="$(git rev-parse HEAD)" if [[ "$existing_sha" == "$head_sha" ]]; then echo "Tag v$new already points at HEAD. No retag needed." retag_only=1 else echo "WARN: Tag v$new exists ($existing_sha) and points at a different commit." echo "Re-cut it? (yes/no): " # set the retag flag after user confirmation fi fi
Step 3: Version bump (plugin.json)
Unless retag_only, update only the version field of .claude-plugin/plugin.json:
jq --arg v "$new" '.version = $v' .claude-plugin/plugin.json > .claude-plugin/plugin.json.tmp \
&& mv .claude-plugin/plugin.json.tmp .claude-plugin/plugin.json
Do not touch CHANGELOG.md (mumei has none; release notes are auto-generated from git log by gh release create --generate-notes). CI badges track the latest GitHub Actions run, so no manual update. The README carries no version string either (badge / Status line are not present).
Step 4: Approval
git status --short
git diff --stat HEAD
Show these to the user and ask: "Create the release commit with this content and push it to main? (yes / no)". Make it explicit that any uncommitted changes will all be included in the commit, and that approval also authorizes the direct push to main (an AI push to main always requires user confirmation).
If no, abort.
Step 5: Commit on main and push
git add -A
git commit -m "chore: release v${new}"
git push origin main
Follow the global rule (~/.claude/rules/git-commit.md):
- single-line subject
- Conventional Commits (
chore:) - imperative mood
- no Co-Authored-By
- never
--no-verify
Step 6: Watch the main CI run
The push triggers ci.yml on main. Find the run for the release commit and wait synchronously:
head_sha="$(git rev-parse HEAD)"
main_run_id=""
for _ in $(seq 1 15); do
main_run_id="$(gh run list --workflow ci.yml --branch main \
--json databaseId,headSha --jq ".[] | select(.headSha == \"${head_sha}\") | .databaseId" | head -n1)"
[[ -n "$main_run_id" ]] && break
sleep 6
done
if [[ -z "$main_run_id" ]]; then
echo "WARN: no ci.yml run appeared for ${head_sha} within 90 seconds."
echo "Check manually: gh run list --workflow ci.yml --branch main"
exit 1
fi
echo "Watching main CI run ${main_run_id}..."
if ! gh run watch "$main_run_id" --exit-status; then
echo "× main CI failed. Aborting the release."
echo "Failure log: gh run view ${main_run_id} --log-failed"
echo "See the Recovery section of this skill."
exit 1
fi
echo "main CI passed."
If CI fails, do not create the tag. Abort and report to the user.
Step 7: Create and push the annotated tag
Tag the commit whose CI just passed:
# Delete the existing tag if present (retag scenario)
if git rev-parse "v$new" >/dev/null 2>&1; then
git tag -d "v$new"
git push origin ":refs/tags/v$new" 2>/dev/null || true
fi
# Annotated tag (unsigned). git commit / tag signing is disabled
# (the Sigstore-signed badge in the README
# refers to the tarball asset signature, not a git tag signature).
git tag -a "v$new" -m "Release ${new}"
git push origin "v$new"
Step 8: Create the GitHub Release page
Pushing the tag alone leaves the GitHub Releases tab empty. Create the release page so watcher notifications and the "Latest release" badge appear:
release_url=""
if release_url="$(gh release create "v$new" --title "v$new" --generate-notes 2>&1)"; then
: # success; release_url holds the URL
else
# A failed release-page creation (permissions, network, pre-existing release,
# etc.) is a warning. The tag is already pushed (Step 7 done), so the release
# stands; the page can be created manually later. Log the failure and do not
# stop the process.
echo "WARN: failed to create the release page. Create manually: gh release create v$new --title 'v$new' --generate-notes"
echo " Failure log: $release_url"
release_url=""
fi
If release_url stays empty, it signals the failed page creation in the Step 9 report.
--generate-notes auto-classifies the commits since the previous tag by their Conventional Commits subjects. Add --draft to publish later after editing (normally unnecessary: generate, then edit in the GitHub UI if needed).
In the retag scenario, gh release create fails when a release already exists. To retag, delete it first with gh release delete "v$new" --yes and recreate (this skill never does that automatically; the user decides).
Step 8.5: Watch release.yml
When Step 7's git push origin "v$new" lands, release.yml starts on the tag-push trigger (on: push: tags: ['v*']) and runs SLSA / Sigstore signing / provenance generation / tarball asset upload. Skipping this watch makes the completion report a lie — even with the tag pushed and the release page created, a failed release.yml means assets / signatures are incomplete. Wait for the workflow with this pattern:
push_tag_sha="$(git rev-parse "v${new}^{commit}")"
release_run_id=""
for _ in $(seq 1 15); do
release_run_id="$(gh run list --workflow release.yml \
--json databaseId,headSha --jq ".[] | select(.headSha == \"${push_tag_sha}\") | .databaseId" | head -n1)"
[[ -n "$release_run_id" ]] && break
sleep 6
done
if [[ -n "$release_run_id" ]]; then
echo "Release workflow: https://github.com/iroha924/mumei/actions/runs/${release_run_id}"
gh run watch "$release_run_id" --exit-status \
|| { echo "× release.yml failed. The tag is pushed, but assets / signatures may be
incomplete. Inspect with gh run view ${release_run_id} --log-failed."; }
else
echo "WARN: no release.yml run was registered within 90 seconds (tag_sha=${push_tag_sha}).
Check manually with gh run list --workflow release.yml."
fi
Ordering note: Step 8 (gh release create) and Step 8.5 are swappable (the release page and the release.yml workflow are independent). The skill places Step 8 first so the release page URL is reliably available for the Step 9 report, and keeps the release.yml watch as the final gate in Step 8.5.
Step 9: Final report
✓ Released v${new}
- commit: $(git rev-parse HEAD)
- tag: v${new} (annotated)
- main CI: passed (run ${main_run_id})
- release.yml: passed (run ${release_run_id})
- release page: ${release_url:-"(creation failed; create manually)"}
- changes: git log v<prev>..v${new} --oneline
Error handling
Abort immediately and report state when any step fails:
- Pre-flight failure → state which condition is unsatisfied. Propose a fix.
- jq failure (malformed plugin.json) → ask for a manual repair and abort.
- Push rejected →
mainmay have moved (fetch and rerun the pre-flight checks), or branch protection may have changed; inspectgh api .../branches/main/protectionfor the current rules. - main CI failure → see Recovery below.
Recovery: main CI failed after the push (Step 6)
When Step 6 exits on a failed run, this state remains:
- the release commit is already on
main - no tag has been created
Recovery steps:
-
Identify the cause:
gh run view <run_id> --log-failed. -
Transient (network etc.):
Re-run failed jobsin the GitHub UI orgh run rerun --failed <run_id>. On green, resume at Step 7 (tag). -
Real bug: fix forward through a PR — branch from
main, commit the fix, push,gh pr create(follow.github/PULL_REQUEST_TEMPLATE.md). The PR runs the full required status checks:lint,lint-extra,bats (ubuntu-latest),bats (macos-latest),codeql (actions),scan / osv-scan,mutable-tag-guard,pr-target-guard..github/workflows/review.yml(a caller workflow that delegates toreview-reusable.yml, running claude-code-action) reviews the PR and posts review threads; branch protection'srequired_conversation_resolution: trueblocks merge while any thread is unresolved. List the threads with:gh api graphql -f query=" query { repository(owner: \"iroha924\", name: \"mumei\") { pullRequest(number: ${pr_number}) { reviewThreads(first: 50) { nodes { id isResolved path line comments(first:5){nodes{author{login} body}} } } } } }"Triage each finding: valid → push a fix commit; invalid → reply with the reason and close via the
resolveReviewThreadmutation. When CI is green and all threads are resolved, ask the user whether to merge; on approval the AI runsgh pr merge --squash(gh pr *is allowed in.claude/settings.local.json; only localgit mergeis denied). Then rerun/release <X.Y.Z>from the updatedmain. -
Manual fallback (cutting the release without the skill):
# bump plugin.json → commit on main → push git push origin main # after the main CI run is green: git tag -a "v${new}" -m "Release ${new}" git push origin "v${new}" gh release create "v${new}" --title "v${new}" --generate-notes
What this skill does not do
- Never pushes to
mainwithout the user's explicit approval in Step 4. Branch protection permits the admin push (enforce_admins: false), but user confirmation is mandatory. - Never merges a recovery PR without user confirmation.
gh pr merge --squashis permitted (.claude/settings.local.jsonallowsgh pr *; only localgit mergeis denied), but only after CI is green, review threads are resolved, and the user approves. - Never
--force-pushes tomain. A retag is the two-step delete (git push origin :refs/tags/<tag>) followed by pushing the new tag. - Never skips hooks with
--no-verify(global rule). - Never edits CHANGELOG.md (mumei does not maintain one).
- Never edits the README CI badges (they track the latest Actions run automatically); the README carries no version string to bump.
- Never creates the release page as a draft. Publish with
--generate-notes; drafts are rarely needed and the user can edit in the GitHub UI afterwards, so keep the complexity down. If the user wants a draft, they rungh release create --draftmanually. - Never deletes an existing release automatically on retag.
gh release deleteis destructive, so the skill does not run it; Step 8 fails and the user decides. - Never releases multiple versions at once. One
/release, one version. - Never silently includes uncommitted changes. Step 4 asks for explicit approval.