#!/usr/bin/env node import fs from "node:fs"; import process from "node:process"; import { spawnSync } from "node:child_process"; import { commandExists, run } from "./lib/exec.mjs"; const flags = new Set(process.argv.slice(2)); const structureOnly = flags.has("--structure-only"); const fast = flags.has("--fast"); const ci = flags.has("--ci"); const release = flags.has("--release"); const publicSourceExport = fs.existsSync("PUBLIC_SOURCE_EXPORT.md"); function findWorkingPython() { // On some Windows setups `python3` resolves via PATH to the Microsoft Store // app-execution-alias stub (present even when no Store install exists), // which `where`/commandExists reports as found but which fails at runtime. // Probe candidates by actually invoking them rather than trusting PATH // resolution alone. const candidates = ["python3", "python"]; for (const candidate of candidates) { if (!commandExists(candidate)) { continue; } const probe = spawnSync(candidate, ["--version"], { stdio: "ignore", shell: false }); if (probe.status === 0) { return candidate; } } throw new Error("No working Python 3 interpreter found (tried python3, python)."); } const python = findWorkingPython(); async function section(name, action) { console.log(`\n=== ${name} ===`); await action(); } try { await section("Bundle structure", async () => { if (publicSourceExport) { await run(process.execPath, ["scripts/validate-public-source.mjs"]); } else { await run(process.execPath, ["scripts/validate-bundle.mjs"]); await run(process.execPath, ["scripts/test-safety-hook.mjs"]); } }); await section("LeafWiki inspector tests", async () => { await run(python, [ "-m", "unittest", "discover", "-s", "tools/leafwiki-inspector/tests", "-p", "test_*.py" ]); }); await section("Knowledge-ingestion policy tests", async () => { if (!publicSourceExport) { await run(process.execPath, ["--test", "tools/knowledge-ingestion/tests/*.test.mjs"]); } }); if (structureOnly) { console.log("\nStructure-only verification passed."); process.exit(0); } await section(".NET restore, format, build and tests", async () => { await run("dotnet", ["restore", "ItWorx.KnowledgeHub.slnx"]); await run("dotnet", [ "format", "ItWorx.KnowledgeHub.slnx", "--verify-no-changes", "--no-restore" ]); await run("dotnet", [ "build", "ItWorx.KnowledgeHub.slnx", "--configuration", "Release", "--no-restore" ]); await run("dotnet", [ "test", "ItWorx.KnowledgeHub.slnx", "--configuration", "Release", "--no-build", // Excludes the M8 full-scale search/tree suite (100,000 pages / 1,000,000 // revisions, docs/10-search-and-indexing.md "Scale verification") — a // deliberate, occasional release-gate exercise (run explicitly in the // "release" section below), not a per-commit check; it alone would add well // over a minute to every routine verification run. "--filter", "Category!=ReleaseScale", // No explicit LogFileName: `dotnet test` runs every project in the solution // through the same invocation, so a fixed name would have each project's // results overwrite the previous one. Omitting it lets each project write its // own default `_.trx` file instead. "--logger", "trx", "--results-directory", "artifacts/test-results" ]); }); await section("Frontend install and verification", async () => { const installArgs = fs.existsSync("pnpm-lock.yaml") ? ["pnpm", "install", "--frozen-lockfile"] : ["pnpm", "install", "--no-frozen-lockfile"]; if (!fs.existsSync("pnpm-lock.yaml")) { console.warn("Pre-M0: generating pnpm-lock.yaml. Commit it after reviewing dependency versions."); } await run("corepack", installArgs); await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "lint"]); await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "typecheck"]); await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "test"]); await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "build"]); // Depends on the root workspace's node_modules (js-yaml), so it must run after // `pnpm install` above, not in the earlier structure-only "Bundle structure" // section that runs before any dependency install exists on a clean checkout. await run(process.execPath, ["scripts/validate-openapi.mjs"]); // Contract-coverage gate (docs/adr/ADR-029-v2-r5-final-hardening.md): fails when a // registered /api/v1 endpoint is not documented in docs/api/openapi.yaml. Same // js-yaml dependency, same placement. await run(process.execPath, ["scripts/check-openapi-coverage.mjs"]); }); if (!fast) { await section("Docker configuration", async () => { await run("docker", ["compose", "config"], { env: { POSTGRES_PASSWORD: "verification-only-not-for-production" } }); }); } if (ci || release) { await section("Production image build", async () => { await run("docker", ["compose", "build", "app"], { env: { POSTGRES_PASSWORD: "verification-only-not-for-production", BUILD_COMMIT: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? "verification" } }); }); } if (release) { await section("Full-scale search/tree suite (release-gate only)", async () => { await run("dotnet", [ "test", "tests/backend/ItWorx.KnowledgeHub.IntegrationTests/ItWorx.KnowledgeHub.IntegrationTests.csproj", "--configuration", "Release", "--no-build", "--filter", "Category=ReleaseScale" ]); }); await section(".NET dependency vulnerability scan", async () => { const result = await run( "dotnet", ["list", "ItWorx.KnowledgeHub.slnx", "package", "--vulnerable", "--include-transitive"], { capture: true } ); // `dotnet list package --vulnerable` always exits 0 and just prints findings — // the gate itself must fail the release on any reported advisory (M8-A02: "No // known Critical or High security finding remains unresolved" — since the // command does not separate by severity in its text output, any finding at all // blocks release pending explicit triage, matching the release gate's // zero-tolerance-until-reviewed posture). if (/has the following vulnerable packages/i.test(result.stdout)) { throw new Error( "dotnet reported vulnerable NuGet packages — review the output above before releasing.\n" + result.stdout ); } }); await section("Frontend dependency vulnerability scan", async () => { await run("corepack", ["pnpm", "audit", "--prod"]); }); await section("Secret scan", async () => { if (!commandExists("gitleaks")) { throw new Error("gitleaks is required for the release secret scan but was not found on PATH."); } await run("gitleaks", ["detect", "--source", ".", "--redact"]); }); if (commandExists("trivy")) { await section("Container vulnerability scan", async () => { await run("trivy", [ "image", "--severity", "CRITICAL,HIGH", "--exit-code", "1", "--scanners", "vuln", "itworx/knowledge-hub:local" ]); }); } else { console.warn( "\ntrivy not found on PATH — container vulnerability scan skipped. Install " + "trivy before a real release; this is not a substitute for the scan." ); } console.warn( "\nRelease verification still requires the milestone-defined E2E (desktop + " + "mobile, axe), backup/restore rehearsal and runtime-inspection gates to be run " + "manually against the built image, per the documented procedure in EVIDENCE.md " + "— these involve bringing up docker compose and are recorded there rather than " + "orchestrated here." ); } console.log("\nVerification completed successfully for the selected baseline gates."); } catch (error) { console.error(`\nVerification failed: ${error.message}`); process.exit(typeof error.code === "number" ? error.code : 1); }