Prepare a release — size it, and write its migration guide
A release has two questions this skill answers before anyone clicks "Run":
- How big is it? — patch or minor, read from the commit history, not guessed. (A major is not sized
here; it is a planned
vX-line initiative — see step 2.) - What must a consumer do to adopt it? — captured as a migration guide that ships with the code and lives forever, so the answer is never lost in a PR description or a Slack thread.
You are advisory: read the diff, propose the version, draft the guide. The human reviews and cuts the
release from the Actions tab. You never tag, push, or publish — releases are human-only (see
manage-versions).
This skill is the manual stand-in for a future agent that runs it automatically before every release. Until then, invoke it deliberately when a release is near.
1. Read the unpublished diff
Find the last release and everything since, on the branch you'll release from (the default branch):
git fetch --tags origin
last=$(git tag --list 'v*' --sort=-v:refname | head -1) # or <subdir>/v* for a sub-dir module
git log --no-merges --pretty='%s' "$last..origin/master"
Read the subjects (the conventional-commit type/scope/!) and, for anything that might break or
deprecate, the body/footers (BREAKING CHANGE:). A squash-merge repo has one commit per PR, so the
subjects are the PR titles — usually enough; open the PR/diff when a subject is ambiguous about consumer
impact.
With no prior tag, this is the first release (v0.1.0 or v1.0.0 per the team's call) — there is
nothing to migrate from, so a guide is rarely needed.
2. Propose the bump
Apply this mapping — a local variant of Conventional Commits; the highest wins across the range:
| Commit signal | Bump |
|---|---|
any feat:, or a ! / BREAKING CHANGE: too small for its own major (see below) |
minor |
only fix: / perf: / refactor: / chore: |
patch |
A major is never derived from the commit history here. This is the deliberate departure from stock
Conventional Commits: a ! or BREAKING CHANGE: footer does not size a release to major. A major
is a planned, initiative-led migration on its own vX release line — a global sweep, a paradigm
shift, a change whose migration is large enough to track as its own initiative — and it is cut from that
line when it lands, not proposed by this pass. If the range you are sizing contains a ! and you think
it warrants a major, that is a signal to stop and raise an initiative, not to bump the second digit.
So a breaking change small enough to absorb in a normal release stays a minor: removing a capability
(especially one that no longer works), dropping a dead action, tightening a validation. It ships as a
minor with a migration guide (below), because it still asks a consumer to act, but it does not touch the
major. Name it plainly in the proposal — "minor, carries one small breaking change (#NNN removes the
dead foo action)" — so the operator sees the break without a version jump implying a bigger one.
The highest across the range is therefore minor whenever any feat: or an absorbable break is
present, else patch. Docs/CI-only ranges (docs:, ci:, chore:) are a patch (still a release if
you want the notes), or skip the release entirely.
(There is no pre-1.0 special case: majors are initiative-gated regardless of the current major digit.)
State the proposed version plainly — e.g. "v1.0.3 → v1.1.0 (minor: three feat:, no breaking
footer)" — and name the one or two commits that drove it. That version is also the release-type the
human picks in the release workflow and the filename of the guide.
3. Decide whether a migration guide is needed
A guide documents consumer action, not a changelog. Use this gate:
| Release shape | Guide? |
|---|---|
A major (the planned vX-line migration) |
Required |
| Minor carrying an absorbable breaking change (a removal, a tightened validation) | Required |
| Minor that deprecates a path, adds a preferred alternative, or recommends a change | Recommended |
| Minor that is purely additive (new optional thing, no consumer change) | No (notes only) |
| Patch (bug/perf/internal) | No |
The middle row is the subtle one: a backward-compatible release can still ask consumers to move (a new recommended input, a renamed-but-aliased field, a new pattern). Nothing forces the change, but the migration is real and worth writing down once. When in doubt for a minor, ask: "is there anything a consumer should change to fully benefit, even though nothing breaks?" If yes, write the guide.
4. Write the guide
One immutable file per version, never a single growing UPGRADING.md — the convention every mature
migration system uses (Flyway V1__…, Rails / GitLab timestamped migrations). A single appended file is
a merge-conflict magnet across parallel release branches and blurs which change maps to which release;
per-version files are additive (no conflicts) and map 1:1 to a tag.
- Path:
docs/migrations/vX.Y.Z.md, named by the version that introduces the change. (A sub-dir Go module — e.g.cli/— keys the file by its version too; a repo releases one module, so the version is unambiguous.) - Immutable once shipped. Never edit a released guide; a correction goes in the next version's guide, so history stays truthful.
- Indexed. Keep
docs/migrations/README.mdas a table, newest-first, linking each guide. - Linked from the release. The release notes for
vX.Y.Zshould point at its guide (add the link to the generated notes / the GitHub Release body). - Committed with the change, before the release is cut — so the released tag already contains its own guide.
Template
# Upgrading to vX.Y.Z
One-line summary of what changed and why it matters to a consumer.
## Do I need to act?
**Required / Recommended / No** — and the one-sentence reason. If "No", say why the release is safe to
take as-is and stop here.
## <Change 1 — imperative title, e.g. "Switch bot inputs to client_id">
What changed and **why**. Then the concrete migration, before → after:
\`\`\`yaml
# before
app_id: ${{ vars.AGENT_BOT_ID }}
# after
client_id: ${{ vars.AGENT_BOT_CLIENT_ID }}
\`\`\`
Any **prerequisite** (a new variable, a new permission), whether the old path **still works**
(deprecated vs removed), and how to **verify** the migration landed.
## <Change 2 …>
…
## Notes
Anything optional, related follow-ups, or pointers to a larger effort this is part of.
Lead with "Do I need to act?" — most readers want exactly that, and a clear "No" for a safe minor is a feature. Keep each change section to what changed → why → before/after → caveats.
5. Present — advisory only
Hand the operator: the proposed version, the commits that drove it, whether a guide was
written (and where), and the release-type to pick in the workflow. Then stop. The human cuts the
release (Actions ▸ release ▸ pick the bump); release-core stamps versions, tags, and publishes.
You do not run it.
If the change ships in stages or spans repos, that ordering is a manage-versions concern — note it
and defer.
Principles
- The guide is consumer-facing. Release notes list what changed; the guide says what to do about it. A change needing no consumer action belongs in the notes.
- One file per version, immutable. Additive, conflict-free, 1:1 with a tag.
- Derive patch-vs-minor from the commit history. If the history is wrong (a
featmislabeledfix), fix the discipline (git-conventions) rather than silently overriding the math. - A major is never derived; it is planned. No commit range proposes a major. A major is an
initiative with its own
vXbranch that becomes master when it lands. A!in an ordinary range is an absorbable breaking change (a minor with a guide), or a prompt to raise that initiative — never a second-digit bump on the current line. - Advisory, never the trigger. You propose; the human releases.
How it composes
prepare-release ─ (reads the unpublished diff) ─> proposed bump + docs/migrations/vX.Y.Z.md
│
├─ git-conventions — the commit→bump mapping it applies
├─ manage-versions — versioning mechanics, staged/cross-repo rollouts the guide may reference
└─ release workflow — the human cuts the release with the proposed bump; the guide ships in the tag
A migration guide written here is the instruction; rolling it out across consumer repos is execution
(implement-feature / manage-versions), often tracked as its own follow-up issue.