import fs from "node:fs"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const zeroShaPattern = /^0{40}$/; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); export const repoRoot = path.resolve(scriptDir, "../.."); export const artifactsDir = path.join(repoRoot, ".artifacts", "harness"); export const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; export const suiteDefinitions = { static: { description: "Fast static validation: lint and workspace typecheck.", commands: [ { args: ["lint"], label: "Lint repository" }, { args: ["typecheck"], label: "Typecheck workspace" } ] }, component: { description: "Fast component feedback: lint, typecheck, and unit coverage.", commands: [ { args: ["lint"], label: "Lint repository" }, { args: ["typecheck"], label: "Typecheck workspace" }, { args: ["test"], label: "Run component tests" } ] }, docs: { description: "Build the Storybook review surface.", commands: [{ args: ["build:docs"], label: "Build Storybook" }] }, a11y: { description: "Run Storybook accessibility validation and report violations/incomplete results.", commands: [{ args: ["test:e2e:a11y"], label: "Run Storybook accessibility checks" }] }, "docs-smoke": { description: "Exercise high-value Storybook flows with Playwright.", commands: [{ args: ["test:e2e:smoke"], label: "Run Storybook smoke tests" }] }, consumers: { description: "Validate generated registry metadata and downstream consumers.", commands: [ { args: ["registry:check"], label: "Check registry metadata" }, { args: ["test:registry:consumer"], label: "Run registry consumer smoke test" }, { args: ["test:package:consumer"], label: "Run package consumer smoke test" } ] }, pr: { description: "Baseline pull request gate for source, docs, and consumer surfaces.", commands: [ { args: ["lint"], label: "Lint repository" }, { args: ["typecheck"], label: "Typecheck workspace" }, { args: ["test"], label: "Run component tests" }, { args: ["build"], label: "Build packages" }, { args: ["build:docs"], label: "Build Storybook" }, { args: ["test:e2e:a11y"], label: "Run Storybook accessibility checks" }, { args: ["registry:check"], label: "Check registry metadata" }, { args: ["test:registry:consumer"], label: "Run registry consumer smoke test" }, { args: ["test:package:consumer"], label: "Run package consumer smoke test" } ] }, release: { description: "Full release gate, including browser-driven Storybook coverage.", commands: [ { args: ["lint"], label: "Lint repository" }, { args: ["typecheck"], label: "Typecheck workspace" }, { args: ["test"], label: "Run component tests" }, { args: ["build"], label: "Build packages" }, { args: ["build:docs"], label: "Build Storybook" }, { args: ["test:e2e:a11y"], label: "Run Storybook accessibility checks" }, { args: ["registry:check"], label: "Check registry metadata" }, { args: ["test:registry:consumer"], label: "Run registry consumer smoke test" }, { args: ["test:package:consumer"], label: "Run package consumer smoke test" }, { args: ["test:e2e:smoke"], label: "Run Storybook smoke tests" } ] } }; const suiteOrder = [ "static", "component", "docs", "a11y", "docs-smoke", "consumers", "pr", "release" ]; const prSupersededSuites = ["static", "component", "docs", "a11y", "docs-smoke", "consumers"]; const releaseSupersededSuites = [...prSupersededSuites, "pr"]; const rootStaticFiles = new Set([ "eslint.config.mjs", "package.json", "playwright.config.ts", "pnpm-lock.yaml", "pnpm-workspace.yaml", "tsconfig.base.json", "tsconfig.json", "vitest.config.ts" ]); function normalizeFilePath(filePath) { return filePath.replace(/\\/g, path.posix.sep).split(path.sep).join(path.posix.sep); } function isExactMatch(filePath, exactPaths) { return exactPaths.has(filePath); } function isWithin(filePath, directory) { return filePath === directory || filePath.startsWith(`${directory}/`); } function isPackageSourceChange(filePath) { return isWithin(filePath, "packages/ui/src") || isWithin(filePath, "packages/tokens/src"); } function isPackageContractChange(filePath) { return ( isPackageSourceChange(filePath) || filePath === "packages/ui/package.json" || filePath === "packages/tokens/package.json" ); } function isDocsSurfaceChange(filePath) { return isWithin(filePath, "apps/docs") || isWithin(filePath, ".storybook"); } function isDocsInteractiveChange(filePath) { return ( isWithin(filePath, "apps/docs/src") || isWithin(filePath, ".storybook") || filePath === "playwright.config.ts" ); } function isConsumerSurfaceChange(filePath) { return ( isWithin(filePath, "registry") || isWithin(filePath, "tests/package-consumer") || isWithin(filePath, "tests/registry") || filePath === "scripts/build-registry.mjs" || filePath === "scripts/registry-install.mjs" || isPackageContractChange(filePath) ); } function isHarnessControlPlaneChange(filePath) { return ( isWithin(filePath, "scripts/harness") || filePath === "playwright.config.ts" || filePath === "vitest.config.ts" || isWithin(filePath, "tests/e2e/support") || filePath === "AGENTS.md" || filePath === "CONTRIBUTING.md" || filePath === "README.md" || filePath === "docs/harness-engineering.md" || filePath === "docs/orchestration.md" || isWithin(filePath, "docs/exec-plans") ); } function isReleaseRiskChange(filePath) { return ( filePath === "package.json" || filePath === "docs/releasing.md" || filePath === "docs/registry.md" || filePath === "scripts/release-metadata.mjs" || filePath === "scripts/build-registry.mjs" || filePath === "scripts/registry-install.mjs" || filePath === "packages/ui/package.json" || filePath === "packages/tokens/package.json" || isWithin(filePath, ".github/workflows") || isWithin(filePath, "tests/package-consumer") || isWithin(filePath, "tests/registry") ); } function isStaticSurfaceChange(filePath) { return ( isPackageContractChange(filePath) || isDocsSurfaceChange(filePath) || isWithin(filePath, "tests") || isWithin(filePath, "scripts") || isWithin(filePath, ".github/workflows") || isExactMatch(filePath, rootStaticFiles) || isHarnessControlPlaneChange(filePath) || isReleaseRiskChange(filePath) ); } function isLikelyCodeChange(filePath) { return /\.(?:cjs|css|cts|js|json|jsx|mdx|mjs|mts|scss|ts|tsx|yaml|yml)$/.test(filePath); } function runGitCommand(args) { return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }) .split("\n") .map((line) => line.trim()) .filter(Boolean) .map((line) => normalizeFilePath(line)); } function getWorkingTreeChangedFiles(runGitCommandFn = runGitCommand) { const files = new Set([ ...runGitCommandFn(["diff", "--name-only", "--diff-filter=ACMR", "HEAD"]), ...runGitCommandFn(["diff", "--cached", "--name-only", "--diff-filter=ACMR", "HEAD"]), ...runGitCommandFn(["ls-files", "--others", "--exclude-standard"]) ]); return [...files].sort(); } export function getChangedFiles(options = {}) { const changedFiles = options.changedFiles ?? []; const from = options.from ?? null; const to = options.to ?? null; const runGitCommandFn = options.runGitCommandFn ?? runGitCommand; if (changedFiles.length > 0) { return [...new Set(changedFiles.map((filePath) => normalizeFilePath(filePath)))].sort(); } if (from || to) { const toRef = to ?? "HEAD"; if (from && zeroShaPattern.test(from)) { return [ ...new Set([ ...runGitCommandFn(["ls-files"]), ...runGitCommandFn(["ls-files", "--others", "--exclude-standard"]) ]) ].sort(); } if (!from) { throw new Error("Expected --from when selecting changed files from git refs."); } return [...runGitCommandFn(["diff", "--name-only", "--diff-filter=ACMR", `${from}...${toRef}`])].sort(); } return getWorkingTreeChangedFiles(runGitCommandFn); } function addReason(reasonMap, suiteName, filePath, reason) { const currentReasons = reasonMap.get(suiteName) ?? []; currentReasons.push({ file: filePath, reason }); reasonMap.set(suiteName, currentReasons); } export function selectSuitesForChangedFiles(changedFiles) { const normalizedFiles = [...new Set(changedFiles.map((filePath) => normalizeFilePath(filePath)))].sort(); const selectedSuites = new Set(); const reasons = new Map(); const unmatchedFiles = []; for (const filePath of normalizedFiles) { let matched = false; if (isReleaseRiskChange(filePath)) { selectedSuites.add("release"); addReason(reasons, "release", filePath, "Release or consumer control plane changed."); matched = true; } if (isHarnessControlPlaneChange(filePath)) { selectedSuites.add("pr"); addReason(reasons, "pr", filePath, "Harness control plane changed."); matched = true; } if (isPackageContractChange(filePath)) { selectedSuites.add("component"); selectedSuites.add("docs"); selectedSuites.add("a11y"); selectedSuites.add("consumers"); addReason(reasons, "component", filePath, "Package contract or source changed."); addReason(reasons, "docs", filePath, "Package changes affect the Storybook review surface."); addReason(reasons, "a11y", filePath, "Package changes can alter Storybook accessibility results."); addReason(reasons, "consumers", filePath, "Package changes affect downstream consumers."); matched = true; if (isPackageSourceChange(filePath)) { selectedSuites.add("docs-smoke"); addReason(reasons, "docs-smoke", filePath, "Interactive package source changed."); } } if (isDocsSurfaceChange(filePath)) { selectedSuites.add("static"); selectedSuites.add("docs"); selectedSuites.add("a11y"); addReason(reasons, "static", filePath, "Docs or Storybook source changed."); addReason(reasons, "docs", filePath, "Docs or Storybook source changed."); addReason(reasons, "a11y", filePath, "Docs or Storybook source changed."); matched = true; if (isDocsInteractiveChange(filePath)) { selectedSuites.add("docs-smoke"); addReason(reasons, "docs-smoke", filePath, "Storybook stories or smoke harness changed."); } } if (isConsumerSurfaceChange(filePath)) { selectedSuites.add("static"); selectedSuites.add("consumers"); addReason(reasons, "static", filePath, "Registry or consumer validation surface changed."); addReason(reasons, "consumers", filePath, "Registry or consumer validation surface changed."); matched = true; } if (isStaticSurfaceChange(filePath)) { selectedSuites.add("static"); addReason(reasons, "static", filePath, "Static validation surface changed."); matched = true; } if (!matched) { unmatchedFiles.push(filePath); } } if (selectedSuites.has("component")) { selectedSuites.delete("static"); reasons.delete("static"); } if (selectedSuites.has("pr")) { for (const suiteName of prSupersededSuites) { selectedSuites.delete(suiteName); reasons.delete(suiteName); } } if (selectedSuites.has("release")) { for (const suiteName of releaseSupersededSuites) { selectedSuites.delete(suiteName); reasons.delete(suiteName); } } if (selectedSuites.size === 0 && unmatchedFiles.some((filePath) => isLikelyCodeChange(filePath))) { selectedSuites.add("static"); for (const filePath of unmatchedFiles) { if (isLikelyCodeChange(filePath)) { addReason(reasons, "static", filePath, "Fallback static validation for unmatched code change."); } } } return { changedFiles: normalizedFiles, reasons: Object.fromEntries( [...reasons.entries()].map(([suiteName, suiteReasons]) => [suiteName, suiteReasons]) ), suites: suiteOrder.filter((suiteName) => selectedSuites.has(suiteName)), unmatchedFiles }; } export function getCommandsForSuites(suiteNames) { const seenCommands = new Set(); const commands = []; for (const suiteName of suiteNames) { const suite = suiteDefinitions[suiteName]; if (!suite) { throw new Error(`Unknown suite "${suiteName}".`); } for (const command of suite.commands) { const key = command.args.join("\u0000"); if (seenCommands.has(key)) { continue; } seenCommands.add(key); commands.push({ ...command, suite: suiteName }); } } return commands; } export function ensureKnownSuite(name) { if (name === "changed") { return; } if (!(name in suiteDefinitions)) { const knownSuites = [...suiteOrder, "changed"].join(", "); throw new Error(`Unknown suite "${name}". Expected one of: ${knownSuites}.`); } } export function writeHarnessArtifact(name, payload, outputDir = artifactsDir) { fs.mkdirSync(outputDir, { recursive: true }); const outputPath = path.join(outputDir, `${name}.json`); fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); return outputPath; } export function formatSuiteListing() { return [...suiteOrder, "changed"] .map((suiteName) => { if (suiteName === "changed") { return { name: suiteName, description: "Select suites from git diff or working tree changes before validating." }; } return { name: suiteName, description: suiteDefinitions[suiteName].description }; }); }