* feat(release): npm staged publishing + pre-publish boot-smoke (WS1.3) v3.8.47 shipped an npm tarball that crashed on every boot and had to be deprecated — the publish path had no runtime gate and the owner's 2FA happened BEFORE any proof. Two changes to npm-publish.yml: - check:pack-boot runs right before any publish (dist/ is already assembled by build:cli in the same job) — a non-booting tarball now fails the workflow before anything reaches the registry. - npm publish becomes 'npm stage publish' (staged publishing, GA 2026-05-22, npm >= 11.15 ensured in-job): the exact bytes are parked on the registry but NOT installable until the owner runs 'npm stage approve <id>' with 2FA. The workflow summary prints the approve/verify/reject flow; RELEASE_CHECKLIST documents the owner flow, the one-time Trusted Publisher stage-only config, and the deprecate-first rollback playbook. publish_mode=direct (workflow_dispatch) is the emergency fallback to the legacy immediate publish. First real-registry exercise happens on the next release with the fallback one dispatch away (D2 decision, v3.8.49 plan). GitHub Packages secondary publish unchanged. YAML parse validated. * docs(release): reference upcoming verifier without file paths (docs-all strict) * fix(release): pin npm 11.15.0 in the staged-publish version guard (no @latest in the publish job)
19 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| Release Checklist | 3.8.40 | 2026-06-28 |
Release Checklist
Last updated: 2026-06-28 — v3.8.40 Streamlined release flow that leverages Claude Code skills for automation.
Keep the queue/branch green between releases: see RELEASE_GREEN.md (
/green-prsfamily +npm run check:release-green+/babysit+ nightly). Running this periodically — and especially before this checklist — makes the release PR start green.
TL;DR
# 1. Bump version + generate CHANGELOG (skill)
/version-bump-cc patch # or minor/major
# 2. Run quality gate locally
npm run check # lint + tests
npm run test:coverage # full coverage gate (60/60/60/60)
# 3. Build & smoke
npm run build
npm run test:e2e # optional but recommended
# 4. Generate release (skill)
/generate-release-cc
# 5. Deploy (skill)
/deploy-vps-both-cc # or akamai-cc / local-cc
# 6. Capture release evidences (skill)
/capture-release-evidences-cc
npm Staged Publishing (default since v3.8.49 — WS1.3/D2)
The npm-publish workflow no longer publishes directly: it boots the packed tarball
(check:pack-boot) and then runs npm stage publish — the exact bytes are parked on
the registry, not installable until the owner approves. The human 2FA gate moved
to AFTER the proof, not before it.
Owner flow after the workflow goes green:
npm stage list omniroute— find the stage id (also printed in the workflow summary).- Verify the staged bytes (recommended):
npm stage download <id>, then install the downloaded tarball into a temp prefix and boot it (npm run check:pack-bootautomates the same pack→install→boot verdict in CI). npm stage approve <id>— the 2FA prompt IS the publish.npm stage reject <id>discards.- Post-publish net: the post-publish verifier (WS1.4 of the v3.8.49 plan) installs the published version from the public registry in a clean container and boots it.
Emergency fallback: workflow_dispatch with publish_mode=direct restores the
legacy immediate npm publish (use only if staging itself misbehaves; record why).
One-time hardening (owner, npmjs.com): configure the Trusted Publisher for
omniroute in stage-only mode so a leaked long-lived token cannot npm publish
directly from anywhere — CI can only stage; only the owner's 2FA releases.
Broken-artifact playbook (unchanged): npm deprecate omniroute@<bad> "<reason> — use <fixed>"
as the default reflex (minutes, reversible); npm unpublish only inside the 72h/no-dependents
window and never as the first move. Docker: never rewrite a version tag — rollback is
repointing latest to the last good digest.
Hotfix Fast-Lane (label hotfix)
A PR labeled hotfix skips the heavy CI matrix (9-shard E2E, coverage ratchet,
quality-gate, quality-extended) and keeps the fast, high-signal gates: build,
unit shards, integration, vitest, lint/typecheck, docs-sync, check:pack-artifact
and the tarball boot-smoke (check:pack-boot). Target: green in ≤15min instead of ~33min.
Entry policy — all four required (modeled on Chromium/VS Code/Node emergency lanes):
- Severity: production is broken — a published artifact crashes on boot / a security fix / every user of the release is affected. "Important" is not "broken".
- Authority: only the repository owner applies the
hotfixlabel. The label IS the approval — never self-serve on a campaign PR. - Evidence: the PR body links the previous fully-green heavy run (the suite the skipped jobs would re-validate) plus the fix's own failing-then-passing test.
- Scope: cherry-pick-only — the minimal fix, no refactors, no ride-alongs.
The skipped coverage/ratchet surface is re-validated by the next full run on the
release branch (continuous release-green) — the lane skips WAITING, never validation.
Tests-only diffs (all files under tests/, none under tests/e2e/) skip the E2E
matrix automatically, without any label.
Detailed Checklist
Pre-release
- All PRs targeted to this release are merged to
release/vX.Y.0 - All open Linear/issue items for this version are closed or pushed to next milestone
- CI green on
release/vX.Y.0branch - No
TODO(release)markers in code:grep -r "TODO(release)" src/ open-sse/ - Docker base image up to date (currently
node:24.15.0-trixie-slim)
Version & Changelog
- Run
/version-bump-cc <patch|minor|major>(Claude Code skill)- Bumps
package.json,electron/package.json - Regenerates
CHANGELOG.mdfrom git commits since last tag - Updates README.md badges
- Bumps
- Manually review CHANGELOG.md and clean up commit messages if needed
- Ensure the latest semver section in
CHANGELOG.mdequalspackage.jsonversion - Keep
## [Unreleased]as the first changelog section for upcoming work - Update
docs/openapi.yaml→info.versionmust equalpackage.jsonversion
Code Quality
npm run lint— 0 errors (warnings are pre-existing)npm run typecheck:core— cleannpm run typecheck:noimplicit:core— clean (strict)npm run check:cycles— no circular depsnpm run check:any-budget:t11— within budgetnpm run check:route-validation:t06— cleannpm run check:node-runtime— supported runtime floor met (>=22.22.2 <23,>=24.0.0 <27, perSUPPORTED_NODE_RANGEinsrc/shared/utils/nodeRuntimeSupport.ts; aligned withpackage.jsonengines)
Testing
npm run test:unit— passnpm run test:vitest— pass (MCP server, autoCombo, cache)npm run test:coverage— gate 60/60/60/60 satisfied (statements/lines/functions/branches)npm run test:integration— pass (if changes touch DB / handlers)npm run test:combo:matrix— pass (combo strategy matrix: proves all 17 routing strategies' selection decisions deterministically; run when touching combo routing, strategy resolution, or fallback logic)RUN_COMBO_LIVE=1 npm run test:combo:live— optional/manual (gated real-upstream smoke; sources a read-only DB snapshot from VPSroot@192.168.0.15; hits real providers, costs credits; never runs in CI; skips cleanly without the gate)npm run test:combo:live:vps— optional/manual (Phase-3 VPS live smoke: 7 HTTP scenarios against the live.15server via plain Node ESM; requiresssh root@192.168.0.15; creates/deletes only__live_test__*combos; hits real providers; never runs in CI)npm run test:e2e— pass (UI changes)npm run test:protocols:e2e— pass (MCP/A2A changes)npm run test:ecosystem— pass
Hooks (Husky validated)
Husky hooks live in .husky/ and run automatically on git operations.
- pre-commit:
npx lint-staged + node scripts/check/check-docs-sync.mjs + npm run check:any-budget:t11 - pre-push: fast deterministic gates —
npm run check:any-budget:t11 && npm run check:tracked-artifacts(activated 2026-06-13). Intentionally excludestest:unit(slow; covered by the CItest-unitjob).- Run
npm run test:unitmanually before pushing release branches.
- Run
If a hook fails: fix the underlying issue, don't bypass with --no-verify.
Conventional Commits
All release-bound commits must follow type(scope): subject format.
Valid types: feat, fix, refactor, docs, test, chore, perf, style, ci
Valid scopes: db, sse, oauth, dashboard, api, cli, docker, ci, mcp, a2a, memory, skills, cloud-agent, guardrails, compression, auto-combo, resilience, providers, executors, translator, domain, authz
Breaking changes: add BREAKING CHANGE: footer or ! after the scope (e.g. feat(api)!: drop /v0).
Documentation
npm run check:docs-syncpasses (auto-run by pre-commit)npm run check:docs-allpasses (umbrella: docs-sync + docs-counts + env-doc-sync + deprecated-versions + doc-links)npm run check:env-doc-syncexits 0 — code ↔.env.example↔docs/reference/ENVIRONMENT.mdenv contract is intactnpm run check:doc-linksexits 0 — no broken internal markdown references after restructuringdocs/architecture/ARCHITECTURE.mdreviewed for storage/runtime driftdocs/guides/TROUBLESHOOTING.mdreviewed for env var and operational drift- If
.env.examplechanged:docs/reference/ENVIRONMENT.mdupdated - If new feature has a UI:
docs/guides/USER_GUIDE.mdmentions it - If new feature has API:
docs/reference/API_REFERENCE.md+docs/openapi.yamlupdated - If new feature is a module: dedicated
docs/<MODULE>.mdexists - If breaking change:
docs/guides/TROUBLESHOOTING.mdhas migration note
i18n
npm run i18n:checkexits 0 — translation state (.i18n-state.json) in sync with source docs (no drifted sources in strict mode; warn-mode advisory is acceptable for last-minute doc touch-ups, but should be 0 before tagging)npm run i18n:check-ui-coverageexits 0 — every UI locale at or above the 80% coverage floornpm run i18n:sync-ui:dryreports 0 missing keys across all 42 locales- If source English docs changed, run
npm run i18n:run(requiresOMNIROUTE_TRANSLATION_API_KEYin.env) before tagging - Translation contributions can be deferred to next release if minor (track in CHANGELOG)
Database Migrations
- If
src/lib/db/migrations/has new files:- Each migration is idempotent (
CREATE TABLE IF NOT EXISTS, etc.) - Migrations wrapped in transactions
- Numbered correctly (no gaps in sequence)
- Each migration is idempotent (
- Test on fresh install: delete
~/.omniroute/omniroute.dband runnpm run dev - Test on existing install: backup DB, run migration, verify schema
- WAL files (
-wal,-shm) handled correctly if migration rewrites tables
Provider Catalog (Zod-validated)
src/shared/constants/providers.tsZod schema valid at load time- All providers have required fields (
id,label,kind, etc.) freeNoteprovided for new free providers- OAuth providers have
oauthConfigregistered insrc/lib/oauth/constants/oauth.ts
- All providers have required fields (
- If new provider added: corresponding executor in
open-sse/executors/ - If non-OpenAI format: translator in
open-sse/translator/ - Models registered in
open-sse/config/providerRegistry.ts - Unit tests in
tests/unit/cover provider classification and routing
Desktop (Electron)
If electron/ changed:
npm run electron:smoke:packagedpasses- Builds tested for at least one of
:win,:mac,:linux - Code signing certs not expired (if signing)
electron/package.jsonversion matches rootpackage.json- Auto-update channel pointer updated if releasing to
stable
Build Layout
The repository uses three distinct output directories — never mix them up:
| Directory | Purpose | Tracked? |
|---|---|---|
src/ |
Application source (TypeScript / TSX) | Yes |
.build/ |
Build intermediates — next build output (distDir) |
No (gitignored) |
dist/ |
Shippable npm bundle — assembled by assembleStandalone |
No (gitignored) |
Operator note: the remote VPS image directory remains
/usr/lib/node_modules/omniroute/app/. Only the in-repo build output moved (app/→dist/). The deploy skills rsyncdist/contents into the remoteapp/dir — no VPS path changes required.
Single-build flow:
npm run build:release
└─ rm -rf .build dist (clean)
└─ next build → .build/next/ (intermediates)
└─ assembleStandalone (copies standalone + static + public + natives → dist/)
└─ writes dist/BUILD_SHA (HEAD sentinel)
Do NOT run npm run build followed by a separate npm run build:cli for deploy — use
npm run build:release which does a clean rebuild + sentinel in one command.
Artifact Validation
npm run build:releasesucceeds anddist/BUILD_SHA==git rev-parse --short HEADnpm run check:pack-artifactclean — noapp.__qa_backup,scripts/scratch,package-lock.json, or other local residuedist/server.jsexists after build
Tagging & Release
- Run
/generate-release-cc(Claude Code skill):- Creates tag
vX.Y.Z - Pushes tag and branch
- Opens GitHub Release with changelog body
- Attaches Electron installers (if built)
- Creates tag
- Or manually:
git tag -a vX.Y.Z -m "Release vX.Y.Z" git push origin vX.Y.Z gh release create vX.Y.Z --notes-from-tag
Deploy
Deploy skills use the light rsync flow — no npm pack, no npm i -g:
- Use deploy skill that matches target:
/deploy-vps-local-cc— local VPS (192.168.0.15)/deploy-vps-akamai-cc— Akamai VPS (69.164.221.35)/deploy-vps-both-cc— both
- Before deploying, confirm
dist/BUILD_SHA==git rev-parse --short HEAD - Build must run where
node_modulesis real (main checkout ornpm ci'd worktree — NOT a symlinked worktree) - Smoke test deployed instance:
- Open
/dashboard/health→ check version string matches release - Run a
/v1/chat/completionsrequest against a known provider - Verify
/api/monitoring/healthreturnsCLOSEDcircuit breakers - Confirm MCP transports respond (
/mcpHTTP,/mcp-sseSSE)
- Open
Post-release
- Run
/capture-release-evidences-cc(Claude Code skill)- Captures WebP screenshots/recordings of new features
- Attaches to release notes / blog post
- Update GitHub Discussions / Discord with release announcement
- Open milestone for next version
- If critical: pin discussion or post in
news.jsonfor in-app banner
Embedded Services smoke (v3.8.4+)
Before shipping any release that includes embedded services changes, verify:
Fresh-DB boot (catches migration collisions — added after v3.8.4 hotfix)
DATA_DIR=$(mktemp -d) npm start &— wait 10 s for bootcurl -s http://127.0.0.1:20128/api/services/9router/status | jq '.tool'returns"9router"(NOT 404, NOT 500). Confirms migration071_services.sqlapplied + row seeded.sqlite3 $DATA_DIR/storage.sqlite "PRAGMA table_info(version_manager);" | grep -E "provider_expose|logs_buffer_path|last_sync_at"returns 3 rows.sqlite3 $DATA_DIR/storage.sqlite "PRAGMA table_info(webhooks);" | grep -E "kind|metadata_encrypted"returns 2 rows (validates070_webhooks_kind_metadata.sqlapplied).node --import tsx/esm --test tests/unit/db/no-migration-collisions.test.tspasses — guards against future collisions.
9Router
POST /api/services/9router/installreturns 200 withinstalledVersionin under 2 minPOST /api/services/9router/startreturns 200 andstate: "running"in under 30 sGET /api/services/9router/statusreportshealth: "healthy"POST /v1/chat/completionswith"model": "9router/auto/..."returns 200 (end-to-end routing through 9Router)GET /dashboard/providers/services/9router/embed/dashboardrenders the 9Router native UI inside the proxy (no direct127.0.0.1:portiframe)POST /api/services/9router/rotate-keyreturns{ keyRotated: true }and service restarts cleanlyPOST /api/services/9router/stopreturns 200 andstate: "stopped"GET /api/services/9router/logs?tail=50returns SSE stream withsnapshotevent containing recent lines- Install in environment without
npmin PATH returns 500 with a friendly (non-stack-trace) error message
CLIProxyAPI
POST /api/services/cliproxy/installreturns 200 in under 2 minPOST /api/services/cliproxy/startreturns 200 andstate: "running"in under 30 sGET /api/services/cliproxy/statusreportshealth: "healthy"POST /api/services/cliproxy/stopreturns 200 andstate: "stopped"GET /api/services/cliproxy/logs?tail=50returns SSE stream
Security regression
curl -H "X-Forwarded-For: 1.2.3.4" http://localhost:20128/api/services/9router/startreturns403 LOCAL_ONLYcurl -H "X-Forwarded-For: 1.2.3.4" http://localhost:20128/api/services/cliproxy/startreturns403 LOCAL_ONLY- Error responses from
/api/services/*do not containerr.stackor absolute file paths
v3.8.0+ checks
Before shipping any v3.8.x release, verify these additional items:
omniroute --trayboots on macOS (systray2 installed into~/.omniroute/runtime/)omniroute --trayboots on Linux (requires DISPLAY; graceful error if not set)omniroute --trayboots on Windows (PowerShell NotifyIcon, no extra binaries)omniroute config tray enablecreates autostart entry; disable removes itnpm install -g omniroute@<this-version>runs postinstall without fatal exit- Update path keeps optional deps:
omniroute update --applyand the auto-updater runnpm install -g … --include=optionalsooptionalDependencies(better-sqlite3, keytar, tls-client, and the llmlingua SLM stack:@atjsh/llmlingua-2,@huggingface/transformers@3.5.2,@tensorflow/tfjs,js-tiktoken) survive an update.@huggingface/transformersstays optional so itsonnxruntime-nodeCUDA provider postinstall cannot abort installation on CUDA 11 hosts. The ultramodelPathSLM tier also needs the tinybert model, auto-downloaded to${DATA_DIR}/models/llmlinguaon first use. Postinstall (scripts/build/colocateOptionals.mjs) then co-locates the SLM optional closure intodist/node_modulesso the worker resolves a SINGLE@huggingface/transformers3.5.2 optional instance — the standalone trace bundles only transformers, not the dynamically-imported optionals, so without this the worker would load llmlingua-2 against the root's transformers and the SLM tier would silently fail-open. omniroute statusworks with no.env(CLI token path, loopback only)curl http://localhost:20128/api/shutdownreturns 401 (always-protected route)curl -H "host: evil.com" http://localhost:20128/api/mcp/ssereturns 401 (loopback guard)- SQLite runtime resolves to
bundledon first run (bundled binary valid for platform) - SQLite runtime falls back to
runtimewhennode_modules/better-sqlite3is deleted - Smart MCP filter compresses real
playwright-mcp browser_snapshotoutput (≥50% reduction) - All 10
skills/omniroute*/SKILL.mdfiles are publicly fetchable via raw GitHub URL - Onboarding wizard shows "How It Works" tier tour step on fresh setup
- Home dashboard tier coverage widget shows configured/active counts
Rollback
If release has critical issue:
gh release edit vX.Y.Z --prerelease(marks as not latest)git tag -d vX.Y.Z && git push --delete origin vX.Y.Z(only if not yet adopted by users)- Or: hotfix on
release/vX.Y.0→ patch releasevX.Y.(Z+1) - Communicate in GitHub Discussions and Discord immediately
Hard Rules
- Never commit directly to
main - Never use
git push --forcetomainorrelease/*branches - Never skip Husky hooks (
--no-verify) - Never commit secrets, credentials, or
.envfiles - Coverage must stay ≥60/60/60/60 (statements/lines/functions/branches)
- Always include or update tests when changing production code in
src/,open-sse/,electron/, orbin/
Automated Sync Check
Run the docs sync guard locally before opening a PR:
npm run check:docs-sync
CI also runs this check in .github/workflows/ci.yml (lint job).