Cuts a STABLE CLI release (vX.Y.Z-cli marker tag; not for beta or alpha markers, which this lane silently skips while burning the tag) โ the `๐ Release (CLI)` lane builds the Linux packages, publishes the marker release, and dispatches npm publish with provenance. Covers version alignment across package.json and server.json, why the tag must be pushed by a human, and how to verify from the registry rather than through a stale global install. Refuses to auto-run; user must explicitly invoke. For an engine release use release-engine.
Install
npx skillscat add drakulavich/kesha-voice-kit/release-cli Install via the SkillsCat registry.
release-cli
Cuts a CLI release. NEVER auto-runs โ user invokes via /release-cli vX.Y.Z-cli.
For an engine release (bare vX.Y.Z, GitHub Release only) use the release-engine skill. A full ship is /release-engine first, then this one: the CLI release is what carries the new engine pin to users, and what ships the Linux packages.
Inputs
$1: target tag, e.g.v1.27.0-cli. The-climarker routes it:build-engine.ymlexcludes it,release-cli.ymlclaims it.
Stable versions only. cli-release-plan.mjs returns packages: false for anything with a prerelease identifier, which skips the packages job and โ because publish-npm needs it โ the npm dispatch too. A v1.28.0-beta.1-cli tag therefore does nothing at all while permanently consuming the name. Betas go through the legacy hand-cut path in release-mechanics; alphas have their own lane and no GitHub release.
What the tag sets off
Pushing vX.Y.Z-cli runs ๐ Release (CLI), which in one run:
- classifies the tag and refuses one that already has a release;
- builds and verifies the Linux
.deb/.rpm; - creates the marker release as a draft carrying those packages plus
SHA256SUMS, then un-drafts it; - dispatches
๐ฆ npm Publishand waits for it, then verifies the version is on npm.
Steps 2โ4 must happen together: the .deb is named from package.json#version, and nothing downstream re-checks that npm ever served it since #727 removed the assertion that did.
A human has to push the tag. A GITHUB_TOKEN push fires no on.push.tags โ which is exactly why the alpha lane's own -cli tags never reach this workflow, alphas having no GitHub release by design.
Do not npm publish from a laptop. npm-publish.yml is the one entry workflow npm's trusted publisher accepts (#731), and it publishes with provenance from an OIDC identity. Un-drafting from a workflow raises no release: published, so the dispatch is explicit rather than cascading.
Cut one stable CLI release at a time. npm-publish.yml's concurrency: queue: max serialises runs but does not order them by version, so two stable markers in flight can finish out of order and leave latest on the older one.
Pre-flight
# 1. Root checkout clean, on main, up to date. Edit in a worktree, never here.
git fetch origin main && git status -sb | head -3
# 2. Version fields agree, and the pinned engine is a published, non-draft release
bun run check:versions
node -p "require('./package.json').version"
python3 -c "import json;d=json.load(open('server.json'));print(d['version'], d['packages'][0]['version'])"
gh release view "v$(node -p "require('./package.json').keshaEngine.version")" --json isDraft --jq 'if .isDraft then error("engine pin is still a draft") else "pin published" end'
# 3. CI green on main
gh run list --workflow ci.yml --branch main --limit 1
# 4. Local sanity
bunx tsc --noEmit && bun testTwo things sink a run if they are wrong:
package.json#versionat the tag must be exactly the version the tag names. The lane refuses otherwise, because the packages carry that field.- The engine pin must name a published release. Shipping a CLI whose pin 404s means
kesha installfails for every new user.
If anything fails, STOP.
Procedure
Step 1 โ Align the versions (often already done)
Three fields must equal the target version: package.json#version, server.json#version, server.json#packages[0].version. Leave keshaEngine.version and rust/Cargo.toml alone โ that is the engine line.
Since #691 main carries the next unreleased CLI version, so these are frequently already at the target and there is nothing to bump. Then skip to step 3: no diff, no PR.
server.json is the MCP registry manifest, and check:versions rule 4 requires the match โ its version tells registries which npm release to resolve.
Step 2 โ Merge through a PR (only if step 1 changed something)
Branch release/X.Y.Z; integration-tests-full skips on release/*.
Step 3 โ Push the tag
Write the notes into the tag annotation โ that is the only window there is. publish-cli-release.sh creates the release and un-drafts it in consecutive lines, and gh release edit --notes is silently dropped once a release is published. The engine lane already authors this way; since #788 the CLI lane does too.
git tag -a --cleanup=verbatim vX.Y.Z-cli -F notes.md
git push origin vX.Y.Z-cli--cleanup=verbatim is not optional: git's default tag-message cleanup strips every line starting with #, which silently eats Markdown headings. A lightweight git tag vX.Y.Z-cli still works and contributes no notes โ cli-release-body.mjs refuses to read %(contents) off a lightweight tag, because there git hands back the commit message.
Underneath the notes the lane appends one generated line, always. Its engine half is computed against the previous stable -cli tag's keshaEngine.version โ Engine: v1.24.7 โ v1.24.9. when this release moves the pin, Engine: v1.24.9 (unchanged). when it does not, and a bare Engine: v1.24.9. when there is no previous release to compare against. Prerelease markers are not baselines; this lane publishes no release for them.
User-facing upgrade text says bun, never npm: bun add -g @drakulavich/kesha-voice-kit@latest.
Step 4 โ Watch the lane
gh run list --workflow release-cli.yml --limit 1
gh run list --workflow npm-publish.yml --limit 1
npm dist-tag ls @drakulavich/kesha-voice-kitnpm-dist-tag.mjs derives the tag from the SemVer prerelease identifier: stable โ latest, -beta.N โ beta, -alpha.N โ alpha. A prerelease never lands on latest.
If the run dies between create --draft and --draft=false it leaves a draft that blocks a re-run; the failing run prints the recovery, and gh release view vX.Y.Z-cli tells you the state directly. The full recovery table lives in release-mechanics.
Step 5 โ Verify from the registry, not from the repo
npm view @drakulavich/kesha-voice-kit@X.Y.Z --json | jq '.dist.attestations.provenance.predicateType'
npm pack @drakulavich/kesha-voice-kit@X.Y.Z && tar -xzOf *.tgz package/package.json | jq '.version, .keshaEngine.version'Then install it somewhere isolated and run it โ a fresh directory with KESHA_ENGINE_BIN pointing inside it, never the global install:
V=$(mktemp -d) && cd "$V" # never in the repo: bun add here rewrites package.json
bun add @drakulavich/kesha-voice-kit@X.Y.Z
export KESHA_ENGINE_BIN="$V/eng/kesha-engine"
./node_modules/.bin/kesha --version # X.Y.Z
./node_modules/.bin/kesha install # must fetch the pinned engine
./node_modules/.bin/kesha <repo>/tests/fixtures/benchmark/09-ustanovi-poka-klod-kod.oggjust smoke-test can false-green here. It runs whatever kesha resolves to, and a previously bun add -g'd install outranks bun link; if its output prints an older version, it tested an older CLI and proves nothing about this release.
Step 6 โ Re-lead the base version on main
main must carry the next unreleased CLI version (#691), and this release just consumed the current one. Open a follow-up PR bumping package.json#version, server.json#version, and server.json#packages[0].version to the next minor. Skipping this step is how #802 happened: the alpha derivation kept emitting X.Y.Z-alpha.N for an already-released X.Y.Z, so the next labelled merge would point @alpha at a version older than @latest.
Hard rules
- NEVER
npm publishfrom a laptop โ GHA owns it, with provenance. - NEVER push a
-beta.N-clior alpha marker through this lane; it no-ops and burns the tag. - NEVER hand-cut
gh release create vX.Y.Z-cli. The release would carry no packages, andassert-release-absent.shthen blocks the lane on that tag forever. - NEVER let a bot push the marker tag; a token push triggers nothing.
- NEVER reuse a tag name; GitHub reserves them permanently.
- NEVER write release notes after the release is published.
- NEVER ship a CLI whose
keshaEngine.versionhas no published release. - User-facing install/upgrade text says bun, never npm.
Output
๐ Released X.Y.Z
- GitHub: https://github.com/drakulavich/kesha-voice-kit/releases/tag/vX.Y.Z-cli
- npm: https://www.npmjs.com/package/@drakulavich/kesha-voice-kit/v/X.Y.Z
- dist-tag: latest Provenance: yes
- Linux packages: .deb + .rpm on the marker release
- Engine pin: A.B.C (verified published)
Verified from the registry: version โ pin โ install โ transcribe โOn failure
Report the last successful stage of the lane and what gh release view vX.Y.Z-cli shows. A draft left behind blocks a re-run and must be resolved before retrying. If npm already has the version, it is spent: fix forward under the next patch rather than unpublishing by reflex.