| name | release |
| description | 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.
|
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:
- Run once with
--dry-run → show the rendered changelog block back
to the user and ask if it looks right.
- 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.