From 9213a6d75a6ced2bd9c937101be5c0a422dc1cdd Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:40:42 +0100 Subject: [PATCH] Keep publication checks independent of selector and fixture formatting Accept a stable exact Node pin only within the governed major instead of comparing its entire string with the major selector. Keep all other lines, ranges and unresolved selectors rejected. This repairs the deterministic required-check failure on the published-RC browser workflow without removing its exact Node pin. Give fake npm audit tests their own default timing controls and exercise poisoned caller environments while preserving explicit watchdog tests. No production audit verdict, dependency graph, benchmark threshold or release gate changes. Change-source: pulse-maintainer --- .../subsystems/deployment-installability.md | 15 ++++++- .../container_dependency_contract_test.go | 26 +++++++++++- scripts/tests/test-npm-audit-retry.sh | 7 ++++ scripts/tests/test_npm_audit_retry.py | 41 +++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index a04ff4da33..4fca768883 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -3992,7 +3992,12 @@ Every `actions/setup-node` release, governance, security, integration, and native-agent job, the release-preflight worker, the developer container, and the release frontend stages must stay on Node.js `24`; local integration setup must reject a different major instead of treating newer non-LTS majors as -equivalent. Shipped and test Dockerfiles must pin every external base to a full +equivalent. Workflow selectors may use the governed major or an exact stable +`major.minor.patch` pin within it; ranges, previews, other majors and unresolved +selectors are not parity evidence. `TestNodeToolchainParity` checks the actual +workflows, and `TestNodeToolchainSelectors` checks both allowed forms and those +rejections without removing an exact runtime pin from a qualification job. +Shipped and test Dockerfiles must pin every external base to a full manifest-list digest, and checked-in integration Compose images must do the same. Node.js `24` is normally supported through `2028-04-30` and Alpine `3.24` through `2028-06-01`; a weekly repository check must fail once either @@ -5003,7 +5008,13 @@ advisory fails whenever the graph moves or the mode value is not exactly `false`, that an inherited advisory warns and is still named, that an unparseable or unrecognised report is never read as clean, and that neither a hung attempt nor an exhausted budget can outlive -its bound. `frontend-modern/src/security/__tests__/dependencySecurity.test.ts` +its bound. Both fake-provider suites own their default timing controls rather +than inheriting a caller's shortened or exhausted budget. Python's hostile-env +controls exercise both exact single-request advisory verdicts and the real +shell fixture, including its explicit hung-attempt and total-deadline cases. +This fixture isolation does not change the production runner, retry budget or +audit verdict, and cannot establish the cause of an earlier extra invocation. +`frontend-modern/src/security/__tests__/dependencySecurity.test.ts` pins the known safe floors for advisories remediated by commit `6ba85a185`, including DOMPurify `GHSA-55q2-fjhq-7xh7`, brace-expansion `GHSA-mh99-v99m-4gvg` and `GHSA-rgw5-rvv9-x895`, and nanoid diff --git a/scripts/installtests/container_dependency_contract_test.go b/scripts/installtests/container_dependency_contract_test.go index 9645a3396e..6c0a17c2ad 100644 --- a/scripts/installtests/container_dependency_contract_test.go +++ b/scripts/installtests/container_dependency_contract_test.go @@ -84,7 +84,7 @@ func TestNodeToolchainParity(t *testing.T) { setupCount += strings.Count(content, "actions/setup-node@") for _, match := range versionPattern.FindAllStringSubmatch(content, -1) { versionCount++ - if match[1] != nodeToolchainLine { + if !usesNodeToolchainLine(match[1], nodeToolchainLine) { t.Fatalf("%s selects Node.js %s; all workflows must use governed line %s", path, match[1], nodeToolchainLine) } } @@ -111,3 +111,27 @@ func TestNodeToolchainParity(t *testing.T) { t.Fatalf("devcontainer must use Node.js %s from a digest-pinned feature", nodeToolchainLine) } } + +// A reproducible job may pin an exact stable version within the governed +// major. Reject ranges, previews and unresolved selectors rather than treating +// a literal same-major pin as a different toolchain line. +func usesNodeToolchainLine(selector, line string) bool { + return regexp.MustCompile(`^` + regexp.QuoteMeta(line) + `(\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*))?$`).MatchString(selector) +} + +func TestNodeToolchainSelectors(t *testing.T) { + for _, selector := range []string{"24", "24.0.0", "24.20.0", "24.123.456"} { + if !usesNodeToolchainLine(selector, "24") { + t.Errorf("governed stable selector %q rejected", selector) + } + } + for _, selector := range []string{ + "23", "25", "23.20.0", "25.20.0", "240.20.0", "24.20", "24.x", "24.*", + "^24.20.0", ">=24", "latest", "node", "lts/*", "${{ matrix.node }}", + "24.20.0-rc.1", "24.20.0+build", "24.020.0", "24.20.00", "24.20.0 ", "", + } { + if usesNodeToolchainLine(selector, "24") { + t.Errorf("ungoverned or unresolved selector %q accepted", selector) + } + } +} diff --git a/scripts/tests/test-npm-audit-retry.sh b/scripts/tests/test-npm-audit-retry.sh index 165af753a7..dc8706ed0e 100755 --- a/scripts/tests/test-npm-audit-retry.sh +++ b/scripts/tests/test-npm-audit-retry.sh @@ -9,6 +9,13 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" SCRIPT="${ROOT_DIR}/scripts/npm-audit-retry.sh" +# Own the fake-provider baseline. Individual deadline controls below still +# override these values at the function call, but a CI caller's audit budget +# must not silently alter all the other canned-verdict tests. +NPM_AUDIT_ATTEMPTS=3 +NPM_AUDIT_ATTEMPT_TIMEOUT=60 +NPM_AUDIT_MAX_SECONDS=240 + WORK_DIR="$(mktemp -d)" trap 'rm -rf "${WORK_DIR}"' EXIT diff --git a/scripts/tests/test_npm_audit_retry.py b/scripts/tests/test_npm_audit_retry.py index 3bcb17c7cb..ea17e7306d 100755 --- a/scripts/tests/test_npm_audit_retry.py +++ b/scripts/tests/test_npm_audit_retry.py @@ -9,6 +9,7 @@ import subprocess import tempfile import textwrap import unittest +from unittest.mock import patch import yaml @@ -99,6 +100,8 @@ class NpmAuditRetryTest(unittest.TestCase): "FAKE_NPM_MODE": mode, "FAKE_NPM_REPORT": str(report_path), "NPM_AUDIT_ATTEMPTS": "3", + "NPM_AUDIT_ATTEMPT_TIMEOUT": "60", + "NPM_AUDIT_MAX_SECONDS": "240", "NPM_AUDIT_CMD": str(fake_npm), "NPM_AUDIT_REQUIRE_RESULT": require, "NPM_AUDIT_RETRY_DELAY": "0", @@ -119,6 +122,44 @@ class NpmAuditRetryTest(unittest.TestCase): ) return result, recorded_calls + def test_fake_audit_budget_is_not_inherited_from_the_caller(self) -> None: + # These are fixture controls, not a real advisory query. A caller's + # exhausted/short budget must not prevent the canned verdict from + # being read or change the exact one-request assertion. + with patch.dict(os.environ, { + "NPM_AUDIT_ATTEMPTS": "0", + "NPM_AUDIT_ATTEMPT_TIMEOUT": "0", + "NPM_AUDIT_MAX_SECONDS": "0", + "NPM_AUDIT_RETRY_DELAY": "10000", + "NPM_AUDIT_CMD": "/must-not-be-executed", + "NPM_AUDIT_REQUIRE_RESULT": "false", + }): + for require, status, annotation in [ + ("true", 1, "::error::"), ("false", 0, "::warning::") + ]: + with self.subTest(require=require): + result, calls = self.run_check("vulnerability", "all", require=require) + self.assertEqual(result.returncode, status, result.stdout) + self.assertEqual(calls, ["audit --json"]) + self.assertIn(annotation, result.stdout) + self.assertIn("vulnerabilities present", result.stdout) + self.assertNotIn("retrying", result.stdout) + + def test_shell_fixture_owns_its_default_and_explicit_timing_controls(self) -> None: + env = os.environ.copy() + env.update(NPM_AUDIT_ATTEMPTS="0", NPM_AUDIT_ATTEMPT_TIMEOUT="0", + NPM_AUDIT_MAX_SECONDS="0", NPM_AUDIT_RETRY_DELAY="10000", + NPM_AUDIT_CMD="/must-not-be-executed") + result = subprocess.run( + ["bash", str(ROOT / "scripts/tests/test-npm-audit-retry.sh")], + cwd=ROOT, env=env, text=True, capture_output=True, check=False, + timeout=90, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("all npm-audit-retry tests passed", result.stdout) + self.assertIn("a hung audit is stopped at the per-attempt limit", result.stdout) + self.assertIn("the wall-clock budget ends the retry sequence", result.stdout) + def test_passes_a_clean_production_audit_and_forwards_arguments(self) -> None: result, calls = self.run_check( "success", "production", "--package-lock-only"