Files
kandev/docs/specs/npm-nightly-channel/spec.md
T
2026-08-16 21:12:19 +00:00

207 lines
11 KiB
Markdown

---
status: shipped
created: 2026-07-31
owner: kandev
---
# npm nightly channel
## Why
Users who want fixes from `main` currently have to build Kandev themselves or wait for the next
stable release. Maintainers also lack one supported prerelease path that exercises the same npm
launcher and native runtime packages users install in production.
## What
- Kandev publishes an npm nightly from `main` at 12:00 UTC when `main` contains commits after the
latest stable release and that exact commit has not already been published.
- Maintainers can run the same Nightly path manually from `main`. A manual Nightly dry run performs
metadata and registry preflight only, with no platform builds or npm writes.
- A nightly for stable `X.Y.Z` and commit `abcdef123456...` has version
`X.Y.(Z+1)-nightly.shaabcdef123456`.
- The 12-hex abbreviation is an accepted compactness trade-off. Before an existing version can
skip publication, Git must resolve that abbreviation to the exact scheduled commit; an ambiguous
prefix or identity mismatch fails closed for maintainer resolution.
- Every nightly publishes `kandev` and all five `@kdlbs/runtime-*` packages at the same immutable
version under the npm `nightly` dist-tag. The stable `latest` dist-tag does not move.
- Users persistently install the channel with `npm install -g kandev@nightly`. The command
`npx -y kandev@nightly` runs a transient Nightly copy without changing a global installation.
- Settings > System > Updates exposes Stable and Nightly for a verified, Kandev-managed npm or
npx user service. Stable is the default and the setting is install-wide.
- An npx run is managed only after it installs the service (for example,
`npx -y kandev@nightly service install`); a transient `npx -y kandev@nightly` run alone neither
creates a managed service nor persists an update-channel selection.
- Desktop, Homebrew, Scoop, local-checkout, unknown, unmanaged, and system-service installations remain
on the Stable channel.
- Stable update discovery continues to use GitHub Releases. Nightly discovery follows npm's
`kandev@nightly` dist-tag.
- Applying an npm/npx update submits the exact version shown in the Updates page. The backend
accepts it only while it still matches the selected channel's cached target, then installs that
immutable version; a changed cache returns `409 Conflict` and requires a refresh.
- Nightly publication creates no Git tag, GitHub Release, changelog commit, desktop update,
container tag, Homebrew update, or Scoop update. Scoop remains a Stable-only channel.
Decision: [ADR-2026-07-31-npm-nightly-release-channel](../../decisions/2026-07-31-npm-nightly-release-channel.md).
Implementation plan: [npm nightly channel](../../plans/npm-nightly-channel/plan.md).
## Data model
The install-wide `settings` table contains key `updates_channel`. Its value is the UTF-8 string
`stable` or `nightly`; a missing or invalid value reads as `stable`.
The existing stable target cache remains in these `kandev_meta` keys:
- `latest_version`
- `latest_version_url`
- `latest_version_checked_at`
Nightly uses an isolated cache:
- `latest_version_nightly`
- `latest_version_nightly_url`
- `latest_version_nightly_checked_at`
Changing channel never reinterprets cached data from the other source.
## API surface
`GET /api/v1/system/updates` and `POST /api/v1/system/updates/check` retain their current fields
and add:
```json
{
"channel": "stable",
"channel_editable": true,
"channel_unsupported_reason": ""
}
```
`PATCH /api/v1/system/updates/channel` accepts:
```json
{ "channel": "nightly" }
```
It returns the complete updates response for the selected channel. Invalid channel names return
`400`. Selecting Nightly for an unsupported installation returns `409`. Persistence or resolver
failures return `500` or `502` using the existing System handler error conventions.
The npm registry contract is the public metadata document for package `kandev`. The resolver reads
`dist-tags.nightly`, requires a valid SemVer, and requires the same version to exist in `versions`.
## Permissions
Anyone allowed to view System settings may read update status. The existing admin guard applies to
manual checks, channel changes, and update application. Channel choice is shared by all users of
the installation.
## Failure modes
- A GitHub or npm discovery failure preserves that channel's previous cache and surfaces the stale
checked time plus the request error.
- Apply never re-resolves a moving channel tag. It locks the selected-channel cache, rejects a
submitted `target_version` that no longer matches, and writes the matching exact version into the
update intent.
- A malformed or missing npm `nightly` tag fails closed; it is never offered or installed.
- A failed channel save keeps the draft dirty and surfaces that save failure instead of retaining a
stale manual-check error or retry countdown.
- A scheduled run with no commits after the stable tag exits successfully without building.
- A manual Nightly dispatch from a ref other than `main` fails before resolving metadata.
- Manual Nightly rejects the Stable-only desktop-validation and backfill modes. The workflow's
required bump selector is ignored for Nightly, while `dry_run` remains supported.
- A scheduled retry for an already-published commit exits successfully only when the main package
and `nightly` tag agree.
- Before building, the workflow resolves the commit prefix in the current `nightly` tag. A rerun
behind a newer published commit is superseded and exits without building. The same commit skips
only when every package exists and its `nightly` tag matches; an incomplete current target
proceeds to repair. Divergent or unresolvable history fails closed.
- A 12-hex collision makes Git abbreviation resolution ambiguous, so the run fails closed instead
of treating the colliding commit as already published.
- Stable and Nightly workflow runs share one non-cancelling release-wide concurrency group. This
covers Stable tag creation through npm publication, so Nightly cannot derive a version while a
newly tagged Stable release is still pending on npm.
- Before building and again before publishing, Nightly requires the highest stable Git tag to match
`kandev@latest`. A pending Stable tag or a scheduled commit superseded by the current Stable tag
exits successfully without publishing.
- Before publishing, a Nightly run rechecks `kandev@latest`. If the baseline moved while Nightly
was building, the stale run exits without publishing.
- The same locked preflight requires `kandev@nightly` to equal the value observed before building;
an overlapping run that already moved the tag supersedes the stale run.
- Runtime packages publish before `kandev`. If any runtime fails, the main launcher is not
published, so no visible launcher references missing exact dependencies.
- Trusted-publisher OIDC is used only by `npm publish --tag nightly`. An existing version whose
`nightly` tag does not match fails with recovery guidance because OIDC cannot run
`npm dist-tag add`.
- GitHub's scheduled start may be later than 12:00 UTC; delayed execution does not change the
deterministic version.
## Persistence guarantees
- Channel choice and both target caches survive backend restarts.
- One source failure never overwrites the other source's cache.
- Each full commit deterministically maps to one npm version, so a retry never creates a second
version for the same source state. The accepted 12-hex collision case halts automatic
publication rather than mapping a second commit to the existing package.
- npm nightly versions are immutable and retained; this feature performs no automated deletion.
## Scenarios
- **GIVEN** stable `0.82.0` and `main` commit `abc123def456...`, **WHEN** the nightly schedule runs,
**THEN** all six packages publish as `0.82.1-nightly.shaabc123def456` under `nightly` and
`kandev@latest` is unchanged.
- **GIVEN** an eligible `main` commit, **WHEN** a maintainer manually dispatches the Nightly
channel, **THEN** the same metadata, shared builds, safeguards, and npm publication run as the
schedule.
- **GIVEN** a manual Nightly with `dry_run=true`, **WHEN** its preflight resolves an eligible
target, **THEN** the run reports that exact version and commit without building bundles or
changing npm.
- **GIVEN** a manual Nightly dispatched from a non-`main` ref or combined with desktop validation
or backfill, **WHEN** validation runs, **THEN** it fails with an actionable input error.
- **GIVEN** `main` points at the latest stable tag's commit, **WHEN** the nightly schedule runs,
**THEN** it exits successfully without a platform build or npm publication.
- **GIVEN** the highest Stable Git tag is newer than npm `latest`, **WHEN** Nightly prepares or
publishes, **THEN** it exits successfully rather than deriving from the stale npm baseline.
- **GIVEN** a Nightly schedule was queued before a Stable release superseded its commit, **WHEN**
that Nightly starts after Stable completes, **THEN** it exits successfully without building.
- **GIVEN** the current `main` nightly already exists and `kandev@nightly` points to it, **WHEN**
the schedule runs again, **THEN** it exits successfully without rebuilding.
- **GIVEN** a previous run published only some runtime packages, **WHEN** the same commit retries,
**THEN** matching packages are accepted, missing packages publish, and `kandev` publishes last.
- **GIVEN** an older run left only some runtime tags advanced, **WHEN** a later `main` commit runs,
**THEN** ancestor-tagged partial packages are repaired by the later publish without manual tag
edits.
- **GIVEN** no channel setting, **WHEN** a user opens Updates, **THEN** Stable is selected and the
target comes from GitHub Releases.
- **GIVEN** a verified npm managed user service, **WHEN** an admin selects and saves Nightly,
**THEN** the setting survives reload and the target resolves from `kandev@nightly`.
- **GIVEN** a Nightly save is pending, **WHEN** the admin changes the draft back to Stable before
the response arrives, **THEN** the returned Nightly state becomes the saved baseline while the
newer Stable draft remains selected, dirty, and available for a follow-up save.
- **GIVEN** a Homebrew, Desktop, unmanaged, system-service, local, or unknown installation,
**WHEN** Updates renders, **THEN** Nightly is unavailable with a visible reason and Stable stays
effective.
- **GIVEN** an installed nightly whose SHA sorts lexically after the new target SHA, **WHEN** npm's
`nightly` tag changes, **THEN** the new unequal target is offered; SHA text is not treated as a
chronological counter.
- **GIVEN** a user running a nightly selects Stable, **WHEN** a valid stable target differs,
**THEN** the UI offers an explicit return to that exact stable version without announcing it as
a normal upgrade notification.
- **GIVEN** the Updates page shows target A and discovery changes the selected-channel cache to B,
**WHEN** the user submits Apply for A, **THEN** the backend returns `409 Conflict` and installs
neither target.
- **GIVEN** a Pixel 5 viewport, **WHEN** the user selects Nightly and saves, **THEN** the same
persisted outcome is reachable through 44px rows with no horizontal document overflow.
## Out of scope
- Homebrew `HEAD`, a nightly formula, or a second tap
- Desktop nightly updater feeds
- GHCR nightly tags
- Nightly GitHub Releases or Git tags
- Automatic update application
- Per-user channels
- Additional beta/canary channels
- Timestamped versions