diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 58ce67ae61..f91d61997d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -129,7 +129,7 @@ jobs: # Consume the full list: grep -q can close the pipe on an early # match, making printf fail with SIGPIPE under pipefail. A large # dependency change must not enter the inherited-advisory mode. - if printf '%s\n' "$changed" | grep -E '^frontend-modern/package(-lock)?\.json$|^scripts/npm-audit-retry\.sh$' >/dev/null; then + if printf '%s\n' "$changed" | grep -E '^frontend-modern/package(-lock)?\.json$|^scripts/(npm-audit-retry\.sh|npm_audit_process\.py)$' >/dev/null; then frontend_deps=true else frontend_deps=false diff --git a/scripts/dev-prepush.sh b/scripts/dev-prepush.sh index 215cbeb946..e2520fe82f 100755 --- a/scripts/dev-prepush.sh +++ b/scripts/dev-prepush.sh @@ -57,7 +57,7 @@ CHANGED_GO=$(printf '%s\n' "$CHANGED" | grep -E '\.go$' || true) # Unchanged graphs are left to CI's existing inherited-finding warning policy. # Consume the whole list rather than using grep -q: under pipefail an early # match in a large diff must not turn the producer's SIGPIPE into a false skip. -if printf '%s\n' "$CHANGED" | grep -E '^frontend-modern/package(-lock)?\.json$|^scripts/npm-audit-retry\.sh$' >/dev/null; then +if printf '%s\n' "$CHANGED" | grep -E '^frontend-modern/package(-lock)?\.json$|^scripts/(npm-audit-retry\.sh|npm_audit_process\.py)$' >/dev/null; then step "Frontend dependency audit (changed graph or audit runner)" if ! (cd frontend-modern && NPM_AUDIT_REQUIRE_RESULT=true bash "$ROOT_DIR/scripts/npm-audit-retry.sh" all); then echo "Frontend dependency audit failed; remaining source checks have not run." >&2 diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 081a29d9e8..41508f594c 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -5154,19 +5154,23 @@ func TestFrontendChangeClassificationPreservesStrictAudit(t *testing.T) { {name: "manifest", files: "frontend-modern/package.json\n", code: "true", dependencies: "true"}, {name: "lock", files: "frontend-modern/package-lock.json\n", code: "true", dependencies: "true"}, {name: "audit_runner", files: "scripts/npm-audit-retry.sh\n", code: "true", dependencies: "true"}, + {name: "audit_process", files: "scripts/npm_audit_process.py\n", code: "true", dependencies: "true"}, {name: "large_manifest_first", files: "frontend-modern/package.json\n" + docs, code: "true", dependencies: "true"}, {name: "large_lock_first", files: "frontend-modern/package-lock.json\n" + docs, code: "true", dependencies: "true"}, {name: "large_runner_first", files: "scripts/npm-audit-retry.sh\n" + docs, code: "true", dependencies: "true"}, + {name: "large_process_first", files: "scripts/npm_audit_process.py\n" + docs, code: "true", dependencies: "true"}, + {name: "large_process_last", files: docs + "scripts/npm_audit_process.py\n", code: "true", dependencies: "true"}, {name: "large_lock_last", files: docs + "frontend-modern/package-lock.json\n", code: "true", dependencies: "true"}, {name: "large_docs_only", files: docs, code: "false", dependencies: "false"}, {name: "large_unrelated_code", goCompile: true, files: docs + "internal/config/config.go\n", code: "true", dependencies: "false"}, - {name: "exact_path_lookalikes", files: "frontend-modern/package.json.extra\nother/frontend-modern/package-lock.json\nscripts/npm-audit-retry.sh.extra\n", code: "true", dependencies: "false"}, + {name: "exact_path_lookalikes", files: "frontend-modern/package.json.extra\nother/frontend-modern/package-lock.json\nscripts/npm-audit-retry.sh.extra\nscripts/npm_audit_process.py.extra\n", code: "true", dependencies: "false"}, {name: "pull_request_docs", files: "docs/FAQ.md\n", event: "pull_request", code: "false", dependencies: "false"}, {name: "zero_push_base", goCompile: true, base: strings.Repeat("0", 40), code: "true", dependencies: "true"}, {name: "missing_base", goCompile: true, missingBase: true, code: "true", dependencies: "true"}, {name: "manual_dispatch", goCompile: true, event: "workflow_dispatch", base: "empty", code: "true", dependencies: "true"}, {name: "failed_diff", diffStatus: 42}, {name: "large_changed_graph_critical_blocks", files: "frontend-modern/package-lock.json\n" + docs, code: "true", dependencies: "true", audit: true, auditStatus: 1}, + {name: "process_critical_blocks", files: "scripts/npm_audit_process.py\n", code: "true", dependencies: "true", audit: true, auditStatus: 1}, {name: "unchanged_graph_critical_warns", goCompile: true, files: "internal/config/config.go\n", code: "true", dependencies: "false", audit: true, auditStatus: 0}, } for _, tc := range cases { diff --git a/scripts/tests/test_dev_prepush_frontend.py b/scripts/tests/test_dev_prepush_frontend.py index b1a3ba2bcc..04938f902c 100644 --- a/scripts/tests/test_dev_prepush_frontend.py +++ b/scripts/tests/test_dev_prepush_frontend.py @@ -16,6 +16,7 @@ import yaml ROOT = Path(__file__).resolve().parents[2] PREPUSH = ROOT / "scripts/dev-prepush.sh" AUDIT = ROOT / "scripts/npm-audit-retry.sh" +AUDIT_PROCESS = ROOT / "scripts/npm_audit_process.py" class DevPrepushFixture(unittest.TestCase): @@ -53,6 +54,7 @@ class DevPrepushFixture(unittest.TestCase): ) self.write("scripts/dev-prepush.sh", PREPUSH.read_text()) self.write("scripts/npm-audit-retry.sh", AUDIT.read_text()) + self.write("scripts/npm_audit_process.py", AUDIT_PROCESS.read_text()) self.write("frontend-modern/package.json", '{"private":true}\n') self.write("frontend-modern/package-lock.json", '{"lockfileVersion":3}\n') self.write("frontend-modern/src/example.ts", "export const value = 1;\n") @@ -170,6 +172,17 @@ class DevPrepushFrontendTest(DevPrepushFixture): self.env["PREPUSH_AUDIT_MODE"] = "unavailable" self.assert_refused_before_source_checks(*self.run_gate(), reads=2) + def test_changed_audit_process_requires_a_result(self): + self.change("scripts/npm_audit_process.py") + self.env["PREPUSH_AUDIT_MODE"] = "unavailable" + self.assert_refused_before_source_checks(*self.run_gate(), reads=2) + + def test_changed_audit_process_cannot_relax_positive_findings(self): + self.change("scripts/npm_audit_process.py") + self.env["PREPUSH_AUDIT_MODE"] = "critical" + self.env["NPM_AUDIT_REQUIRE_RESULT"] = "false" + self.assert_refused_before_source_checks(*self.run_gate()) + def test_failed_zero_report_is_not_a_passing_local_gate(self): self.change("frontend-modern/package-lock.json") self.env["PREPUSH_AUDIT_MODE"] = "zero-failure" @@ -251,16 +264,18 @@ class DevPrepushFrontendTest(DevPrepushFixture): self.assertEqual(guards, []) def test_audit_path_lookalike_does_not_query(self): - self.change("scripts/npm-audit-retry.sh.extra") - result, calls, guards = self.run_gate(dependencies=False) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - self.assertEqual(calls, []) - self.assertEqual(len(guards), 1) + for path in ("scripts/npm-audit-retry.sh.extra", "scripts/npm_audit_process.py.extra"): + with self.subTest(path=path): + self.change(path) + result, calls, guards = self.run_gate(dependencies=False) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(calls, []) + self.assertEqual(len(guards), 1 if path.endswith(".sh.extra") else 2) def test_local_strict_paths_match_ci(self): workflow = yaml.safe_load((ROOT / ".github/workflows/build-and-test.yml").read_text()) classification = next(step for step in workflow["jobs"]["changes"]["steps"] if step.get("id") == "filter")["run"] - paths = r"^frontend-modern/package(-lock)?\.json$|^scripts/npm-audit-retry\.sh$" + paths = r"^frontend-modern/package(-lock)?\.json$|^scripts/(npm-audit-retry\.sh|npm_audit_process\.py)$" self.assertIn(paths, classification) self.assertIn(paths, PREPUSH.read_text())