diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index a6ba37fc2..5aec02bd0 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -2191,7 +2191,9 @@ artifact-selection behaviour. dependency audits may retry only explicit registry transport or endpoint failures, with a bounded request timeout and attempt count; an advisory finding must fail immediately, and exhausted service failures must remain a - failed check. In `.github/workflows/build-and-test.yml`, the aggregate audit + failed check. If npm emits an advisory report together with a transport + marker, the advisory result takes precedence and must not be retried. In + `.github/workflows/build-and-test.yml`, the aggregate audit verdict must run after formatting, lint, tests, type-checking, the production build, and the bundle-size check so an unavailable advisory service cannot suppress those results. The preceding `npm ci` must disable its duplicate diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 323adcf3c..5ff1be820 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -3680,6 +3680,7 @@ func TestFrontendDependencySecurityAuditsAreRequired(t *testing.T) { `readonly max_attempts=3`, `readonly fetch_timeout_ms=60000`, `npm audit --fetch-timeout="${fetch_timeout_ms}" "$@"`, + `grep -Fq '# npm audit report'`, `if ! grep -Eiq`, `if (( attempt == max_attempts )); then`, ) diff --git a/scripts/npm-audit-retry.sh b/scripts/npm-audit-retry.sh index 94c348768..343848735 100755 --- a/scripts/npm-audit-retry.sh +++ b/scripts/npm-audit-retry.sh @@ -20,6 +20,12 @@ while (( attempt <= max_attempts )); do exit 0 fi + # An advisory result takes precedence if npm also emits a transport warning. + # Never turn a real vulnerability finding into a retryable service failure. + if grep -Fq '# npm audit report' "${output}"; then + exit "${status}" + fi + # npm audit uses a registry POST. npm's fetch retries cover idempotent reads, # so a transient timeout or audit endpoint error otherwise consumes the full # default five-minute timeout and fails the check without another attempt. diff --git a/scripts/tests/test_npm_audit_retry.py b/scripts/tests/test_npm_audit_retry.py index 3b5a96569..c1e45c930 100755 --- a/scripts/tests/test_npm_audit_retry.py +++ b/scripts/tests/test_npm_audit_retry.py @@ -58,6 +58,13 @@ class NpmAuditRetryTest(unittest.TestCase): echo '1 high severity vulnerability' exit 1 ;; + vulnerability-and-transient) + echo '# npm audit report' + echo 'example <2.0.0' + echo '1 high severity vulnerability' + echo 'npm error code ETIMEDOUT' + exit 1 + ;; esac exit 64 """ @@ -130,6 +137,16 @@ class NpmAuditRetryTest(unittest.TestCase): self.assertIn("1 high severity vulnerability", result.stdout) self.assertNotIn("retrying", result.stderr) + def test_vulnerability_report_takes_precedence_over_transport_marker( + self, + ) -> None: + result, calls, sleeps = self.run_check("vulnerability-and-transient") + self.assertEqual(result.returncode, 1) + self.assertEqual(calls, ["audit --fetch-timeout=60000"]) + self.assertEqual(sleeps, []) + self.assertIn("1 high severity vulnerability", result.stdout) + self.assertNotIn("retrying", result.stderr) + def test_persistent_registry_failure_remains_fatal(self) -> None: result, calls, sleeps = self.run_check("transient-failure") self.assertEqual(result.returncode, 42)