diff --git a/package.json b/package.json index bcb2724b5..f04e06b81 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "scripts": { "toolchain:check": "node scripts/check-node-toolchain.mjs", - "install:all": "npm ci --prefix backend && npm ci --prefix frontend && npm ci --prefix website", + "install:all": "node scripts/install-all.mjs", "test": "npm run test:workflow-sources && npm run test:public-content && npm run test:release-manifest && npm run test:operations && npm run test:evaluation && npm run test:final && npm run test:baseline && npm run test:chat --prefix backend && npm run test:legal-sources --prefix backend && npm run test:security --prefix backend", "test:evaluation": "node --test tests/evaluation/*.test.mjs && node scripts/evaluate-ontario.mjs --check && node scripts/check-professional-validation.mjs && node scripts/check-release-readiness.mjs", "evaluate:ontario": "node scripts/evaluate-ontario.mjs", diff --git a/scripts/install-all.mjs b/scripts/install-all.mjs new file mode 100644 index 000000000..b835ececc --- /dev/null +++ b/scripts/install-all.mjs @@ -0,0 +1,55 @@ +import { spawnSync } from "node:child_process"; +import { rmSync } from "node:fs"; + +const workspaces = ["backend", "frontend", "website"]; +const maxAttempts = 3; +const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; + +const pause = (milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds)); + +for (const workspace of workspaces) { + let installed = false; + let lastStatus = 1; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + console.log( + `Installing ${workspace} dependencies (attempt ${attempt}/${maxAttempts})...`, + ); + + const result = spawnSync(npmCommand, ["ci", "--prefix", workspace], { + env: process.env, + stdio: "inherit", + }); + + if (result.error) { + console.error(`Failed to start npm for ${workspace}:`, result.error); + } + + lastStatus = result.status ?? 1; + if (lastStatus === 0) { + installed = true; + break; + } + + if (attempt < maxAttempts) { + console.warn( + `${workspace} dependency installation failed; cleaning node_modules before retry.`, + ); + rmSync(`${workspace}/node_modules`, { + force: true, + maxRetries: 3, + recursive: true, + retryDelay: 250, + }); + await pause(attempt * 2_000); + } + } + + if (!installed) { + console.error( + `${workspace} dependency installation failed after ${maxAttempts} attempts.`, + ); + process.exit(lastStatus); + } +} diff --git a/tests/baseline/ross-website-contract.test.mjs b/tests/baseline/ross-website-contract.test.mjs index 4454f7c8a..f30540660 100644 --- a/tests/baseline/ross-website-contract.test.mjs +++ b/tests/baseline/ross-website-contract.test.mjs @@ -11,13 +11,16 @@ const json = (path) => JSON.parse(read(path)); test("the public website is a separate build target", () => { const rootPackage = json("package.json"); const websitePackage = json("website/package.json"); + const installer = read("scripts/install-all.mjs"); assert.equal(existsSync(resolve(root, "website/.openai/hosting.json")), true); assert.equal( existsSync(resolve(root, "website/build/sites-vite-plugin.ts")), true, ); assert.equal(websitePackage.name, "ross-ontario"); - assert.match(rootPackage.scripts["install:all"], /--prefix website/); + assert.match(rootPackage.scripts["install:all"], /scripts\/install-all\.mjs/); + assert.match(installer, /\["backend", "frontend", "website"\]/); + assert.match(installer, /\["ci", "--prefix", workspace\]/); assert.match(rootPackage.scripts.build, /build:website/); assert.match(rootPackage.scripts.lint, /lint:website/); assert.match(rootPackage.scripts.check, /test:website/); @@ -87,7 +90,7 @@ test("the website scaffold exposes every governed public route", () => { "readiness", ]; for (const key of requiredKeys) { - assert.match(content, new RegExp(`[\"']?${key}[\"']?\\s*:`), key); + assert.match(content, new RegExp(`["']?${key}["']?\\s*:`), key); } const route = read("website/app/[...slug]/page.tsx");