diff --git a/.github/actions/setup-node-cached/action.yml b/.github/actions/setup-node-cached/action.yml index 0d0fe1b191..61ffc24450 100644 --- a/.github/actions/setup-node-cached/action.yml +++ b/.github/actions/setup-node-cached/action.yml @@ -1,9 +1,8 @@ -name: Setup Node with cached node_modules +name: Setup Node with npm cache description: >- - Set up Node from .nvmrc, restore node_modules from an exact-key cache, and run - `npm ci` only on a cache miss. postinstall/preinstall are inert in CI (git-hook - install + engine assert), so skipping the install on a hit is safe. Native deps - are platform prebuilds keyed by runner.os, so there is no ABI mismatch risk. + Set up Node from .nvmrc, restore npm's download cache, and run the immutable + clean install on every fresh runner. node_modules is never restored, so each + job proves package.json and package-lock.json can reproduce the toolchain. runs: using: composite steps: @@ -14,16 +13,6 @@ runs: cache: npm cache-dependency-path: package-lock.json - - name: Restore node_modules - id: node-modules-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: node_modules - # Include every install-contract input. In particular, an .npmrc-only PR - # must not restore old modules and skip the npm ci it is meant to validate. - key: node-modules-${{ runner.os }}-${{ hashFiles('.nvmrc', 'package.json', 'package-lock.json', '.npmrc') }} - - - name: Install dependencies (cache miss only) - if: steps.node-modules-cache.outputs.cache-hit != 'true' + - name: Install dependencies shell: bash - run: npm ci + run: npm ci --include=dev diff --git a/.github/workflows/dependency-report.yml b/.github/workflows/dependency-report.yml index 423e6dd915..41bd712a17 100644 --- a/.github/workflows/dependency-report.yml +++ b/.github/workflows/dependency-report.yml @@ -39,7 +39,7 @@ jobs: cache-dependency-path: package-lock.json - name: Install dependencies - run: npm ci + run: npm ci --include=dev - name: Render dependency report id: report diff --git a/.github/workflows/eval-canary.yml b/.github/workflows/eval-canary.yml index 6f5f7b12c8..b476a0312e 100644 --- a/.github/workflows/eval-canary.yml +++ b/.github/workflows/eval-canary.yml @@ -143,7 +143,7 @@ jobs: - name: Install dependencies id: install - run: npm ci + run: npm ci --include=dev - name: Guard Supabase project identity id: project_guard diff --git a/.github/workflows/ingestion-autopilot.yml b/.github/workflows/ingestion-autopilot.yml index 55164f6722..a7276509fc 100644 --- a/.github/workflows/ingestion-autopilot.yml +++ b/.github/workflows/ingestion-autopilot.yml @@ -62,7 +62,7 @@ jobs: cache-dependency-path: package-lock.json - name: Install dependencies - run: npm ci + run: npm ci --include=dev - name: Guard Supabase project identity run: npm run check:supabase-project diff --git a/.github/workflows/live-drift.yml b/.github/workflows/live-drift.yml index 3153eae9f1..56247f5967 100644 --- a/.github/workflows/live-drift.yml +++ b/.github/workflows/live-drift.yml @@ -50,7 +50,7 @@ jobs: cache-dependency-path: package-lock.json - name: Install dependencies - run: npm ci + run: npm ci --include=dev - name: Guard Supabase project identity run: npm run check:supabase-project diff --git a/.github/workflows/ops-digest.yml b/.github/workflows/ops-digest.yml index 40423dc15d..4ab355f42d 100644 --- a/.github/workflows/ops-digest.yml +++ b/.github/workflows/ops-digest.yml @@ -2,7 +2,8 @@ # hit-rate, answer spend, degraded/truncation rates) into a one-screen summary # and keeps it in a rolling GitHub issue, commenting only when something is off. # -# The repo variable and matching deployment/GitHub secret are provisioned. +# The repo variable and matching deployment/GitHub secret must both be +# provisioned. The preflight below fails closed when either name is absent. # Keep workflow_dispatch for operator verification and the daily schedule for # the normal morning digest. name: Ops Digest diff --git a/.github/workflows/staging-tenancy.yml b/.github/workflows/staging-tenancy.yml index 57cf330f6c..ee6898dd3d 100644 --- a/.github/workflows/staging-tenancy.yml +++ b/.github/workflows/staging-tenancy.yml @@ -31,7 +31,7 @@ jobs: cache-dependency-path: package-lock.json - name: Install dependencies - run: npm ci + run: npm ci --include=dev - name: Run cross-tenant staging checks env: diff --git a/README.md b/README.md index b89de39830..7ac58e2152 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,13 @@ questions with source citations that link back to the original PDF/document. 2. Install dependencies: ```bash -npm install +npm ci --include=dev ``` +This is the clean-checkout and validation install contract. Use `npm install` +only when intentionally changing dependencies and regenerating +`package-lock.json`. + 3. Copy the full `.env.example` to `.env.local` and fill in Supabase and OpenAI values. Copy the worker and upload defaults too — they are conservative local-first settings, not optional extras. diff --git a/scripts/check-installed-lock-parity.mjs b/scripts/check-installed-lock-parity.mjs index adcdee1171..9062606939 100644 --- a/scripts/check-installed-lock-parity.mjs +++ b/scripts/check-installed-lock-parity.mjs @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -export const criticalInstalledPackages = ["next", "react", "react-dom", "playwright", "typescript"]; +export const criticalInstalledPackages = ["next", "react", "react-dom", "eslint", "playwright", "typescript", "vitest"]; function readJson(filePath) { return JSON.parse(readFileSync(filePath, "utf8")); diff --git a/tests/ci-cache-safety.test.ts b/tests/ci-cache-safety.test.ts index aad6f69e0f..ae7e742c4c 100644 --- a/tests/ci-cache-safety.test.ts +++ b/tests/ci-cache-safety.test.ts @@ -7,12 +7,12 @@ const uiSetup = readFileSync(new URL("../.github/actions/setup-ui-e2e/action.yml const workflow = readFileSync(new URL("../.github/workflows/ci.yml", import.meta.url), "utf8"); describe("CI cache safety", () => { - it("invalidates cached node_modules for every install-contract input", () => { - const cacheKey = nodeSetup.match(/^\s*key:\s*(node-modules-.*)$/m)?.[1] ?? ""; - expect(cacheKey).toContain(".nvmrc"); - expect(cacheKey).toContain("package.json"); - expect(cacheKey).toContain("package-lock.json"); - expect(cacheKey).toContain(".npmrc"); + it("uses npm's download cache but recreates node_modules on every job", () => { + expect(nodeSetup).toContain("cache: npm"); + expect(nodeSetup).toContain("cache-dependency-path: package-lock.json"); + expect(nodeSetup).toContain("run: npm ci --include=dev"); + expect(nodeSetup).not.toContain("path: node_modules"); + expect(nodeSetup).not.toContain("cache-hit"); }); it("keeps quarantined and mockup UI specs in one advisory lane", () => { diff --git a/tests/installed-lock-parity.test.ts b/tests/installed-lock-parity.test.ts index 28e4578355..0aeb6f6b47 100644 --- a/tests/installed-lock-parity.test.ts +++ b/tests/installed-lock-parity.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "nod import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { installedLockParity } from "../scripts/check-installed-lock-parity.mjs"; +import { criticalInstalledPackages, installedLockParity } from "../scripts/check-installed-lock-parity.mjs"; const temporaryRoots: string[] = []; @@ -28,6 +28,10 @@ afterEach(() => { }); describe("installedLockParity", () => { + it("covers the local validation toolchain as well as runtime packages", () => { + expect(criticalInstalledPackages).toEqual(expect.arrayContaining(["eslint", "playwright", "typescript", "vitest"])); + }); + it("accepts an installed package that exactly matches the lockfile", () => { expect(installedLockParity(fixture("16.2.11", "16.2.11"), ["next"])).toEqual([ expect.objectContaining({ packageName: "next", lockedVersion: "16.2.11", installedVersion: "16.2.11", ok: true }),