mcpField
R
releasekit

releasekit

github.com/mimukitDevelopment2203 installsUpdated today
Repository

About releasekit

releasekit is a release-preparation skill for repositories using Conventional Commits. It derives a semantic version and changelog, then creates a tag and GitHub release after preview and confirmation; it does not publish packages to registries.

Key features

  1. 01Derives the next semantic version
  2. 02Generates Keep a Changelog entries
  3. 03Previews changes and requires confirmation
  4. 04Creates a version tag and GitHub release

Use cases

  1. 01Prepare a release from merged commits
  2. 02Review the next version and changelog
  3. 03Create a tag that triggers a CI publishing workflow

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/mimukit/skills --skill releasekit

After installing, type/releasekitin Claude Code to confirm it loaded.

Details

From the project README

releasekit

A repo that writes Conventional Commits has already answered the two questions a release asks: what changed, and how much did it break. releasekit reads that answer back out. It resolves the commit range since the last release, derives the next semver version and a Keep a Changelog section from the commits in it, and cuts the release: a CHANGELOG.md entry, a version bump in the ecosystem's manifest, an annotated tag, and a GitHub release.

Everything it does is previewed before anything mutates, because a version number is spent permanently. A tag that ships broken code cannot be reused, only superseded.

releasekit stops at the tag. It does not run npm publish, twine upload, or cargo publish. The tag it creates is precisely the trigger a publish workflow already listens for, and credentials plus irreversibility are two good reasons to leave that where it is.

When this fires

The user wants to turn merged work into a released version: "cut a release", "release this", "tag a version", "what would the next version be", "generate the changelog", "ship v2", "/releasekit".

Three boundaries matter:

  • It does not write the commits it reads. Authoring Conventional Commits belongs to the commit step; releasekit only ever parses what is already in the log.
  • It is not a publisher. The registry upload is CI's job, triggered by the tag.
  • It is interactive only. The confirmation below is a real gate with no exemption, so releasekit never runs inside an unattended pipeline. A run with nobody to answer stops at the preview rather than assuming a yes.

Procedure

1. Preflight

Every check runs before anything mutates, and each failure names itself rather than falling through to a later one. Preflight is done when every check below has passed or has stopped the run with its named refusal.

Environment. Confirm git, a remote, and a usable gh. Without gh, releasekit still writes the changelog and cuts the tag, so say once that the GitHub release and the CI check are both skipped, and why. Do not fail wholesale for a missing enrichment.

Base ref. Never assume main; repos default to develop, trunk, and master in the wild. When gitkit is installed, ask it, because it owns this answer. Otherwise resolve it directly:

gh repo view --json defaultBranchRef --jq .defaultBranchRef.name

falling back to git symbolic-ref --short refs/remotes/origin/HEAD, and asking the user rather than guessing past that.

Repo state. Refuse, and say which one failed, when:

  • the working tree is dirty, because a release must describe committed code;
  • HEAD is not the base ref, because releasing a feature branch tags code nobody merged;
  • the branch is behind its upstream, because the changelog would omit commits that are already public.

Competing release tooling. Look for .changeset/, .releaserc*, release-please-config.json, or a semantic-release dependency. Any of them owns versioning end to end, and a second writer corrupts its state. Stop before mutating anything, name the tool, and name its command instead. This is a refusal, not a warning.

Workspaces. Look for workspaces in package.json, a pnpm-workspace.yaml, or [workspace] in Cargo.toml. A marker alone is not a monorepo, because plenty of repos have a packages/ directory and one published artifact. Refuse only when a marker is present and more than one manifest carries a version, and list every versioned manifest you found. A workspace where only the root is versioned has one version and releases normally.

Branch protection. Probe the base branch once:

gh api "repos/{owner}/{repo}/branches/$BASE/protection"

A 404 means unprotected and selects the direct path. Any other answer means protected and selects the release-PR path. Getting this wrong is not cosmetic: the direct path pushes a commit straight to the base, and a protected repo rejects it after the changelog has already been written.

Phase. Work out which phase this run is from the repo, never from state you keep. Check in this order, and take the first that matches:

  1. Resume. An earlier run stopped part way. The evidence is the newest chore(release): vX.Y.Z commit on HEAD's line, plus one of these: tag vX.Y.Z exists only locally (git ls-remote --tags origin vX.Y.Z is empty), the tag is on the remote but gh release view vX.Y.Z finds no release, or, on the direct path, the commit is not on the upstream and has no tag. Resume that version and never derive a new one, because a second derivation spends a second number on the same code. Pick up at the first step that did not land, and take the release body from that version's section in CHANGELOG.md.
  2. Finish, release-PR path only. A merged chore(release): vX.Y.Z commit on the base with no tag pointing at it.
  3. Prepare on the release-PR path, or a fresh release on the direct path. Anything else.

Resume and finish both read the version from the commit subject and skip Derive the version and Render the changelog. The check is done when the preview can name one phase and, for resume, the first step still to run.

2. Derive the version

Resolve the last release. Take the highest stable semver tag merged into HEAD, not the highest version in the repo:

tags=$(git tag --merged HEAD \
  | grep -E '^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' \
  | awk -F. '{ m = $1; sub(/^v/, "", m); if (m + 0 < 1000) print }')
nv=$(printf '%s\n' "$tags" | grep -c '^v')
nb=$(printf '%s\n' "$tags" | grep -c '^[0-9]')
if [ "$nb" -gt "$nv" ]; then style='^[0-9]'; else style='^v'; fi
last=$(printf '%s\n' "$tags" | grep "$style" | sort -V | tail -n 1)

An empty $last means no previous release. Each filter has one job:

  • --merged HEAD keeps the lineage. A repo running v1.x alongside v2.x reads its own line, because a v2 tag is not merged into the v1 maintenance branch.
  • The anchored regex accepts exactly MAJOR.MINOR.PATCH with an optional v and no leading zeros. It rejects a prerelease (v1.3.0-rc.1), a build or backup suffix (v1.4.0.bak), a zero-padded calver date (2026.08.01), and a name such as nightly-2026-08-01. A git describe --match glob cannot do this, because a trailing * accepts any suffix.
  • The awk filter drops a tag whose major is 1000 or more. That shape is a calver date such as 2026.8.1, which the regex alone accepts. List every dropped tag in the preview so a real semver major that high is visible.
  • The prefix style follows the majority of the surviving tags, and a tie goes to v. A repo that tags v1.2.3 ignores a stray bare 2.0.0, and a bare-tagged repo ignores a stray v tag. List the tags of the other style in the preview as skipped.
  • sort -V orders by version, so v1.10.0 sorts after v1.9.0. On a host whose sort lacks -V, sort on the three numeric fields instead.

Prereleases are skipped here, so the commits a v1.3.0-rc.1 shipped still appear in the v1.3.0 changelog. Cut them at the resolver, so nothing downstream has to know prereleases exist.

Read the range <lasttag>..HEAD, or the whole history when no tag exists. Parse each commit's subject and body, because a BREAKING CHANGE: footer lives in the body, so bodies cannot be skipped.

Bump from what the range contains, taking the highest that applies:

in the rangebump on 1.x and abovebump on 0.x
a ! marker or a BREAKING CHANGE: footermajorminor
any featminorpatch
any fix, hotfix, perf, or revertpatchpatch
only docs, chore, style, test, build, ci, refactornonenone

The 0.x column is not a typo, and it is the rule most likely to surprise someone. Bumping major on a pre-1.0 repo takes 0.3.0 to 1.0.0, which declares the API stable off the back of a single commit footer. This follows the release-please and Cargo convention instead. State the rule and its arithmetic in words in the preview, not just the resulting number.

A first release asks. With no tag in the repo, prompt once with 0.1.0 prefilled. This happens exactly once per repo and cannot be corrected afterwards, and 1.0.0 is a public declaration of API stability that must never be guessed on the user's behalf. Skip the prompt when a manifest already carries a version, because that version is the user's answer.

The tag prefix is inherited, not chosen. v1.2.0 gives v, 1.2.0 gives none. Default to v on a first release.

Two refusals come out of this step, both with a named override so the refusal is discoverable rather than a dead end:

  • Nothing user-visible. The range holds no commit that forces a bump. Say so concretely ("nothing user-visible since v1.2.0") and name --allow-empty for a user who has a reason. A patch bump for every CI tweak makes the version meaningless; a prompt every time trains people to dismiss it.
  • Nothing parses. No commit in the range is a Conventional Commit, so any bump would be a guess. Refuse the same way.

Commits that do not parse otherwise proceed. Collect them for an Other section and count them in the preview. Squash-merge repos build subjects from PR titles, and any repo predating the convention has a mixed log, so refusing until every commit parses would make releasekit unusable on both. Nothing is silently dropped; the floor above is what stops the bump becoming fiction.

This step is done when the preview can state the last release (or none), every tag the resolver skipped, the next version, and the rule that produced it.

3. Render the changelog

Group the parsed commits into Keep a Changelog sections by type:

commit typesection
any type with ! or a BREAKING CHANGE: footerBreaking changes
featAdded
fix, hotfixFixed
perf, revertChanged
docs, refactor, style, test, build, ci, chorenone; count them by type in the preview
unparsedOther

A hotfix commit is a fix that shipped on an urgent path, so it reads as a fix to the user. The omitted types change nothing a user of the release can see, and the preview count shows they were read, not lost. On an --allow-empty release, list the omitted commits under Other, so the section is never empty. Render a section only when it has an entry.

  • Breaking changes come first, always in their own section, carrying the migration note from the footer. A reader scanning a release wants the thing that will break them above the thing that delights them.
  • Keep the scope, drop the type. Under an Added heading, auth: add token refresh retry reads better than feat(auth): add token refresh retry, because the heading already said feat.
  • Link each entry to its commit, and to its PR when the merge commit names one.
  • Account for every commit. The step is done when each commit in the range sits in exactly one section or in the omitted count.
  • Render exactly once. The CHANGELOG.md section and the GitHub release body are the same string. Two renders are two texts that can disagree, and the disagreement always surfaces after the release is public.
4. Preview and confirm

The preview is mandatory and always renders. releasekit preview ends here having mutated nothing; a bare releasekit waits for a confirmation here and mutates nothing until it gets one.

Lead with the path, then the facts:

  • first line. The path (direct or release-PR) and the phase (resume, finish, prepare, or fresh). A misdetected phase is the one failure that costs a duplicate release, so it has to be visible before it is actionable.
  • the computed version, and the rule that produced it stated in words;
  • the rendered changelog in full;
  • the manifest file and its old → new version, or that no manifest was found;
  • the tag name;
  • the CI check rollup result, and the SHA it belongs to;
  • the count of unparsed commits;
  • every command about to run.
5. Cut the release

Verify CI first, on either path, and bind it to a SHA. Read the check rollup for the exact commit the CI result has to cover, and refuse to tag a failing one, because a tag on red code spends a version number on something nobody can use. --allow-red overrides it for a known-flaky required check. When gh is unusable there is no rollup, so say the check was skipped; never let a missing signal read as green. A pending rollup is not green either, so stop and report it.

gh api "repos/{owner}/{repo}/commits/$SHA/check-runs" \
  --jq '.check_runs[] | [.name, .status, .conclusion] | @tsv'
gh api "repos/{owner}/{repo}/commits/$SHA/status" --jq .state
  • Finish, and a resume whose release commit is on the upstream. $SHA is the release commit that gets the tag.
  • Direct path, and a resume whose release commit is not on the upstream. CI cannot have run on the release commit, so $SHA is its parent: HEAD before the release commit. The release commit only adds the changelog and the manifest bump to it. Re-check that origin/<base> still equals $SHA just before the push, and stop if it moved.
The direct path

Unprotected base. In this order, because each step depends on the last landing:

  1. Bump the detected manifest (package.json, Cargo.toml, pyproject.toml, a gemspec). No manifest found is fine and is not an error.
  2. Prepend the rendered section to CHANGELOG.md, creating the file with a header when it does not exist.
  3. Commit both together as chore(release): v<version>. One commit, because the bump and the changelog describe the same event.
  4. Create an annotated tag whose message is the release title.
  5. Push the commit and the tag.
  6. Create the GitHub release with gh release create, using the rendered body.
The release-PR path

Protected base. Two invocations, and the phase resolved in Preflight decides which one runs.

Prepare. Do the manifest bump, the changelog, and the commit on a release-v<version> branch. Push it and open a PR titled chore(release): v<version>, whose body is the rendered changelog. The title is load-bearing: a squash merge turns the PR title into the commit subject, and the finish phase finds the release by that subject. Stop there. The user reviews and merges it.

Finish. The merged, untagged chore(release): commit is the trigger. Verify the rollup on that commit's SHA, tag that commit, push the tag, and create the GitHub release. The changelog is already in the repo from the merge, so nothing is rewritten and nothing is committed.

This step is done when every command the preview listed has run, or when the run has stopped at a failed step. If any step on either path fails, stop at that step and report exactly what landed and what did not. The next run detects the gap as resume and continues from there. Never roll back a pushed tag; see the refusals in Notes.

6. Hand off

Write this section in the procedural register: one instruction per sentence, active voice, present tense, no metaphor.

The hand-off differs by path, so report the one that ran.

Direct path, release-PR finish, and resume. This is terminal.

  • What changed. The version, the files bumped, and the commit created.
  • Where it landed. The tag name, the release URL, and the branch pushed.
  • Next. There is no next kit. Say that pushing the tag may have started a publish workflow, and that releasekit does not own or watch it. Name gh release edit for a typo in the notes.

Release-PR path, prepare. This is not terminal.

  • What changed. The release branch, the commit on it, and the PR opened. Say plainly that no tag exists yet.
  • Where it landed. The branch name and the PR URL.
  • Next. Review and merge the PR. Use mergekit when it is installed, otherwise gh pr merge. Then run releasekit again to tag the merged commit.

Notes

  • Four refusals, and each one is a refusal rather than a caution. releasekit stands down when another release tool owns the repo. It refuses a workspace with several versioned packages. It refuses to tag a red base. It never moves or deletes a published tag, because re-tagging breaks every consumer that already resolved it. A bad release is fixed by cutting the next one.
  • Fixing a released typo is out of scope. The notes on a published release are editable by nature, but adding an edit path would make a third entry point out of a skill with one procedure and two exits. Run gh release edit instead.
  • Prereleases are read, never written. releasekit passes over an existing -rc.N tag so a repo already using them can still cut a stable release. It does not create one.
  • One version per repo. Per-package versioning in a monorepo is a different skill with a different data model, and guessing at it silently is worse than refusing.
  • It never runs unattended. The confirmation in Preview and confirm is a consent gate with no exemption. An orchestrator that reaches it with nobody to answer must stop and escalate, not proceed.
  • Follow the repo over these defaults. A repo with its own changelog format, a CHANGELOG at a different path, or a documented release procedure gets that, and say you followed it.
  • Skill is declared for one call only, which is asking gitkit for the base ref. releasekit invokes no other skill; naming mergekit in the hand-off is routing, not launching.
  • No filesystem or shell (a browser-based agent, say)? Then you cannot read the log or cut anything. Instead print the derived version, the rendered changelog as a codeblock, and the exact git tag and gh release create commands for the user to run.

Comments

Markdown supported