--- name: release description: Kandev release and version-channel conventions — unified Stable SemVer plus deterministic npm-only Nightlies. Use when cutting a release, changing channels, debugging artifacts, or answering version-channel questions. --- # Release & Versioning Kandev Stable releases use a **single SemVer** `X.Y.Z` shared across all distribution channels. npm also has an explicit prerelease-only `nightly` channel; it is not part of the unified Stable artifact set. ## Version targets - `apps/cli/package.json` version → `X.Y.Z` - npm main package: `kandev@X.Y.Z` - npm runtime packages: `@kdlbs/runtime-{platform}@X.Y.Z` (5 platforms; declared as `optionalDependencies` in main package) - Git tag: `vX.Y.Z` (three-part; legacy `vM.m` tags normalize to `M.m.0`) - Homebrew formula: `kdlbs/homebrew-kandev` `Formula/kandev.rb` `version "X.Y.Z"` - Scoop bucket: `kdlbs/scoop-kandev` `bucket/kandev.json` version, URL, and hash - GitHub release: `vX.Y.Z` with standard platform archives `kandev-{platform}.tar.gz`, full offline archives `kandev-{platform}-full.tar.gz`, checksums, Windows ZIP equivalents, remote helper assets, and `runtime-size-report.md` Stable default archive names contain the standard runtime: host `kandev`, host `agentctl`, and `remote-helpers.json`. The `-full` archives also contain all four remote helper executables. Stable npm packages, Homebrew, Scoop, winget, Chocolatey, and Desktop use standard archives. Containers and npm Nightlies stay full. Desktop keeps one standard installer and updater track. **npm, Homebrew, and Scoop are sibling channels**, not chained. All three consume the same GitHub release artifacts; none depends on another package-manager channel. For npm Nightly, Stable `X.Y.Z` plus a full `main` SHA produces `X.Y.(Z+1)-nightly.sha`. `kandev` and all five runtime packages publish at that exact version under npm's `nightly` dist-tag. Nightly never moves `latest` and creates no Git tag, GitHub Release, Homebrew formula, Scoop bucket update, Desktop feed/build, or container tag. ## Release flow Stable runs entirely in CI via `.github/workflows/release.yml`, triggered by a maintainer from the GitHub Actions UI: 1. Maintainer clicks "Run workflow" → keeps `channel=stable` → picks `bump` (patch/minor/major) → optional `dry_run` or `desktop_validation_only`. 2. `prepare` job bumps version + regenerates CHANGELOG, opens release PR, squash-merges, tags `vX.Y.Z`. 3. `build-web`, `build-bundles`, `build-remote-helpers`, `build-desktop`, and both Docker builds create the channel inputs. 4. `publish-release` promotes staged standard archives to the existing default names, publishes full archives and helper assets, then attaches checksums, the size report, desktop artifacts, and notes. 5. `publish-npm` publishes 5 `@kdlbs/runtime-*` packages + main `kandev` package to npmjs. 6. `update-homebrew-tap` pushes updated `Formula/kandev.rb` to `kdlbs/homebrew-kandev` via SSH deploy key. 7. `update-scoop-bucket` pushes updated `bucket/kandev.json` to `kdlbs/scoop-kandev` via its SSH deploy key. ## Contributor notices The `Release` workflow has a `notify_contributors` checkbox. It defaults to false. When selected, the workflow calls the reusable notification workflow after GitHub Release, npm, Homebrew, and Scoop publication all succeed. Dry runs, desktop validation, Nightly, cancellation before the notification job starts, and publication errors skip the call. Cancellation after posting starts can leave partial notices; rerun with the exact tag to complete safely. For manual notices or recovery, run **Notify release contributors** from the `main` ref. Leave `release_tag` empty to select the latest published Stable release. Enter an exact tag to select another release. The `dry_run` checkbox previews the same PR selection and comment text without posting. The helper reads PR links from the release notes. It posts only to merged PRs from this repository that belong to the selected release tag. It excludes bots and maintainers from `cliff.toml`. The job token posts as `github-actions[bot]`. Only notices from that bot or a listed maintainer count as already sent. Each comment includes a hidden release ID marker. Repeat runs skip comments with that marker and the exact unmarked notice used for `v0.97.0`. After a partial run, use the separate workflow with the exact tag. It skips confirmed notices and retries only missing notices. Wait for GitHub rate limits to clear before retrying. ## Release PR ruleset bypass A normal Stable release creates its branch and pull request with `GITHUB_TOKEN`. It uses `RELEASE_PR_BYPASS_TOKEN` only for an exact-head `gh pr merge --admin`. Store this fine-grained personal access token in the protected `release` environment. Its owner must remain an organization administrator. Select only `kdlbs/kandev` and grant `contents: write` repository permission. Record the token owner and expiration date. Rotate the environment secret before the token expires or the owner loses administrator access. The workflow must stop before tag creation when the token is missing or cannot bypass the ruleset. After the merge, use `GITHUB_TOKEN` to read the PR state. Tag only the merge commit that GitHub reports after it appears on `origin/main`. **Workflow-control invariant:** When a channel intentionally skips a job, every downstream job reachable through that dependency chain must use a status function such as `!cancelled()` plus explicit `needs..result == 'success'` checks. For a partial Stable release, preserve the existing signed tag and rerun with `backfill_tag`; never run a normal bump against an existing tag. Declare Stable complete only after `publish-release`, `publish-npm`, `update-homebrew-tap`, and `update-scoop-bucket` each succeed and their artifacts are verified—an aggregate green run can hide skipped publication jobs. Required web, runtime, and desktop artifact uploads attempt up to three times, with waits of 30 seconds and 60 seconds between attempts. They fail explicitly when an expected file is missing. Desktop matrix targets use `fail-fast: false` so a transient upload failure does not cancel sibling targets, but publication still requires the complete matrix to succeed. Rerun a failed producer job in the same workflow run after a transient failure. If the signed tag already exists and the run remains partial, use `backfill_tag` for that tag after checking which channels already succeeded. Stable has no local release driver; the entire Stable flow runs in GHA. The Nightly metadata and publication revalidation state machine lives in `scripts/release/nightly-release.sh`, which GHA invokes for scheduled and manual Nightly runs. The same workflow schedules npm Nightly with cron `0 12 * * *`. It skips before building when `main` has no commit after the latest Stable tag, the exact commit is already published, or a same or newer `main` Nightly supersedes the scheduled commit. Eligible runs build only the shared web bundle and five native runtime archives, then publish runtimes first and `kandev` last with OIDC provenance. Stable and Nightly workflow runs share one non-cancelling release-wide concurrency slot. Before publishing, Nightly rechecks the stable Git/npm baseline and the previously observed `nightly` tag; a pending Stable tag or moved value safely suppresses stale publication. Maintainers may run that same Nightly path from the Actions UI with the `main` ref and `channel=nightly`. `dry_run=true` retains the real metadata and registry preflight but skips shared builds and all npm writes. The shared form's required `bump` value is ignored for Nightly; `desktop_validation_only` and `backfill_tag` are Stable-only and rejected when combined with it. Validate Nightly automation changes with: ```bash node --test scripts/release/nightly-version.test.mjs scripts/release/nightly-release.test.mjs python3 .github/scripts/release-workflow-contract_test.py bash -n scripts/release/nightly-release.sh scripts/release/publish-npm.sh ``` ## Release-tag signing configuration The release workflow reads signing configuration from the GitHub `release` environment. `RELEASE_GPG_PRIVATE_KEY` and the optional `RELEASE_GPG_PASSPHRASE` are environment secrets. The full 40-character `RELEASE_GPG_FINGERPRINT` is an environment variable, not a secret: the workflow reads `vars.RELEASE_GPG_FINGERPRINT`, so storing it as a secret makes normal-release preflight treat it as missing. `.github/release-signing-key.asc` must contain exactly one public primary key whose fingerprint matches that variable; never commit private key material. `backfill_tag` repairs publication for an already-signed existing tag only and does not bypass the normal-release signing checks. Desktop signing is automatic. Complete macOS/Windows signing and notarization secrets produce signed artifacts; missing or incomplete signing inputs produce unsigned desktop artifacts and the GitHub release notes get an unsigned-artifact warning. `desktop_validation_only=true` builds artifacts from the current workflow ref for maintainer inspection and skips the release PR, tag, GitHub release, npm publish, Homebrew update, Scoop update, and public container tags. ## Runtime resolution The published npm shim (`apps/cli/bin/native-shim.js`) locates its bundled runtime via: 1. `KANDEV_BUNDLE_DIR` env var (set by Homebrew wrapper, used by tests). 2. Installed `@kdlbs/runtime-{platform}` npm package via `require.resolve()`. 3. The Homebrew/manual install path execs `bin/kandev` directly. (`--runtime-version` is rejected by the native launcher.) ## Runtime helper binary checklist When adding, renaming, or removing bundled helper binaries such as `agentctl--`, update every packaging surface in the same PR: - backend build targets and scripts - Docker/runtime image copy steps - `.github/workflows/release.yml` bundle, macOS signing, and notarization loops - `scripts/release/prepare-desktop-runtime.sh` - `scripts/release/verify-desktop-runtime.sh` - `scripts/release/remote-helper-assets.mjs` - `apps/backend/internal/agent/runtime/lifecycle/remote_helper_manifest.go` and the cache resolver - `scripts/release-desktop.test.sh` - `apps/desktop/AGENTS.md` runtime resource list Verify with the helper build plus release-runtime tests, for example: ```bash make -C apps/backend build-agentctl-remote bash scripts/release-desktop.test.sh ```