Kandev documentation
Release Process
Run and verify Kandev's version, runtime, desktop, container, npm, GitHub, updater, Homebrew, and Scoop release automation.
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, Scoop bucket, and plugin SDK verification.
Kandev runtime distribution 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. Stable releases publish standard and full runtime
archives plus one version-bound asset for each remote helper.
Quick path
- Choose one workflow mode.
- Verify
main, checks, credentials, signing inputs, and affected targets. - Run the release workflow and verify every published channel.
- 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 host version or host tag; can complete a missing SDK 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
- Open the Release workflow and explicitly select the
mainref. Normal mode creates its release branch from the selected ref. - Confirm required checks are green on
mainand no release or release PR is active. - Confirm merged PR titles/commits use the conventional categories consumed by
cliff.toml. The workflow generatesCHANGELOG.mdand release notes. - Verify platform-sensitive launcher, agentctl, container, and desktop changes on affected targets.
- Check GitHub/GHCR access, npm trusted publishing,
RELEASE_PR_BYPASS_TOKEN, signing keys, the Discord webhook secret, and configured desktop signing or notarization secrets. - 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.
- Sign in to the organization administrator account that will own the token.
- Open Settings > Developer settings > Personal access tokens > Fine-grained tokens.
- Select Generate new token.
- Enter a clear name, such as
kandev-release-pr-bypass. - Set an expiration date that follows the organization policy.
- Select
kdlbsas the resource owner. - Under Repository access, select Only select repositories.
- Select only the
kandevrepository. - Under Repository permissions, set Contents: Read and write.
- Leave all other optional permissions set to No access.
- Generate the token and complete organization approval when GitHub requires it.
- Copy the token before you leave the page.
Add the token to the protected release environment:
gh secret set RELEASE_PR_BYPASS_TOKEN --env release --repo kdlbs/kandevRecord 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:
python3 .github/scripts/release-workflow-contract_test.py
bash scripts/release-desktop.test.sh
make test-cliUse 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:
-
Preflight and prepare version. Compute the next version from packages and tags, then verify that the committed public key matches the
releaseenvironment fingerprint before changing release files. Update the CLI package/lock, desktop package and Tauri/Cargo manifests, andCHANGELOG.md. -
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.Ztag locally before the push. -
Build web and runtimes. Build the SPA and five runtime targets: Linux x64/arm64, macOS x64/arm64, and Windows x64. Stable standard archives contain
kandev, hostagentctl, andremote-helpers.json. Stable-fullarchives add all four remote helpers. Windows also gets standard and full ZIP archives. The workflow publishes checksums for every archive and helper asset. -
Build desktop. Embed the matching runtime and package the same five platform/architecture targets into macOS, Linux, and Windows installer formats.
-
Build containers. Publish amd64/arm64 base manifests, enforce the universal-image size gate, then publish multi-architecture universal images.
-
Publish GitHub Release. Attach runtime archives, checksums, desktop artifacts, notes, and the updater feed when eligible.
-
Publish npm. Use OIDC trusted publishing for five
@kdlbs/runtime-*packages first, then thekandevlauncher. Existing versions are skipped; the main package is not published after a runtime-package failure. -
Update Homebrew. Push the formula update to
kdlbs/homebrew-kandevusing release checksums and its deploy key. The generated formula recordsX.Y.Zexplicitly; platform archive suffixes such asx64are not version identifiers. -
Update Scoop. Push
bucket/kandev.jsontokdlbs/scoop-kandevusing the Windows tarball checksum and its separate deploy key. The manifest records the exact Stable version, release URL, and SHA-256 value. -
Verify plugin SDKs. Retain and sign the original SDK candidate, publish or verify its independent Go/npm version, and pass public consumer checks.
stable-completerequires all publication jobs.
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.
Plugin SDK publication
The Go module github.com/kdlbs/kandev/pluginsdk and npm package
@kdlbs/plugin-sdk share an independent version starting at 0.1.0. They are
not runtime platform packages and do not follow the host's version or Nightly
channel. A Stable release verifies them even when their version is unchanged.
This implementation has not established their first public availability.
Prepare ownership and trust
Before the first release, a maintainer must confirm control of npm scope
@kdlbs and package name @kdlbs/plugin-sdk. Package absence does not prove
ownership. Set SDK_SCOPE_OWNERSHIP_CONFIRMED=true in the protected release
environment, and expose the same confirmation to the read-only
release-validation environment used for dry-run/backfill preflight.
Configure the package's trusted publisher for GitHub owner kdlbs, repository
kandev, workflow release.yml, and environment release. Use GitHub-hosted
runners. Enable direct publication and dist-tag changes for that
publisher; staged-only permission cannot complete this workflow. Set
SDK_FIRST_PUBLISH_OIDC_READY=true only if first-package OIDC is configured and
supported. Otherwise leave it unset and use the explicit bootstrap below.
There is no CI token fallback.
The packaging configuration pins exact Node and npm versions, currently Node 24.19.0 and npm 11.21.0. The latter supports the required OIDC dist-tag operation. Check current npm trusted-publisher requirements when changing these pins. The release jobs load the original SDK's pins on a retry, rather than using whichever npm version a later host happens to carry.
Run and verify
- Dispatch Stable or latest-tag backfill from
main. Preflight checks shared version intent, contract changes, metadata copies, and registry readability. A new SDK starts with a null originating host; normal release preparation stamps the actual host version and any pending SDK-wide floor into the release PR. Backfill never restamps the signed host record. - After
publish-release, protectedprepare-plugin-sdkverifies the signed originating host tag, packs the original source once, and tests those exact bytes with an independent frontend and Go consumer. It retains the archive, JSON receipt, and detached GPG receipt signature on that host release before it creates the signedpluginsdk/vStag. The tag points at the original host commit. Its signing fingerprint must matchRELEASE_GPG_FINGERPRINT. publish-plugin-sdkhas OIDC permission and read-only Git access, with no signing secret. It verifies retained identities, publishes the exact tarball with provenance, and verifies public Go/npm installations with fresh caches. Both consumers run the same compatibility assertions as local tests. npm dependency signatures and attestations are checked withnpm audit signatures.- Confirm
stable-completesucceeds. Bootstrap, mismatched identities, or failed public installation leave the release incomplete and block automatic contributor notices. The other package-manager jobs can finish independently.
The receipt assets are plugin-sdk-S.tgz, plugin-sdk-S.receipt.json, and
plugin-sdk-S.receipt.json.asc on release vH, where H is the record's
host_release. The receipt binds the source commit, host/SDK tags, canonical
record digest, archive SHA-512 integrity, file inventory, exact tools, and
workflow ref/SHA/run/attempt. It is signed with the approved release key.
An unchanged SDK reuses these original assets even if a later host changes its
README. Changed package bytes need a new SDK version if they are to be published.
A maintainer can rerun public verification without publishing, from a checkout with full tag history and the approved public key. Supply the actual host tag and use the original Node/npm versions from its SDK receipt:
RELEASE_GPG_FINGERPRINT=FFB03BCD68F5BCBBD2D1767A84EAB9CE3B8EF52F \
node scripts/pluginsdk/consumer-smoke.mjs --mode public --host-tag vHvH is a placeholder, not a release claim. The verifier resolves the signed
host record and signed SDK tag, checks the receipt and public archive, and
rejects inherited local registries, Go proxies, and checksum exemptions before
using isolated caches. A local archive/proxy smoke test proves packaging only.
First-package bootstrap
bootstrap_required is an incomplete release state. It is returned only after
confirmed whole-package absence and retained, signed first-release evidence.
An existing package with a missing version, an authentication failure, malformed
registry response, or network failure does not qualify.
The run summary provides exact asset links, SDK version, and integrity. A
maintainer must download all three assets, verify the detached receipt signature
against the approved release fingerprint, and check the archive's SHA-512 against
archive_integrity in that receipt. Do not run npm pack again. Then, with
maintainer npm authentication outside CI, publish those exact retained bytes:
# Replace S with the SDK version from the verified receipt.
npm publish ./plugin-sdk-S.tgz --access public --ignore-scripts \
--registry https://registry.npmjs.org --tag sdk-releaseConfigure the trusted publisher and its direct-publish/dist-tag permissions,
then retry the failed SDK job or use the existing latest Stable backfill. The
retry verifies the npm bytes against the signed receipt, repairs latest, and
runs public consumers. It reports manual_bootstrap when the first upload has
no workflow provenance; the signed candidate receipt does not turn a manual
upload into an OIDC provenance claim. Later automated uploads require provenance.
Recover partial SDK publication
Inspect both immutable states before changing anything:
| SDK tag | npm version | Recovery |
|---|---|---|
| Absent | Absent | Build/test/retain the original candidate, then create the tag and publish or report bootstrap |
| Present | Absent | Verify the tag and retained signed receipt, then publish those bytes |
| Absent | Present | Require the retained signed receipt and matching npm integrity/record before creating the missing tag |
| Present | Present | Verify both identities and public consumers; repair only missing mutable-channel work |
A lost mutation response triggers bounded rereads, not blind repeated writes. Existing tag targets, signatures, receipts, and uploaded assets must agree; the workflow never force-moves a tag or overwrites an existing release asset. A missing archive can be rebuilt only from the original signed source and exact tools, and only if its hash matches the authenticated receipt. Missing receipt or signature evidence after publication stops recovery. An interrupted initial receipt upload can finish its missing signature only before SDK publication, after workflow identity and rebuilt bytes are verified.
latest advances only after identity verification. An already newer SDK remains
latest after its own signed mapping and retained archive are verified. Keep
manual dist-tag changes outside active release runs; npm has no compare-and-swap
operation for this mutable tag. A dist-tag permission failure remains a failed
release even if the immutable upload succeeded. Never delete/reuse an npm
version or SDK tag to repair conflicting bytes. Publish a new SDK version after
reviewing the conflict.
Announce a release on Discord
Normal Stable releases automatically post one compact announcement after GitHub Release, npm, Homebrew, Scoop, and public plugin SDK verification succeed and stable-complete passes. The message shows whole-release category counts, up to three Features and three Fixes with contributor credits, and the exact GitHub release link. Other populated categories retain their counts. It sends no attachment or full changelog series.
Store the general-channel webhook URL as DISCORD_GENERAL_WEBHOOK_URL in Settings > Environments > release > Environment secrets. The existing environment must permit main. To enter the URL through an interactive prompt:
gh secret set DISCORD_GENERAL_WEBHOOK_URL --env release --repo kdlbs/kandevA missing secret fails the announcement job with a configuration error. A Discord failure does not undo package publication. Nightlies, dry runs, desktop validation, failed publication, and cancellation before delivery send no announcement. Reruns and backfills skip automatic delivery and report that manual recovery is required.
To preview or recover an announcement:
- Open Actions > Announce Discord release > Run workflow and select
main. - Enter the exact published Stable tag, such as
v1.2.3. - Use the default
previewmode to inspect the job summary and preview artifact. Preview needs no webhook secret. - For delivery, first verify all publication channels and inspect the Discord channel for an existing announcement. Use
updatewith its message ID if it exists. Otherwise usecreateand select the acknowledgement checkbox.
Successful delivery records the message ID and Discord link in the job summary and a receipt artifact retained for 30 days. Update requires a message from the configured webhook with the same release tag and GitHub link. It stops if the message is missing or does not match.
A timeout or lost response can leave a message in Discord even when the job fails. The helper retries explicit rate limits within a bounded budget, but does not automatically resend uncertain requests. Inspect the channel before recovery; repeating create can produce a duplicate. Cancellation during delivery also requires inspection.
Notify release contributors
The Release workflow has a notify_contributors checkbox. It defaults to unchecked. If selected, it posts notices after GitHub Release, npm, Homebrew, Scoop, and plugin SDK verification succeed. The release job passes its exact tag, including on a backfill. Nightly, dry-run, desktop-validation, cancellations before the notification job starts, and failed publication paths skip notices. Cancelling after posting starts can leave partial results; rerun the exact tag to finish safely.
Use the separate workflow to post notices, preview them, or recover after a partial notification run:
- Open Actions.
- Select Notify release contributors.
- Select Run workflow to show the input form.
- Choose
mainas the branch. - Leave Release tag empty to select the latest published Stable release.
- To select another release, enter its exact tag, such as
v1.2.3. - To preview notices, select Preview targets and comments without posting.
- To post notices, clear the preview checkbox.
- Select Run workflow.
- Read the run summary for the result of each PR.
Notices target merged PRs from this repository that appear in the release notes. The workflow skips bot accounts and maintainers listed in cliff.toml. It posts as github-actions[bot]. Only notices posted by that bot or a listed maintainer count as already sent.
If a notification run stops after publication succeeds, start a new manual run with the exact release tag. The workflow skips confirmed notices and tries the remaining PRs. It also recognizes the unmarked notices posted for v0.97.0. If GitHub applies a rate limit, wait for it to clear before the next run.
Stable default archive names contain the standard runtime. The -full names contain the offline
command-line runtime. npm Stable packages, Homebrew, Scoop, winget, Chocolatey, and Desktop use the
standard form. Desktop has one installer and updater track. Containers and npm Nightlies retain all
remote helpers. The release attaches runtime-size-report.md with compressed sizes from the same
build. Read it to compare each standard archive, full archive, and helper asset.
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 to try Nightly or return to Stable.
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.
The Windows runtime bundle is signed separately from the desktop installer through SignPath. Configure the repository secret SIGNPATH_API_TOKEN and repository variables SIGNPATH_ORGANIZATION_ID, SIGNPATH_PROJECT_SLUG, and SIGNPATH_SIGNING_POLICY_SLUG. The bundle job has no environment, so environment-scoped values are unavailable. Missing or blank inputs produce an unsigned runtime bundle with a workflow notice.
Signing runs before packaging, so both Stable archive variants and their checksums contain the same signed host binaries. Signed output is staged separately and adopted only after the action succeeds and both binaries are present and non-empty; their executable modes are restored before packaging. Upload, signing, or incomplete-output failures retain the original unsigned binaries and report a warning.
A test-signing policy (slug test or starting with test-) is allowed only during desktop_validation_only, where nothing is published. Its self-signed certificate is not trusted by Windows; a Stable publishing run skips that policy with a warning. Scheduled and manual nightlies are never signed because the SignPath Foundation requires manual approval for each release signing request. Stable signing waits up to one hour for approval before falling back to an unsigned bundle.
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, standard and full runtime archives, checksums, helper assets, the runtime size report, expected desktop installers, and conditional
latest.json; - all five runtime npm packages plus
kandev, including a cleannpx kandev@latest; - the independent plugin SDK tag, signed retained receipt, exact npm archive, public Go/npm consumer checks, and
stable-complete; - Homebrew install/upgrade, a
Cellar/kandev/X.Y.Zinstall path, andkandev --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
mainwhere 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:
gpg --import .github/release-signing-key.asc
git fetch --tags origin
git tag -v vX.Y.ZCompare 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:
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 signaturesRepair 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:
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.ZBefore 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.