import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { OVERRIDE_LABEL, classifyChangedFiles, isTerraformInfrastructurePath, mayAccompanyTerraform, } from "./check-terraform-isolation.mjs"; const SCRIPT = path.join( path.dirname(fileURLToPath(import.meta.url)), "check-terraform-isolation.mjs", ); function runGate(files, env = {}) { return spawnSync(process.execPath, [SCRIPT, "--stdin"], { input: `${files.join("\n")}\n`, encoding: "utf8", env: { ...process.env, TERRAFORM_ISOLATION_OVERRIDE: "", ...env }, }); } test("terraform tree, docs, and terraform tooling may accompany a Terraform change", () => { for (const file of [ "terraform/live/dev/main.tf", "terraform/live/modules/environment-owned/main.tf", "terraform/README.md", "README.md", "docs/adr/0003-terraform.md", "scripts/check-terraform-import-plan.py", "scripts/terraform_import_plan_resources.py", "scripts/test-terraform-import-plan-check.py", "scripts/terraform-validate.mjs", "scripts/check-terraform-isolation.mjs", ]) { assert.equal(mayAccompanyTerraform(file), true, file); } }); test("application, workflow, CDK, and dependency files count as application changes", () => { for (const file of [ "src/App.tsx", "public/favicon.ico", "index.html", "package.json", "package-lock.json", ".env.production", "vite.config.ts", ".github/workflows/deploy.yml", "infra/cdk/lib/frontend-stack.ts", "scripts/deploy-web.sh", "scripts/governance-check.mjs", "e2e/login.spec.ts", ]) { assert.equal(mayAccompanyTerraform(file), false, file); } }); test("terraform-only and application-only changes are not mixed", () => { assert.equal( classifyChangedFiles(["terraform/live/dev/main.tf", "terraform/README.md"]).mixed, false, ); assert.equal( classifyChangedFiles(["src/App.tsx", ".github/workflows/deploy.yml", "README.md"]).mixed, false, ); assert.equal(classifyChangedFiles([]).mixed, false); }); test("terraform documentation does not mix with application or workflow changes", () => { assert.equal(isTerraformInfrastructurePath("terraform/README.md"), false); assert.equal(isTerraformInfrastructurePath("terraform/live/dev/main.tf"), true); assert.equal( classifyChangedFiles(["terraform/README.md", ".github/workflows/ci.yaml"]).mixed, false, ); const docsOnly = runGate(["terraform/README.md", ".github/workflows/ci.yaml"]); assert.equal(docsOnly.status, 0, docsOnly.stdout + docsOnly.stderr); assert.match(docsOnly.stdout, /PASS/); }); test("terraform plus application is mixed and lists the offending files", () => { const result = classifyChangedFiles([ "terraform/live/dev/main.tf", "src/App.tsx", "README.md", " ", "src/App.tsx", ]); assert.equal(result.mixed, true); assert.deepEqual(result.terraform, ["terraform/live/dev/main.tf"]); assert.deepEqual(result.application, ["src/App.tsx"]); }); test("CLI exits 1 on a mixed change and 0 when isolated", () => { const mixed = runGate(["terraform/live/dev/main.tf", "src/App.tsx"]); assert.equal(mixed.status, 1, mixed.stdout + mixed.stderr); assert.match(mixed.stdout, /FAIL/); assert.match(mixed.stdout, /src\/App\.tsx/); const isolated = runGate(["terraform/live/dev/main.tf", "terraform/README.md"]); assert.equal(isolated.status, 0, isolated.stdout + isolated.stderr); assert.match(isolated.stdout, /PASS/); }); test("CLI override downgrades a mixed change to a warning that names the label", () => { const result = runGate(["terraform/live/dev/main.tf", "src/App.tsx"], { TERRAFORM_ISOLATION_OVERRIDE: "true", }); assert.equal(result.status, 0, result.stdout + result.stderr); assert.match(result.stdout, /WARNING/); assert.match(result.stdout, new RegExp(OVERRIDE_LABEL)); const notTrue = runGate(["terraform/live/dev/main.tf", "src/App.tsx"], { TERRAFORM_ISOLATION_OVERRIDE: "yes", }); assert.equal(notTrue.status, 1); }); test("removing the override fails a mixed change that was previously green", () => { const files = ["terraform/live/dev/main.tf", ".github/workflows/deploy.yml"]; const previouslyGreen = runGate(files, { TERRAFORM_ISOLATION_OVERRIDE: "true", }); assert.equal(previouslyGreen.status, 0, previouslyGreen.stdout + previouslyGreen.stderr); assert.match(previouslyGreen.stdout, /WARNING/); // CI sets TERRAFORM_ISOLATION_OVERRIDE from contains(...labels), which is // the string "false" after the label is removed. A stale green check must // not survive that. const afterLabelRemoved = runGate(files, { TERRAFORM_ISOLATION_OVERRIDE: "false", }); assert.equal(afterLabelRemoved.status, 1, afterLabelRemoved.stdout + afterLabelRemoved.stderr); assert.match(afterLabelRemoved.stdout, /FAIL/); assert.match(afterLabelRemoved.stdout, /deploy\.yml/); }); test("CLI refuses to run without a base ref or --stdin", () => { const result = spawnSync(process.execPath, [SCRIPT], { encoding: "utf8" }); assert.notEqual(result.status, 0); }); test("isolation workflow re-evaluates on labeled and unlabeled without rerunning Frontend checks", () => { const workflows = path.join( path.dirname(fileURLToPath(import.meta.url)), "..", ".github/workflows", ); const ciYaml = readFileSync(path.join(workflows, "ci.yaml"), "utf8"); const isolationYaml = readFileSync(path.join(workflows, "terraform-isolation.yaml"), "utf8"); for (const eventType of ["opened", "synchronize", "reopened", "labeled", "unlabeled"]) { assert.match(isolationYaml, new RegExp(`^ {6}- ${eventType}$`, "m"), eventType); } assert.doesNotMatch(ciYaml, /^ {6}- labeled$/m); assert.doesNotMatch(ciYaml, /^ {6}- unlabeled$/m); assert.doesNotMatch(ciYaml, /^ {2}terraform-isolation:\n/m); assert.doesNotMatch(ciYaml, /github\.event\.action != 'labeled'/); assert.match(isolationYaml, /^ {2}terraform-isolation:\n/m); assert.match(isolationYaml, /name: Terraform and application changes are isolated/); assert.doesNotMatch(isolationYaml, /github\.event\.action != 'labeled'/); });