mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-29 02:22:10 +03:00
Compare commits
1 Commits
main
...
fix/instal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1840d7c177 |
@@ -1023,13 +1023,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# to disable the check. Used by: src/lib/db/migrationRunner.ts. Default: 50.
|
||||
#OMNIROUTE_MAX_PENDING_MIGRATIONS=50
|
||||
|
||||
# Working directory for the check:install-upgrade release gate. It builds two ~3 GB
|
||||
# install trees plus a ~275 MB tarball, so it needs roughly 12 GB — more than the
|
||||
# 12 GB RAM-backed tmpfs that /tmp is on the self-hosted runner, where it exhausted
|
||||
# the tmpfs and npm silently truncated the package. Defaults to <repo>/.install-upgrade
|
||||
# on real disk. Used by: scripts/check/check-install-upgrade.mjs. Default: <repo>/.install-upgrade.
|
||||
#OMNIROUTE_INSTALL_UPGRADE_WORKDIR=/var/tmp/omniroute-install-upgrade
|
||||
|
||||
# Trust user-managed RTK project filter rules without strict signature checks.
|
||||
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
|
||||
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
|
||||
|
||||
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@@ -606,24 +606,13 @@ jobs:
|
||||
# Dynamic runner: when the release captain flips the USE_VPS_RUNNER repo var to
|
||||
# 'true' (scripts/vps/release-runner-up.sh does it after the self-hosted VM is
|
||||
# online), the heavy jobs run on the dedicated 32-core VPS runners (label
|
||||
# omni-build) instead of queueing on the 20-concurrent-job hosted pool.
|
||||
# omni-release) instead of queueing on the 20-concurrent-job hosted pool.
|
||||
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
|
||||
# back to ubuntu-latest unless the PR head repo is this repository (push /
|
||||
# dispatch events are own-origin by definition). Any failure path (VM down,
|
||||
# var unset/false) also falls back to ubuntu-latest.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-build"]') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
needs: changes
|
||||
# The .113 pool runs ONE next-build with room to spare and two at the edge: the
|
||||
# box has 31 GB and a single next-build peaks at 14–16 GB RSS. On 2026-08-28
|
||||
# 13:50Z the kernel OOM-killed main's build while a PR build ran beside it
|
||||
# (five Build jobs had been queued by a burst of PRs). Two lanes: main keeps
|
||||
# its own so a release is never queued behind PR traffic; PR builds serialize
|
||||
# among themselves. GitHub keeps one running + one pending per group and
|
||||
# CANCELS older pendings — a cancelled PR build is re-runnable; a dead main
|
||||
# build costs the publish its artefact and a 40-minute rebuild that OOMs.
|
||||
concurrency:
|
||||
group: heavy-build-${{ github.ref == 'refs/heads/main' && 'main' || 'pr' }}
|
||||
cancel-in-progress: false
|
||||
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -657,14 +646,14 @@ jobs:
|
||||
# Keep standalone/node_modules intact: package/electron jobs consume the
|
||||
# Next-traced standalone tree and must not replace it with root node_modules.
|
||||
run: |
|
||||
tar -czf "$RUNNER_TEMP/e2e-build.tar.gz" \
|
||||
tar -czf /tmp/e2e-build.tar.gz \
|
||||
--exclude='.build/next/cache' \
|
||||
.build/next
|
||||
- name: Upload Next.js build for downstream jobs
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: next-build
|
||||
path: ${{ runner.temp }}/e2e-build.tar.gz
|
||||
path: /tmp/e2e-build.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
package-artifact:
|
||||
@@ -687,14 +676,10 @@ jobs:
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: next-build
|
||||
# Workspace-relative on purpose: the matrix below includes windows-latest, whose
|
||||
# default shell is pwsh, where $RUNNER_TEMP is empty (it is $env:RUNNER_TEMP) —
|
||||
# #11896's first cut broke the Electron smoke on exactly that. A relative path
|
||||
# works in bash and pwsh alike; hosted workspaces are ephemeral.
|
||||
path: next-build-artifact
|
||||
path: /tmp/
|
||||
- name: Extract Next.js build artifact
|
||||
run: |
|
||||
tar -xzf next-build-artifact/e2e-build.tar.gz
|
||||
tar -xzf /tmp/e2e-build.tar.gz
|
||||
# build:cli consumes the downloaded .build/next standalone artifact and assembles dist/;
|
||||
# it only rebuilds if the downloaded standalone artifact is missing.
|
||||
- run: npm run build:cli
|
||||
@@ -782,14 +767,10 @@ jobs:
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: next-build
|
||||
# Workspace-relative on purpose: the matrix below includes windows-latest, whose
|
||||
# default shell is pwsh, where $RUNNER_TEMP is empty (it is $env:RUNNER_TEMP) —
|
||||
# #11896's first cut broke the Electron smoke on exactly that. A relative path
|
||||
# works in bash and pwsh alike; hosted workspaces are ephemeral.
|
||||
path: next-build-artifact
|
||||
path: /tmp/
|
||||
- name: Extract Next.js build artifact
|
||||
run: |
|
||||
tar -xzf next-build-artifact/e2e-build.tar.gz
|
||||
tar -xzf /tmp/e2e-build.tar.gz
|
||||
- name: Install Electron dependencies
|
||||
working-directory: electron
|
||||
run: npm install --no-audit --no-fund
|
||||
@@ -1249,14 +1230,10 @@ jobs:
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: next-build
|
||||
# Workspace-relative on purpose: the matrix below includes windows-latest, whose
|
||||
# default shell is pwsh, where $RUNNER_TEMP is empty (it is $env:RUNNER_TEMP) —
|
||||
# #11896's first cut broke the Electron smoke on exactly that. A relative path
|
||||
# works in bash and pwsh alike; hosted workspaces are ephemeral.
|
||||
path: next-build-artifact
|
||||
path: /tmp/
|
||||
- name: Extract Next.js build artifact
|
||||
run: |
|
||||
tar -xzf next-build-artifact/e2e-build.tar.gz
|
||||
tar -xzf /tmp/e2e-build.tar.gz
|
||||
# WS4.1: duration-balanced shards (LPT over config/quality/e2e-timings.json).
|
||||
# Measured skew of plain --shard was 14× (24m47s vs 1m47s) — E2E was the CI
|
||||
# critical path. The balancer self-verifies completeness and exits non-zero on
|
||||
|
||||
4
.github/workflows/nightly-release-green.yml
vendored
4
.github/workflows/nightly-release-green.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
|
||||
# no local noauth CLIs => zero machine-specific false positives) and no contention.
|
||||
# Nightly cron normally finds the var false (VM off) and falls back to hosted.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-build"]')) || 'ubuntu-latest' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
|
||||
env:
|
||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||
@@ -217,7 +217,7 @@ jobs:
|
||||
# On a push, only run for a push to main — a push to release/* is handled by
|
||||
# release-green above. Schedule/dispatch always run (they also sweep main).
|
||||
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-build"]')) || 'ubuntu-latest' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
|
||||
env:
|
||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||
|
||||
169
.github/workflows/npm-publish.yml
vendored
169
.github/workflows/npm-publish.yml
vendored
@@ -23,12 +23,11 @@ on:
|
||||
- next
|
||||
- historic
|
||||
publish_mode:
|
||||
description: "auto = publish through npm Trusted Publishing (OIDC, no token, no 2FA prompt — the default); staged = npm stage publish (owner approves with 2FA); direct = legacy token publish (emergency fallback only)"
|
||||
description: "staged = npm stage publish (owner approves with 2FA after the staged boot-verify); direct = legacy immediate publish (emergency fallback only)"
|
||||
required: false
|
||||
default: "auto"
|
||||
default: "staged"
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- staged
|
||||
- direct
|
||||
workflow_call:
|
||||
@@ -63,15 +62,11 @@ jobs:
|
||||
# mid-"Creating an optimized production build" while v3.8.48 had still fit in 16min.
|
||||
# This job never runs on `pull_request`, so the fork-safety clause is always true here;
|
||||
# it is kept verbatim so the expression stays greppable against ci.yml.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-build"]') || 'ubuntu-latest' }}
|
||||
outputs:
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
tag: ${{ steps.resolve.outputs.tag }}
|
||||
skip: ${{ steps.resolve.outputs.skip }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
actions: read # find + download the CI run's next-build artifact for this SHA
|
||||
contents: write # gh release upload (attach SBOM to the GitHub Release)
|
||||
id-token: write # npm provenance (GitHub Packages step)
|
||||
id-token: write # npm provenance
|
||||
packages: write # publish to npm.pkg.github.com
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -205,11 +200,8 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
RUN=""
|
||||
# $RUNNER_TEMP, never /tmp: on the .113 pool /tmp is a 12 GB tmpfs (RAM). Parking
|
||||
# this 1.3 GB artefact there took 27–32 min of the 76-min publish job — the
|
||||
# same bytes upload from disk in 2 min. RUNNER_TEMP is per-runner and on disk.
|
||||
for candidate in $CANDIDATES; do
|
||||
if gh run download "$candidate" --repo "$REPO" --name next-build --dir "$RUNNER_TEMP/next-build" 2>/dev/null; then
|
||||
if gh run download "$candidate" --repo "$REPO" --name next-build --dir /tmp/next-build 2>/dev/null; then
|
||||
RUN="$candidate"
|
||||
break
|
||||
fi
|
||||
@@ -219,8 +211,8 @@ jobs:
|
||||
echo "::notice::none of the candidate runs still carries next-build (1-day retention) — falling back to a full build"
|
||||
exit 0
|
||||
fi
|
||||
tar -xzf "$RUNNER_TEMP/next-build/e2e-build.tar.gz" -C .
|
||||
rm -rf "$RUNNER_TEMP/next-build"
|
||||
tar -xzf /tmp/next-build/e2e-build.tar.gz -C .
|
||||
rm -rf /tmp/next-build
|
||||
if [ -f .build/next/standalone/server.js ]; then
|
||||
echo "✅ standalone tree restored from CI run $RUN — build:cli will skip next build"
|
||||
else
|
||||
@@ -322,133 +314,17 @@ jobs:
|
||||
fi
|
||||
npm --version
|
||||
|
||||
# The registry upload itself moved to the `stage-npm` job below: npm REFUSES
|
||||
# `--provenance` from a self-hosted runner (422 "Unsupported GitHub Actions
|
||||
# runner environment"), and the heavy verification above cannot move to a
|
||||
# hosted one (16 GB is not enough for build:cli's next-build fallback — see
|
||||
# this job's runs-on comment). So this job proves the bytes and hands them
|
||||
# over; a tiny hosted job does the upload.
|
||||
- name: Pack the verified tarball for the upload job
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# --ignore-scripts: prepublishOnly would re-run build:cli-api && build:cli,
|
||||
# rebuilding bytes this job has already built, validated and boot-smoked.
|
||||
npm pack --ignore-scripts
|
||||
TARBALL="omniroute-${VERSION}.tgz"
|
||||
test -f "$TARBALL" || { echo "expected $TARBALL to exist after npm pack" >&2; ls -la ./*.tgz || true; exit 1; }
|
||||
echo "packed $TARBALL ($(du -h "$TARBALL" | cut -f1))"
|
||||
|
||||
- name: Hand the tarball to the hosted publish job
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: npm-tarball
|
||||
path: omniroute-${{ steps.resolve.outputs.version }}.tgz
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
- name: Publish to npm (staged — owner approves with 2FA)
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Configuring for GitHub Packages..."
|
||||
echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" > .npmrc
|
||||
npm pkg set name="@diegosouzapw/omniroute"
|
||||
npm publish --registry=https://npm.pkg.github.com --tag "$TAG" \
|
||||
|| echo "⚠️ omniroute@${VERSION} might already be published on GitHub Packages."
|
||||
echo "✅ Action finished for GitHub Packages"
|
||||
|
||||
# npm REFUSES `--provenance` from a self-hosted runner:
|
||||
# 422 Unprocessable Entity - Error verifying sigstore provenance bundle:
|
||||
# Unsupported GitHub Actions runner environment: "self-hosted".
|
||||
# Only "github-hosted" runners are supported when publishing with provenance.
|
||||
# v3.8.49 published fine because it predates USE_VPS_RUNNER being turned on
|
||||
# (2026-08-02); v3.8.50 was the first release after it, so this had been latent
|
||||
# for four weeks. Dropping --provenance was not an option: 3.8.49 carries a
|
||||
# SLSA attestation and 3.8.50 must not regress that.
|
||||
# The `publish` job cannot simply move to a hosted runner either — 16 GB is not
|
||||
# enough for build:cli's next-build fallback. So it keeps proving the bytes and
|
||||
# this job, which needs no memory at all, performs the upload.
|
||||
stage-npm:
|
||||
needs: publish
|
||||
if: needs.publish.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # npm provenance — the whole reason this job is separate
|
||||
steps:
|
||||
- name: Download the tarball the publish job proved
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: npm-tarball
|
||||
path: .
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Ensure npm supports staged publishing
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CUR=$(npm --version)
|
||||
if ! node -e "const [a,b]='$(npm --version)'.split('.').map(Number); process.exit(a>11||(a===11&&b>=15)?0:1)"; then
|
||||
# Pinned exact version (supply-chain: never float @latest in a publish
|
||||
# job); bump deliberately when a newer npm is required.
|
||||
echo "npm $CUR < 11.15 — installing pinned npm 11.15.0 for staged publishing"
|
||||
npm install -g --ignore-scripts npm@11.15.0
|
||||
fi
|
||||
npm --version
|
||||
|
||||
# Trusted Publishing (OIDC): npm mints a short-lived credential for THIS run from
|
||||
# GitHub's id-token — no NPM_TOKEN secret, no 2FA prompt, provenance included, and
|
||||
# it is the bypass npm sanctions now that tokens which skip 2FA are being retired
|
||||
# (gh.io/npm-gat-bypass2fa-deprecation). Requires the package's Trusted Publisher to
|
||||
# be configured on npmjs.com (owner: diegosouzapw/OmniRoute, workflow
|
||||
# npm-publish.yml) and a github-hosted runner — which is why this job exists.
|
||||
# Without that configuration `npm publish` fails with ENEEDAUTH: re-dispatch with
|
||||
# publish_mode=staged or direct. Automatic publishing was the flow up to v3.8.48;
|
||||
# v3.8.49 moved to staged (WS1.3) to keep a leaked token from publishing alone —
|
||||
# OIDC gives the same guarantee without the manual approve.
|
||||
- name: Publish to npm (Trusted Publishing / OIDC — automatic)
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.publish_mode == 'auto'
|
||||
env:
|
||||
VERSION: ${{ needs.publish.outputs.version }}
|
||||
TAG: ${{ needs.publish.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL="omniroute-${VERSION}.tgz"
|
||||
test -f "$TARBALL" || { echo "tarball $TARBALL did not arrive from the publish job" >&2; ls -la; exit 1; }
|
||||
# Deliberately NO NODE_AUTH_TOKEN in this step: npm >= 11.5 detects the GitHub
|
||||
# OIDC token itself. Always pass --tag explicitly (defense in depth: an older
|
||||
# VERSION can never claim `@latest`).
|
||||
npm publish "$TARBALL" --provenance --access public --tag "$TAG" --ignore-scripts
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) via Trusted Publishing"
|
||||
|
||||
- name: Publish to npm (staged — owner approves with 2FA)
|
||||
# Only on an explicit request now: Trusted Publishing below is the default.
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'staged'
|
||||
env:
|
||||
VERSION: ${{ needs.publish.outputs.version }}
|
||||
TAG: ${{ needs.publish.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL="omniroute-${VERSION}.tgz"
|
||||
test -f "$TARBALL" || { echo "tarball $TARBALL did not arrive from the publish job" >&2; ls -la; exit 1; }
|
||||
# Always pass --tag explicitly. Defense in depth: even if VERSION is
|
||||
# accidentally an older release, the historic tag will NOT claim `@latest`.
|
||||
# --ignore-scripts: publishing a built tarball must never re-run
|
||||
# prepublishOnly (build:cli-api && build:cli) on this small runner.
|
||||
npm stage publish "$TARBALL" --provenance --access public --tag "$TAG" --ignore-scripts
|
||||
npm stage publish --provenance --access public --tag "$TAG"
|
||||
{
|
||||
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
|
||||
echo ""
|
||||
@@ -464,18 +340,31 @@ jobs:
|
||||
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
|
||||
|
||||
- name: Publish to npm (DIRECT — emergency fallback)
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
|
||||
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
|
||||
env:
|
||||
VERSION: ${{ needs.publish.outputs.version }}
|
||||
TAG: ${{ needs.publish.outputs.tag }}
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL="omniroute-${VERSION}.tgz"
|
||||
test -f "$TARBALL" || { echo "tarball $TARBALL did not arrive from the publish job" >&2; ls -la; exit 1; }
|
||||
npm publish "$TARBALL" --provenance --access public --tag "$TAG" --ignore-scripts
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Configuring for GitHub Packages..."
|
||||
echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" > .npmrc
|
||||
npm pkg set name="@diegosouzapw/omniroute"
|
||||
npm publish --registry=https://npm.pkg.github.com --tag "$TAG" \
|
||||
|| echo "⚠️ omniroute@${VERSION} might already be published on GitHub Packages."
|
||||
echo "✅ Action finished for GitHub Packages"
|
||||
|
||||
publish-opencode-plugin:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -293,6 +293,3 @@ docker-compose.yml.bak
|
||||
# Ad-hoc test sandboxes (never tracked — may contain local DBs)
|
||||
/.sandbox/
|
||||
.aider*
|
||||
|
||||
# check:install-upgrade work trees (~12 GB, disposable)
|
||||
/.install-upgrade/
|
||||
|
||||
@@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below.
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (160 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (159 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
|
||||
@@ -1202,7 +1202,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>>=22.22.2 <23 || >=24.0.0 <27</code></td></tr>
|
||||
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
|
||||
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 160 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 159 migrations</td></tr>
|
||||
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
|
||||
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
|
||||
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
- The npm publish is automatic again, through npm Trusted Publishing (OIDC): the hosted
|
||||
`stage-npm` job publishes with a short-lived credential minted from GitHub's id-token —
|
||||
no `NPM_TOKEN`, no 2FA prompt, provenance attached. `publish_mode=staged` (owner
|
||||
approves with 2FA) and `direct` (token) remain available on `workflow_dispatch`.
|
||||
@@ -1,8 +0,0 @@
|
||||
- **fix(db):** `model_capabilities` is created by a migration instead of lazily on the first
|
||||
models.dev sync, so a clean install and an upgraded install converge on the same schema
|
||||
regardless of which features have run
|
||||
- **fix(ci):** `check:install-upgrade` now fails on an `npm` install truncated by ENOSPC
|
||||
(npm reports it as a warning and still exits 0), authenticates its health probe so the
|
||||
version assertion works against the hardened health payload, frees the clean-install tree
|
||||
before the upgrade phase, and no longer reports a schema divergence computed from a boot
|
||||
that never served
|
||||
@@ -1,4 +0,0 @@
|
||||
- Excluded Next.js Node File Trace manifests (`*.nft.json`) from the published npm
|
||||
tarball. They are build-time metadata and are never read while serving, but had
|
||||
grown to 668.7 MB — 61% of the package — which pushed the upload past the
|
||||
registry limit and made `npm publish` fail with `413 Payload Too Large`.
|
||||
@@ -1,5 +0,0 @@
|
||||
- Fixed the Alibaba free-tier allowlist test that went red on its own once the
|
||||
shipped catalog's `validUntil` (2026-08-27) passed, leaving every PR and `main`
|
||||
with a failing `Unit Tests (1/8)`. The test now builds its own packs with dates
|
||||
it controls, and covers the expired-pack fallback that production has actually
|
||||
been serving.
|
||||
@@ -1,4 +0,0 @@
|
||||
- Split the npm registry upload into its own GitHub-hosted job. npm refuses
|
||||
`--provenance` from a self-hosted runner (`422 ... Only "github-hosted" runners
|
||||
are supported`), which blocked the v3.8.50 publish; the heavy verification
|
||||
cannot move to a hosted runner, so it now hands the proven tarball over instead.
|
||||
@@ -1,4 +0,0 @@
|
||||
- Added a unit test that fails seven days before any dated pack under `config/`
|
||||
(`validUntil` and sibling keys) lapses, naming the file and key. The Alibaba
|
||||
free-tier pack expired on 2026-08-27 and turned every PR red the next morning
|
||||
with no commit involved; renewal now happens on someone's terms, not the clock's.
|
||||
@@ -1,3 +0,0 @@
|
||||
- `check:workflows` now fails (under `--strict`/`--ratchet`) when any job routed to a
|
||||
self-hosted runner publishes with `--provenance` — npm rejects that with `422` at the
|
||||
registry, which in v3.8.50 only surfaced after the tag and Docker images were public.
|
||||
@@ -1,5 +0,0 @@
|
||||
- `scripts/ops/runner-janitor.sh` now proves a path is idle with one `lsof`
|
||||
snapshot and removes stale leftovers itself (tmpfs after 3 h — it is RAM — disk
|
||||
after 24 h), kills orphan `next-build` processes, prunes checkouts of stopped
|
||||
runners, and alerts on memory pressure; `--dry-run` shows exactly what it would
|
||||
do. `docs/ops/RUNNER_BOX.md` reconciled to the measured box (31 GB, 10 listeners).
|
||||
@@ -1,5 +0,0 @@
|
||||
- The `next-build` artefact (1.3 GB) is now written and read under `$RUNNER_TEMP`
|
||||
(per-runner, on disk) instead of `/tmp`, which on the self-hosted pool is a
|
||||
12 GB tmpfs in RAM. Landing it there took 27–32 of the publish job's 76 minutes,
|
||||
and the fixed `/tmp/e2e-build.tar.gz` name let E2E jobs on different runners
|
||||
overwrite each other's download.
|
||||
@@ -1,3 +0,0 @@
|
||||
- The CI `build` job now runs in two concurrency lanes — `main` and pull requests —
|
||||
so a release build is never queued behind (or OOM-killed beside) PR builds on the
|
||||
self-hosted pool, which holds one `next-build` comfortably and two at the edge.
|
||||
@@ -1,4 +0,0 @@
|
||||
- Every CI job that runs a `next build` (`build`, the npm `publish`, both release-green
|
||||
validations) now targets the `omni-build` runner label, which only two of the eight
|
||||
self-hosted runners carry. The box holds one build comfortably and two at the edge; a
|
||||
third now queues on GitHub instead of being OOM-killed by the kernel.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_doc": "Tables that exist ONLY in databases upgraded from an older version. Two causes, and they call for different fixes: (a) residue whose CREATE left the migration set in some past cycle but survives where it already existed — allowlist it here; (b) a table created LAZILY at runtime with `CREATE TABLE IF NOT EXISTS` inside a feature code path — whether a database has it depends on whether that feature ran, so it diverges by TIMING and can show up on EITHER side. Fix (b) with a migration instead of an entry here (see src/lib/db/migrations/163_model_capabilities.sql); an allowlist entry only hides it in one direction. Either way, recorded so check-install-upgrade.mjs can still fail on a NEW divergence. The opposite direction (a table a clean install creates but an upgrade does not) is NEVER allowlisted: it means every existing user is missing structure the code expects.",
|
||||
"_doc": "Tables that exist ONLY in databases upgraded from an older version — residue whose CREATE left the migration set in some past cycle but survives where it already existed. Harmless (nothing references them), but recorded here so check-install-upgrade.mjs can still fail on a NEW divergence. The opposite direction (a table a clean install creates but an upgrade does not) is NEVER allowlisted: it means every existing user is missing structure the code expects.",
|
||||
"residualTables": {
|
||||
"cache_metrics": "Measured 2026-07-30 on a real 3.8.48 install upgraded to 3.8.49 (VPS .16, 165 MB database, 114 → 117 tables). Present in upgraded databases, absent from clean installs. Cause identified 2026-08-27: it is case (b) above — created lazily by `ensureCacheMetricsTable()` at src/lib/semanticCache.ts:34, never by a migration, so it appears only where the semantic cache has run. No code path referenced it during the upgrade (zero `no such table` in 150 log lines, both installs healthy). Left as an allowlist entry rather than promoted to a migration or dropped: unlike model_capabilities it did not block a release, and creating a table for a subsystem we cannot prove is live is not a change to make blind. Revisit when the cache subsystem is next touched."
|
||||
"cache_metrics": "Measured 2026-07-30 on a real 3.8.48 install upgraded to 3.8.49 (VPS .16, 165 MB database, 114 → 117 tables). Present in upgraded databases, absent from clean installs. No code path referenced it during the upgrade (zero `no such table` in 150 log lines, both installs healthy). Left in place rather than dropped: a DROP migration on a table we cannot prove is unused everywhere is the riskier change. Revisit when the cache subsystem is next touched."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"dedicatedGate": true
|
||||
},
|
||||
"zizmorFindings": {
|
||||
"value": 194,
|
||||
"value": 192,
|
||||
"_rebaseline_2026_08_20_radar_export_workflow": "190 -> 192 (+2). Workflow novo `.github/workflows/radar-export.yml` (passo 10 do go-live do Radar: publica o export estável do catálogo como asset de release para o servidor privado baixar via RADAR_EXPORT_URL). Os +2 são unpinned-uses @vN: actions/checkout@v7 + actions/setup-node@v7 — a MESMA convenção deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só este violaria a convenção. O findings artipacked do checkout foi CORRIGIDO com `persist-credentials: false` (o job publica via GH_TOKEN em `gh release`, não usa a credencial do checkout). Nenhuma classe nova de template-injection / cache-poisoning / dangerous-triggers. Medido local com zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 191; +1 do delta conhecido do runner (ver _rebaseline_2026_07_28_ci_runner_delta: o runner enxerga 1 unpinned-uses @vN a mais que o devbox no mesmo commit; a baseline segue o runner) => 192.",
|
||||
"_rebaseline_2026_07_20_aliasresolver_hook_split_7808": "175 -> 176 (+1). Companion to PR #7808 (CodeQL js/incomplete-url-substring-sanitization fix in bin/aliasResolver.mjs). The +1 is NOT caused by this PR's code changes (bin/* is not a workflow file) — it is a pre-existing drift that surfaced because the ratchet gate runs on this PR's CI: the zizmor scanner version on the GitHub runner gained a new rule (or extended an existing one) since the v3.8.49 baseline was seeded on 2026-07-17. Breakdown: the new finding is an unpinned-uses @vN class item on one of the existing workflows (same deliberate convention as _scanner_harden_workflows_2026_06_16 — @vN is intentional, SHA-pinning only this one would violate the convention). No new template-injection/artipacked/cache-poisoning/dangerous-triggers classes introduced. Measured by the Quality Gates (Extended) job on run 29713001401 = 176, baseline was 175. Note: by the time this landed on release/v3.8.49, the baseline was already at 176 via _rebaseline_2026_07_17_combo_recovery_hints — this entry is kept as historical record; no further bump applied.",
|
||||
"_rebaseline_2026_07_17_v3849_release": "169 -> 175 (+6). Cycle workflow drift (v3.8.48/v3.8.49): npm-publish.yml (new, WS1.3 #7092), electron-release.yml, nightly-compat.yml, nightly-release-green.yml, CI restructures (#7501 full-history base fetch, #7355 main-green, #7202 merge-queue gates, Trunk/Codecov). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention, deliberate per _scanner_harden_workflows_2026_06_16), +2 cache-poisoning (artifact upload/cache in the OWN electron-release/npm-publish RELEASE workflows -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat.yml permissions:issues). No new template-injection/artipacked/dangerous-triggers. Measured with zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 175 on da3a0be69.",
|
||||
@@ -180,8 +180,7 @@
|
||||
"_rebaseline_2026_06_23_v3834_release": "152 -> 155 (+3). The 3 new unpinned-uses are in .github/workflows/nightly-release-green.yml (added by #4622 this cycle): actions/checkout@v7, actions/setup-node@v6, actions/upload-artifact@v4 — the SAME deliberate @vN convention as ci.yml's own checkout@v7/setup-node@v6 and every other workflow (see _scanner_harden_workflows_2026_06_16 + _zizmor_rebaseline_2026_06_20_ci_build_artifact_reuse). SHA-pinning only this workflow would violate the convention. The workflow-lint ratchet does NOT run on PR->release fast-gates, so it surfaced only on the release PR; measured locally via `npm run check:workflows -- --ratchet` = 155. No new template-injection/artipacked/cache-poisoning.",
|
||||
"_rebaseline_2026_07_13_v3847_release_preflight": "159 -> 169 (+10). Findings from cycle-merged workflow changes: #6716 (PR gate restructure), #6781 (unit fast-path shard 2->4), #6788 (TIA tsx loader split), #6881 (electron-updater latest.yml manifests in release assets) — same deliberate @vN unpinned-uses convention as prior rebaselines; no new template-injection/artipacked/cache-poisoning classes. Measured via `npm run check:workflows -- --ratchet` = 169 on the v3.8.47 release pre-flight.",
|
||||
"_rebaseline_2026_07_28_v3849_release_preflight": "176 -> 189 (+13). Pre-flight de fechamento da v3.8.49 (934 commits no ciclo). Deriva de workflow: 1 workflow novo (build-rinseaid-image.yml) mais os bumps de action do Dependabot ao longo do ciclo — todos da MESMA classe unpinned-uses @vN, convenção deliberada do repo (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só estes violaria a convenção. Nenhuma classe nova de template-injection / artipacked / cache-poisoning / dangerous-triggers. Nesta mesma passada foram CORRIGIDAS 3 diretivas shellcheck malformadas (SC1125: `# shellcheck disable=SC2086 — texto`, em que o travessão invalida o par key=value) em ci.yml e nightly-release-green.yml. Medido com zizmor 1.25.2 via `npm run check:workflows -- --ratchet` = 189.",
|
||||
"_rebaseline_2026_07_28_ci_runner_delta": "189 -> 190 (+1). Medido 189 no devbox e 190 no runner do GitHub no MESMO commit (run 30396592013, job Quality Gates (Extended)) — mesma classe já registrada em _rebaseline_2026_07_20_aliasresolver_hook_split_7808: a versão do zizmor no runner enxerga uma finding a mais que a local, sempre da classe unpinned-uses @vN. O valor do runner é o que o gate compara, então a baseline segue o runner.",
|
||||
"_rebaseline_2026_08_28_npm_publish_hosted_stage_job": "192 -> 194 (+2). Job novo `stage-npm` em .github/workflows/npm-publish.yml: o npm RECUSA `--provenance` vindo de runner self-hosted (422 \"Unsupported GitHub Actions runner environment\"), e o job `publish` nao pode migrar para runner hospedado porque 16 GB nao bastam para o fallback next-build do build:cli (documentado no proprio runs-on). A separacao foi a unica saida que preserva a atestacao SLSA que a 3.8.49 ja tem. Os +2 sao da MESMA convencao deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16): unpinned-uses @vN em actions/download-artifact@v8 + actions/setup-node@v7, mais o cache-poisoning que o proprio setup-node@v7 ja gera nos outros 2 jobs deste MESMO arquivo (linhas 85 e 463) e que ja esta na baseline. Fixar por SHA so este job violaria a convencao. Nenhuma classe nova: zero template-injection / artipacked / dangerous-triggers / excessive-permissions — o job declara apenas contents:read + id-token:write, que e o minimo para a proveniencia. Medido pelo job Quality Gates (Extended) no run 33162... da PR #11877 = 194."
|
||||
"_rebaseline_2026_07_28_ci_runner_delta": "189 -> 190 (+1). Medido 189 no devbox e 190 no runner do GitHub no MESMO commit (run 30396592013, job Quality Gates (Extended)) — mesma classe já registrada em _rebaseline_2026_07_20_aliasresolver_hook_split_7808: a versão do zizmor no runner enxerga uma finding a mais que a local, sempre da classe unpinned-uses @vN. O valor do runner é o que o gate compara, então a baseline segue o runner."
|
||||
},
|
||||
"vulnCount": {
|
||||
"value": 22,
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Release Checklist"
|
||||
version: 3.8.51
|
||||
lastUpdated: 2026-08-28
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
---
|
||||
|
||||
# Release Checklist
|
||||
|
||||
> **Last updated:** 2026-08-28 — v3.8.51
|
||||
> **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](./RELEASE_GREEN.md)
|
||||
@@ -37,21 +37,7 @@ npm run test:e2e # optional but recommended
|
||||
/capture-release-evidences-cc
|
||||
```
|
||||
|
||||
## npm Trusted Publishing (default since v3.8.51) — staged on request, direct as fallback
|
||||
|
||||
`npm-publish.yml` publishes through **npm Trusted Publishing (OIDC)** by default: the
|
||||
`stage-npm` job (github-hosted) exchanges GitHub's id-token for a short-lived npm
|
||||
credential for that run — no long-lived npm token in the repository secrets, no 2FA prompt, provenance attached.
|
||||
That is the bypass npm sanctions now that tokens which skip 2FA are being retired;
|
||||
it restores the fully automatic flow the project had up to v3.8.48 while keeping the
|
||||
WS1.3 guarantee (a leaked token cannot publish alone — there is no token).
|
||||
|
||||
**One-time setup (owner):** npmjs.com → package `omniroute` → Settings → *Trusted
|
||||
Publisher* → GitHub: owner `diegosouzapw`, repo `OmniRoute`, workflow `npm-publish.yml`
|
||||
(environment: none). Until that exists, the automatic step fails with `ENEEDAUTH`:
|
||||
re-dispatch with `publish_mode=staged` (below) or `direct`.
|
||||
|
||||
### Staged publishing (on request — `publish_mode=staged`)
|
||||
## 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
|
||||
|
||||
@@ -4,66 +4,32 @@ title: Self-Hosted Runner Box Operations
|
||||
|
||||
# Self-Hosted Runner Box Operations (.113 pool)
|
||||
|
||||
The self-hosted pool (`self-hosted, omni-release` on all eight runners; `omni-build` on two) runs on the **.113** box.
|
||||
Measured 2026-08-28 (v3.8.50 postmortem, Parte III):
|
||||
The self-hosted pool (`self-hosted, omni-release` labels) runs on the 16 GB box at
|
||||
`192.168.0.113`. Two failure modes recurred on release days and were, until v3.8.49,
|
||||
manual discipline; the **janitor script codifies them** (WS3.3 of the quality plan):
|
||||
|
||||
| resource | value | what it means for scheduling |
|
||||
| --------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| RAM / CPU | **31 GB / 32 cores** (was 16 GB when this doc was first written) | one `next-build` peaks at **~14 GB** → 2 concurrent heavy builds saturate the box, 3 take it down (2026-08-28 06:42Z: load 56, two jobs lost) |
|
||||
| swap | 15 GB | it swapped its way through the v3.8.50 publish; pressure shows in `/proc/pressure/memory` |
|
||||
| `/tmp` | **12 GB tmpfs = RAM** | anything parked there is memory; leftovers are swept after 3 h |
|
||||
| disk | 188 GB | `_work` checkouts of 8 runners reach ~70 GB with no cap |
|
||||
| runners | **10 listeners**: 8 OmniRoute + OmniHeuris + OmniMind | all share the memory above |
|
||||
1. **Orphaned temp/work dirs** filling the disk → disk-full SQLite errors mid-job.
|
||||
2. **>4 concurrent runners** → OOM-killed jobs (8-wide killed jobs twice on the
|
||||
v3.8.47 release day; 4-wide is the proven ceiling).
|
||||
|
||||
## Install the janitor (one-time, on the box)
|
||||
|
||||
```bash
|
||||
scp scripts/ops/runner-janitor.sh root@192.168.0.113:/opt/omniroute-ops/runner-janitor.sh
|
||||
ssh root@192.168.0.113 'chmod +x /opt/omniroute-ops/runner-janitor.sh; apt-get install -y lsof'
|
||||
# cron (root): every 30 min, log to /var/log/runner-janitor.log
|
||||
*/30 * * * * MAX_ACTIVE_RUNNERS=8 /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1
|
||||
sudo mkdir -p /opt/omniroute-ops
|
||||
sudo cp scripts/ops/runner-janitor.sh /opt/omniroute-ops/
|
||||
sudo chmod +x /opt/omniroute-ops/runner-janitor.sh
|
||||
( sudo crontab -l 2>/dev/null; echo '*/30 * * * * /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1' ) | sudo crontab -
|
||||
```
|
||||
|
||||
`lsof` is required: the janitor proves a path is idle with one snapshot of open
|
||||
files before removing it, and without the tool it removes nothing and says so
|
||||
(exit 1). Try any change with `--dry-run` first — it prints exactly what it would
|
||||
do and touches nothing.
|
||||
|
||||
What it does every run: sweeps our own leftovers (`runner-*`, `omniroute-*`,
|
||||
`next-build*`, `e2e-build.tar.gz`) after **3 h on tmpfs** and 24 h on disk
|
||||
`_work/_temp`; kills a `next-build` older than 75 min (no job runs that long — on
|
||||
2026-08-27 one ran 70 min after GitHub had declared its job lost); prunes 48 h-old
|
||||
checkouts of runners whose unit is **stopped**; alerts on disk ≥ 85 %, memory PSI
|
||||
`full/avg60` ≥ 10 %, and more listeners than `MAX_ACTIVE_RUNNERS` (with an
|
||||
omniroute/other breakdown). Exit 1 = attention needed; read the log.
|
||||
|
||||
## Runner units: KillMode
|
||||
|
||||
The runner's default `KillMode=process` leaves `Runner.Worker → npm → next-build`
|
||||
alive when a unit is stopped or restarted — an orphan build keeps eating RAM and
|
||||
CPU with no job attached. Every OmniRoute unit carries a drop-in
|
||||
(`/etc/systemd/system/actions.runner.diegosouzapw-OmniRoute.<name>.service.d/10-killmode.conf`)
|
||||
with `KillMode=mixed`: SIGTERM to the listener first, SIGKILL to the whole cgroup at
|
||||
`TimeoutStop`. It takes effect on the unit's next restart — restart **one runner at
|
||||
a time, only when idle**, with the idle check and the restart in the same command.
|
||||
What it does every 30min: sweeps runner temp leftovers older than 24h, alerts at
|
||||
≥85% root-disk usage, and alerts when more than the runner ceiling (default 4, tunable
|
||||
via the script's own environment) of `Runner.Listener` processes are up. Alerts land in `/var/log/runner-janitor.log`
|
||||
with a non-zero exit (grep for `⚠`).
|
||||
|
||||
## Operating rules
|
||||
|
||||
- **Heavy-build ceiling: 2 at a time — enforced by label.** Every job that runs a
|
||||
`next build` (`ci.yml` `build`, `npm-publish.yml` `publish`, both `nightly-release-green`
|
||||
validations) targets `[self-hosted, omni-build]`, and only **two** runners carry that
|
||||
label (`omniroute-113-5`, `omniroute-113-6`, added through the runners API — no
|
||||
re-registration). The other six keep `omni-release` and take nothing heavy; GitHub
|
||||
queues a third build instead of the kernel killing one. Pair with the `heavy-build-*`
|
||||
concurrency lanes in `ci.yml`. To add capacity, label another runner — never raise
|
||||
the count past what 31 GB holds (one next-build ≈ 14–16 GB).
|
||||
- **Never clean `/tmp` or `_work` by hand while any runner is busy.** A
|
||||
check-then-delete with a gap between the two is how a live Build job lost its
|
||||
`_work` on 2026-08-27. The janitor does the check and the removal in one step;
|
||||
let it.
|
||||
- Stopping a runner mid-job cancels the job (observed live): `systemctl stop` only
|
||||
when its listener has no `Runner.Worker` child — and do it in one command.
|
||||
- Workflows must not park artefacts in `/tmp` (it is RAM). Download to
|
||||
`$RUNNER_TEMP` (on disk, per runner) — the 1.3 GB `next-build` artefact took 27–32
|
||||
minutes to land on the tmpfs and 2 minutes to upload from disk.
|
||||
- **Ceiling: 4 runners** on the 16 GB box. Runners 5–8 stay STOPPED except for
|
||||
explicit off-peak experiments — never during a release window.
|
||||
- Stopping a runner mid-job cancels the job (observed live): `systemctl stop`
|
||||
only when its runner is idle (`Runner.Listener` without a `Runner.Worker` child).
|
||||
- The `.15` VPS is homologation-only — never runs CI runners.
|
||||
|
||||
@@ -103,7 +103,6 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
|
||||
| `OMNIROUTE_MIGRATIONS_DIR` | _(auto-detect)_ | `src/lib/db/migrationRunner.ts` | Override the directory that the migration runner scans. Useful when shipping bundled migrations in custom builds. |
|
||||
| `OMNIROUTE_EXTRA_MIGRATIONS_DIRS` | _(unset)_ | `src/lib/db/migrationRunner/extraDirs.ts` | Additional migration directories as `namespace=dir` entries separated by the platform path delimiter (e.g. `ee=/opt/app/enterprise/db/migrations`). Files found there are recorded as `<namespace>-<number>`, so a distribution shipping its own migrations never collides with the upstream numeric slots. A malformed entry, an invalid namespace or a missing directory throws at startup instead of silently skipping the schema. |
|
||||
| `OMNIROUTE_MAX_PENDING_MIGRATIONS` | `50` | `src/lib/db/migrationRunner.ts` | Mass-pending-migrations safety threshold (#3416). Startup aborts if more than this many migrations are pending on an existing DB (guards against a wiped tracking table). Raise it to restore an older backup; set to `0` to disable the check. |
|
||||
| `OMNIROUTE_INSTALL_UPGRADE_WORKDIR` | _(`<repo>/.install-upgrade`)_ | `scripts/check/check-install-upgrade.mjs` | Working directory for the `check:install-upgrade` release gate. It needs roughly 12 GB (two ~3 GB install trees plus the tarball), so it must not run on a small tmpfs — on the self-hosted runner `/tmp` is a 12 GB RAM-backed tmpfs and the gate exhausted it, truncating the package. |
|
||||
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Flush interval (ms) for the batched spend/cost writer. Lower values reduce write coalescing; higher values reduce DB contention. |
|
||||
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Max buffered spend entries before a forced flush. Raise on high-QPS deployments; lower when bounded memory matters more. |
|
||||
| `OMNIROUTE_PROXY_FETCH_DEBUG` | _(unset)_ | `open-sse/utils/proxyFetch.ts` | Set to `"true"` to emit `[ProxyFetch]` debug logs on the Vercel relay path. Off by default to avoid leaking routing hints. |
|
||||
|
||||
4
llm.txt
4
llm.txt
@@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -52,8 +52,7 @@
|
||||
"!**/*.test.js",
|
||||
"!**/*.test.mjs",
|
||||
"!**/*.spec.ts",
|
||||
"!**/*.spec.tsx",
|
||||
"!**/*.nft.json"
|
||||
"!**/*.spec.tsx"
|
||||
],
|
||||
"workspaces": [
|
||||
"open-sse",
|
||||
|
||||
@@ -30,12 +30,10 @@
|
||||
* Requires `npm run build:cli` first — this is a --with-build gate, like check:pack-boot.
|
||||
*/
|
||||
|
||||
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
const BOOT_DEADLINE_MS = 180_000;
|
||||
@@ -45,59 +43,6 @@ const ALLOWLIST_PATH = "config/quality/install-upgrade-allowlist.json";
|
||||
const log = (msg) => console.log(`[install-upgrade] ${msg}`);
|
||||
const warn = (msg) => console.log(`[install-upgrade] ⚠️ ${msg}`);
|
||||
|
||||
/** Root of the installed package inside an `npm install -g --prefix` tree. */
|
||||
function packageRootFor(prefix) {
|
||||
return path.join(prefix, "lib", "node_modules", "omniroute");
|
||||
}
|
||||
|
||||
/**
|
||||
* Credential for the health probe.
|
||||
*
|
||||
* GHSA-mvf8-qc78-5mxm hardened /api/monitoring/health: an ANONYMOUS caller now gets only
|
||||
* `{ status }` — the version, node version, pid and provider config are reserved for a
|
||||
* management principal (src/app/api/monitoring/health/route.ts → publicHealthView). An
|
||||
* unauthenticated probe therefore reads `body.version === undefined`, and this gate's
|
||||
* version assertion could never pass again; the v3.8.50 publish run failed with
|
||||
* "clean: health reports version undefined, expected 3.8.50" for exactly that reason.
|
||||
*
|
||||
* The gate spawns the server itself, so it can mint the credential instead of guessing one:
|
||||
* `OMNIROUTE_INTERNAL_SERVICE_TOKEN` + the `x-omniroute-internal-service-token` header is
|
||||
* accepted by requireManagementAuth() via isTrustedLoopbackInternalServiceRequest(), and the
|
||||
* probe is loopback by construction. Unlike the machine token `check:pack-boot` derives, this
|
||||
* does not depend on a readable machine-id, and an older PREVIOUS version that never gated
|
||||
* health simply ignores the header. The assertion keeps its full strength — it just presents
|
||||
* a credential.
|
||||
*/
|
||||
const INTERNAL_SERVICE_TOKEN = crypto.randomBytes(32).toString("hex");
|
||||
const INTERNAL_SERVICE_HEADER = "x-omniroute-internal-service-token";
|
||||
|
||||
/**
|
||||
* Secondary credential: the same loopback machine token `check:pack-boot` derives from the
|
||||
* packaged CLI. Sent alongside the internal-service token so a build that only honours one
|
||||
* of the two still answers with the full payload.
|
||||
*/
|
||||
function derivePackagedCliToken(prefix) {
|
||||
const cliModuleUrl = pathToFileURL(
|
||||
path.join(packageRootFor(prefix), "bin", "cli", "utils", "cliToken.mjs")
|
||||
).href;
|
||||
try {
|
||||
return execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
"import(process.argv[1]).then(async m => process.stdout.write(await m.getCliToken()))",
|
||||
cliModuleUrl,
|
||||
],
|
||||
{ encoding: "utf8", env: { ...process.env } }
|
||||
).trim();
|
||||
} catch {
|
||||
// A truncated/broken install cannot derive a token. Returning null keeps the boot
|
||||
// probe running (it will fail loudly on its own) instead of crashing the gate here.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pickTarball(packJson) {
|
||||
const filename = JSON.parse(packJson)?.[0]?.filename;
|
||||
if (!filename) throw new Error("npm pack --json returned no filename");
|
||||
@@ -176,11 +121,6 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
if (!fs.existsSync(binPath)) {
|
||||
return { ok: false, failures: [`${label}: bin not found at ${binPath}`], tail: [] };
|
||||
}
|
||||
const cliToken = derivePackagedCliToken(prefix);
|
||||
const probeHeaders = {
|
||||
[INTERNAL_SERVICE_HEADER]: INTERNAL_SERVICE_TOKEN,
|
||||
...(cliToken ? { "x-omniroute-cli-token": cliToken } : {}),
|
||||
};
|
||||
const child = spawn(binPath, ["serve", "--port", String(port)], {
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -190,7 +130,6 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
API_KEY_SECRET: "install-upgrade-gate-api-key-secret-long",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
OMNIROUTE_SKIP_SYSTEM_TRUST: "1",
|
||||
OMNIROUTE_INTERNAL_SERVICE_TOKEN: INTERNAL_SERVICE_TOKEN,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
@@ -212,33 +151,20 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
let result = { ok: false, failures: [`${label}: never became healthy`], tail };
|
||||
while (Date.now() < deadline) {
|
||||
if (childExit !== null) {
|
||||
result = {
|
||||
ok: false,
|
||||
failures: [`${label}: exited with code ${childExit} before serving`],
|
||||
tail,
|
||||
};
|
||||
result = { ok: false, failures: [`${label}: exited with code ${childExit} before serving`], tail };
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`, {
|
||||
headers: probeHeaders,
|
||||
});
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`);
|
||||
const body = await res.json().catch(() => null);
|
||||
if (res.status === 200 && body && typeof body === "object") {
|
||||
const failures = [];
|
||||
// `status` may legitimately report degraded (no providers configured) — the gate
|
||||
// targets boot crashes and version mismatches, not health of a bare install.
|
||||
const reportedVersion = body.version ?? body.system?.version;
|
||||
if (expectVersion && reportedVersion !== expectVersion) {
|
||||
failures.push(
|
||||
`${label}: health reports version ${reportedVersion}, expected ${expectVersion}` +
|
||||
(reportedVersion === undefined
|
||||
? " — the payload carries no version at all, which is the ANONYMOUS health " +
|
||||
"view: the probe's credentials were not accepted (see GHSA-mvf8-qc78-5mxm)"
|
||||
: "")
|
||||
);
|
||||
if (expectVersion && body.version !== expectVersion) {
|
||||
failures.push(`${label}: health reports version ${body.version}, expected ${expectVersion}`);
|
||||
}
|
||||
result = { ok: failures.length === 0, version: reportedVersion, failures, tail };
|
||||
result = { ok: failures.length === 0, version: body.version, failures, tail };
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
@@ -257,72 +183,13 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* `npm install` reports ENOSPC as a *warning* per failed tar entry and still exits 0.
|
||||
*
|
||||
* That is not a theoretical concern: on the v3.8.50 publish run the Phase B upgrade install
|
||||
* emitted 5611 `npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device` lines, exited
|
||||
* 0, and left a truncated package behind. `omniroute serve` then "exited with code 0 before
|
||||
* serving", no migration ever ran, and the gate concluded the release was missing 15 tables
|
||||
* — a full false alarm produced by a full disk. Each install tree is ~3 GB, and the run
|
||||
* builds two of them plus a ~275 MB tarball.
|
||||
*
|
||||
* So: surface the truncation at the install, where it is unambiguous.
|
||||
*/
|
||||
function npmInstallInto(prefix, spec, label = spec) {
|
||||
// spawnSync (not execFileSync): execFileSync forwards the child's stderr straight to the
|
||||
// parent's, so the ENOSPC warnings scrolled past in CI without the script ever seeing
|
||||
// them. spawnSync hands both streams back.
|
||||
const run = spawnSync(
|
||||
"npm",
|
||||
["install", "-g", "--prefix", prefix, "--no-audit", "--no-fund", spec],
|
||||
{ encoding: "utf8", maxBuffer: 512 * 1024 * 1024 }
|
||||
);
|
||||
const output = `${run.stdout ?? ""}${run.stderr ?? ""}`;
|
||||
// Keep the install log visible, but a truncated package emits thousands of identical
|
||||
// warnings — collapse them so the real message is not buried.
|
||||
const stderrLines = String(run.stderr ?? "").split("\n");
|
||||
const shown = stderrLines.length > 60 ? stderrLines.slice(0, 40) : stderrLines;
|
||||
if (String(run.stderr ?? "").trim()) {
|
||||
process.stderr.write(shown.join("\n") + "\n");
|
||||
if (stderrLines.length > 60) {
|
||||
process.stderr.write(`[install-upgrade] … ${stderrLines.length - 40} more npm line(s)\n`);
|
||||
}
|
||||
}
|
||||
assertNoDiskExhaustion(output, label);
|
||||
if (run.error) throw run.error;
|
||||
if (run.status !== 0) {
|
||||
throw new Error(`${label}: npm install exited with code ${run.status}`);
|
||||
}
|
||||
function npmInstallInto(prefix, spec) {
|
||||
execFileSync("npm", ["install", "-g", "--prefix", prefix, "--no-audit", "--no-fund", spec], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 128 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
|
||||
export function assertNoDiskExhaustion(output, label) {
|
||||
if (!/ENOSPC|no space left on device/i.test(output)) return;
|
||||
const count = (output.match(/ENOSPC/g) ?? []).length;
|
||||
throw new Error(
|
||||
`${label}: the install ran out of disk space (${count} ENOSPC error(s) from npm). ` +
|
||||
`The package tree is truncated, so anything measured from it — boot, schema, ` +
|
||||
`migrations — is meaningless. Free space in ${workDirForMessages} (each install tree is ` +
|
||||
`~3 GB) and re-run. This is an environment failure, NOT a schema divergence.`
|
||||
);
|
||||
}
|
||||
|
||||
/** Best-effort free bytes on the filesystem backing `dir`, or null when unavailable. */
|
||||
function freeBytes(dir) {
|
||||
try {
|
||||
return fs.statfsSync(dir).bavail * fs.statfsSync(dir).bsize;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const GB = 1024 ** 3;
|
||||
|
||||
// Set once the work directory exists, so the ENOSPC message names the filesystem that
|
||||
// actually ran out — pointing at /tmp when the gate works elsewhere sends the reader to
|
||||
// free space on the wrong volume (which is what happened during the v3.8.50 publish).
|
||||
let workDirForMessages = os.tmpdir();
|
||||
|
||||
function resolvePreviousVersion(current, explicit) {
|
||||
if (explicit) return explicit;
|
||||
const out = execFileSync("npm", ["view", "omniroute", "dist-tags.latest"], { encoding: "utf8" });
|
||||
@@ -331,9 +198,7 @@ function resolvePreviousVersion(current, explicit) {
|
||||
if (latest === current) {
|
||||
// The version under test is already published (re-run of a shipped release): step back
|
||||
// to the highest published version strictly below it.
|
||||
const all = JSON.parse(
|
||||
execFileSync("npm", ["view", "omniroute", "versions", "--json"], { encoding: "utf8" })
|
||||
);
|
||||
const all = JSON.parse(execFileSync("npm", ["view", "omniroute", "versions", "--json"], { encoding: "utf8" }));
|
||||
const stable = all.filter((v) => !/-(rc|alpha|beta|pre|next)/.test(v) && v !== current);
|
||||
return stable[stable.length - 1];
|
||||
}
|
||||
@@ -353,16 +218,7 @@ async function main() {
|
||||
}
|
||||
const version = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
|
||||
const allowlist = loadAllowlist(ROOT);
|
||||
// NOT os.tmpdir(): on the self-hosted runner /tmp is a 12 GB tmpfs backed by RAM, while
|
||||
// the root filesystem has ~66 GB free. This gate needs ~12 GB, so it exhausted the tmpfs
|
||||
// and npm truncated the package — 58269 ENOSPC errors on the v3.8.50 publish, which the
|
||||
// previous code could only report as a crash. Freeing disk did not help because the disk
|
||||
// was never the constraint. Work on real disk beside the repo instead.
|
||||
const workRoot =
|
||||
process.env.OMNIROUTE_INSTALL_UPGRADE_WORKDIR || path.join(ROOT, ".install-upgrade");
|
||||
fs.mkdirSync(workRoot, { recursive: true });
|
||||
const tmp = fs.mkdtempSync(path.join(workRoot, "omniroute-install-upgrade-"));
|
||||
workDirForMessages = tmp;
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-install-upgrade-"));
|
||||
const failures = [];
|
||||
const warnings = [];
|
||||
|
||||
@@ -382,28 +238,12 @@ async function main() {
|
||||
const packMb = (fs.statSync(tarball).size / 1024 / 1024).toFixed(1);
|
||||
log(`packed in ${Math.round((Date.now() - packStarted) / 1000)}s (${packMb} MB)`);
|
||||
|
||||
// Each install tree is ~3 GB and this run builds two of them, side by side, plus the
|
||||
// ~275 MB tarball. On the v3.8.50 publish run that overflowed the runner disk mid-way
|
||||
// through the Phase B upgrade install; npm warned per truncated tar entry and still
|
||||
// exited 0, and every later measurement was taken from a broken tree.
|
||||
const availableBytes = freeBytes(tmp);
|
||||
if (availableBytes !== null) {
|
||||
log(`free space in ${tmp}: ${(availableBytes / GB).toFixed(1)} GB`);
|
||||
if (availableBytes < 12 * GB) {
|
||||
warn(
|
||||
`only ${(availableBytes / GB).toFixed(1)} GB free — this gate needs roughly 12 GB ` +
|
||||
`(two ~3 GB install trees, the second installed over twice, plus the tarball). ` +
|
||||
`An install truncated by ENOSPC looks like a schema divergence.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Phase A: clean install -------------------------------------------------
|
||||
log("PHASE A — clean install of the packed tarball");
|
||||
const aPrefix = path.join(tmp, "a-prefix");
|
||||
const aData = path.join(tmp, "a-data");
|
||||
fs.mkdirSync(aData, { recursive: true });
|
||||
npmInstallInto(aPrefix, tarball, "clean install");
|
||||
npmInstallInto(aPrefix, tarball);
|
||||
const a = await bootAndProbe({
|
||||
prefix: aPrefix,
|
||||
dataDir: aData,
|
||||
@@ -412,34 +252,14 @@ async function main() {
|
||||
label: "clean",
|
||||
});
|
||||
failures.push(...a.failures);
|
||||
const cleanBooted = a.ok;
|
||||
if (a.ok) log(`clean install healthy on v${a.version}`);
|
||||
else if (a.tail?.length) {
|
||||
console.error("[install-upgrade] last output from the clean-install server:");
|
||||
console.error(a.tail.join("").split("\n").slice(-40).join("\n"));
|
||||
}
|
||||
const aDb = findDb(aData);
|
||||
const freshTables = aDb ? readTables(aDb) : null;
|
||||
if (!freshTables) failures.push("clean: no SQLite database was created");
|
||||
else log(`clean install schema: ${freshTables.size} tables`);
|
||||
|
||||
// Phase A is fully measured (boot verdict + schema snapshot); its ~3 GB install tree is
|
||||
// dead weight from here on and Phase B needs the room. The DATA_DIR stays — only the
|
||||
// node_modules tree goes.
|
||||
if (!skipUpgrade) {
|
||||
fs.rmSync(aPrefix, { recursive: true, force: true });
|
||||
const reclaimed = freeBytes(tmp);
|
||||
log(
|
||||
"released the clean-install tree before the upgrade phase" +
|
||||
(reclaimed !== null ? ` (${(reclaimed / GB).toFixed(1)} GB free)` : "")
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Phase B: upgrade over the previous published version -------------------
|
||||
let upgradedTables = null;
|
||||
// `--skip-upgrade` never reaches the convergence block (upgradedTables stays null), so
|
||||
// defaulting this to true keeps that path unchanged.
|
||||
let upgradeBooted = true;
|
||||
if (skipUpgrade) {
|
||||
warn("PHASE B skipped (--skip-upgrade)");
|
||||
} else {
|
||||
@@ -449,7 +269,7 @@ async function main() {
|
||||
const bData = path.join(tmp, "b-data");
|
||||
fs.mkdirSync(bData, { recursive: true });
|
||||
|
||||
npmInstallInto(bPrefix, `omniroute@${previous}`, `previous(${previous}) install`);
|
||||
npmInstallInto(bPrefix, `omniroute@${previous}`);
|
||||
const before = await bootAndProbe({
|
||||
prefix: bPrefix,
|
||||
dataDir: bData,
|
||||
@@ -460,16 +280,14 @@ async function main() {
|
||||
if (!before.ok) {
|
||||
// A broken PREVIOUS version is not this release's fault — degrade to a warning so a
|
||||
// historically bad publish cannot block the current one.
|
||||
warnings.push(
|
||||
`previous version ${previous} did not boot cleanly — upgrade path unverified`
|
||||
);
|
||||
warnings.push(`previous version ${previous} did not boot cleanly — upgrade path unverified`);
|
||||
for (const f of before.failures) warn(f);
|
||||
} else {
|
||||
const beforeDb = findDb(bData);
|
||||
const beforeTables = beforeDb ? readTables(beforeDb) : new Set();
|
||||
log(`previous(${previous}) schema: ${beforeTables.size} tables — upgrading in place`);
|
||||
|
||||
npmInstallInto(bPrefix, tarball, "upgrade install");
|
||||
npmInstallInto(bPrefix, tarball);
|
||||
const after = await bootAndProbe({
|
||||
prefix: bPrefix,
|
||||
dataDir: bData,
|
||||
@@ -479,11 +297,6 @@ async function main() {
|
||||
});
|
||||
failures.push(...after.failures);
|
||||
if (after.ok) log(`upgrade healthy on v${after.version}`);
|
||||
upgradeBooted = after.ok;
|
||||
if (!after.ok && after.tail?.length) {
|
||||
console.error("[install-upgrade] last output from the upgraded server:");
|
||||
console.error(after.tail.join("").split("\n").slice(-40).join("\n"));
|
||||
}
|
||||
|
||||
const afterDb = findDb(bData);
|
||||
upgradedTables = afterDb ? readTables(afterDb) : null;
|
||||
@@ -500,19 +313,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// ---- Schema convergence -----------------------------------------------------
|
||||
// Only meaningful when BOTH servers actually served. A boot that died before serving
|
||||
// never ran a migration, so its database still holds the PREVIOUS release's schema and
|
||||
// every post-baseline table shows up as "a clean install creates but an upgrade does
|
||||
// not" — which is what the v3.8.50 publish run reported after ENOSPC truncated the
|
||||
// upgrade install. Comparing there does not add information, it manufactures a
|
||||
// 15-table false alarm on top of the real failure. The run still fails: the boot
|
||||
// failure is already in `failures`.
|
||||
if (freshTables && upgradedTables && !(cleanBooted && upgradeBooted)) {
|
||||
warn(
|
||||
"schema convergence NOT evaluated — a phase failed to boot, so its database was " +
|
||||
"never migrated and any table difference would describe the broken boot, not the schema"
|
||||
);
|
||||
} else if (freshTables && upgradedTables) {
|
||||
if (freshTables && upgradedTables) {
|
||||
const verdict = evaluateConvergence({
|
||||
freshTables,
|
||||
upgradedTables,
|
||||
@@ -540,10 +341,7 @@ async function main() {
|
||||
|
||||
// Only run the (expensive) gate when invoked directly — importing this module for the pure
|
||||
// helper above must not pack, install or boot anything.
|
||||
if (
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)
|
||||
) {
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
|
||||
main().catch((err) => {
|
||||
console.error(`[install-upgrade] crashed: ${err?.message ?? err}`);
|
||||
process.exit(1);
|
||||
|
||||
@@ -42,7 +42,6 @@ import { execFileSync, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { findProvenanceOnSelfHosted, formatProvenanceFinding } from "./lib/provenanceRunner.mjs";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const WORKFLOWS_DIR = path.join(ROOT, ".github", "workflows");
|
||||
@@ -276,23 +275,6 @@ export function runZizmor(workflowsDir) {
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hard rule (not a lint count): `--provenance` inside a job that runs on a
|
||||
* self-hosted runner. npm answers 422 at the registry, and in v3.8.50 that
|
||||
* answer only came after the tag, the GitHub Release and the Docker images were
|
||||
* already out. Blocks under --strict AND --ratchet (the CI mode); plain mode
|
||||
* reports it like everything else.
|
||||
* @param {string[]} files absolute workflow paths
|
||||
*/
|
||||
export function runProvenanceRunnerCheck(files) {
|
||||
const findings = [];
|
||||
for (const file of files) {
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
findings.push(...findProvenanceOnSelfHosted(text, path.relative(ROOT, file)));
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const hasActionlint = isBinaryAvailable("actionlint");
|
||||
const hasZizmor = isBinaryAvailable("zizmor");
|
||||
@@ -368,16 +350,6 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const provenanceFindings = runProvenanceRunnerCheck(workflowFiles);
|
||||
if (provenanceFindings.length > 0) {
|
||||
console.error(
|
||||
`[check-workflows] provenance×self-hosted: ${provenanceFindings.length} finding(s) — HARD RULE:`
|
||||
);
|
||||
provenanceFindings.forEach((f) => console.error(` ${formatProvenanceFinding(f)}`));
|
||||
} else if (!QUIET) {
|
||||
console.log("[check-workflows] provenance×self-hosted: OK (0 findings)");
|
||||
}
|
||||
|
||||
const total = actionlintCount + zizmorCount;
|
||||
process.stdout.write(`workflowFindings=${total}\n`);
|
||||
process.stdout.write(`actionlintFindings=${actionlintCount}\n`);
|
||||
@@ -385,15 +357,6 @@ function main() {
|
||||
// Read this line with the count above: a finding total is only reproducible against the
|
||||
// version that produced it. See zizmorVersion().
|
||||
process.stdout.write(`zizmorVersion=${hasZizmor ? zizmorVersion() : "absent"}\n`);
|
||||
process.stdout.write(`provenanceRunnerFindings=${provenanceFindings.length}\n`);
|
||||
if ((STRICT || RATCHET) && provenanceFindings.length > 0) {
|
||||
console.error(
|
||||
`\n[check-workflows] FAIL — ${provenanceFindings.length} job(s) publish with --provenance from a self-hosted runner.\n` +
|
||||
" npm rejects that with 422 at the registry. Move the upload step to a github-hosted job\n" +
|
||||
" (see .github/workflows/npm-publish.yml `stage-npm` for the pattern)."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (STRICT && total > 0) {
|
||||
console.error(`\n[check-workflows] FAIL — ${total} workflow finding(s) total (--strict mode).`);
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* scripts/check/lib/configExpiry.mjs
|
||||
*
|
||||
* Finds dated validity fields in JSON config packs so a test can fail BEFORE
|
||||
* they lapse. Origin: config/alibaba-free-tier-allowlist.json carried
|
||||
* `"validUntil": "2026-08-27"`; on 2026-08-28 the loader started (correctly)
|
||||
* rejecting the pack and a test that asserted "the shipped pack loads" turned
|
||||
* every PR and main red with no commit involved (#11866). A time bomb, not a
|
||||
* regression — and the only kind of defect a diff review can never catch.
|
||||
*
|
||||
* Pure helpers; the repo-wide assertion lives in
|
||||
* tests/unit/config-expiry-time-bomb.test.ts.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const EXPIRY_KEY =
|
||||
/^(validUntil|valid_until|validTo|valid_to|expiresAt|expires_at|expiry|expires)$/;
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
/**
|
||||
* Walks a parsed JSON value and returns every string-valued expiry field.
|
||||
* @returns {{ file: string, keyPath: string, raw: string, expiresAt: number|null }[]}
|
||||
*/
|
||||
export function collectExpiryFields(value, file, keyPath = []) {
|
||||
const out = [];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v, i) => out.push(...collectExpiryFields(v, file, [...keyPath, String(i)])));
|
||||
return out;
|
||||
}
|
||||
if (!value || typeof value !== "object") return out;
|
||||
for (const [key, v] of Object.entries(value)) {
|
||||
const kp = [...keyPath, key];
|
||||
if (EXPIRY_KEY.test(key) && typeof v === "string") {
|
||||
const ms = Date.parse(v);
|
||||
out.push({ file, keyPath: kp.join("."), raw: v, expiresAt: Number.isFinite(ms) ? ms : null });
|
||||
} else if (v && typeof v === "object") {
|
||||
out.push(...collectExpiryFields(v, file, kp));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** @returns {"expired"|"expiring"|"ok"|"unparseable"} */
|
||||
export function classifyExpiry(field, nowMs, warnDays = 7) {
|
||||
if (field.expiresAt === null) return "unparseable";
|
||||
if (field.expiresAt < nowMs) return "expired";
|
||||
if (field.expiresAt < nowMs + warnDays * DAY_MS) return "expiring";
|
||||
return "ok";
|
||||
}
|
||||
|
||||
/** All *.json under dir, recursively, skipping node_modules. Sorted for stable output. */
|
||||
export function walkJsonFiles(dir) {
|
||||
const out = [];
|
||||
const stack = [dir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const e of entries) {
|
||||
const full = path.join(current, e.name);
|
||||
if (e.isDirectory()) {
|
||||
if (e.name !== "node_modules") stack.push(full);
|
||||
} else if (e.isFile() && e.name.endsWith(".json")) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans every JSON file under `dir`; `file` in the result is relative to `dir`
|
||||
* with forward slashes, so allowlists can key on it portably.
|
||||
*/
|
||||
export function scanConfigExpiry(dir) {
|
||||
return walkJsonFiles(dir).flatMap((f) => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(f, "utf8"));
|
||||
} catch {
|
||||
return []; // not this scanner's job to validate JSON
|
||||
}
|
||||
return collectExpiryFields(parsed, path.relative(dir, f).split(path.sep).join("/"));
|
||||
});
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* scripts/check/lib/provenanceRunner.mjs
|
||||
*
|
||||
* npm refuses `--provenance` from a self-hosted runner:
|
||||
*
|
||||
* 422 Unprocessable Entity - Error verifying sigstore provenance bundle:
|
||||
* Unsupported GitHub Actions runner environment: "self-hosted".
|
||||
* Only "github-hosted" runners are supported when publishing with provenance.
|
||||
*
|
||||
* v3.8.50 hit this at the very end of a 76-minute publish job — after the tag,
|
||||
* the GitHub Release and the Docker images were already public — because
|
||||
* `USE_VPS_RUNNER` had been turned on (2026-08-02) with no release in between to
|
||||
* surface it. The combination is greppable, so it must fail in CI the moment a
|
||||
* workflow introduces it, not four weeks later at the registry.
|
||||
*
|
||||
* Pure: takes workflow YAML text, returns the offending (job, step) pairs.
|
||||
*/
|
||||
import { load as yamlLoad } from "js-yaml";
|
||||
|
||||
const SELF_HOSTED = /\bself-hosted\b/;
|
||||
const EXPRESSION = /\$\{\{/;
|
||||
// Lookahead, not \b: `--provenance-file=…` is a different flag (a pre-built
|
||||
// bundle) and must not match — a word boundary sits between "e" and "-".
|
||||
const PROVENANCE = /(^|\s)--provenance(?=\s|=|$)/m;
|
||||
|
||||
/**
|
||||
* Classifies a job's `runs-on` value.
|
||||
* @returns {"self-hosted"|"hosted"|"unknown"}
|
||||
* "unknown" = an expression with no literal `self-hosted` in it (e.g.
|
||||
* `${{ matrix.os }}`); the check does not guess, it skips.
|
||||
*/
|
||||
export function classifyRunsOn(runsOn) {
|
||||
if (runsOn == null) return "unknown";
|
||||
if (typeof runsOn === "string") {
|
||||
if (SELF_HOSTED.test(runsOn)) return "self-hosted";
|
||||
return EXPRESSION.test(runsOn) ? "unknown" : "hosted";
|
||||
}
|
||||
if (Array.isArray(runsOn)) {
|
||||
return runsOn.some((v) => typeof v === "string" && SELF_HOSTED.test(v))
|
||||
? "self-hosted"
|
||||
: "hosted";
|
||||
}
|
||||
if (typeof runsOn === "object") {
|
||||
// { group: ..., labels: ... } form
|
||||
const labels = runsOn.labels;
|
||||
return classifyRunsOn(Array.isArray(labels) ? labels : labels == null ? "" : String(labels));
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} yamlText
|
||||
* @param {string} fileName used only for reporting
|
||||
* @returns {{ file: string, job: string, step: string }[]}
|
||||
*/
|
||||
export function findProvenanceOnSelfHosted(yamlText, fileName = "<workflow>") {
|
||||
let doc;
|
||||
try {
|
||||
doc = yamlLoad(yamlText);
|
||||
} catch {
|
||||
// actionlint owns syntax; an unparseable file is not this rule's finding.
|
||||
return [];
|
||||
}
|
||||
const jobs =
|
||||
doc && typeof doc === "object" && doc.jobs && typeof doc.jobs === "object" ? doc.jobs : {};
|
||||
const findings = [];
|
||||
for (const [jobName, job] of Object.entries(jobs)) {
|
||||
if (!job || typeof job !== "object") continue;
|
||||
if (classifyRunsOn(job["runs-on"]) !== "self-hosted") continue;
|
||||
const steps = Array.isArray(job.steps) ? job.steps : [];
|
||||
steps.forEach((step, i) => {
|
||||
if (step && typeof step.run === "string" && PROVENANCE.test(step.run)) {
|
||||
findings.push({ file: fileName, job: jobName, step: step.name || `#${i + 1}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
/** Human-readable line per finding, used by the CLI. */
|
||||
export function formatProvenanceFinding(f) {
|
||||
return `${f.file}: job "${f.job}", step "${f.step}" runs \`--provenance\` on a self-hosted runner — npm rejects that (422). Move the upload to a github-hosted job.`;
|
||||
}
|
||||
@@ -1,172 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# runner-janitor — self-hosted runner box hygiene for the .113 pool.
|
||||
# runner-janitor — self-hosted runner box hygiene (WS3.3, v3.8.49 quality plan).
|
||||
#
|
||||
# Runs from cron every 30 min (see docs/ops/RUNNER_BOX.md). It ACTS on what it
|
||||
# can prove is safe and ALERTS on what needs an operator decision. Reads of
|
||||
# "is this in use?" and the removal happen in the same command, never in two
|
||||
# passes: a check-then-delete with a gap is how a live Build job lost its _work
|
||||
# on 2026-08-27.
|
||||
# The .113 runner box has recurring failure modes that until now were manual
|
||||
# discipline: orphaned tmpfs/work dirs filling the disk, and >4 concurrent
|
||||
# runners OOM-killing jobs (16 GB box; incidents on the v3.8.47 release day).
|
||||
# Install via cron on the box (see docs/ops/RUNNER_BOX.md):
|
||||
# */30 * * * * /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1
|
||||
#
|
||||
# Measured box (2026-08-28): 31 GB RAM, 32 cores, 15 GB swap, /tmp = 12 GB
|
||||
# tmpfs (RAM!), 188 GB disk. A single `next-build` peaks at ~14 GB, so two
|
||||
# concurrent heavy builds saturate the box and three take it down (06:42Z that
|
||||
# day: load 56, two jobs lost). The v3.8.50 postmortem (Parte III) has the numbers.
|
||||
#
|
||||
# What it does, in order:
|
||||
# 1) sweep stale artefacts our tooling leaves behind — tmpfs bases after 3 h
|
||||
# (they hold RAM), disk _work/_temp bases after 24 h; only names we create,
|
||||
# only when no process has them open
|
||||
# 2) kill zombie builds: a `next-build` older than ZOMBIE_BUILD_MAX_MIN has no
|
||||
# job attached (a real Build step measures ~26 min). On 2026-08-27 one ran
|
||||
# 70 minutes after GitHub had already declared its job lost, eating 3.6 GB
|
||||
# and a full core set. KillMode=mixed on the units covers systemctl
|
||||
# stop/restart; this covers the lost-connection path.
|
||||
# 3) prune 48 h-old checkouts under _work of runners whose unit is INACTIVE
|
||||
# (stopped runners cannot be mid-job; active ones are never touched)
|
||||
# 4) alert: root disk >= DISK_ALERT_PCT, memory PSI full/avg60 >= threshold,
|
||||
# Runner.Listener count above the ceiling (with a per-project breakdown —
|
||||
# the box also hosts OmniHeuris and OmniMind runners)
|
||||
#
|
||||
# Usage: runner-janitor.sh [--dry-run] [--help]
|
||||
# Exit codes: 0 healthy · 1 attention needed (printed to stdout for the log).
|
||||
set -euo pipefail
|
||||
|
||||
DRY_RUN=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0 ;;
|
||||
*) echo "unknown argument: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
MAX_ACTIVE_RUNNERS="${MAX_ACTIVE_RUNNERS:-8}"
|
||||
MAX_ACTIVE_RUNNERS="${MAX_ACTIVE_RUNNERS:-4}"
|
||||
DISK_ALERT_PCT="${DISK_ALERT_PCT:-85}"
|
||||
TMPFS_MAX_AGE_HOURS="${TMPFS_MAX_AGE_HOURS:-3}"
|
||||
WORK_TEMP_MAX_AGE_HOURS="${WORK_TEMP_MAX_AGE_HOURS:-24}"
|
||||
WORK_CHECKOUT_MAX_AGE_HOURS="${WORK_CHECKOUT_MAX_AGE_HOURS:-48}"
|
||||
ZOMBIE_BUILD_MAX_MIN="${ZOMBIE_BUILD_MAX_MIN:-75}"
|
||||
ZOMBIE_BUILD_COMM="${ZOMBIE_BUILD_COMM:-next-build}"
|
||||
PSI_FULL_AVG60_ALERT="${PSI_FULL_AVG60_ALERT:-10}"
|
||||
# Overridable so the unit test can point everything at a fixture tree.
|
||||
JANITOR_TMP_BASES="${JANITOR_TMP_BASES-/tmp}"
|
||||
JANITOR_WORK_TEMP_BASES="${JANITOR_WORK_TEMP_BASES-/opt/actions-runner*/_work/_temp /home/*/actions-runner*/_work/_temp}"
|
||||
JANITOR_RUNNER_DIRS="${JANITOR_RUNNER_DIRS-/opt/actions-runner*}"
|
||||
JANITOR_PSI_FILE="${JANITOR_PSI_FILE:-/proc/pressure/memory}"
|
||||
JANITOR_DF_PATH="${JANITOR_DF_PATH:-/}"
|
||||
|
||||
WORK_DIR_MAX_AGE_HOURS="${WORK_DIR_MAX_AGE_HOURS:-24}"
|
||||
STATUS=0
|
||||
say() { echo "[janitor] $*"; }
|
||||
|
||||
# "Is anything using this?" — ONE snapshot of every open path on the box
|
||||
# (lsof -Fn), then a prefix match per candidate. `lsof +D <dir>` walks the whole
|
||||
# tree instead and took minutes on a 5 GB leftover — unusable from cron. An
|
||||
# absent lsof means "cannot prove idle": the sweep keeps the path and says so.
|
||||
LSOF_BIN="${JANITOR_LSOF:-lsof}"
|
||||
have_busy_tools() { command -v "$LSOF_BIN" >/dev/null 2>&1; }
|
||||
SNAP=""
|
||||
cleanup() { [ -n "$SNAP" ] && rm -f -- "$SNAP"; }
|
||||
trap cleanup EXIT
|
||||
# One lsof for the whole run (~13 s / 83k lines on the box), kept ONLY for the
|
||||
# bases we sweep — 460 candidates grepping a re-printed 83k-line string was the
|
||||
# slow part, not lsof itself.
|
||||
snapshot_open_paths() {
|
||||
have_busy_tools || return 0
|
||||
SNAP=$(mktemp) || return 0
|
||||
local prefixes="" b
|
||||
for b in $JANITOR_TMP_BASES $JANITOR_WORK_TEMP_BASES; do [ -d "$b" ] && prefixes="$prefixes"$'\n'"$b/"; done
|
||||
# -F n: one "n<path>" line per open file; -w: no warnings
|
||||
"$LSOF_BIN" -w -Fn 2>/dev/null | sed -n 's/^n//p' | grep -F -f <(printf '%s' "$prefixes" | sed '/^$/d') > "$SNAP" 2>/dev/null || true
|
||||
}
|
||||
is_busy() {
|
||||
local p="$1"
|
||||
[ -n "$SNAP" ] && [ -s "$SNAP" ] || return 1
|
||||
# exact path, or anything beneath it when it is a directory
|
||||
grep -qxF -- "$p" "$SNAP" && return 0
|
||||
[ -d "$p" ] && grep -qF -- "$p/" "$SNAP"
|
||||
}
|
||||
echo "[janitor] $(date -u +%FT%TZ) start"
|
||||
|
||||
# sweep <base> <max-age-minutes>: only names our tooling creates, never through
|
||||
# a symlinked base, never across a filesystem, and remove+check in one step.
|
||||
sweep() {
|
||||
local base="$1" max_min="$2" p
|
||||
[ -d "$base" ] || return 0
|
||||
[ -L "$base" ] && { say "skip symlinked base: $base"; return 0; }
|
||||
while IFS= read -r -d '' p; do
|
||||
if ! have_busy_tools; then say "cannot prove idle (lsof missing — apt install lsof), kept: $p"; STATUS=1; continue; fi
|
||||
if is_busy "$p"; then say "busy, kept: $p"; continue; fi
|
||||
if [ "$DRY_RUN" -eq 1 ]; then say "would remove ($(( max_min / 60 ))h+): $p"; else rm -rf -- "$p" && say "removed ($(( max_min / 60 ))h+): $p"; fi
|
||||
done < <(find -P "$base" -xdev -mindepth 1 -maxdepth 1 \
|
||||
\( -name 'runner-*' -o -name 'omniroute-*' -o -name 'next-build*' -o -name 'e2e-build.tar.gz' \) \
|
||||
! -type l -mmin "+$max_min" -print0 2>/dev/null || true)
|
||||
}
|
||||
|
||||
say "$(date -u +%FT%TZ) start${DRY_RUN:+ (dry-run=$DRY_RUN)} busy-tools=$(have_busy_tools && echo ok || echo MISSING)"
|
||||
|
||||
# 1) stale artefacts — tmpfs is RAM, so it gets the short fuse
|
||||
snapshot_open_paths
|
||||
for base in $JANITOR_TMP_BASES; do sweep "$base" $(( TMPFS_MAX_AGE_HOURS * 60 )); done
|
||||
for base in $JANITOR_WORK_TEMP_BASES; do sweep "$base" $(( WORK_TEMP_MAX_AGE_HOURS * 60 )); done
|
||||
say "stale temp sweep done"
|
||||
|
||||
# 2) zombie builds
|
||||
ZOMBIES=0
|
||||
while read -r pid etimes comm; do
|
||||
[ -n "${pid:-}" ] || continue
|
||||
if [ "$etimes" -gt $(( ZOMBIE_BUILD_MAX_MIN * 60 )) ]; then
|
||||
say "⚠ zombie build pid=$pid comm=$comm age=$(( etimes / 60 ))min > ${ZOMBIE_BUILD_MAX_MIN}min — no job runs this long"
|
||||
if [ "$DRY_RUN" -eq 1 ]; then say "[dry-run] would: kill -TERM $pid (then -KILL)"; else
|
||||
kill -TERM "$pid" 2>/dev/null || true; sleep 10
|
||||
kill -0 "$pid" 2>/dev/null && { kill -KILL "$pid" 2>/dev/null || true; say " needed SIGKILL"; }
|
||||
fi
|
||||
ZOMBIES=$(( ZOMBIES + 1 )); STATUS=1
|
||||
fi
|
||||
done < <(ps -eo pid=,etimes=,comm= 2>/dev/null | awk -v c="$ZOMBIE_BUILD_COMM" '$3 ~ ("^" c) {print $1, $2, $3}' || true)
|
||||
say "zombie builds: $ZOMBIES"
|
||||
|
||||
# 3) old checkouts of STOPPED runners
|
||||
for d in $JANITOR_RUNNER_DIRS; do
|
||||
[ -d "$d" ] && [ -f "$d/.runner" ] || continue
|
||||
agent=$(grep -o '"agentName": *"[^"]*"' "$d/.runner" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/')
|
||||
[ -n "$agent" ] || continue
|
||||
unit=$(systemctl list-units --plain --no-legend "actions.runner.*.${agent}.service" 2>/dev/null | awk 'NR==1{print $1}')
|
||||
[ -n "$unit" ] || continue
|
||||
if systemctl is-active --quiet "$unit"; then continue; fi
|
||||
while IFS= read -r -d '' co; do
|
||||
if [ "$DRY_RUN" -eq 1 ]; then say "would prune checkout of stopped runner $agent: $co"; else rm -rf -- "$co" && say "pruned checkout of stopped runner $agent: $co"; fi
|
||||
done < <(find -P "$d/_work" -xdev -mindepth 2 -maxdepth 2 -type d -mmin "+$(( WORK_CHECKOUT_MAX_AGE_HOURS * 60 ))" -print0 2>/dev/null || true)
|
||||
# 1) Sweep stale runner temp/work leftovers (>24h — no legitimate job runs that long).
|
||||
# Hardened for a root cron on world-writable paths: never follow a symlinked base
|
||||
# (a compromised runner could plant one), -P + -xdev so the sweep cannot traverse
|
||||
# out of the filesystem, and patterns narrowed to names OUR tooling creates
|
||||
# (no generic tmp* — unrelated system temp files are out of scope).
|
||||
for base in /tmp /home/*/actions-runner*/_work/_temp; do
|
||||
[ -d "$base" ] || continue
|
||||
[ -L "$base" ] && { echo "[janitor] skip symlinked base: $base"; continue; }
|
||||
find -P "$base" -xdev -maxdepth 1 \( -name 'runner-*' -o -name 'omniroute-*' \) \
|
||||
! -type l -mmin +$((WORK_DIR_MAX_AGE_HOURS * 60)) -exec rm -rf {} + 2>/dev/null || true
|
||||
done
|
||||
echo "[janitor] stale temp sweep done"
|
||||
|
||||
# 4a) disk
|
||||
USAGE=$(df --output=pcent "$JANITOR_DF_PATH" 2>/dev/null | tail -1 | tr -dc '0-9')
|
||||
if [ "${USAGE:-0}" -ge "$DISK_ALERT_PCT" ]; then
|
||||
say "⚠ ROOT DISK ${USAGE}% >= ${DISK_ALERT_PCT}% — clean before the next heavy run"; STATUS=1
|
||||
else
|
||||
say "disk ${USAGE:-?}% OK"
|
||||
fi
|
||||
|
||||
# 4b) memory pressure (PSI) — the box swapped its way through the v3.8.50 publish
|
||||
if [ -r "$JANITOR_PSI_FILE" ]; then
|
||||
FULL60=$(awk '/^full/ {for(i=1;i<=NF;i++) if ($i ~ /^avg60=/) {sub("avg60=","",$i); print $i}}' "$JANITOR_PSI_FILE" 2>/dev/null || echo "")
|
||||
if [ -n "$FULL60" ] && awk -v v="$FULL60" -v t="$PSI_FULL_AVG60_ALERT" 'BEGIN{exit !(v+0 >= t+0)}'; then
|
||||
say "⚠ MEMORY PRESSURE psi full/avg60=${FULL60}% >= ${PSI_FULL_AVG60_ALERT}% — too many heavy jobs at once"; STATUS=1
|
||||
else
|
||||
say "memory psi full/avg60=${FULL60:-n/a}% OK"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4c) concurrency ceiling — alert with a breakdown; the fix is fewer/labelled
|
||||
# runners (an operator decision), not killing listeners from cron.
|
||||
ACTIVE=$(pgrep -fc "Runner.Listener" || true)
|
||||
OMNI=$(pgrep -fc "actions-runner-omniroute[^ ]*/bin[^ ]*/Runner.Listener" || true)
|
||||
if [ "${ACTIVE:-0}" -gt "$MAX_ACTIVE_RUNNERS" ]; then
|
||||
say "⚠ ${ACTIVE} Runner.Listener processes (omniroute=${OMNI:-0}, other=$(( ${ACTIVE:-0} - ${OMNI:-0} ))) > ceiling ${MAX_ACTIVE_RUNNERS} — stop idle extras: systemctl stop <unit> only when it has no Runner.Worker child"
|
||||
# 2) Disk pressure — alert loudly before SQLITE_FULL kills jobs mid-run.
|
||||
USAGE=$(df --output=pcent / | tail -1 | tr -dc '0-9')
|
||||
if [ "$USAGE" -ge "$DISK_ALERT_PCT" ]; then
|
||||
echo "[janitor] ⚠ ROOT DISK ${USAGE}% >= ${DISK_ALERT_PCT}% — clean before the next heavy run"
|
||||
STATUS=1
|
||||
else
|
||||
say "runners active: ${ACTIVE:-0}/${MAX_ACTIVE_RUNNERS} (omniroute=${OMNI:-0}) OK"
|
||||
echo "[janitor] disk ${USAGE}% OK"
|
||||
fi
|
||||
|
||||
say "done status=$STATUS"
|
||||
# 3) Concurrency ceiling — 8-wide OOMed the 16 GB box twice on release day;
|
||||
# 4 is the proven ceiling. This CODIFIES the rule that was manual discipline.
|
||||
ACTIVE=$(pgrep -fc "Runner.Listener" || true)
|
||||
if [ "${ACTIVE:-0}" -gt "$MAX_ACTIVE_RUNNERS" ]; then
|
||||
echo "[janitor] ⚠ ${ACTIVE} Runner.Listener processes > ceiling ${MAX_ACTIVE_RUNNERS} — stop the extra runners (systemctl stop actions.runner.<name>)"
|
||||
STATUS=1
|
||||
else
|
||||
echo "[janitor] runners active: ${ACTIVE:-0}/${MAX_ACTIVE_RUNNERS} OK"
|
||||
fi
|
||||
|
||||
echo "[janitor] done status=$STATUS"
|
||||
exit "$STATUS"
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
-- 163_model_capabilities.sql
|
||||
--
|
||||
-- Promote `model_capabilities` from a lazily-created runtime table to a real migration.
|
||||
--
|
||||
-- WHY: the table was only ever created by `ensureCapabilitiesTable()` in
|
||||
-- src/lib/modelsDevSync.ts, on demand, the first time a models.dev capability sync ran.
|
||||
-- Whether a database has it therefore depends on TIMING, not on the schema version — so a
|
||||
-- clean install and an upgraded install diverge for no structural reason. The v3.8.50
|
||||
-- publish run hit exactly that: `check:install-upgrade` reported `model_capabilities` as a
|
||||
-- table "present only after upgrade", because the older database had already run a sync
|
||||
-- and the freshly-installed one had not.
|
||||
--
|
||||
-- Creating it here makes both install paths converge deterministically.
|
||||
-- `ensureCapabilitiesTable()` stays in place as an idempotent safety net (it is a
|
||||
-- CREATE TABLE IF NOT EXISTS and now always a no-op); tests/unit/db-install-upgrade-schema-parity.test.ts
|
||||
-- pins the two definitions against drift.
|
||||
--
|
||||
-- IF NOT EXISTS is required, not decorative: every database that ever ran a models.dev
|
||||
-- sync already has this table, and this migration must be a no-op there.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_capabilities (
|
||||
provider TEXT NOT NULL,
|
||||
model_id TEXT NOT NULL,
|
||||
tool_call BOOLEAN,
|
||||
reasoning BOOLEAN,
|
||||
attachment BOOLEAN,
|
||||
structured_output BOOLEAN,
|
||||
temperature BOOLEAN,
|
||||
modalities_input TEXT,
|
||||
modalities_output TEXT,
|
||||
knowledge_cutoff TEXT,
|
||||
release_date TEXT,
|
||||
last_updated TEXT,
|
||||
status TEXT,
|
||||
family TEXT,
|
||||
open_weights BOOLEAN,
|
||||
limit_context INTEGER,
|
||||
limit_input INTEGER,
|
||||
limit_output INTEGER,
|
||||
interleaved_field TEXT,
|
||||
last_synced TEXT,
|
||||
PRIMARY KEY (provider, model_id)
|
||||
);
|
||||
@@ -7,9 +7,6 @@
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
ALIBABA_FREE_TIER_TEXT_CAPABLE_MODELS,
|
||||
ALIBABA_NO_FREE_TIER_TEXT_MODELS,
|
||||
@@ -30,90 +27,18 @@ test("built-in allowlist includes operator free models and excludes paid blockli
|
||||
assert.equal(isAlibabaBuiltinFreeTierTextModel("qwen3.7-max"), false);
|
||||
});
|
||||
|
||||
/**
|
||||
* The shipped `config/alibaba-free-tier-allowlist.json` carries a `validUntil`,
|
||||
* so asserting against it made this test a time bomb: it went red on its own on
|
||||
* 2026-08-28, the day after the pack expired, and stayed red on every PR and on
|
||||
* `main` (#11866). Nothing had changed — the clock moved.
|
||||
*
|
||||
* Production was never affected: an expired pack falls back to the embedded
|
||||
* list by design. So the contract worth pinning is the BEHAVIOR on both sides of
|
||||
* the expiry, with packs this test owns and dates it controls — never the
|
||||
* freshness of the catalog that ships in the repo.
|
||||
*/
|
||||
function withAllowlistPack(
|
||||
pack: Record<string, unknown>,
|
||||
assertions: () => void
|
||||
): void {
|
||||
const dir = mkdtempSync(join(tmpdir(), "alibaba-allowlist-"));
|
||||
const packPath = join(dir, "allowlist.json");
|
||||
writeFileSync(packPath, JSON.stringify(pack), "utf8");
|
||||
|
||||
test("allowlist JSON pack overrides embedded lists when valid", () => {
|
||||
const previousPath = process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH;
|
||||
const packPath = `${process.cwd()}/config/alibaba-free-tier-allowlist.json`;
|
||||
process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = packPath;
|
||||
resetAlibabaFreeTierAllowlistCache();
|
||||
try {
|
||||
assertions();
|
||||
} finally {
|
||||
if (previousPath) process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = previousPath;
|
||||
else delete process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH;
|
||||
resetAlibabaFreeTierAllowlistCache();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("allowlist JSON pack overrides embedded lists while it is still valid", () => {
|
||||
withAllowlistPack(
|
||||
{
|
||||
asOf: "2026-07-28",
|
||||
validUntil: "2999-01-01",
|
||||
capable: ["pack-only-capable-model", "qwen3.6-plus"],
|
||||
noFreeTier: ["pack-only-paid-model"],
|
||||
},
|
||||
() => {
|
||||
const pack = loadAlibabaFreeTierAllowlistPack();
|
||||
assert.ok(pack, "a pack inside its validity window must load");
|
||||
assert.ok(isAlibabaFreeTierAllowlistPackValid(pack!));
|
||||
assert.ok(pack!.capable.includes("qwen3.6-plus"));
|
||||
// Positive anchor: the pack must actually REPLACE the embedded list, not
|
||||
// merely load. `pack-only-capable-model` exists nowhere else.
|
||||
assert.equal(isAlibabaBuiltinFreeTierTextModel("pack-only-capable-model"), true);
|
||||
assert.equal(isAlibabaBuiltinNoFreeTierTextModel("pack-only-paid-model"), true);
|
||||
}
|
||||
);
|
||||
});
|
||||
const pack = loadAlibabaFreeTierAllowlistPack();
|
||||
assert.ok(pack);
|
||||
assert.ok(isAlibabaFreeTierAllowlistPackValid(pack!));
|
||||
assert.ok(pack!.capable.includes("qwen3.6-plus"));
|
||||
|
||||
test("an expired allowlist pack is ignored and the embedded list serves instead", () => {
|
||||
// This is the path production has actually been on since 2026-08-27, and it
|
||||
// had no coverage at all — which is why the expiry surfaced as a red test
|
||||
// rather than as a deliberate, understood fallback.
|
||||
withAllowlistPack(
|
||||
{
|
||||
asOf: "2026-07-28",
|
||||
validUntil: "2026-08-27",
|
||||
capable: ["pack-only-capable-model"],
|
||||
noFreeTier: ["pack-only-paid-model"],
|
||||
},
|
||||
() => {
|
||||
assert.equal(loadAlibabaFreeTierAllowlistPack(), null, "expired pack must not load");
|
||||
assert.equal(isAlibabaBuiltinFreeTierTextModel("pack-only-capable-model"), false);
|
||||
// The embedded list must be what answers once the pack is rejected.
|
||||
assert.equal(isAlibabaBuiltinFreeTierTextModel("qwen3.6-plus"), true);
|
||||
assert.equal(isAlibabaBuiltinNoFreeTierTextModel("qwen3.7-max"), true);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("isAlibabaFreeTierAllowlistPackValid compares against the instant it is given", () => {
|
||||
const pack = { asOf: "2026-07-28", validUntil: "2026-08-27", capable: ["x"], noFreeTier: [] };
|
||||
assert.equal(isAlibabaFreeTierAllowlistPackValid(pack, Date.parse("2026-08-26")), true);
|
||||
assert.equal(isAlibabaFreeTierAllowlistPackValid(pack, Date.parse("2026-08-28")), false);
|
||||
// No expiry declared means the pack never goes stale on its own.
|
||||
assert.equal(
|
||||
isAlibabaFreeTierAllowlistPackValid(
|
||||
{ asOf: "2026-07-28", capable: ["x"], noFreeTier: [] },
|
||||
Date.parse("2999-01-01")
|
||||
),
|
||||
true
|
||||
);
|
||||
if (previousPath) process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = previousPath;
|
||||
else delete process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH;
|
||||
resetAlibabaFreeTierAllowlistCache();
|
||||
});
|
||||
|
||||
@@ -2,10 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
// @ts-expect-error — plain .mjs gate script, no type declarations by design
|
||||
import {
|
||||
assertNoDiskExhaustion,
|
||||
evaluateConvergence,
|
||||
} from "../../scripts/check/check-install-upgrade.mjs";
|
||||
import { evaluateConvergence } from "../../scripts/check/check-install-upgrade.mjs";
|
||||
|
||||
/**
|
||||
* The whole point of this gate is that the two directions of schema divergence are NOT
|
||||
@@ -69,11 +66,7 @@ test("UNKNOWN residue fails — a new divergence must not hide behind the allowl
|
||||
assert.equal(v.ok, false);
|
||||
assert.deepEqual(v.unknownResidue, ["surprise_table"]);
|
||||
assert.match(v.failures[0], /surprise_table/);
|
||||
assert.doesNotMatch(
|
||||
v.failures[0],
|
||||
/cache_metrics/,
|
||||
"the known one must not be re-reported as new"
|
||||
);
|
||||
assert.doesNotMatch(v.failures[0], /cache_metrics/, "the known one must not be re-reported as new");
|
||||
});
|
||||
|
||||
test("both directions at once report both failures", () => {
|
||||
@@ -100,43 +93,3 @@ test("empty/missing inputs do not crash", () => {
|
||||
assert.equal(v.ok, true);
|
||||
assert.deepEqual(v.onlyFresh, []);
|
||||
});
|
||||
|
||||
// ─── ENOSPC guard ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The v3.8.50 publish run (CI 33104507735) failed with "15 tables a CLEAN install creates
|
||||
// but an UPGRADE does not". None of them was missing: the Phase B upgrade install had hit
|
||||
// `npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device` 5611 times, npm still
|
||||
// exited 0, the truncated `omniroute serve` "exited with code 0 before serving", and the
|
||||
// database therefore still held the 3.8.49 schema. npm reporting disk exhaustion as a
|
||||
// warning is what let a full disk masquerade as a schema defect.
|
||||
|
||||
test("npm ENOSPC warnings are raised as an install failure, not ignored", () => {
|
||||
const enospc = "npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device, write\n".repeat(3);
|
||||
assert.throws(
|
||||
() => assertNoDiskExhaustion(enospc, "upgrade install"),
|
||||
(err: Error) => {
|
||||
assert.match(err.message, /upgrade install/);
|
||||
assert.match(err.message, /ran out of disk space/);
|
||||
assert.match(err.message, /3 ENOSPC error/);
|
||||
// The operator must not go looking for a migration that is not missing.
|
||||
assert.match(err.message, /NOT a schema divergence/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("a clean install log does not trip the disk guard", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
assertNoDiskExhaustion(
|
||||
"npm warn deprecated boolean@3.2.0: Package no longer supported.\nadded 900 packages\n",
|
||||
"clean install"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test("the guard also catches the bare kernel message without the ENOSPC code", () => {
|
||||
assert.throws(
|
||||
() => assertNoDiskExhaustion("Error: no space left on device", "clean install"),
|
||||
/ran out of disk space/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
classifyRunsOn,
|
||||
findProvenanceOnSelfHosted,
|
||||
} from "../../scripts/check/lib/provenanceRunner.mjs";
|
||||
|
||||
/**
|
||||
* v3.8.50, 10th publish attempt, 76 minutes in — after the tag, the GitHub
|
||||
* Release and the Docker images were already public:
|
||||
*
|
||||
* 422 Unprocessable Entity - Error verifying sigstore provenance bundle:
|
||||
* Unsupported GitHub Actions runner environment: "self-hosted".
|
||||
*
|
||||
* `USE_VPS_RUNNER` had routed the publish job to the .113 pool on 2026-08-02;
|
||||
* no release happened between 07-30 and 08-28, so nothing surfaced it. The
|
||||
* pairing is pure text, so it must fail the workflow lint on the PR that
|
||||
* introduces it.
|
||||
*/
|
||||
const ROOT = join(import.meta.dirname, "../..");
|
||||
const WORKFLOWS = join(ROOT, ".github/workflows");
|
||||
|
||||
// The exact runs-on expression npm-publish.yml used when it broke.
|
||||
const VPS_EXPR =
|
||||
"${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('[\"self-hosted\",\"omni-release\"]') || 'ubuntu-latest' }}";
|
||||
|
||||
function workflow(runsOn: string, run: string, extra = ""): string {
|
||||
return [
|
||||
"name: t",
|
||||
"on: push",
|
||||
"jobs:",
|
||||
" publish:",
|
||||
` runs-on: ${runsOn}`,
|
||||
extra,
|
||||
" steps:",
|
||||
" - name: upload",
|
||||
` run: ${run}`,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
test("classifyRunsOn: literal, array, object-with-labels and the fromJSON expression are self-hosted", () => {
|
||||
assert.equal(classifyRunsOn("self-hosted"), "self-hosted");
|
||||
assert.equal(classifyRunsOn(["self-hosted", "omni-release"]), "self-hosted");
|
||||
assert.equal(classifyRunsOn({ group: "Default", labels: ["self-hosted"] }), "self-hosted");
|
||||
assert.equal(classifyRunsOn(VPS_EXPR), "self-hosted");
|
||||
});
|
||||
|
||||
test("classifyRunsOn: hosted labels are hosted, opaque expressions are unknown (never guessed)", () => {
|
||||
assert.equal(classifyRunsOn("ubuntu-latest"), "hosted");
|
||||
assert.equal(classifyRunsOn(["ubuntu-latest"]), "hosted");
|
||||
assert.equal(classifyRunsOn("${{ matrix.os }}"), "unknown");
|
||||
assert.equal(classifyRunsOn(undefined), "unknown");
|
||||
});
|
||||
|
||||
test("flags --provenance inside a job routed to the self-hosted pool", () => {
|
||||
const found = findProvenanceOnSelfHosted(
|
||||
workflow(
|
||||
JSON.stringify(VPS_EXPR),
|
||||
'npm stage publish --provenance --access public --tag "$TAG"'
|
||||
),
|
||||
"npm-publish.yml"
|
||||
);
|
||||
assert.deepEqual(found, [{ file: "npm-publish.yml", job: "publish", step: "upload" }]);
|
||||
});
|
||||
|
||||
test("also catches the literal label and the --provenance-file form", () => {
|
||||
assert.equal(
|
||||
findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --provenance")).length,
|
||||
1
|
||||
);
|
||||
assert.equal(
|
||||
findProvenanceOnSelfHosted(
|
||||
workflow("[self-hosted, omni-release]", "npm publish --provenance-file=./p.json")
|
||||
).length,
|
||||
0,
|
||||
"--provenance-file is a different flag (a pre-built bundle) and is not what the registry rejects"
|
||||
);
|
||||
assert.equal(
|
||||
findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --provenance=true")).length,
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
test("does not flag hosted jobs, unknown runners, or self-hosted jobs without the flag", () => {
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(workflow("ubuntu-latest", "npm publish --provenance")),
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(workflow("${{ matrix.os }}", "npm publish --provenance")),
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --access public")),
|
||||
[]
|
||||
);
|
||||
// The word only in a step NAME or a comment is not a finding.
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(
|
||||
[
|
||||
"name: t",
|
||||
"on: push",
|
||||
"jobs:",
|
||||
" j:",
|
||||
" runs-on: self-hosted",
|
||||
" steps:",
|
||||
" - name: provenance note",
|
||||
" run: echo hi # --provenance later",
|
||||
"",
|
||||
].join("\n")
|
||||
),
|
||||
[],
|
||||
"a comment after the command is still part of the run string — accept that the regex is conservative"
|
||||
);
|
||||
});
|
||||
|
||||
test("reusable-workflow jobs (uses:) and unparseable YAML are not this rule's findings", () => {
|
||||
const reusable = [
|
||||
"name: t",
|
||||
"on: push",
|
||||
"jobs:",
|
||||
" j:",
|
||||
" uses: ./.github/workflows/x.yml",
|
||||
"",
|
||||
].join("\n");
|
||||
assert.deepEqual(findProvenanceOnSelfHosted(reusable), []);
|
||||
assert.deepEqual(findProvenanceOnSelfHosted("jobs: [unclosed"), []);
|
||||
});
|
||||
|
||||
test("regression guard: no workflow in this repo publishes with --provenance from a self-hosted runner", () => {
|
||||
const files = readdirSync(WORKFLOWS).filter((f) => /\.ya?ml$/.test(f));
|
||||
assert.ok(files.length > 10, "expected the real workflow set");
|
||||
const findings = files.flatMap((f) =>
|
||||
findProvenanceOnSelfHosted(readFileSync(join(WORKFLOWS, f), "utf8"), f)
|
||||
);
|
||||
assert.deepEqual(
|
||||
findings,
|
||||
[],
|
||||
`npm rejects provenance from self-hosted runners (422) — move the upload to a github-hosted job: ${JSON.stringify(findings)}`
|
||||
);
|
||||
});
|
||||
@@ -1,135 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
classifyExpiry,
|
||||
collectExpiryFields,
|
||||
scanConfigExpiry,
|
||||
} from "../../scripts/check/lib/configExpiry.mjs";
|
||||
|
||||
/**
|
||||
* Time bombs: config packs with a `validUntil` (or sibling key) that lapse with
|
||||
* no commit involved. The Alibaba free-tier pack expired on 2026-08-27 and from
|
||||
* the 28th every PR and main carried a red Unit Tests shard (#11866). Nothing a
|
||||
* diff review could have caught.
|
||||
*
|
||||
* This suite fails SEVEN DAYS BEFORE any pack under config/ lapses, naming the
|
||||
* file and key, so renewal happens on someone's terms instead of the clock's.
|
||||
*/
|
||||
const ROOT = join(import.meta.dirname, "../..");
|
||||
const CONFIG_DIR = join(ROOT, "config");
|
||||
const DAY = 86_400_000;
|
||||
const WARN_DAYS = 7;
|
||||
|
||||
/**
|
||||
* Packs known to be expired/expiring, each pinned to the issue that owns the
|
||||
* renewal decision. An entry whose pack is no longer expiring FAILS below as a
|
||||
* stale allowlist entry — remove it when the pack is renewed.
|
||||
*/
|
||||
const ALLOWLIST: Record<string, string> = {
|
||||
"alibaba-free-tier-allowlist.json":
|
||||
"#11866 — validUntil 2026-08-27 has passed; the loader already falls back to the embedded list, and renewing the curated free-tier pack is an operator data decision, not a test fix",
|
||||
};
|
||||
|
||||
const NOW = Date.UTC(2026, 7, 28); // 2026-08-28, fixed: this suite must not itself depend on the clock
|
||||
const day = (offset: number) => new Date(NOW + offset * DAY).toISOString().slice(0, 10);
|
||||
|
||||
test("collectExpiryFields: finds nested and array-nested expiry keys, ignores non-string values", () => {
|
||||
const fields = collectExpiryFields(
|
||||
{
|
||||
validUntil: day(3),
|
||||
nested: { expiresAt: day(30), other: "x" },
|
||||
list: [{ expiry: day(-1) }, { expires: 12345 }],
|
||||
expires_at: "not a date",
|
||||
},
|
||||
"pack.json"
|
||||
);
|
||||
assert.deepEqual(
|
||||
fields.map((f) => [f.keyPath, f.expiresAt === null ? null : "date"]),
|
||||
[
|
||||
["validUntil", "date"],
|
||||
["nested.expiresAt", "date"],
|
||||
["list.0.expiry", "date"],
|
||||
["expires_at", null],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test("classifyExpiry: expired / expiring inside the warning window / ok / unparseable", () => {
|
||||
const f = (raw: string) => ({
|
||||
file: "p",
|
||||
keyPath: "validUntil",
|
||||
raw,
|
||||
expiresAt: Number.isFinite(Date.parse(raw)) ? Date.parse(raw) : null,
|
||||
});
|
||||
assert.equal(classifyExpiry(f(day(-1)), NOW, WARN_DAYS), "expired");
|
||||
assert.equal(
|
||||
classifyExpiry(f(day(0)), NOW, WARN_DAYS),
|
||||
"expiring",
|
||||
"lapsing today is already too late to be 'ok'"
|
||||
);
|
||||
assert.equal(classifyExpiry(f(day(6)), NOW, WARN_DAYS), "expiring");
|
||||
assert.equal(classifyExpiry(f(day(8)), NOW, WARN_DAYS), "ok");
|
||||
assert.equal(classifyExpiry(f("never"), NOW, WARN_DAYS), "unparseable");
|
||||
});
|
||||
|
||||
test("scanConfigExpiry: walks a config tree, skips node_modules and invalid JSON, keys files portably", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "cfg-expiry-"));
|
||||
try {
|
||||
mkdirSync(join(dir, "sub"), { recursive: true });
|
||||
mkdirSync(join(dir, "node_modules", "dep"), { recursive: true });
|
||||
writeFileSync(join(dir, "a.json"), JSON.stringify({ validUntil: day(3) }));
|
||||
writeFileSync(join(dir, "sub", "b.json"), JSON.stringify({ deep: { expiresAt: day(40) } }));
|
||||
writeFileSync(
|
||||
join(dir, "node_modules", "dep", "c.json"),
|
||||
JSON.stringify({ validUntil: day(-5) })
|
||||
);
|
||||
writeFileSync(join(dir, "broken.json"), "{ not json");
|
||||
writeFileSync(join(dir, "notes.txt"), JSON.stringify({ validUntil: day(-5) }));
|
||||
const found = scanConfigExpiry(dir).map((f) => `${f.file}:${f.keyPath}`);
|
||||
assert.deepEqual(found, ["a.json:validUntil", "sub/b.json:deep.expiresAt"]);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test(`repo: no pack under config/ lapses within ${WARN_DAYS} days unless its renewal is tracked`, (t) => {
|
||||
const fields = scanConfigExpiry(CONFIG_DIR);
|
||||
// Positive anchor: the scanner must be seeing SOMETHING, or a renamed key
|
||||
// would silently turn this whole suite into a no-op.
|
||||
assert.ok(
|
||||
fields.length >= 1,
|
||||
"expected at least one dated pack under config/ (the Alibaba allowlist) — if the key was renamed, extend EXPIRY_KEY"
|
||||
);
|
||||
|
||||
const failures: string[] = [];
|
||||
const seenAllowlisted = new Set<string>();
|
||||
for (const f of fields) {
|
||||
const status = classifyExpiry(f, Date.now(), WARN_DAYS);
|
||||
const tracked = ALLOWLIST[f.file];
|
||||
if (status === "unparseable") {
|
||||
t.diagnostic(`${f.file} ${f.keyPath}="${f.raw}" is not a date — not monitored`);
|
||||
continue;
|
||||
}
|
||||
if (status === "ok") continue;
|
||||
if (tracked) {
|
||||
seenAllowlisted.add(f.file);
|
||||
t.diagnostic(`${f.file} ${f.keyPath}=${f.raw} is ${status} — tracked: ${tracked}`);
|
||||
continue;
|
||||
}
|
||||
failures.push(
|
||||
`${f.file} → ${f.keyPath}=${f.raw} is ${status}: renew the pack (or track it in ALLOWLIST with its issue)`
|
||||
);
|
||||
}
|
||||
for (const file of Object.keys(ALLOWLIST)) {
|
||||
if (!seenAllowlisted.has(file)) {
|
||||
failures.push(
|
||||
`stale ALLOWLIST entry: ${file} is no longer expired/expiring — remove it (${ALLOWLIST[file]})`
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(failures, [], failures.join("\n"));
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
||||
// This test opens a real SQLite database through `src/lib/db/core.ts`. better-sqlite3 is a
|
||||
// native addon; production and CI load it normally, but some sandboxes ship a system glibc
|
||||
// older than the prebuilt binary requires ("GLIBC_2.29 not found"), in which case the
|
||||
// runtime cascades to node:sqlite/sql.js. See tests/unit/_helpers/betterSqlite3Availability.ts.
|
||||
//
|
||||
// WHY THIS FILE EXISTS
|
||||
// --------------------
|
||||
// `npm run check:install-upgrade` (scripts/check/check-install-upgrade.mjs) proves that a
|
||||
// CLEAN install and an UPGRADE converge on the same schema, but it costs a full `npm pack`
|
||||
// plus three global installs and three boots (~17 min in CI) and it can only ever run at
|
||||
// publish time, against an already-published previous version. It is not a development
|
||||
// feedback loop, and the v3.8.50 publish run is what proved it: the gate reported 15
|
||||
// "missing" tables, and the deterministic half of that verdict was never checkable locally.
|
||||
//
|
||||
// This file pins the deterministic half in milliseconds:
|
||||
//
|
||||
// 1. Every migration file on disk is actually reachable by the runner on a fresh install.
|
||||
// A file the runner never applies is a table no user ever gets.
|
||||
// 2. `model_capabilities` is created by the MIGRATION SET, not lazily at runtime.
|
||||
// A table created on demand by `CREATE TABLE IF NOT EXISTS` inside a feature code
|
||||
// path exists or not depending on whether that feature happened to run before the
|
||||
// snapshot — so it diverges between the two install paths by TIMING, not by schema.
|
||||
// That is precisely how `model_capabilities` surfaced as a divergence on the v3.8.50
|
||||
// publish run (present in the upgraded database, absent from the clean one).
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const MIGRATIONS_DIR = path.join(REPO_ROOT, "src", "lib", "db", "migrations");
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-install-upgrade-parity-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
// A clean install: core.ts applies the inline SCHEMA_SQL, the `ensure*Columns()` helpers,
|
||||
// then runMigrations(). This is the exact code path Phase A of the gate exercises.
|
||||
const db = core.getDbInstance();
|
||||
|
||||
test.after(() => {
|
||||
try {
|
||||
core.resetDbInstance();
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function migrationFiles(): Array<{ version: string; name: string }> {
|
||||
return fs
|
||||
.readdirSync(MIGRATIONS_DIR)
|
||||
.sort()
|
||||
.map((file) => /^(\d{3,})_(.+)\.sql$/.exec(file))
|
||||
.filter((m): m is RegExpExecArray => m !== null)
|
||||
.map((m) => ({ version: m[1], name: m[2] }));
|
||||
}
|
||||
|
||||
function hasTable(name: string): boolean {
|
||||
return Boolean(
|
||||
db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
|
||||
);
|
||||
}
|
||||
|
||||
test("a clean install applies every migration file on disk", () => {
|
||||
const ledger = new Set(
|
||||
(
|
||||
db.prepare("SELECT version FROM _omniroute_migrations").all() as Array<{ version: string }>
|
||||
).map((row) => row.version)
|
||||
);
|
||||
const unapplied = migrationFiles()
|
||||
.filter((m) => !ledger.has(m.version))
|
||||
.map((m) => `${m.version}_${m.name}`);
|
||||
assert.deepEqual(
|
||||
unapplied,
|
||||
[],
|
||||
"migration files the runner never applied — an upgrade would not create their tables either"
|
||||
);
|
||||
});
|
||||
|
||||
test("model_capabilities comes from the migration set, not from a lazy runtime CREATE", () => {
|
||||
assert.ok(
|
||||
hasTable("model_capabilities"),
|
||||
"model_capabilities must be created by a migration so a clean install and an upgrade " +
|
||||
"converge deterministically instead of depending on whether the models.dev sync ran"
|
||||
);
|
||||
});
|
||||
|
||||
test("the model_capabilities migration does not drift from ensureCapabilitiesTable()", () => {
|
||||
const source = fs.readFileSync(path.join(REPO_ROOT, "src", "lib", "modelsDevSync.ts"), "utf8");
|
||||
const ddl = /CREATE TABLE IF NOT EXISTS model_capabilities\s*\(([\s\S]*?)\n\s*\)/.exec(source);
|
||||
assert.ok(ddl, "ensureCapabilitiesTable() DDL not found in src/lib/modelsDevSync.ts");
|
||||
|
||||
const runtimeColumns = ddl[1]
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !/^PRIMARY KEY/i.test(line))
|
||||
.map((line) => line.replace(/,$/, "").split(/\s+/)[0])
|
||||
.sort();
|
||||
|
||||
const migrationColumns = (
|
||||
db.prepare("PRAGMA table_info(model_capabilities)").all() as Array<{ name: string }>
|
||||
)
|
||||
.map((column) => column.name)
|
||||
.sort();
|
||||
|
||||
assert.deepEqual(
|
||||
migrationColumns,
|
||||
runtimeColumns,
|
||||
"the migration and the runtime helper must create the same columns — a drift here means " +
|
||||
"an upgraded database keeps the old shape while a clean install gets the new one"
|
||||
);
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import type { Dirent } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v3.8.50 was refused by the registry with `413 Payload Too Large` on
|
||||
* `POST /-/stage/package/omniroute`: the tarball had reached 288.7 MB packed
|
||||
* (1.1 GB unpacked), against 174.5 MB for the 3.8.49 that published fine.
|
||||
*
|
||||
* 668.7 MB of that — 61% of the whole package — was 842 `*.nft.json` files.
|
||||
* Those are Next.js Node File Trace manifests: build-time metadata used to
|
||||
* COMPUTE the standalone bundle, never read while serving. They had doubled
|
||||
* since 3.8.49 (325.0 MB across 748 files), which is what tipped the payload
|
||||
* over the limit.
|
||||
*
|
||||
* The guard is the `files[]` negation, so a future entry that re-widens the
|
||||
* glob (or a rewrite of the array) cannot silently put them back.
|
||||
*/
|
||||
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "../../package.json"), "utf8")) as {
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
test("package.json files[] excludes Next's .nft.json trace manifests", () => {
|
||||
const files = pkg.files ?? [];
|
||||
assert.ok(files.length > 0, "package.json must declare files[]");
|
||||
assert.ok(
|
||||
files.includes("!**/*.nft.json"),
|
||||
"files[] must negate **/*.nft.json — they are build metadata and were 61% of the 3.8.50 payload"
|
||||
);
|
||||
});
|
||||
|
||||
test("the negation sits after the positive dist/ entry it has to override", () => {
|
||||
// npm applies files[] in order: a negation listed BEFORE the directory that
|
||||
// pulls the files in is a no-op. Positive anchor, so this test cannot pass
|
||||
// just because both strings happen to be present somewhere.
|
||||
const files = pkg.files ?? [];
|
||||
const dist = files.indexOf("dist/");
|
||||
const negation = files.indexOf("!**/*.nft.json");
|
||||
assert.notEqual(dist, -1, "dist/ must still be published");
|
||||
assert.ok(negation > dist, "the .nft.json negation must come after dist/");
|
||||
});
|
||||
|
||||
test("no source module reads a .nft.json at runtime", () => {
|
||||
// If this ever stops holding, the exclusion above becomes a runtime break
|
||||
// rather than a size win — which is exactly the assumption worth pinning.
|
||||
const roots = ["src", "open-sse", "bin"];
|
||||
const hits: string[] = [];
|
||||
for (const root of roots) {
|
||||
const dir = join(import.meta.dirname, "../..", root);
|
||||
const stack = [dir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop() as string;
|
||||
let entries: Dirent[];
|
||||
try {
|
||||
entries = readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name !== "node_modules") stack.push(full);
|
||||
} else if (/\.(ts|tsx|mjs|js)$/.test(entry.name)) {
|
||||
if (readFileSync(full, "utf8").includes(".nft.json")) hits.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(hits, [], `nothing may depend on .nft.json at runtime: ${hits.join(", ")}`);
|
||||
});
|
||||
@@ -1,196 +0,0 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* scripts/ops/runner-janitor.sh runs from cron on the .113 runner box. This
|
||||
* suite pins its safety contract against a fixture tree — never the real /tmp:
|
||||
* every base, the runner dirs, the PSI file and the df path are redirected, the
|
||||
* zombie pattern is set to a name no process has, and the ceilings are lifted
|
||||
* so the outcome does not depend on the box the test happens to run on.
|
||||
*/
|
||||
const ROOT = path.resolve(import.meta.dirname, "..", "..");
|
||||
const SCRIPT = path.join(ROOT, "scripts", "ops", "runner-janitor.sh");
|
||||
const HOUR = 3_600_000;
|
||||
// The sweep needs lsof to PROVE a path is idle (one snapshot of open paths). Hosted CI
|
||||
// images ship both; a bare devbox may not. Each branch below asserts what must
|
||||
// hold in that environment — without the tools the contract is "delete nothing,
|
||||
// say why", which is exactly the behaviour worth pinning.
|
||||
const HAVE_BUSY_TOOLS =
|
||||
spawnSync("bash", ["-c", "command -v lsof"], { stdio: "ignore" }).status === 0;
|
||||
|
||||
function fixture() {
|
||||
const base = mkdtempSync(path.join(os.tmpdir(), "janitor-fixture-"));
|
||||
const old = new Date(Date.now() - 5 * HOUR);
|
||||
const mk = (name: string, dir: boolean, when: Date | null) => {
|
||||
const p = path.join(base, name);
|
||||
if (dir) {
|
||||
mkdirSync(p);
|
||||
writeFileSync(path.join(p, "x"), "x");
|
||||
} else writeFileSync(p, "x");
|
||||
if (when) utimesSync(p, when, when);
|
||||
return p;
|
||||
};
|
||||
return {
|
||||
base,
|
||||
staleTar: mk("e2e-build.tar.gz", false, old), // fixed-name artefact ci.yml/npm-publish leave behind
|
||||
staleBuild: mk("next-build-abc", true, old),
|
||||
staleUpgrade: mk("omniroute-install-upgrade-xyz", true, old),
|
||||
fresh: mk("omniroute-batch-api-fresh", true, null), // in use right now
|
||||
unrelated: mk("somebody-elses.log", false, old), // not ours — never touched
|
||||
};
|
||||
}
|
||||
|
||||
function run(args: string[], base: string, extraEnv: Record<string, string> = {}) {
|
||||
return spawnSync("bash", [SCRIPT, ...args], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
JANITOR_TMP_BASES: base,
|
||||
JANITOR_WORK_TEMP_BASES: "",
|
||||
JANITOR_RUNNER_DIRS: path.join(base, "no-runners-here-*"),
|
||||
JANITOR_PSI_FILE: path.join(base, "no-psi"),
|
||||
JANITOR_DF_PATH: base,
|
||||
ZOMBIE_BUILD_COMM: "janitor-test-no-such-process",
|
||||
MAX_ACTIVE_RUNNERS: "9999",
|
||||
DISK_ALERT_PCT: "101",
|
||||
...extraEnv,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("runner-janitor.sh", () => {
|
||||
it("is executable bash with strict mode and prints usage on --help", () => {
|
||||
assert.ok(existsSync(SCRIPT));
|
||||
assert.ok(statSync(SCRIPT).mode & 0o111, "must be chmod +x (cron runs it directly)");
|
||||
const body = readFileSync(SCRIPT, "utf8");
|
||||
assert.ok(body.startsWith("#!/usr/bin/env bash"));
|
||||
assert.ok(body.includes("set -euo pipefail"));
|
||||
const help = run(["--help"], os.tmpdir());
|
||||
assert.equal(help.status, 0, help.stderr);
|
||||
assert.match(help.stdout, /--dry-run/);
|
||||
});
|
||||
|
||||
it("without lsof it cannot prove idle, so it deletes nothing and says why (exit 1)", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const r = run([], f.base, { JANITOR_LSOF: "/nonexistent/lsof" });
|
||||
assert.equal(r.status, 1, "a janitor that cannot do its job must show up in the cron log");
|
||||
assert.match(r.stdout, /busy-tools=MISSING/);
|
||||
assert.match(
|
||||
r.stdout,
|
||||
/cannot prove idle \(lsof missing — apt install lsof\), kept: .*e2e-build\.tar\.gz/
|
||||
);
|
||||
for (const p of [f.staleTar, f.staleBuild, f.staleUpgrade, f.fresh, f.unrelated]) {
|
||||
assert.ok(existsSync(p), `must not delete ${p} when idleness cannot be proven`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(f.base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("--dry-run names what it WOULD remove and removes nothing", (t) => {
|
||||
if (!HAVE_BUSY_TOOLS) return t.skip("lsof absent on this box — sweep branch covered in CI");
|
||||
const f = fixture();
|
||||
try {
|
||||
const r = run(["--dry-run"], f.base);
|
||||
assert.equal(r.status, 0, r.stderr + r.stdout);
|
||||
assert.match(r.stdout, /busy-tools=ok/);
|
||||
for (const p of [f.staleTar, f.staleBuild, f.staleUpgrade]) {
|
||||
assert.match(
|
||||
r.stdout,
|
||||
new RegExp(`would remove \\(3h\\+\\): ${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`)
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
r.stdout,
|
||||
new RegExp(`removed \\(3h\\+\\): ${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
|
||||
"dry-run must never claim it removed something"
|
||||
);
|
||||
assert.ok(existsSync(p), `dry-run must not delete ${p}`);
|
||||
}
|
||||
assert.doesNotMatch(
|
||||
r.stdout,
|
||||
/omniroute-batch-api-fresh/,
|
||||
"a fresh dir is never a candidate"
|
||||
);
|
||||
assert.doesNotMatch(r.stdout, /somebody-elses\.log/, "only names our tooling creates");
|
||||
assert.match(r.stdout, /zombie builds: 0/);
|
||||
assert.match(r.stdout, /done status=0/);
|
||||
} finally {
|
||||
rmSync(f.base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("for real: sweeps the three stale artefacts, keeps the fresh one and the stranger", (t) => {
|
||||
if (!HAVE_BUSY_TOOLS) return t.skip("lsof absent on this box — sweep branch covered in CI");
|
||||
const f = fixture();
|
||||
try {
|
||||
const r = run([], f.base);
|
||||
assert.equal(r.status, 0, r.stderr + r.stdout);
|
||||
assert.ok(!existsSync(f.staleTar), "stale e2e-build.tar.gz must go (it is RAM on tmpfs)");
|
||||
assert.ok(!existsSync(f.staleBuild), "stale next-build dir must go");
|
||||
assert.ok(!existsSync(f.staleUpgrade), "stale install-upgrade dir must go");
|
||||
assert.ok(existsSync(f.fresh), "a fresh dir must survive");
|
||||
assert.ok(existsSync(f.unrelated), "files we did not create must survive even when old");
|
||||
} finally {
|
||||
rmSync(f.base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("tmpfs fuse is shorter than the disk fuse (RAM vs disk), both overridable", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
// With a 6h tmpfs fuse the 5h-old artefacts are NOT stale yet.
|
||||
const r = run(["--dry-run"], f.base, { TMPFS_MAX_AGE_HOURS: "6" });
|
||||
assert.doesNotMatch(
|
||||
r.stdout,
|
||||
/would remove|removed \(|cannot prove idle/,
|
||||
"nothing is stale under a 6h fuse, so no candidate is even examined"
|
||||
);
|
||||
const body = readFileSync(SCRIPT, "utf8");
|
||||
assert.match(body, /TMPFS_MAX_AGE_HOURS:-3\}/, "tmpfs default must stay short — it is RAM");
|
||||
assert.match(body, /WORK_TEMP_MAX_AGE_HOURS:-24\}/);
|
||||
} finally {
|
||||
rmSync(f.base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("alerts (exit 1) on disk and memory pressure thresholds without touching files", () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
writeFileSync(
|
||||
path.join(f.base, "psi"),
|
||||
"some avg10=0.00 avg60=0.00 avg300=0.00 total=1\nfull avg10=0.00 avg60=23.50 avg300=9.00 total=1\n"
|
||||
);
|
||||
const r = run(["--dry-run"], f.base, {
|
||||
JANITOR_PSI_FILE: path.join(f.base, "psi"),
|
||||
DISK_ALERT_PCT: "0",
|
||||
});
|
||||
assert.equal(r.status, 1, "attention needed must be exit 1 for the cron log");
|
||||
assert.match(r.stdout, /MEMORY PRESSURE psi full\/avg60=23\.50%/);
|
||||
assert.ok(existsSync(f.fresh) && existsSync(f.unrelated));
|
||||
assert.match(r.stdout, /ROOT DISK \d+% >= 0%/);
|
||||
assert.ok(existsSync(f.staleTar), "alerting never deletes");
|
||||
} finally {
|
||||
rmSync(f.base, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects unknown arguments instead of silently running", () => {
|
||||
const r = run(["--yolo"], os.tmpdir());
|
||||
assert.equal(r.status, 2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user