# Release

> Cut a new release: group Conventional Commits since the last tag, append a CHANGELOG section, create a release commit, and add an annotated git tag. TRIGGER when the user says "cut a release", "tag a release", "release v1.2.0", "ship version 1.2.0", "create a release for 1.2.0", "bump the version to 1.2.0", "make a release", "generate a changelog and tag", "what version should I release", "what's the next version", "bump minor", "bump major", "bump patch", "suggest a version", "/nyann:release". Do NOT trigger on "release branch" / "cut a release branch" — that is the `new-branch` skill with `--purpose release`. Do NOT trigger on "publish to npm" / "push to pypi" — those are package-manager operations outside nyann's wedge.

- Skill: `thettwe/release` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thettwe/release`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thettwe/release/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: thettwe (https://skillmd.com/u/thettwe)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thettwe/release

---


# release

Wraps `bin/release.sh`. Default strategy is `conventional-changelog`:
walk the commit range since the last matching tag, group commits by
Conventional Commits type, prepend a CHANGELOG block, commit the
changelog, then annotated-tag `v<version>`.

## 0. Drift check (quick, non-blocking)

Run `bash bin/session-check.sh --flow=release`. If it produces output,
surface the line to the user verbatim. Do not block the flow.

## 1. Suggest version (when not explicitly provided)

When the user does **not** supply an explicit `--version`, run
`bin/recommend-version.sh` to suggest one:

```
bin/recommend-version.sh --target <cwd> [--tag-prefix <p>]
```

Show the recommendation to the user:

> Based on commits since `<current>`, I'd suggest **`<recommended>`**
> (`<bump>` bump — `<reason>`). Shall I proceed with `<recommended>`,
> or would you prefer a different version?

Wait for confirmation before proceeding. The user can accept, override
with a different version, or abort.

When the user **does** supply an explicit version (e.g. "release v2.0.0"),
skip this step entirely — don't second-guess them.

When the user says "bump minor" / "bump major" / "bump patch", run
`recommend-version.sh` to get the current version, then apply the
requested bump type (ignore the script's own recommendation). Confirm
the computed version with the user before proceeding.

## 2. Collect inputs

- **`--version <x.y.z>`** — required for `release.sh`. Must be semver
  (or `x.y.z-prerelease`). Populated from step 1 when the user
  accepted the suggestion, or from their explicit input.
- **`--strategy`** — defaults to `conventional-changelog`. Override to
  `manual` when the user says "just tag it, skip the changelog".
  `changesets` / `release-please` are soft-skip values: the script
  emits a skip record pointing the user at those tools directly.
- **`--changelog <path>`** — defaults to `CHANGELOG.md`. Override when
  the repo uses a different file.
- **`--tag-prefix <p>`** — defaults to `v`. Override for monorepos
  that namespace tags (`api-v1.2.0`).
- **`--from <ref>`** — defaults to the latest tag matching
  `tag_prefix`. Override when the user says "include everything
  since commit X".

When the repo's active profile has a `release` block, prefer its
values over the defaults. Ask before overriding what the profile
declares.

## 3. Pre-flight

- Current tag `<prefix><version>` must not already exist. If it does,
  `release.sh` dies with a clear message; relay it.
- Working tree must be clean. Refuse on dirty tree and ask the user
  to commit or stash first. (Exception: `--dry-run` skips this
  gate so the user can preview.)
- Branch doesn't matter — release can cut from any branch (main is
  the common case, but patch releases from `release/*` or
  `hotfix/*` are legitimate).

### Pre-action guards

```
bash bin/pre-action-guard.sh --flow release --target <cwd> [--profile <resolved-profile.json>]
```

The built-in `clean-tree` guard catches a dirty working tree before
`release.sh` would (with a more actionable message). Profiles may add
flow-specific guards via `guards.release` — surface those too.

| Exit | Meaning | Action |
|---|---|---|
| 0 | Pass | Continue |
| 3 | Critical guard failed | Refuse to release. Relay the guard's `message` verbatim — the user fixes the underlying issue. `--skip-guards` is available but should be rare for a release. |
| 4 | Confirm-severity guard failed | AskUserQuestion: "Proceed with the release?" — explicit confirm. |

Skip this step entirely on `--dry-run` (the user is previewing, not
mutating).

## 4. Dry-run first if uncertain

When the user says "what would a release look like" or the commit
range is large (>20 commits), run `--dry-run` first and show the
rendered changelog block back to them. Ask for confirmation before
the real run.

## 5. Invoke

```
bin/release.sh --target <cwd> --version <x.y.z> \
  [--strategy conventional-changelog|manual] \
  [--changelog <path>] [--tag-prefix <p>] \
  [--from <ref>] [--push] [--dry-run] [--yes] \
  [--wait-for-checks] [--no-wait-for-checks] [--allow-no-pr] \
  [--wait-for-checks-timeout <sec>] \
  [--wait-for-checks-interval <sec>] \
  [--bump-manifests] [--gh-release] [--profile <path>]
```

**Preview-before-mutate:** when `--strategy conventional-changelog` and
the run is NOT `--dry-run`, `release.sh` requires `--yes`. Without it,
the script prints the rendered CHANGELOG block to stderr and exits 2
so the caller is forced to confirm before the mutation. The intended
flow is:

1. Run once with `--dry-run` → show the rendered `changelog` block back
   to the user and ask if it looks right.
2. If yes, re-run with `--yes` (same args) → actual write + commit.

Skip `--yes` handling for `--strategy manual` (no CHANGELOG write).

`--push` pushes the tag (and the release commit for
`conventional-changelog`) to `origin`. Use when the user explicitly
says "push the tag" or "publish the release". For quieter flows, skip
`--push` and let the user run `git push --follow-tags` themselves.

**CI gate by default:** because a pushed tag is consumed by the
marketplace, `--push` enables the CI gate automatically when the origin
is a GitHub remote and `gh` is authenticated. The auto-enabled gate
degrades gracefully — a release cut on main with no open PR, or a host
without authenticated gh, proceeds with a warning instead of failing.
Pass `--no-wait-for-checks` to push without any gate (e.g. a repo whose
CI genuinely lives elsewhere).

`--wait-for-checks` resolves the PR by exact HEAD SHA (`headRefOid`)
and gates the tag step on that PR's CI
via `bin/wait-for-pr-checks.sh`. Passing it **explicitly** makes the
gate strict: CI failure / timeout / unreachable gh / no PR all
hard-fail (the auto-enabled default would instead degrade). Use the
explicit form when the user says "tag the release once CI is green" or
"don't tag if CI is broken".

When **no PR matches HEAD** (legitimate for first-cut releases or
local-only commits, but also a CI-bypass risk in squash/rebase
release flows), the default is also a hard-fail. Pass
`--allow-no-pr` alongside `--wait-for-checks` to opt in to the
proceed-without-PR path (output shows `ci_gate.outcome:"no-pr-found"`);
without it the release exits 2 and points the user at the flag.

Skipped silently on `--dry-run` so a "show me the plan" call never
burns 30 minutes polling.

### 5.1 Manifest bumps + GitHub release (`--bump-manifests`, `--gh-release`)

When the resolved profile declares `release.bump_files[]`, default the
invocation to `--bump-manifests` so manifest version strings (`plugin.json`,
`marketplace.json`, `package.json`, `pyproject.toml`, etc.) get rewritten
to `--version` and land in the same release commit as `CHANGELOG.md`.
The dry-run output's `bumped_files[]` array previews each file's
`from_version` and `action` (`bumped` vs `unchanged`); show that to the
user before the real run.

When the user says "publish the release on GitHub" / "create the GH
release too", also pass `--gh-release`. The flag requires `--push`
(the GH release attaches to the just-pushed tag) and runs
`gh release create <tag> --notes-file <rendered-changelog>` after
the push succeeds. For pre-release versions (`-rc.N`, `-beta.N`), the
script auto-passes `--prerelease` to gh. There is no profile-level
auto-toggle for `--gh-release` in v1.5.0 — gate on explicit user
intent only.

Both flags are opt-in. On profiles WITHOUT `release.bump_files`, do not
auto-pass `--bump-manifests` — it would be a no-op but the skill should
stay quiet about flags the profile doesn't declare. On `--strategy
manual`, `--bump-manifests` is rejected up-front (no commit for the
bumps to land in).

When passing `--bump-manifests`, also pass `--profile <path>` if the
caller already has the resolved profile snapshot in hand (otherwise
`release.sh` re-resolves the default profile via `load-profile.sh`).

## 6. Interpret the output

| status | what happened |
|---|---|
| `released` | Tag created (and CHANGELOG + release commit for conventional-changelog). Report the tag name and pushed? flag. |
| `noop` | No commits since `from` ref. Nothing to release. Tell the user. |
| `skipped` | Strategy was `changesets` or `release-please` — relay the reason; point them at those tools. |

The JSON includes the rendered `changelog` block — read it back to
the user so they know what landed in CHANGELOG.md before pushing.

When `--bump-manifests` was used, the JSON includes `bumped_files[]`
with one record per declared file (`bumped` vs `unchanged`). Read this
back too — especially the `unchanged` entries, which surface as a quiet
"the file was already at the target version" rather than a silent
no-op.

When `--gh-release` was used, the JSON includes `gh_release` with one
of three outcomes: `created` (URL), `skipped` (gh missing/unauthed,
with a recovery command in `next_steps[]`), or `failed` (with the
redacted error string and a manual recovery command). The tag stays on
origin in every case — nyann never undoes a successful tag push.

## 7. Breaking changes

Commits with `!` after the type/scope (e.g. `feat(api)!: remove X`)
and commits whose body contains `BREAKING CHANGE:` are currently
detected from the subject `!` marker only (body parsing is out of
scope for v1). If the user tells you a commit without `!` is
actually a breaking change, suggest they either amend the subject or
manually edit the rendered CHANGELOG block before committing.

## When to hand off

- "Push the tag now" — use `--push`, or invoke `git push --follow-tags`
  via Bash after the fact.
- "Draft GitHub release notes" — out of scope; point the user at
  `gh release create <tag> --generate-notes` as a follow-up.
- "Revert the release" — `undo` skill for the commit;
  `git tag -d <tag>` + `git push origin :refs/tags/<tag>` for the tag
  itself. Destructive — always confirm first.

