docs/ops/RELEASE_CHECKLIST.md
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.
# 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
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).npm stage download <id>, then install the
downloaded tarball into a temp prefix and boot it (npm run check:pack-boot automates
the same pack→install→boot verdict in CI).npm stage approve <id> — the 2FA prompt IS the publish. npm stage reject <id> discards.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)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):
hotfix label. The label IS
the approval — never self-serve on a campaign PR.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.
release/vX.Y.0release/vX.Y.0 branchTODO(release) markers in code: grep -r "TODO(release)" src/ open-sse/node:24.15.0-trixie-slim)/version-bump-cc <patch|minor|major> (Claude Code skill)
package.json, electron/package.jsonCHANGELOG.md from git commits since last tagCHANGELOG.md equals package.json version## [Unreleased] as the first changelog section for upcoming workdocs/openapi.yaml → info.version must equal package.json versionnpm 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, per SUPPORTED_NODE_RANGE in src/shared/utils/nodeRuntimeSupport.ts; aligned with package.json engines)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 19 public 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 VPS [email protected]; 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 .15 server via plain Node ESM; requires ssh [email protected]; 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 — passHusky hooks live in .husky/ and run automatically on git operations.
npx lint-staged + node scripts/check/check-docs-sync.mjs + npm run check:any-budget:t11npm run check:any-budget:t11 && npm run check:tracked-artifacts (activated 2026-06-13). Intentionally excludes test:unit (slow; covered by the CI test-unit job).
npm run test:unit manually before pushing release branches.If a hook fails: fix the underlying issue, don't bypass with --no-verify.
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).
npm run check:docs-sync passes (auto-run by pre-commit)npm run check:docs-all passes (umbrella: docs-sync + docs-counts + env-doc-sync + deprecated-versions + doc-links)npm run check:env-doc-sync exits 0 — code ↔ .env.example ↔ docs/reference/ENVIRONMENT.md env contract is intactnpm run check:doc-links exits 0 — no broken internal markdown references after restructuringdocs/architecture/ARCHITECTURE.md reviewed for storage/runtime driftdocs/guides/TROUBLESHOOTING.md reviewed for env var and operational drift.env.example changed: docs/reference/ENVIRONMENT.md updateddocs/guides/USER_GUIDE.md mentions itdocs/reference/API_REFERENCE.md + docs/openapi.yaml updateddocs/<MODULE>.md existsdocs/guides/TROUBLESHOOTING.md has migration notenpm run i18n:check exits 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-coverage exits 0 — every UI locale at or above the 80% coverage floornpm run i18n:sync-ui:dry reports 0 missing keys across all 43 localesnpm run i18n:run (requires OMNIROUTE_TRANSLATION_API_KEY in .env) before taggingsrc/lib/db/migrations/ has new files:
CREATE TABLE IF NOT EXISTS, etc.)~/.omniroute/omniroute.db and run npm run dev-wal, -shm) handled correctly if migration rewrites tablessrc/shared/constants/providers.ts Zod schema valid at load time
id, label, kind, etc.)freeNote provided for new free providersoauthConfig registered in src/lib/oauth/constants/oauth.tsopen-sse/executors/open-sse/translator/open-sse/config/providerRegistry.tstests/unit/ cover provider classification and routingIf electron/ changed:
npm run electron:smoke:packaged passes:win, :mac, :linuxelectron/package.json version matches root package.jsonstableThe 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.
npm run build:release succeeds and dist/BUILD_SHA == git rev-parse --short HEADnpm run check:pack-artifact clean — no app.__qa_backup, scripts/scratch, package-lock.json, or other local residuedist/server.js exists after build/generate-release-cc (Claude Code skill):
vX.Y.Zgit 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 skills use the light rsync flow — no npm pack, no npm i -g:
/deploy-vps-local-cc — local VPS (192.168.0.15)/deploy-vps-akamai-cc — Akamai VPS (69.164.221.35)/deploy-vps-both-cc — bothdist/BUILD_SHA == git rev-parse --short HEADnode_modules is real (main checkout or npm ci'd worktree — NOT a symlinked worktree)/dashboard/health → check version string matches release/v1/chat/completions request against a known provider/api/monitoring/health returns CLOSED circuit breakers/mcp HTTP, /mcp-sse SSE)/capture-release-evidences-cc (Claude Code skill)
news.json for in-app bannerBefore shipping any release that includes embedded services changes, verify:
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 migration 071_services.sql applied + 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 (validates 070_webhooks_kind_metadata.sql applied).node --import tsx/esm --test tests/unit/db/no-migration-collisions.test.ts passes — guards against future collisions.POST /api/services/9router/install returns 200 with installedVersion in under 2 minPOST /api/services/9router/start returns 200 and state: "running" in under 30 sGET /api/services/9router/status reports health: "healthy"POST /v1/chat/completions with "model": "9router/auto/..." returns 200 (end-to-end routing through 9Router)GET /dashboard/providers/services/9router/embed/dashboard renders the 9Router native UI inside the proxy (no direct 127.0.0.1:port iframe)POST /api/services/9router/rotate-key returns { keyRotated: true } and service restarts cleanlyPOST /api/services/9router/stop returns 200 and state: "stopped"GET /api/services/9router/logs?tail=50 returns SSE stream with snapshot event containing recent linesnpm in PATH returns 500 with a friendly (non-stack-trace) error messagePOST /api/services/cliproxy/install returns 200 in under 2 minPOST /api/services/cliproxy/start returns 200 and state: "running" in under 30 sGET /api/services/cliproxy/status reports health: "healthy"POST /api/services/cliproxy/stop returns 200 and state: "stopped"GET /api/services/cliproxy/logs?tail=50 returns SSE streamcurl -H "X-Forwarded-For: 1.2.3.4" http://localhost:20128/api/services/9router/start returns 403 LOCAL_ONLYcurl -H "X-Forwarded-For: 1.2.3.4" http://localhost:20128/api/services/cliproxy/start returns 403 LOCAL_ONLY/api/services/* do not contain err.stack or absolute file pathsBefore shipping any v3.8.x release, verify these additional items:
omniroute --tray boots on macOS (systray2 installed into ~/.omniroute/runtime/)omniroute --tray boots on Linux (requires DISPLAY; graceful error if not set)omniroute --tray boots on Windows (PowerShell NotifyIcon, no extra binaries)omniroute config tray enable creates autostart entry; disable removes itnpm install -g omniroute@<this-version> runs postinstall without fatal exitomniroute update --apply and the auto-updater
run npm install -g … --include=optional so optionalDependencies (better-sqlite3,
keytar, tls-client, and the llmlingua SLM stack: @atjsh/llmlingua-2,
@huggingface/[email protected], @tensorflow/tfjs, js-tiktoken) survive an update.
@huggingface/transformers stays optional so its onnxruntime-node CUDA provider postinstall
cannot abort installation on CUDA 11 hosts. The ultra modelPath SLM tier also needs the
tinybert model, auto-downloaded to ${DATA_DIR}/models/llmlingua on first use. Postinstall
(scripts/build/colocateOptionals.mjs) then co-locates the SLM optional closure into
dist/node_modules so the worker resolves a SINGLE @huggingface/transformers 3.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 status works with no .env (CLI token path, loopback only)curl http://localhost:20128/api/shutdown returns 401 (always-protected route)curl -H "host: evil.com" http://localhost:20128/api/mcp/sse returns 401 (loopback guard)bundled on first run (bundled binary valid for platform)runtime when node_modules/better-sqlite3 is deletedplaywright-mcp browser_snapshot output (≥50% reduction)skills/omniroute*/SKILL.md files are publicly fetchable via raw GitHub URLIf 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)release/vX.Y.0 → patch release vX.Y.(Z+1)maingit push --force to main or release/* branches--no-verify).env filessrc/, open-sse/, electron/, or bin/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).