release
Cuts a release. Never auto-runs: the maintainer invokes /release vX.Y.Z (stable), /release beta X.Y.Z-beta.N or /release alpha. Pushing a tag and dispatching release.yml publish immediately and permanently; an agent does neither unless the maintainer asks in this conversation.
The model
- One version.
package.json#versionnames the CLI and the Engine;rust/Cargo.toml,rust/Cargo.lockand bothserver.jsonversions mirror it (bun run check:versions).maincarries the next unreleased version: thepost-releasejob opens a PR that leads it to the next minor after every stable release. - One workflow.
.github/workflows/release.ymlclassifies the trigger (release-classify.ts).plangates the run;buildhas three rows, one Cargo profile each, followed bydarwin-synthesis-smokeandroundtrip-smoke. The stable-onlypackagesjob runs alongside those jobs.assemblewaits for the build, smokes, and (on stable) packages; thengithub-releaseandnpmpackaging can run in parallel.npm-publishwaits for both, followed bynpm-smoke. Stablehomebrewanddockerjobs start aftergithub-releaseand can overlap the npm smoke; stablepost-releasewaits for both the GitHub release and npm smoke. A pull request that touches it runs the build and smoke rehearsal and publishes nothing. - The Engine pin is injected, never committed.
npmwritespackage.json#kesha.engine = { version, sha256, size }from the release'sSHA256SUMSand asset list (engine-pin.ts);kesha installrefuses a download that does not match it. A source checkout carries no pin and verifies against the release's ownSHA256SUMS. - Releases are immutable and tag names one-use. A release is published once, with every asset, by
github-release; nothing is drafted. A broken release is fixed by the next patch, never by reusing its tag. Never tag to test: open a PR that touchesrelease.ymlfor a rehearsal. - npm Trusted Publishing names
release.yml. It is the only workflow that publishes, and the package trusts exactly one workflow; a second entry point 404s at the registry (#732).
| Channel | Trigger | Publishes | npm dist-tag |
|---|---|---|---|
| stable | just release-tag vX.Y.Z notes.md |
Engine, Linux packages, SHA256SUMS, manifest, SBOM, Sigstore bundles, npm, Homebrew tap, Docker image |
latest |
| beta | gh workflow run release.yml -f channel=beta -f version=X.Y.Z-beta.N, or a pushed vX.Y.Z-beta.N tag on a commit whose package.json#version is that beta |
Engine Prerelease, npm | beta |
| alpha, per merge | a merged PR labelled alpha that changed what npm packs |
npm only, against the newest stable Engine; the tag records it | alpha |
| alpha, dispatched | gh workflow run release.yml -f channel=alpha [-f engine-prerelease=vX.Y.Z-beta.N] |
Without engine-prerelease: Engine Prerelease and npm. With it: npm only, pinned to the named Engine |
alpha |
A dispatched beta's X.Y.Z must equal package.json#version. An alpha version is derived from tags inside the publish queue and is never committed; a pushed -alpha.N tag starts nothing. The alpha label is read live from the merged PR, so removing it before plan runs skips the publish. Engine alpha Prereleases are pruned by 🌙 Nightly after 30 days; their tags stay.
Stable release
Pre-flight. On fresh
origin/main:just check, CI green on the head, no open P1/P2 on the last merged PRs.gh release view vX.Y.Zmust 404: the name is unused.Version. If
package.json#versionis not alreadyX.Y.Z, open a PR in a worktree that sets it inpackage.json, bothserver.jsonfields,rust/Cargo.tomlandrust/Cargo.lock(cargo check), and merge it.release-classify.tsrefuses a tag whose version differs frompackage.json#versionat that commit.Notes. Write
notes.md: features, platform changes, breaking changes, merged PRs, follow-ups, and the upgrade commandbun add -g @drakulavich/kesha-voice-kit@latest. Run the dogfood checklist against the candidate and paste it ticked as## Dogfood:release-tagrefuses notes without it. The annotation becomes the release body (engine-release-notes.mjs); a lightweight tag contributes none.Tag. From the clean root checkout:
just release-tag vX.Y.Z notes.md. It tagsorigin/main, pushes, reads the tag back and waits for the push-triggeredrelease.ymlrun (runbook). The tag must be pushed by a person: aGITHUB_TOKENpush triggers nothing.Watch.
gh run watch <id> --exit-status. Every job must succeed;homebrewneeds theHOMEBREW_TAP_TOKENsecret.Verify from the registry, not a global install. A previous
bun add -goutranksbun link, so a local run can test an old CLI:npm view @drakulavich/kesha-voice-kit@X.Y.Z kesha.engine.version # X.Y.Z SMOKE=$(mktemp -d) && cd "$SMOKE" && bun add @drakulavich/kesha-voice-kit@X.Y.Z KESHA_CACHE_DIR="$SMOKE/cache" KESHA_ENGINE_BIN="$SMOKE/kesha-engine" bunx kesha install KESHA_CACHE_DIR="$SMOKE/cache" KESHA_ENGINE_BIN="$SMOKE/kesha-engine" bunx kesha say "Hello" > hello.wavrelease.yml's smokes run on hosted runners, and hosted macOS has no Neural Engine, so darwin-arm64 Transcription is only ever verified here, on real hardware.Merge the pact PR.
post-releasedispatches🌙 Nightlywithjob: capability-pactandrecord: true, and itsopen-capability-pact-prjob openstest(pact): record the capability pacts from the vX.Y.Z engines; until it merges, the weekly pact job fails on all three targets (#1246–#1248). AGITHUB_TOKENPR starts no CI, so close and reopen it first. If no PR appears, run that dispatch by hand. Re-record, never relax the check. Only when the release really changed a published contract do the test's expected lists anddocs/errors.mdmove with it.Merge the
post-releasePR, which leadsmainto the next minor.
Recovery
| State | Recovery |
|---|---|
Failed before github-release |
Nothing is public. Fix on main, then tag the next patch: the tag name is spent once pushed. |
github-release succeeded, a later job failed |
Re-run the failed jobs: npm-publish no-ops on a version already on npm, and the downstream jobs are idempotent. |
The fix lives in release.yml itself |
A re-run uses the workflow stored at the tag, so the fix ships under the next patch. |
A per-merge alpha failed after reserve-tag |
Re-run failed jobs only. Re-running all jobs derives the next number and leaves a gap, which is harmless. |
npm publish is effectively permanent (72 h unpublish window). Never publish from a laptop: it ships without provenance and outside the trusted publisher.
Reference
integration-tests-fulland the other published-Engine lanes inci.ymlresolve the newest stable Engine, so a release follow-up merge gets the same path-filtered coverage as any other pull request or push tomain.- Greptile updates one top-level comment. Confirm a re-review through its "Last reviewed commit" SHA and the issue comment's
updated_at(gh api repos/drakulavich/kesha-voice-kit/issues/<N>/comments). Gate on findings, never on its confidence score. - Where each path installs from, and the release manifest: docs/distribution.md.
