@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
303 lines (251 loc) • 17 kB
Markdown
# CI and release
Pull requests run lint, typecheck, the Node unit suite on Linux Node 24, and
the Python control-plane suite against PostgreSQL 17. Pushes to `main` also run
the Node suite and the full OS × Node matrix.
The complete suite with coverage floors runs nightly, every merge to `main`
cuts a patch release, and every release is verified before it reaches npm.
This page explains which checks run where, how a merge becomes a published
version, and which smoke tests stay manual. It is for contributors and
maintainers.
## The pipeline at a glance
A merged pull request flows through CI, an automatic tag, a verified GitHub
release and an npm publish; the nightly job adds the slower suites.
```mermaid
flowchart LR
PR[Pull request] -->|"Node shards, Python/Postgres, docs lint, plugin validation"| REQ["CI / required"]
REQ --> MERGE{Merged?}
MERGE -->|yes| MAIN[main]
MAIN -->|"unit shards plus validate:pr matrix"| PUSH[Push CI]
MAIN -->|"Auto-Release: next patch tag"| TAG[Tag vX.Y.Z]
TAG -->|dispatch| REL[Release workflow]
REL -->|"verify, stamp version, pack"| GH[GitHub release<br/>kxm-X.Y.Z.tgz]
GH -->|"sha256 verified"| NPM[npm publish]
MAIN -->|"daily 04:00 UTC"| NIGHT[Nightly<br/>complete suite]
```
All workflows live in `.github/workflows/`. Linux jobs run on the organization's
ARC runner scale set. Validate also runs two Windows legs on GitHub-hosted
`windows-latest`. `test/core/ci-contract.test.ts` pins the runner selectors, job
names, coverage floors and release triggers. Change a workflow and that test
together.
## What runs where
| Trigger | Workflow (job) | What it runs |
|---|---|---|
| Before you push | Local | `npm run verify` |
| Pull request, push to `main`, or manual | `ci.yml` (`required`) | Aggregates the lanes below into one pass/fail check named `CI / required` |
| Pull request and push | `ci.yml` (Docs lint) | `lint:docs` (markdownlint and the `plans/` relative-link check) and `check:versions`, always |
| Code pull request and code push | `ci.yml` (Unit engine and Unit, Linux Node 24) | `engine.test.ts` split by test name; `permission.test.ts`, `runtime.test.ts`, and `package-install.test.ts` one file at a time; every other unit file in two light shards. Typecheck and `check-generated` run on `light-1` |
| Code pull request and code push | `ci.yml` (Python control plane) | Python 3.13 `unittest` contracts from the frozen uv workspace against PostgreSQL 17 started by `scripts/ci-postgres-17.sh` |
| Platform-sensitive pull request | `ci.yml` (Unit, Windows Node 24) | The same unit lanes on `windows-latest` |
| Push to `main`, or manual | `ci.yml` (Validate matrix) | `validate:pr` on Linux and Windows for Node 22.19.0 and Node 24; not on a pull request |
| Code pull request and code push | `ci.yml` (Plugin validation) | `claude plugin validate --strict` on the marketplace and the plugin |
| Daily at 04:00 UTC, or manual | `nightly.yml` | `test:coverage:complete`, `check`, `check:generated`, `npm pack --dry-run` |
| Monday at 13:00 UTC, or manual | `refresh-models.yml` | Fetches the public OpenRouter catalog, and opens or updates one pull request on `automation/model-refresh` when `.kxm/prices.yaml` or `.kxm/models/inventory.yaml` changes. Uses `GITHUB_TOKEN` only, then dispatches `ci.yml` on that branch |
| Merged pull request | `auto-release.yml` | Tags the merge commit and dispatches `release.yml` |
| Tag push or dispatch | `release.yml` | Verifies, packs and publishes (see [Release flow](#release-flow)) |
| Manual only | `smoke.yml` | Real Pi smoke, currently disabled (see [Smoke tests](#smoke-tests)) |
| Pull request, or manual | `e2e.yml` | `npm run e2e` on `ubuntu-latest`: Obscura v0.2.3 plus the Playwright smoke test. Separate from `ci.yml` |
The npm scripts behind those rows:
| Script | Composition |
|---|---|
| `verify` | `npm test` (core and package unit tests), `check`, `check:generated` |
| `test:ci-shard` | One unit lane: build, then `engine <index> <total>`, `serial`, `light`, or `light-<index>` over `test/core/*.test.ts` and `packages/core/*/tests/unit/*.test.ts` |
| `validate:pr` | `build`, `typecheck`, a compact contract and smoke set of nine `test/core` files, `check:versions`, and the generated-`dist` check |
| `validate:ci` | `test:coverage` (core and package tests, 91/80/92 floors), `check`, `npm pack --dry-run`; the Release workflow runs it, and it stays available locally |
| `test:coverage:complete` | Core, simulation and package tests with 93/80/93 floors |
| `check` | `typecheck`, `lint:docs`, `check:versions` |
What moved off the pull-request lane, and what did not:
- The four `validate:pr` cells — Linux and Windows, Node 22.19.0 and Node 24 —
run on a push to `main` and on `workflow_dispatch`. They do not run on a
pull request. The nine files inside `validate:pr` still run on a code pull
request, because they are part of the Linux Node 24 unit suite.
- Node 22.19.0 does not run the unit suite on a pull request. It runs
`validate:pr` on `main`.
- Windows runs the unit suite on a pull request only when the classifier marks
the change platform-sensitive. Every push to `main` that changes code still
runs `validate:pr` on Windows.
- Coverage, `test/simulations`, and `npm pack --dry-run` stay in the nightly
complete suite. They were not part of pull-request CI before this split.
- Python control-plane validation and Plugin validation still run on code pull
requests and code pushes.
Run `npm run verify` locally before every push. A nightly failure is a release
blocker: it is the only place the simulation suite and coverage floors run.
### Lanes and the required check
Code pull requests run Docs lint, two Linux Node 24 engine shards, a serial
lane (`permission.test.ts`, then `runtime.test.ts`, then
`package-install.test.ts`), two light shards for every other Node unit file,
the Python 3.13 control-plane suite against PostgreSQL 17, and Plugin
validation. The Python lane stays on `kontextmind-doks`. That scale set has
no container-engine socket, so the job starts PostgreSQL 17 with
`scripts/ci-postgres-17.sh` on `127.0.0.1:5432` and uses the same admin and
test DSNs as the contributor procedure in [Develop KXM](development.md).
`light-1` typechecks and checks generated bundles.
`engine.test.ts` is split by test name because that file alone was 174
seconds; the serial lane keeps the next longest files off the light pool,
which was 268 seconds when every non-engine file shared one job.
`package-install.test.ts` is serial because on Windows it took 171 seconds
and the light job stopped reporting tests after that file finished (cancelled
at 20 minutes on pull requests #361, #362, and #363). The other light files
are round-robin split so each Windows job is about half of that pool. A
locked test directory is abandoned after about three seconds. The previous
retry was about 52 seconds and still failed with `EPERM`, which is what
pushed Windows `light-1` to 13 minutes when many tests hit it.
`test/core/studio-layout.test.ts` is platform-sensitive because it starts
the real runtime supervisor; a Windows file lock after shutdown failed
`light-2` and cancelled the other Windows jobs. Linux jobs use the npm
cache from `actions/setup-node`. Restoring a
`node_modules` tarball was slower than `npm ci` on the Linux runners (about
24s versus 17s on 2026-09-24), so that cache stays on the Windows jobs,
where `npm ci` is the slow step.
The job `required` always runs. Its check name is `CI / required`. It fails
when a lane fails or is cancelled, and it passes when a lane was skipped
because the change did not need it. Add `CI / required` as a required status
check in the `protect-main` ruleset. Skipped matrix legs are not required
names, so they do not block auto-merge. This repository change does not edit
that ruleset.
A newer push cancels an older pull request run. Runs on `main` are never
cancelled. Unit and Validate matrices use `fail-fast`.
The Validate matrix still runs on Node 22.19.0 and Node 24, on Linux and
Windows, for pushes to `main` and for `workflow_dispatch`. Linux uses the ARC
scale set `kontextmind-doks` with a three-minute job timeout. Windows uses
GitHub-hosted `windows-latest` with a fifteen-minute timeout so `npm ci` can
finish. Those four job names are not the required check anymore.
### CI jobs stay queued while a runner is online
Every Linux workflow targets the ARC runner scale set `kontextmind-doks`. That
name is the scale set, not a custom label on a repository runner. The scale set
belongs to the selected-repository runner group `KontextMind DOKS ARC`, which
must allow this public repository. The legacy repository runner `km-gh-rn01`
must not carry the `kontextmind-doks` label; adding it bypasses ARC and
serializes the build queue.
Check GitHub routing first:
```bash
gh api repos/kontextmind/kxm/actions/runners \
--jq '.runners[] | {name, status, busy, labels: [.labels[].name]}'
gh api orgs/kontextmind/actions/runner-groups \
--jq '.runner_groups[] | select(.name == "KontextMind DOKS ARC") |
{name, visibility, allows_public_repositories}'
```
Then check ARC in the cluster:
```bash
kubectl -n arc-runners get autoscalingrunnerset kontextmind-doks
kubectl -n arc-runners get ephemeralrunners,pods
```
The capacity policy keeps one warm runner, bursts to four, and requests three
CPUs per runner so the node-pool autoscaler adds capacity instead of packing
CPU-bound jobs onto busy nodes. If jobs stay queued while the listener is
assigned zero jobs, check the runner group's selected repository and public
repository access. If ARC has pending pods, check node capacity and the cluster
autoscaler. Do not relabel `km-gh-rn01` or push an empty commit as a routing
workaround.
> [!NOTE]
> Windows jobs use GitHub-hosted `windows-latest`, not a self-hosted homelab
> runner and not the ARC scale set. Do not add a `kontextmind-doks` label to a
> Windows runner. Nightly complete coverage and release stay on Linux.
> Platform-sensitive pull requests run the unit suite on Windows, which
> includes `test/core/worker.test.ts`. The main Validate legs run the compact
> `validate:pr` gate.
### The docs-only classifier
The first job, Classify changes, lists the changed paths and sets `code=false`
when every path matches `*.md` (including `plans/**/*.md`), `docs/**`,
`.kxm/assets/**`, `LICENSE`, the issue and PR templates, or `dependabot.yml`.
A non-markdown file under `plans/` is code. Unit lanes, the Python control-plane
lane, Plugin validation, and the Validate matrix are skipped. Docs lint runs,
and `CI / required` passes. `scripts/ci-classify.mjs` and
`ci-contract.test.ts` pin this behavior.
`platform=true` when a path is a package manifest, the lockfile, a file under
`scripts/` or `.github/workflows/`, or a filename that names path, process,
shell, spawn, worker, supervisor, repo-root, or ssh-remote behavior. A
platform-sensitive pull request also runs the Windows Node 24 unit lanes.
> [!WARNING]
> Several tests and code paths read documentation files by path. A pull request
> that only moves or renames a pinned doc is classified documentation-only, so
> CI does not run the tests that would catch the broken path; the next code
> change fails instead. For any docs move, update the pinned readers in the same
> pull request (see [Pinned paths](writing-docs.md#pinned-paths)) and run
> `npm run verify` locally before you push.
## Release flow
KXM releases a patch version for every merged pull request. No one bumps
versions by hand.
1. **Auto-Release** (`auto-release.yml`) runs when a pull request to `main` is
merged. It finds the newest `vX.Y.Z` tag, adds one to the patch number, tags
the merge commit, and dispatches the Release workflow for that tag.
2. **Release** (`release.yml`) checks out the tag, and takes its release helper
scripts from `main`. It runs `npm run check:generated` against the tagged
tree, stamps the tag's version into every version surface, runs
`npm run validate:ci`, and packs `kxm-<version>.tgz`. The step summary records
the tarball's sha256.
3. `scripts/kxm-release-github.mjs` creates a draft GitHub release, uploads the
tarball, and checks the digest GitHub reports against the local one. The
workflow then publishes the draft.
4. **Publish npm** runs in the protected `npm-publish` environment. It skips a
version already on npm. Otherwise `scripts/kxm-publish-npm.mjs` confirms the
GitHub release is published, downloads the asset, verifies its sha256
against the release digest, and runs `npm publish` on that exact file. The
job then polls `npm view @kontextmind/kxm@<version> version` (up to 20
minutes, `KXM_NPM_VISIBLE_WAIT_MS=0` skips the wait) and reports success
only after the registry shows that version. Log lines that say the GitHub
release is public are not an npm publish. The registry can lag the GitHub
release by many minutes.
Every step is idempotent. Rerunning the Release workflow for a tag that is
already published or already on npm reports it and changes nothing.
### Skip a release
Auto-Release skips a merge when the pull request has the `no-release` label, or
when its head branch starts with `chore/release-`.
### Versions
The version in the repository's `package.json` stays at its base value. The
release job stamps the tag's version into the package, the lockfile, the plugin
package and manifest, the marketplace entry, the MCP server constant, and every
workspace package, in the runner's workspace only. The stamp is never committed
back.
- Do not edit version fields in a pull request. `npm run check:versions` fails
when any surface differs from the root `package.json`.
- Auto-Release only increments the patch number. For a minor or major release,
push a `vX.Y.0` tag yourself; `release.yml` also runs on tag pushes, and
later merges continue from that tag.
- `scripts/kxm-bump-version.mjs` and `scripts/check-versions.mjs` read the same
package list from `scripts/package-surfaces.mjs`, so the gate and the writer
cannot disagree.
### Changelog
Add user-visible changes to `CHANGELOG.md` under `## Unreleased`, in the same
pull request. Keep an entry to a few lines and link the doc page that explains
the behavior. Add an `### Upgrade note` when an operator must act.
No release step moves those entries into a dated section. Auto-Release tags a
patch for each merged pull request, but nothing cuts the changelog, so
everything released since its newest dated section is still listed under
`## Unreleased`.
## Smoke tests
Smoke tests prove what unit tests cannot: that the packed artifact installs,
and that real harnesses complete a turn.
| Smoke | How to run | What it proves |
|---|---|---|
| Packed install | Part of the core suite: `test/core/package-install.test.ts` | The packed CLI and hub run from a clean local and global install |
| Real Pi smoke | `KXM_SMOKE=1 KXM_SMOKE_MODELS=<model-a>,<model-b> node scripts/smoke-multi-pi.mjs` | Two supervised Pi workers discover each other, exchange requests and replies, and recover across a restart |
| Container install | `just docker-install-smoke` | The tarball installs into a fresh container with a fresh Pi, and two providers complete a live turn |
The real Pi smoke calls paid models, so it never runs by default. Its workflow,
`smoke.yml`, is manual and stays skipped until the repository variable
`KXM_SMOKE_RUNNER` names a runner with Pi credentials. Both smoke models run as
long-lived Pi workers, so each must pass the Pi native-vendor brake: pick two
non-native routes such as
`openrouter/qwen/qwen3.7-plus,openrouter/z-ai/glm-5.3-flash`, never `xai/…`
or `anthropic/…`. The container smoke needs
Docker, a `pass-cli` session and a local Pi model store. It keeps secrets in a
mode-600 file inside a temporary directory; set `KXM_SMOKE_VAULT` to read keys
from your own vault.
### Manual checks before announcing a release
Automation cannot prove that a harness UI renders correctly. Before you
announce a release:
1. Install the published version:
`npm install --global --omit=peer @kontextmind/kxm@<version>`.
2. Connect two current Pi sessions, run `/kxm hub`, and complete one inbound
round trip.
3. Install the marketplace plugin in a clean Claude Code profile and confirm
that `kxm_list` answers.
4. Check pushed channel delivery only when the target Claude Code version
supports channels.
If a behavior can only be checked by hand, add it to this list rather than
implying automated coverage.
## Related
- [Develop KXM](development.md): the local gate and generated artifacts
- [Test matrix](test-matrix.md): which test proves which behavior
- [Write KXM documentation](writing-docs.md): doc gates and pinned paths
- [Upgrade KXM](../operations/upgrade.md): the operator side of a new version
- [Project knowledge wiki](../../.kxm/knowledge/index.md): authored notes for [CI](../../.kxm/knowledge/patterns/ci.md) and [release](../../.kxm/knowledge/patterns/release-flow.md)