Files
itworx-knowledge-hub-Public/scripts/verify.mjs
T

239 lines
8.3 KiB
JavaScript

#!/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 `<assembly>_<timestamp>.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);
}