Files
kandev/docs/public/release-process.md
T
2026-09-19 12:35:51 +01:00

203 lines
17 KiB
Markdown

---
title: "Release Process"
description: "Run and verify Kandev's version, runtime, desktop, container, npm, GitHub, updater, Homebrew, and Scoop release automation."
---
# Release Process
This how-to guide is for maintainers who run and verify a Stable release. Its
Stable targets are the Git tag, runtime bundles, desktop app, npm packages,
GitHub Release, container images, Homebrew formula, and Scoop bucket.
Kandev uses one semantic version across the Git tag, native runtime bundles, desktop app, npm packages, GitHub release, container images, Homebrew formula, and Scoop bucket. Publish through the manual **Release** GitHub Actions workflow; do not update channels independently.
Each platform release contains a native Go `kandev` binary with the compiled
web frontend embedded. Runtime archives also include `agentctl` binaries for
task environments.
## Quick path
1. Choose one workflow mode.
2. Verify `main`, checks, credentials, signing inputs, and affected targets.
3. Run the release workflow and verify every published channel.
4. Use backfill only to repair the latest release; publish a new patch for defective code or immutable artifacts.
## Choose the workflow mode
The workflow has four mutually exclusive operating modes:
| Mode | Inputs | Result |
| ------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Normal release | `bump=patch`, `minor`, or `major` | Creates and merges a release PR, tags its merge, builds, and publishes |
| Dry run | `dry_run=true` | Computes the next version and exercises CLI package/lock plus changelog generation in the runner; no PR, tag, artifact build, or publication |
| Desktop validation | `desktop_validation_only=true` | Builds web, five runtime bundles, and five desktop targets from the selected commit; no PR, tag, GitHub release, GHCR, npm, Homebrew, or Scoop publication |
| Backfill | `backfill_tag=vX.Y.Z` | Rebuilds and repairs channels for the latest existing release tag without creating a version or tag |
`dry_run` and desktop validation are not release candidates. Backfill cannot be combined with either and accepts only the latest exact SemVer tag after version manifests are checked for agreement.
## Before dispatch
1. Open the **Release** workflow and explicitly select the `main` ref. Normal mode creates its release branch from the selected ref.
2. Confirm required checks are green on `main` and no release or release PR is active.
3. Confirm merged PR titles/commits use the conventional categories consumed by `cliff.toml`. The workflow generates `CHANGELOG.md` and release notes.
4. Verify platform-sensitive launcher, agentctl, container, and desktop changes on affected targets.
5. Check GitHub/GHCR access, npm trusted publishing, `RELEASE_PR_BYPASS_TOKEN`, signing keys, and configured desktop signing or notarization secrets.
6. Update public docs for behavior that is about to ship.
## Configure the release PR bypass
Normal releases use a fine-grained personal access token only for the privileged release PR merge. The normal `GITHUB_TOKEN` creates the branch and PR.
The token owner must remain an organization administrator. The current `main` ruleset grants that role an always-allowed bypass.
1. Sign in to the organization administrator account that will own the token.
2. Open **Settings > Developer settings > Personal access tokens > Fine-grained tokens**.
3. Select **Generate new token**.
4. Enter a clear name, such as `kandev-release-pr-bypass`.
5. Set an expiration date that follows the organization policy.
6. Select `kdlbs` as the resource owner.
7. Under **Repository access**, select **Only select repositories**.
8. Select only the `kandev` repository.
9. Under **Repository permissions**, set **Contents: Read and write**.
10. Leave all other optional permissions set to **No access**.
11. Generate the token and complete organization approval when GitHub requires it.
12. Copy the token before you leave the page.
Add the token to the protected `release` environment:
```bash
gh secret set RELEASE_PR_BYPASS_TOKEN --env release --repo kdlbs/kandev
```
Record the token owner and expiration date in the maintainer credential inventory. Rotate the secret before expiration or an owner-role change.
CAUTION: Never store this token as a repository-wide secret. The token owner can bypass the `main` ruleset.
Normal releases require a dedicated release-tag signing identity and a GitHub Environment named `release`. Restrict that environment to deployments from `main`, but do not configure required reviewers: any maintainer authorized to dispatch the workflow and access the environment may start a normal release. Store the ASCII-armored private key as the environment secret `RELEASE_GPG_PRIVATE_KEY`, its optional passphrase as the environment secret `RELEASE_GPG_PASSPHRASE`, and the exact full fingerprint as the required environment variable `RELEASE_GPG_FINGERPRINT`. Before changing version files or creating the release PR, the workflow rejects a missing, multi-key, or secret-material public-key attachment and requires its sole primary fingerprint to match `RELEASE_GPG_FINGERPRINT`. After the release PR merges, it revalidates the public key from the merged revision, imports the private key, and requires that key to match both the merged key and `RELEASE_GPG_FINGERPRINT`. Do not define this signing material as repository-wide configuration.
The `prepare` job selects its environment by mode: normal releases use `release`, while dry runs, desktop validation, and backfills use `release-validation`. Leave `release-validation` as an empty default environment with no secrets, variables, deployment restrictions, or protection rules so validation and repair modes remain usable without access to the signing identity.
GitHub and the repository are the source of truth for the release public key. The current key is committed at `.github/release-signing-key.asc`; its full fingerprint is `FFB03BCD68F5BCBBD2D1767A84EAB9CE3B8EF52F`. Before rotating the key, update that repository file and the `release` environment secret and fingerprint variable together. Retain prior public keys and revocation information in repository history so historical signatures remain auditable. Key generation, private-key backup, rotation, and revocation are maintainer responsibilities; never place private key material in the repository.
Repository administrators must also enforce a GitHub tag ruleset targeting `v*`. Enable **Restrict updates** and **Restrict deletions**, leave **Restrict creations** off, and keep the bypass list empty. Do not relax this ruleset to perform a backfill; backfill must reuse the existing tag. Enable immutable GitHub Releases when the repository supports that setting, but do not treat release immutability as a substitute for tag protection before the release is published.
For release automation changes, run:
```bash
python3 .github/scripts/release-workflow-contract_test.py
bash scripts/release-desktop.test.sh
make test-cli
```
Use dry run to validate version/changelog preparation. Use desktop validation to validate packaging from the current commit.
## Normal release flow
Normal mode performs these stages:
1. **Preflight and prepare version.** Compute the next version from packages and tags, then verify that the committed public key matches the `release` environment fingerprint before changing release files. Update the CLI package/lock, desktop package and Tauri/Cargo manifests, and `CHANGELOG.md`.
2. **Merge and tag.** Open a release branch and PR. Use the protected administrator token to squash-merge the exact head without queue or CI latency. Select GitHub's reported merge commit, then revalidate the public key before importing the protected signing key. Verify the signed `vX.Y.Z` tag locally before the push.
3. **Build web and runtimes.** Build the SPA and five runtime targets: Linux x64/arm64, macOS x64/arm64, and Windows x64. Embed the SPA in each native Go `kandev` binary. Each archive contains `kandev`, the host `agentctl`, and required remote agentctl helpers. The workflow produces an adjacent checksum for each archive.
4. **Build desktop.** Embed the matching runtime and package the same five platform/architecture targets into macOS, Linux, and Windows installer formats.
5. **Build containers.** Publish amd64/arm64 base manifests, enforce the universal-image size gate, then publish multi-architecture universal images.
6. **Publish GitHub Release.** Attach runtime archives, checksums, desktop artifacts, notes, and the updater feed when eligible.
7. **Publish npm.** Use OIDC trusted publishing for five `@kdlbs/runtime-*` packages first, then the `kandev` launcher. Existing versions are skipped; the main package is not published after a runtime-package failure.
8. **Update Homebrew.** Push the formula update to `kdlbs/homebrew-kandev` using release checksums and its deploy key. The generated formula records `X.Y.Z` explicitly; platform archive suffixes such as `x64` are not version identifiers.
9. **Update Scoop.** Push `bucket/kandev.json` to `kdlbs/scoop-kandev` using the Windows tarball checksum and its separate deploy key. The manifest records the exact Stable version, release URL, and SHA-256 value.
GHCR images are built before the GitHub Release. npm, Homebrew, and Scoop start only after the GitHub Release and may run in parallel. A late failure can therefore leave some channels complete and others missing.
Base image tags include `X.Y.Z`, `vX.Y.Z`, `sha-*`, and `latest`. Universal tags include `X.Y.Z-universal`, `vX.Y.Z-universal`, and the floating `universal`. The weekly universal rebuild updates only floating/dated weekly tags, never a version-specific release tag.
## npm Nightly channel
npm also offers an opt-in `nightly` prerelease for users who want to test recent changes from
`main`. Stable remains the default `latest` channel.
For stable `X.Y.Z` and commit `abcdef123456...`, the corresponding Nightly version is
`X.Y.(Z+1)-nightly.shaabcdef123456`. The `kandev` launcher and all five platform runtime packages
share that immutable version, so an installed build identifies its source commit.
Nightlies are best-effort daily snapshots. A new one appears only when `main` has changed since the
latest Stable release, so some days may have no new Nightly. A Nightly never moves npm's `latest`
tag and does not publish a Git tag, GitHub Release, Homebrew formula, Desktop updater build or feed,
or container tag. It never updates the Scoop bucket. See [CLI installation](cli.md#npm-nightly) to try Nightly or return to Stable.
<details>
<summary>Signing, verification, and partial-release recovery</summary>
## Signing and updater behavior
npm uses GitHub OIDC trusted publishers; there is no `NPM_TOKEN` release path. The main `kandev` package and all five `@kdlbs/runtime-*` packages publish provenance attestations. Homebrew and Scoop each require a separate repository deploy key. Store the Scoop private key as the `SCOOP_BUCKET_DEPLOY_KEY` repository secret; do not reuse the Homebrew key.
Release-tag signing applies to future normal releases only. Backfills reuse the existing tag, and historical unsigned tags are not recreated or moved.
Desktop OS signing and notarization are conditional on a complete secret set. Without them, the workflow can publish unsigned installers and adds a warning to release notes. Tauri updater signatures are stricter: the workflow publishes updater artifacts and `latest.json` only when the required signed set is complete. Do not claim in-app update availability from the presence of installers alone.
Never print signing material, tokens, certificate contents, or generated updater private data in logs.
## Verify every channel
After publication, verify:
- the Git tag points at the generated release merge;
- GitHub notes, five runtime archives, checksums, expected desktop installers, and conditional `latest.json`;
- all five runtime npm packages plus `kandev`, including a clean `npx kandev@latest`;
- Homebrew install/upgrade, a `Cellar/kandev/X.Y.Z` install path, and `kandev --version`;
- Scoop install/update, the manifest version and hash, and `kandev --version`;
- GHCR base and universal images on amd64 and arm64, including their immutable version tags;
- desktop launch on affected platforms and signed/notarized status where configured;
- backend health, a minimal task, agentctl startup, and Updates-screen behavior;
- public docs describe the released behavior rather than unreleased `main` where version differences matter.
Record artifact URLs/digests and the workflow run. Do not treat a successful tag or one working installer as a complete release.
Obtain the release public key from `.github/release-signing-key.asc` in the GitHub repository and confirm that its fingerprint is `FFB03BCD68F5BCBBD2D1767A84EAB9CE3B8EF52F` before importing it into your GPG keyring and verifying a tag:
```bash
gpg --import .github/release-signing-key.asc
git fetch --tags origin
git tag -v vX.Y.Z
```
Compare the displayed fingerprint with the repository's release-key fingerprint. To check npm provenance, inspect the registry attestations for the launcher and every runtime package, then verify installed dependency signatures and attestations with npm:
```bash
VERSION=X.Y.Z
for package in kandev \
@kdlbs/runtime-linux-x64 @kdlbs/runtime-linux-arm64 \
@kdlbs/runtime-darwin-x64 @kdlbs/runtime-darwin-arm64 \
@kdlbs/runtime-win32-x64; do
npm view "$package@$VERSION" dist.attestations
done
npm audit signatures
```
## Repair a partial release
First identify exactly which immutable and mutable channels succeeded. Preserve workflow logs, checksums, package versions, image digests, and signing output.
Required web, runtime, and desktop artifact uploads make up to three total attempts. The workflow waits 30 seconds before the second attempt and 60 seconds before the third attempt. Each required upload also fails when an expected file is missing. Desktop targets run independently, so one upload failure does not cancel the other matrix targets. Publication remains blocked until every required target succeeds. If all attempts fail, rerun the failed job in the same workflow run. If the signed tag already exists and the run remains partial, use `backfill_tag=vX.Y.Z` after confirming which channels already succeeded.
If a GHCR build or manifest step returns a recognized registry-throttling response, such as HTTP 403 with a secondary-rate-limit message, HTTP 429, `TOOMANYREQUESTS`, or `too many requests`, treat it as transient registry throttling. The workflow retries these GHCR mutations with bounded 60/120/240-second backoff and publishes the two architectures sequentially. If the job still fails, wait a few minutes for the throttle to clear, then rerun the failed job in the same workflow run. Do not start a new normal version bump after the signed release tag exists. If the run remains partial, use `backfill_tag=vX.Y.Z` for the existing latest tag after confirming which channels already succeeded.
If both tag push attempts fail after the release PR merges, the workflow logs the exact release merge commit. First confirm that `refs/tags/vX.Y.Z` is absent from the remote. Then, from an audited recovery context authorized by the tag ruleset and using the configured release signing identity, recreate and verify the tag at that exact commit:
```bash
git fetch origin main
git checkout --detach <release-merge-commit>
git tag -s vX.Y.Z -m "release: X.Y.Z"
git tag -v vX.Y.Z
git push origin vX.Y.Z
```
Before pushing, confirm that `git tag -v` reports the full `RELEASE_GPG_FINGERPRINT` recorded with `.github/release-signing-key.asc`. An arbitrary clone cannot recover by pushing alone because the locally created tag existed only on the failed ephemeral runner.
Use `backfill_tag` only for the latest release when shipped source is correct and the failure is missing artifacts or a recoverable publication step. Backfill checks out application source from the tag, validates all version manifests, and uses the current workflow's control-plane helpers to rebuild or reconcile GitHub Release, GHCR, npm, desktop/updater, Homebrew, and Scoop channels. Existing npm versions are not overwritten.
Publish a new patch instead when code is defective, an immutable npm package or version-specific image is wrong, manifests disagree, or repair would require changing tagged source. Never delete/reuse a published tag or move an npm version as a routine fix.
For implementation detail, inspect `.github/workflows/release.yml`, `.github/workflows/universal-rebuild.yml`, `scripts/release/`, `apps/cli/README_internal.md`, and desktop packaging scripts. Those files are automation source; this page is the contributor operating contract.
</details>