diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 89e7110d7..dbcd4eacf 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -2145,6 +2145,17 @@ artifact-selection behaviour. secrets, and seed hosted approvals through a single explicit tenant runtime restart when a release proof needs transactionally visible approval state. + Browser execution must invoke the npm-ci-installed + `/work/node_modules/@playwright/test/cli.js` directly with Node inside the + selected Playwright image. Missing or inaccessible mounted dependencies + must fail admission rather than trigger an npx registry download of a + substitute runner. Preserve the existing container UID/GID, mount, image + version selection and exit status. The executed-shell fixture in + `scripts/release_control/internal/release_preflight_test.py` verifies the + runner path, argument quoting, container identity and failure propagation + with mocked Docker; it is not browser qualification or proof that the + retained spawn-sh EACCES failure has been repaired. + ## Forbidden Paths 1. Leaving deployment bootstrap, installer, or update-runtime files unowned under broad monitoring or generic API ownership diff --git a/scripts/release-preflight-worker.sh b/scripts/release-preflight-worker.sh index ed9166e4f..19a859e96 100755 --- a/scripts/release-preflight-worker.sh +++ b/scripts/release-preflight-worker.sh @@ -257,6 +257,9 @@ else --tag pulse:test \ . fi +# Use only the npm-ci-installed runner. npx may fetch a different version when +# the mounted dependency tree is missing or inaccessible, masking the actual +# admission failure and breaking parity with the selected browser image. run_playwright() { docker run --rm \ --network host \ @@ -269,7 +272,7 @@ run_playwright() { --volume "$REPOSITORY_DIR/tests/integration:/work" \ --workdir /work \ "$PLAYWRIGHT_IMAGE" \ - npx playwright test "$@" + node /work/node_modules/@playwright/test/cli.js test "$@" } run_rehearsal_smoke() { diff --git a/scripts/release_control/internal/release_preflight_test.py b/scripts/release_control/internal/release_preflight_test.py index 0c2cc4950..d8ef5ea87 100755 --- a/scripts/release_control/internal/release_preflight_test.py +++ b/scripts/release_control/internal/release_preflight_test.py @@ -264,6 +264,39 @@ class ReleasePreflightTest(unittest.TestCase): self.assertIn('--env "PLAYWRIGHT_BASE_URL=${PULSE_E2E_BASE_URL}"', worker) self.assertNotIn("localhost:7655", worker) + def test_browser_uses_installed_runner_and_preserves_failure(self) -> None: + worker = (ROOT / "scripts/release-preflight-worker.sh").read_text() + function = re.search( + r"run_playwright\(\) \{.*?\n\}", worker, flags=re.DOTALL + ).group(0) + # Exercise the real shell function without starting Docker or a browser. + # A failed container must propagate, not become a successful admission. + for status in (0, 13): + with self.subTest(status=status): + result = subprocess.run( + ["bash", "-c", """ +docker() { printf '%s\n' "$@"; return """ + str(status) + """; } +REPOSITORY_DIR='/tmp/worker with spaces/repo' +PLAYWRIGHT_IMAGE='mcr.microsoft.com/playwright:v1.61.1-noble' +PULSE_E2E_BASE_URL='http://localhost:27655' +""" + function + """ +run_playwright 'tests/a test.spec.ts' --project=chromium +"""], + text=True, capture_output=True, check=False, + ) + self.assertEqual(result.returncode, status, result.stderr) + args = result.stdout.splitlines() + self.assertEqual(args[-5:], [ + "node", "/work/node_modules/@playwright/test/cli.js", + "test", "tests/a test.spec.ts", "--project=chromium", + ]) + self.assertNotIn("npx", args) + self.assertEqual(args[args.index("--volume") + 1], + "/tmp/worker with spaces/repo/tests/integration:/work") + self.assertEqual(args[args.index("--user") + 1], + f"{os.getuid()}:{os.getgid()}") + self.assertIn("mcr.microsoft.com/playwright:v1.61.1-noble", args) + def test_worker_serializes_resource_intensive_test_suites(self) -> None: worker = (ROOT / "scripts/release-preflight-worker.sh").read_text() scheduling_block = re.search(