From e390a8d63350643c9975b8ec4e91aedbea376793 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 3 Jun 2026 15:51:07 -0300 Subject: [PATCH] build(layer2+3): propagate .build/+dist/ to Docker/Electron/CI; codify light deploy; docs Layer 3: Dockerfile COPY .next/standalone -> .build/next/standalone (+cache mount); electron stage -> .build/electron-standalone + extraResources; CI asserts dist/server.js; eslint ignores .build/**+dist/**. Layer 2: deploy-vps-* skills use build:release + rsync(dist) -> remote app/ + pm2 restart + BUILD_SHA verify (drop npm-i-g/legacy-peer-deps/manual-wreq). Docs (RELEASE_CHECKLIST/CONTRIBUTING/AGENTS/CHANGELOG) describe src/+.build/+dist/ layout. Verified: docker build exit 0 + health 200; electron stage OK; no stale build-output refs. --- .agents/skills/deploy-vps-akamai-cc/SKILL.md | 77 ++++++++++++++-- .agents/skills/deploy-vps-both-cc/SKILL.md | 89 ++++++++++++++++--- .agents/skills/deploy-vps-local-cc/SKILL.md | 77 ++++++++++++++-- .dockerignore | 1 + .github/workflows/ci.yml | 4 + AGENTS.md | 39 +++++--- CHANGELOG.md | 4 + CONTRIBUTING.md | 33 ++++++- Dockerfile | 6 +- docs/ops/RELEASE_CHECKLIST.md | 35 +++++++- electron/package.json | 4 +- eslint.config.mjs | 9 +- scripts/build/prepare-electron-standalone.mjs | 4 +- 13 files changed, 330 insertions(+), 52 deletions(-) diff --git a/.agents/skills/deploy-vps-akamai-cc/SKILL.md b/.agents/skills/deploy-vps-akamai-cc/SKILL.md index 60ee7db988..f9c8fbcbd7 100644 --- a/.agents/skills/deploy-vps-akamai-cc/SKILL.md +++ b/.agents/skills/deploy-vps-akamai-cc/SKILL.md @@ -5,36 +5,99 @@ description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35) # Deploy to Akamai VPS Workflow -Deploy OmniRoute to the Akamai VPS using `npm pack + scp` + PM2. +Deploy OmniRoute to the Akamai VPS using a clean `build:release` + `rsync` + PM2 restart. **Akamai VPS:** `69.164.221.35` **Process manager:** PM2 (`omniroute`) **Port:** `20128` +**Remote install dir:** `/usr/lib/node_modules/omniroute/app/` (VPS image dir — unchanged) + +> [!IMPORTANT] +> **Build must run where `node_modules` is REAL.** A git worktree with symlinked +> `node_modules` breaks the Next.js standalone assembly. Always build from the main +> checkout (or an `npm ci`'d worktree), never from `OmniRoute-*` worktrees unless +> you first ran `npm ci` inside them. ## Steps -### 1. Build + pack locally +### 1. Clean build + sentinel + +Run from a checkout with a **real** `node_modules`: // turbo ```bash -cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts +cd /home/diegosouzapw/dev/proxys/OmniRoute && npm run build:release ``` -### 2. Copy to Akamai VPS and install +`build:release` does: +1. Deletes `.build/` and `dist/` (clean rebuild — no stale cache) +2. Runs `next build` → `.build/next/` (intermediates) +3. Assembles the shippable bundle into `dist/` via `assembleStandalone` +4. Writes `dist/BUILD_SHA` = `git rev-parse --short HEAD` (deploy sentinel) + +Verify the sentinel before shipping: + +```bash +cat /home/diegosouzapw/dev/proxys/OmniRoute/dist/BUILD_SHA && git -C /home/diegosouzapw/dev/proxys/OmniRoute rev-parse --short HEAD +``` + +Both lines must match. If they differ, the build is stale — re-run `build:release`. + +### 2. Back up the running bundle on the VPS + +```bash +OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version") +ssh root@69.164.221.35 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}" +``` + +### 3. Rsync `dist/` → remote `app/` + +The VPS image directory is still named `app/`; we ship the **contents** of our local `dist/`: // turbo-all ```bash -scp omniroute-*.tgz root@69.164.221.35:/tmp/ +rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@69.164.221.35:/usr/lib/node_modules/omniroute/app/ +``` + +### 4. Restart + health check + +```bash +ssh root@69.164.221.35 "pm2 restart omniroute --update-env && pm2 save" ``` ```bash -ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" +sleep 5 && curl -sf http://69.164.221.35:20128/api/monitoring/health | jq '{status:.status, version:.version}' ``` -### 3. Verify the deployment +Expected: HTTP 200, `"status": "ok"` (or equivalent health payload). + +### 5. Verify the deployment ```bash curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/ ``` + +## Rollback + +If the health check fails, restore the backup: + +```bash +OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version") +ssh root@69.164.221.35 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env" +``` + +## Gotchas + +- **Never build in a worktree with symlinked `node_modules`** — the standalone copy step + follows symlinks and produces a broken bundle. Use the main checkout or run `npm ci` + inside the worktree first. +- **`pm2 restart --update-env` does NOT re-read `ecosystem.config.cjs`** — if you changed + env vars in the ecosystem file, use `pm2 reload` or `pm2 delete + pm2 start` instead. +- **Parallel dev + release builds**: to avoid clobbering the dev server's `.build/next`, + pass `NEXT_DIST_DIR=.build/next-release` to `build:release` in CI or concurrent deploys: + `NEXT_DIST_DIR=.build/next-release npm run build:release` + then adjust the rsync source to `dist-release/` (or whatever `build:release` assembles into). +- **`dist/BUILD_SHA` must match the deployed HEAD** — always confirm before rsyncing. + A mismatch means the build ran from a dirty or incorrect commit. diff --git a/.agents/skills/deploy-vps-both-cc/SKILL.md b/.agents/skills/deploy-vps-both-cc/SKILL.md index 27944c311e..bc61de9bb7 100644 --- a/.agents/skills/deploy-vps-both-cc/SKILL.md +++ b/.agents/skills/deploy-vps-both-cc/SKILL.md @@ -5,46 +5,115 @@ description: Deploy the latest OmniRoute code to BOTH the Akamai VPS and the Loc # Deploy to VPS (Both) Workflow -Deploy OmniRoute to the production VPSs using `npm pack + scp` + PM2. +Deploy OmniRoute to both production VPSs using a clean `build:release` + `rsync` + PM2 restart. **Akamai VPS:** `69.164.221.35` **Local VPS:** `192.168.0.15` **Process manager:** PM2 (`omniroute`) **Port:** `20128` -**PM2 entry:** `/usr/lib/node_modules/omniroute/app/server.js` +**Remote install dir:** `/usr/lib/node_modules/omniroute/app/` (VPS image dir — unchanged) > [!IMPORTANT] -> The npm registry rejects packages > 100MB, so deployment uses **npm pack + scp**. +> **Build must run where `node_modules` is REAL.** A git worktree with symlinked +> `node_modules` breaks the Next.js standalone assembly. Always build from the main +> checkout (or an `npm ci`'d worktree), never from `OmniRoute-*` worktrees unless +> you first ran `npm ci` inside them. ## Steps -### 1. Build + pack locally +### 1. Clean build + sentinel + +Build **once** locally — the same `dist/` is shipped to both VPSs: // turbo ```bash -cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts +cd /home/diegosouzapw/dev/proxys/OmniRoute && npm run build:release ``` -### 2. Copy to both VPS and install +`build:release` does: +1. Deletes `.build/` and `dist/` (clean rebuild — no stale cache) +2. Runs `next build` → `.build/next/` (intermediates) +3. Assembles the shippable bundle into `dist/` via `assembleStandalone` +4. Writes `dist/BUILD_SHA` = `git rev-parse --short HEAD` (deploy sentinel) + +Verify the sentinel before shipping: + +```bash +cat /home/diegosouzapw/dev/proxys/OmniRoute/dist/BUILD_SHA && git -C /home/diegosouzapw/dev/proxys/OmniRoute rev-parse --short HEAD +``` + +Both lines must match. If they differ, the build is stale — re-run `build:release`. + +### 2. Back up the running bundle on both VPSs + +```bash +OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version") +ssh root@69.164.221.35 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}" & +ssh root@192.168.0.15 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}" & +wait +``` + +### 3. Rsync `dist/` → remote `app/` on both VPSs + +The VPS image directory is still named `app/`; we ship the **contents** of our local `dist/`: // turbo-all ```bash -scp omniroute-*.tgz root@69.164.221.35:/tmp/ && scp omniroute-*.tgz root@192.168.0.15:/tmp/ +rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@69.164.221.35:/usr/lib/node_modules/omniroute/app/ && rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@192.168.0.15:/usr/lib/node_modules/omniroute/app/ +``` + +### 4. Restart + health check on both VPSs + +```bash +ssh root@69.164.221.35 "pm2 restart omniroute --update-env && pm2 save" ``` ```bash -ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" +ssh root@192.168.0.15 "pm2 restart omniroute --update-env && pm2 save" ``` ```bash -ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'" +sleep 5 +curl -sf http://69.164.221.35:20128/api/monitoring/health | jq '{status:.status, version:.version}' +curl -sf http://192.168.0.15:20128/api/monitoring/health | jq '{status:.status, version:.version}' ``` -### 3. Verify the deployment +Expected: HTTP 200, `"status": "ok"` on both. + +### 5. Verify the deployment ```bash curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/ curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/ ``` + +## Rollback + +If either VPS fails its health check, restore from backup (adjust host as needed): + +```bash +OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version") +# Akamai rollback: +ssh root@69.164.221.35 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env" +# Local rollback: +ssh root@192.168.0.15 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env" +``` + +## Gotchas + +- **Never build in a worktree with symlinked `node_modules`** — the standalone copy step + follows symlinks and produces a broken bundle. Use the main checkout or run `npm ci` + inside the worktree first. +- **`pm2 restart --update-env` does NOT re-read `ecosystem.config.cjs`** — if you changed + env vars in the ecosystem file, use `pm2 reload` or `pm2 delete + pm2 start` instead. +- **Parallel dev + release builds**: to avoid clobbering the dev server's `.build/next`, + pass `NEXT_DIST_DIR=.build/next-release` to `build:release` in CI or concurrent deploys: + `NEXT_DIST_DIR=.build/next-release npm run build:release` + then adjust the rsync source to `dist-release/`. +- **`dist/BUILD_SHA` must match the deployed HEAD** — always confirm before rsyncing. + A mismatch means the build ran from a dirty or incorrect commit. +- **Build once, deploy to both** — never run two separate builds for the two VPSs. + The same `dist/` artifact (same `BUILD_SHA`) must land on both hosts to keep + versions in sync. diff --git a/.agents/skills/deploy-vps-local-cc/SKILL.md b/.agents/skills/deploy-vps-local-cc/SKILL.md index 33e89ab847..273d65ce8d 100644 --- a/.agents/skills/deploy-vps-local-cc/SKILL.md +++ b/.agents/skills/deploy-vps-local-cc/SKILL.md @@ -5,36 +5,99 @@ description: Deploy the latest OmniRoute code to the Local VPS (192.168.0.15) # Deploy to Local VPS Workflow -Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2. +Deploy OmniRoute to the Local VPS using a clean `build:release` + `rsync` + PM2 restart. **Local VPS:** `192.168.0.15` **Process manager:** PM2 (`omniroute`) **Port:** `20128` +**Remote install dir:** `/usr/lib/node_modules/omniroute/app/` (VPS image dir — unchanged) + +> [!IMPORTANT] +> **Build must run where `node_modules` is REAL.** A git worktree with symlinked +> `node_modules` breaks the Next.js standalone assembly. Always build from the main +> checkout (or an `npm ci`'d worktree), never from `OmniRoute-*` worktrees unless +> you first ran `npm ci` inside them. ## Steps -### 1. Build + pack locally +### 1. Clean build + sentinel + +Run from a checkout with a **real** `node_modules`: // turbo ```bash -cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts +cd /home/diegosouzapw/dev/proxys/OmniRoute && npm run build:release ``` -### 2. Copy to Local VPS and install +`build:release` does: +1. Deletes `.build/` and `dist/` (clean rebuild — no stale cache) +2. Runs `next build` → `.build/next/` (intermediates) +3. Assembles the shippable bundle into `dist/` via `assembleStandalone` +4. Writes `dist/BUILD_SHA` = `git rev-parse --short HEAD` (deploy sentinel) + +Verify the sentinel before shipping: + +```bash +cat /home/diegosouzapw/dev/proxys/OmniRoute/dist/BUILD_SHA && git -C /home/diegosouzapw/dev/proxys/OmniRoute rev-parse --short HEAD +``` + +Both lines must match. If they differ, the build is stale — re-run `build:release`. + +### 2. Back up the running bundle on the VPS + +```bash +OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version") +ssh root@192.168.0.15 "cp -a /usr/lib/node_modules/omniroute/app /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION}" +``` + +### 3. Rsync `dist/` → remote `app/` + +The VPS image directory is still named `app/`; we ship the **contents** of our local `dist/`: // turbo-all ```bash -scp omniroute-*.tgz root@192.168.0.15:/tmp/ +rsync -az --delete /home/diegosouzapw/dev/proxys/OmniRoute/dist/ root@192.168.0.15:/usr/lib/node_modules/omniroute/app/ +``` + +### 4. Restart + health check + +```bash +ssh root@192.168.0.15 "pm2 restart omniroute --update-env && pm2 save" ``` ```bash -ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'" +sleep 5 && curl -sf http://192.168.0.15:20128/api/monitoring/health | jq '{status:.status, version:.version}' ``` -### 3. Verify the deployment +Expected: HTTP 200, `"status": "ok"` (or equivalent health payload). + +### 5. Verify the deployment ```bash curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/ ``` + +## Rollback + +If the health check fails, restore the backup: + +```bash +OMNIROUTE_VERSION=$(node -p "require('/home/diegosouzapw/dev/proxys/OmniRoute/package.json').version") +ssh root@192.168.0.15 "rm -rf /usr/lib/node_modules/omniroute/app && cp -a /usr/lib/node_modules/omniroute/app.bak-${OMNIROUTE_VERSION} /usr/lib/node_modules/omniroute/app && pm2 restart omniroute --update-env" +``` + +## Gotchas + +- **Never build in a worktree with symlinked `node_modules`** — the standalone copy step + follows symlinks and produces a broken bundle. Use the main checkout or run `npm ci` + inside the worktree first. +- **`pm2 restart --update-env` does NOT re-read `ecosystem.config.cjs`** — if you changed + env vars in the ecosystem file, use `pm2 reload` or `pm2 delete + pm2 start` instead. +- **Parallel dev + release builds**: to avoid clobbering the dev server's `.build/next`, + pass `NEXT_DIST_DIR=.build/next-release` to `build:release` in CI or concurrent deploys: + `NEXT_DIST_DIR=.build/next-release npm run build:release` + then adjust the rsync source to `dist-release/` (or whatever `build:release` assembles into). +- **`dist/BUILD_SHA` must match the deployed HEAD** — always confirm before rsyncing. + A mismatch means the build ran from a dirty or incorrect commit. diff --git a/.dockerignore b/.dockerignore index 171c9d4631..bc06bf65fd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,6 +9,7 @@ # Dependencies and build output node_modules .next +.build out build dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ce1290e65..d0f34d467e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,11 @@ jobs: cache: npm - run: npm ci - run: npm run check:node-runtime + # build:cli runs a clean build into .build/next and assembles dist/ + # For release builds prefer: npm run build:release (clean rebuild + HEAD sentinel) - run: npm run build:cli + - name: Assert dist/server.js exists + run: test -f dist/server.js || (echo "dist/server.js missing — build:cli did not assemble correctly" && exit 1) - run: npm run check:pack-artifact electron-package-smoke: diff --git a/AGENTS.md b/AGENTS.md index 5d0be6ac04..e26971c64e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,19 +23,32 @@ with **MCP Server** (37 tools), **A2A v0.3 Protocol**, and **Electron desktop ap ## Build, Lint, and Test Commands -| Command | Description | -| ----------------------------------- | --------------------------------- | -| `npm run dev` | Start Next.js dev server | -| `npm run build` | Production build (isolated) | -| `npm run start` | Run production build | -| `npm run build:cli` | Build CLI package | -| `npm run lint` | ESLint on all source files | -| `npm run typecheck:core` | TypeScript core type checking | -| `npm run typecheck:noimplicit:core` | Strict checking (no implicit any) | -| `npm run check` | Run lint + test | -| `npm run check:cycles` | Check for circular dependencies | -| `npm run electron:dev` | Run Electron app in dev mode | -| `npm run electron:build` | Build Electron app for current OS | +| Command | Description | +| ----------------------------------- | ---------------------------------------------------------------- | +| `npm run dev` | Start Next.js dev server | +| `npm run build` | Production build: `next build` → `.build/next/` + assemble `dist/` | +| `npm run build:release` | Clean rebuild + HEAD sentinel (`dist/BUILD_SHA`) — use for deploy | +| `npm run start` | Run production build | +| `npm run build:cli` | Build CLI package | +| `npm run lint` | ESLint on all source files | +| `npm run typecheck:core` | TypeScript core type checking | +| `npm run typecheck:noimplicit:core` | Strict checking (no implicit any) | +| `npm run check` | Run lint + test | +| `npm run check:cycles` | Check for circular dependencies | +| `npm run electron:dev` | Run Electron app in dev mode | +| `npm run electron:build` | Build Electron app for current OS | + +**Build output layout:** + +| Directory | Purpose | Gitignored | +| --------- | --------------------------------------------------- | ---------- | +| `src/` | Application source (TypeScript / TSX) | No | +| `.build/` | Build intermediates (`distDir = .build/next`) | Yes | +| `dist/` | Shippable bundle assembled by `assembleStandalone` | Yes | + +The pipeline is a single `next build` pass — intermediates land in `.build/next/`, the +assembled bundle in `dist/`. VPS deploys rsync `dist/` into the remote +`/usr/lib/node_modules/omniroute/app/` directory (VPS image path is unchanged). ### Running Tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 6786ab4b49..1552cc8c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,10 @@ _Development cycle in progress — entries are added as work merges into `releas - **resilience:** a per-model subscription/permission `403` from a passthrough provider (e.g. Ollama Cloud `deepseek-v4-pro` → _"this model requires a subscription"_) now locks out **only that model** instead of cooling down the whole connection — the free models on the same key keep serving, and repeated paid-model 403s no longer escalate a connection-wide backoff. Generalizes the grok-web 403 precedent to all `hasPerModelQuota` providers; terminal/credential 403s (banned/deactivated key) still deactivate the connection. ([#3027](https://github.com/diegosouzapw/OmniRoute/issues/3027)) - **cache:** preserve client-side `cache_control` breakpoints for Xiaomi MiMo — added `xiaomi-mimo` to the prompt-caching provider allowlist so Claude Code (via cc-switch) cache hints are no longer stripped by the OpenAI-format translator, restoring cache hits. ([#3088](https://github.com/diegosouzapw/OmniRoute/issues/3088)) +### 🔧 Build + +- **build-output-isolation:** unified standalone assembly into one shared `assembleStandalone` module; isolated build output into `.build/` (intermediates, gitignored) and `dist/` (shippable bundle, gitignored), replacing the old repo-root `app/` and `.next/` directories; dropped the duplicate `next build` that prepublish previously ran; added `build:release` script for a clean rebuild with a `dist/BUILD_SHA` HEAD sentinel that guards against deploying stale bundles. **Operators using custom `app/` paths:** the published bundle directory on the VPS image (`/usr/lib/node_modules/omniroute/app/`) is unchanged — only the in-repo build output path moved. Update any local scripts that reference the repo-local `app/` build output to `dist/` instead. + ### 📦 Dependencies - **electron:** bump to 42.3.2 (crash fix desktopCapturer, Chromium 148.0.7778.218, ThinLTO perf) (#3083) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d16220c52c..ba211765d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,13 +59,40 @@ These settings are stored in the database and persist across restarts, overridin npm run dev # Production build -npm run build +npm run build # next build → .build/next/ then assembleStandalone → dist/ npm run start +# Release build (clean rebuild + HEAD sentinel — required for deploy) +npm run build:release # rm -rf .build dist && build + writes dist/BUILD_SHA + # Common port configuration PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev ``` +### Build Output Layout + +| Directory | Contents | Tracked | +| ---------- | ------------------------------------------- | ------- | +| `src/` | Application source (TypeScript / TSX) | Yes | +| `.build/` | Intermediates — `next build` output (gitignored, `distDir = .build/next`) | No | +| `dist/` | Shippable bundle — assembled by `assembleStandalone` (gitignored) | No | + +The build pipeline is a single pass: + +``` +npm run build + └─ next build → .build/next/standalone (Next.js output) + └─ assembleStandalone() (copies standalone + static + public + native assets) + └─ output: dist/ (server.js, .next/static/, public/, node_modules/) +``` + +`npm run build:release` additionally cleans both directories first and writes +`dist/BUILD_SHA` (= `git rev-parse --short HEAD`) as a deploy integrity sentinel. + +> **VPS deploy note:** the remote image directory `/usr/lib/node_modules/omniroute/app/` +> is unchanged. The deploy skills rsync the contents of `dist/` into it. +> Only the in-repo build output path moved (`app/` → `dist/`). + Default URLs: - **Dashboard**: `http://localhost:20128/dashboard` @@ -314,6 +341,10 @@ Write unit tests in `tests/unit/` covering at minimum: Releases are managed via the `/generate-release` workflow. When a new GitHub Release is created, the package is **automatically published to npm** via GitHub Actions. +For VPS deploys, use `npm run build:release` (not `npm run build`) — it performs a clean +rebuild, assembles the bundle into `dist/`, and writes the `dist/BUILD_SHA` sentinel. +Then use the `/deploy-vps-*-cc` skills which rsync `dist/` to the remote `app/` directory. + --- ## Getting Help diff --git a/Dockerfile b/Dockerfile index 135c6abceb..06f740367c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ RUN --mount=type=cache,target=/root/.npm \ ENV OMNIROUTE_USE_TURBOPACK=1 COPY . ./ -RUN --mount=type=cache,target=/app/.next/cache \ +RUN --mount=type=cache,target=/app/.build/next/cache \ mkdir -p /app/data && npm run build # ── Runner base ──────────────────────────────────────────────────────────── @@ -65,9 +65,9 @@ ENV DATA_DIR=/app/data RUN mkdir -p /app/data # The standalone build + syncStandaloneExtraModules bundles all runtime files -# (.next, node_modules, migrations, scripts, docs, etc.) into .next/standalone/. +# (.next, node_modules, migrations, scripts, docs, etc.) into .build/next/standalone/. # Explicit overrides below cover modules that NFT tracing may miss. -COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.build/next/standalone ./ # Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers # Explicitly copy better-sqlite3 — native bindings are not reliably traced by diff --git a/docs/ops/RELEASE_CHECKLIST.md b/docs/ops/RELEASE_CHECKLIST.md index 521f223aa5..ba50a99cda 100644 --- a/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/ops/RELEASE_CHECKLIST.md @@ -147,11 +147,38 @@ If `electron/` changed: - [ ] `electron/package.json` version matches root `package.json` - [ ] Auto-update channel pointer updated if releasing to `stable` +### Build Layout + +The repository uses three distinct output directories — never mix them up: + +| Directory | Purpose | Tracked? | +| ------------- | ------------------------------------------------------------- | -------- | +| `src/` | Application source (TypeScript / TSX) | Yes | +| `.build/` | Build intermediates — `next build` output (`distDir`) | No (gitignored) | +| `dist/` | Shippable npm bundle — assembled by `assembleStandalone` | No (gitignored) | + +> **Operator note:** the remote VPS image directory remains `/usr/lib/node_modules/omniroute/app/`. +> Only the **in-repo** build output moved (`app/` → `dist/`). The deploy skills rsync +> `dist/` contents into the remote `app/` dir — no VPS path changes required. + +**Single-build flow:** + +``` +npm run build:release + └─ rm -rf .build dist (clean) + └─ next build → .build/next/ (intermediates) + └─ assembleStandalone (copies standalone + static + public + natives → dist/) + └─ writes dist/BUILD_SHA (HEAD sentinel) +``` + +Do NOT run `npm run build` followed by a separate `npm run build:cli` for deploy — use +`npm run build:release` which does a clean rebuild + sentinel in one command. + ### Artifact Validation -- [ ] `npm run build:cli` succeeds +- [ ] `npm run build:release` succeeds and `dist/BUILD_SHA` == `git rev-parse --short HEAD` - [ ] `npm run check:pack-artifact` clean — no `app.__qa_backup`, `scripts/scratch`, `package-lock.json`, or other local residue -- [ ] `npm run build` produces a working standalone Next.js bundle +- [ ] `dist/server.js` exists after build ### Tagging & Release @@ -169,10 +196,14 @@ If `electron/` changed: ### Deploy +Deploy skills use the light rsync flow — no `npm pack`, no `npm i -g`: + - [ ] Use deploy skill that matches target: - `/deploy-vps-local-cc` — local VPS (192.168.0.15) - `/deploy-vps-akamai-cc` — Akamai VPS (69.164.221.35) - `/deploy-vps-both-cc` — both +- [ ] Before deploying, confirm `dist/BUILD_SHA` == `git rev-parse --short HEAD` +- [ ] Build must run where `node_modules` is real (main checkout or `npm ci`'d worktree — NOT a symlinked worktree) - [ ] Smoke test deployed instance: - Open `/dashboard/health` → check version string matches release - Run a `/v1/chat/completions` request against a known provider diff --git a/electron/package.json b/electron/package.json index 331d642437..660a4760e8 100644 --- a/electron/package.json +++ b/electron/package.json @@ -57,7 +57,7 @@ ], "extraResources": [ { - "from": "../.next/electron-standalone", + "from": "../.build/electron-standalone", "to": "app", "filter": [ "**/*", @@ -65,7 +65,7 @@ ] }, { - "from": "../.next/electron-standalone/node_modules", + "from": "../.build/electron-standalone/node_modules", "to": "app/node_modules", "filter": [ "**/*" diff --git a/eslint.config.mjs b/eslint.config.mjs index f19c6e72ef..a2af5fa614 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -38,11 +38,13 @@ const eslintConfig = [ // Global ignores — keep ESLint scoped to source files only { ignores: [ - // Next.js build output + // Next.js build output (distDir now .build/next; keep .next for legacy) ".next/**", + ".build/**", "src/.next/**", "out/**", "build/**", + "dist/**", "coverage/**", "next-env.d.ts", // Scripts and binaries @@ -65,12 +67,9 @@ const eslintConfig = [ // Playwright test output "playwright-report/**", "test-results/**", - // Subdirectory .next build output (app/ subdir) + // Legacy app/ and QA backup dirs (renamed to dist/ in Layer 1) "app/**", - "app/.next/**", - "app/bin/**", "app.__qa_backup/**", - "app/app.__qa_backup/**", // CLI package copy directory "clipr/**", ], diff --git a/scripts/build/prepare-electron-standalone.mjs b/scripts/build/prepare-electron-standalone.mjs index 81c7e33fe0..96e504df2d 100644 --- a/scripts/build/prepare-electron-standalone.mjs +++ b/scripts/build/prepare-electron-standalone.mjs @@ -9,10 +9,10 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = join(__dirname, "..", ".."); -const NEXT_DIST_DIR = process.env.NEXT_DIST_DIR || ".next"; +const NEXT_DIST_DIR = process.env.NEXT_DIST_DIR || ".build/next"; const DIST_DIR = join(ROOT, NEXT_DIST_DIR); const STANDALONE_DIR = join(DIST_DIR, "standalone"); -const ELECTRON_STANDALONE_DIR = join(ROOT, NEXT_DIST_DIR, "electron-standalone"); +const ELECTRON_STANDALONE_DIR = join(ROOT, ".build", "electron-standalone"); // --- Electron-UNIQUE: resolve the nested server.js location ----------------