diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index ffc1d7f93a..e10cc6d730 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1998,6 +1998,8 @@ Windows lifecycle controls or macOS installer and logging checks on native CI. 105. `scripts/dependency_advisory_watch.py` 106. `tests/integration/scripts/settings-scroll-audit.mjs` 107. `tests/integration/scripts/managed-local-backend.mjs` +108. `scripts/browser_os_packages.py` +109. `scripts/playwright-apt.conf` `scripts/installtests/run_unix_contracts.py` is the canonical exhaustive native Unix installer execution runner. Its dedicated verification files are `scripts/installtests/native_unix_contract_runner_test.go` and `scripts/tests/test_native_installer_contract_runner.py`. Script-smoke discovery and the native Go wrapper execute that same Python control suite, so phase inventory, failure propagation and real compiled-Go exit behavior retain one verification source. @@ -8323,9 +8325,15 @@ packages for Chromium and WebKit from the runner's Azure mirror. From minute ran past their 10 and 30 minute budgets before any test started. The jobs now restore the newest saved package set with `actions/cache/restore` (v6.1.0, pinned by SHA) and admit it through `scripts/browser_os_packages.py`: -after a bounded `apt-get update`, a cached `.deb` reaches apt's archive +after a bounded `apt-get update` scoped to the runner's existing Ubuntu +Deb822 source definition, a cached `.deb` reaches apt's archive directory only when the SHA256 of the private copy it installs equals a signed index value for the package, version, and architecture named by its filename. +The original Ubuntu mirror and `Signed-By` settings remain authoritative. +An absent, unreadable or empty distribution definition fails acquisition. +Unrelated vendor feeds do not participate in browser OS acquisition. Failed +Ubuntu index updates still fail, and TLS, signature and archive-hash validation +remain enforced. This policy changes no host repository definition files. That check is ours, because apt reuses an archive-directory file after checking only its size. After a successful install, and before any journey runs, the helper rebuilds diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index fada460c70..b1193fc2f1 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -4668,6 +4668,7 @@ "pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py", "pulse-pro:scripts/validate_paid_runtime_distribution.py", "scripts/backfill-release-assets.sh", + "scripts/browser_os_packages.py", "scripts/build-release-binaries.sh", "scripts/build-release.sh", "scripts/build-secure-runtime-qualification.sh", @@ -4697,6 +4698,7 @@ "scripts/installtests/testdata/secure_runtime_docker_fixture.go", "scripts/lib/hot-dev-auth.sh", "scripts/lib/hot-dev-runtime.sh", + "scripts/playwright-apt.conf", "scripts/prepare-release-container-context.sh", "scripts/pulse-auto-update.sh", "scripts/release-preflight-worker.sh", @@ -5357,6 +5359,21 @@ "scripts/tests/test_go_mod_download.py" ] }, + { + "id": "browser-os-acquisition", + "label": "signed Ubuntu browser dependency acquisition proof", + "match_prefixes": [], + "match_files": [ + "scripts/browser_os_packages.py", + "scripts/playwright-apt.conf" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "scripts/tests/test_browser_os_packages.py", + "scripts/tests/test_playwright_acquisition.py" + ] + }, { "id": "e2e-image-distribution", "label": "same-run source-bound E2E image distribution proof", diff --git a/scripts/playwright-apt.conf b/scripts/playwright-apt.conf index f5625baa2b..1da485d658 100644 --- a/scripts/playwright-apt.conf +++ b/scripts/playwright-apt.conf @@ -1,5 +1,10 @@ -// Bound apt transport inside disposable GitHub E2E runners. Do not replace -// repositories, disable TLS/signatures or accept an incomplete index as success. +// Browser OS dependencies come from the hosted Ubuntu runner's existing +// distribution definition. Preserve its mirrors and Signed-By settings while +// keeping unrelated vendor feeds outside this acquisition. Do not disable +// TLS/signatures or accept an incomplete Ubuntu index as success. +Dir::Etc::sourcelist "/etc/apt/sources.list.d/ubuntu.sources"; +Dir::Etc::sourceparts "-"; +APT::Update::Pre-Invoke:: "test -r /etc/apt/sources.list.d/ubuntu.sources && test -s /etc/apt/sources.list.d/ubuntu.sources"; Acquire::http::Timeout "30"; Acquire::https::Timeout "30"; Acquire::Retries "2"; diff --git a/scripts/release_control/dev_runtime_governance_test.py b/scripts/release_control/dev_runtime_governance_test.py index 2365160578..2efc8ab5f7 100644 --- a/scripts/release_control/dev_runtime_governance_test.py +++ b/scripts/release_control/dev_runtime_governance_test.py @@ -100,7 +100,6 @@ class DevRuntimeGovernanceTest(unittest.TestCase): "tests/integration/README.md", "tests/integration/QUICK_START.md", "tests/integration/playwright.config.ts", - "tests/integration/tests/helpers.ts", "tests/integration/tests/runtime-defaults.ts", ] ) @@ -128,6 +127,34 @@ class DevRuntimeGovernanceTest(unittest.TestCase): match["verification_requirement"]["exact_files"], ) + def test_lookup_maps_browser_helpers_to_scoped_fixture_proof(self) -> None: + result = lookup_paths(["tests/integration/tests/helpers.ts"]) + self.assertEqual(result["unowned_runtime_files"], []) + match = result["files"][0]["matches"][0] + self.assertEqual(match["subsystem"], "deployment-installability") + proof = match["verification_requirement"] + self.assertEqual(proof["id"], "browser-fixture-helpers") + self.assertIn("tests/integration/scripts/fixture-auth.test.mjs", proof["exact_files"]) + self.assertIn("tests/integration/scripts/organization-fixtures.test.mjs", proof["exact_files"]) + self.assertFalse(proof["allow_same_subsystem_tests"]) + + def test_lookup_maps_browser_acquisition_to_signed_package_proof(self) -> None: + result = lookup_paths(["scripts/playwright-apt.conf", "scripts/browser_os_packages.py"]) + self.assertEqual(result["unowned_runtime_files"], []) + for entry in result["files"]: + self.assertEqual(entry["classification"], "runtime") + self.assertEqual(len(entry["matches"]), 1) + match = entry["matches"][0] + self.assertEqual(match["subsystem"], "deployment-installability") + proof = match["verification_requirement"] + self.assertEqual(proof["id"], "browser-os-acquisition") + self.assertEqual(proof["exact_files"], [ + "scripts/tests/test_browser_os_packages.py", + "scripts/tests/test_playwright_acquisition.py", + ]) + self.assertFalse(proof["allow_same_subsystem_tests"]) + self.assertEqual(proof["test_prefixes"], []) + def test_lookup_maps_release_promotion_workflows_to_installability(self) -> None: result = lookup_paths( [ diff --git a/scripts/tests/test_playwright_acquisition.py b/scripts/tests/test_playwright_acquisition.py index 7804229cd1..26a4735758 100644 --- a/scripts/tests/test_playwright_acquisition.py +++ b/scripts/tests/test_playwright_acquisition.py @@ -21,6 +21,7 @@ INSTALL_POLICY = ( # successful install is cached even when later journeys fail. CACHE_PIN = "55cc8345863c7cc4c66a329aec7e433d2d1c52a9" CACHE_DIR = '"$HOME/.cache/pulse-browser-os-packages"' +UBUNTU_SOURCES = "/etc/apt/sources.list.d/ubuntu.sources" def job_block(workflow, job): @@ -158,7 +159,7 @@ class PlaywrightAcquisitionTest(unittest.TestCase): self.assertTrue(result.failures) self.assertFalse(result.errors) - def test_real_apt_parser_loads_only_the_four_expected_controls(self): + def test_real_apt_parser_loads_only_the_expected_controls(self): with tempfile.TemporaryDirectory() as temp: config = Path(temp) / "apt.conf" config.write_text( @@ -169,6 +170,9 @@ class PlaywrightAcquisitionTest(unittest.TestCase): capture_output=True, text=True, timeout=10, check=True, ).stdout expected = { + "Dir::Etc::sourcelist": UBUNTU_SOURCES, + "Dir::Etc::sourceparts": "-", + "APT::Update::Pre-Invoke::": f"test -r {UBUNTU_SOURCES} && test -s {UBUNTU_SOURCES}", "Acquire::http::Timeout": "30", "Acquire::https::Timeout": "30", "Acquire::Retries": "2", @@ -181,6 +185,72 @@ class PlaywrightAcquisitionTest(unittest.TestCase): self.assertNotIn("AllowUnauthenticated", parsed) self.assertNotIn("Verify-Peer", parsed) + def test_real_apt_preserves_ubuntu_sources_and_excludes_unrelated_feeds(self): + # Print acquisition URIs without contacting a repository or touching + # host state. Both configurations retain the same signed Ubuntu source; + # the adverse control also admits an unrelated, unreachable vendor feed. + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + parts = root / "sources.list.d" + parts.mkdir() + ubuntu = parts / "ubuntu.sources" + definition = ( + "Types: deb\nURIs: http://127.0.0.1:1/ubuntu/\n" + "Suites: noble\nComponents: main\nArchitectures: amd64\n" + "Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n" + ) + ubuntu.write_text(definition) + (parts / "unrelated-vendor.list").write_text( + "deb http://127.0.0.1:1/vendor/ stable main\n" + ) + (root / "lists/partial").mkdir(parents=True) + (root / "cache/archives/partial").mkdir(parents=True) + prefix = ( + 'Dir::Etc::parts "-";\nDir::Etc::main "-";\n' + f'Dir::Etc::sourcelist "-";\nDir::Etc::sourceparts "{parts}";\n' + f'Dir::State::lists "{root / "lists"}";\n' + f'Dir::Cache "{root / "cache"}";\n' + 'Dir::State::status "/dev/null";\nDebug::NoLocking "true";\n' + ) + policy = POLICY.read_text().replace(UBUNTU_SOURCES, str(ubuntu)) + config = root / "apt.conf" + for scoped in (True, False): + selected = policy + if not scoped: + selected = re.sub(r'^Dir::Etc::source(?:list|parts) .*;\n', '', selected, flags=re.M) + config.write_text(prefix + selected) + result = subprocess.run( + ["apt-get", "update", "--print-uris"], + env={**os.environ, "APT_CONFIG": str(config), "LC_ALL": "C"}, + capture_output=True, text=True, timeout=10, check=True, + ) + with self.subTest(scoped=scoped): + self.assertIn("http://127.0.0.1:1/ubuntu/", result.stdout) + self.assertEqual("http://127.0.0.1:1/vendor/" in result.stdout, not scoped) + self.assertEqual(ubuntu.read_text(), definition) + + def test_missing_ubuntu_definition_fails_before_acquisition(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + (root / "lists/partial").mkdir(parents=True) + (root / "cache/archives/partial").mkdir(parents=True) + missing = root / "missing.sources" + config = root / "apt.conf" + config.write_text( + 'Dir::Etc::parts "-";\nDir::Etc::main "-";\n' + f'Dir::State::lists "{root / "lists"}";\n' + f'Dir::Cache "{root / "cache"}";\n' + 'Dir::State::status "/dev/null";\nDebug::NoLocking "true";\n' + + POLICY.read_text().replace(UBUNTU_SOURCES, str(missing)) + ) + result = subprocess.run( + ["apt-get", "update"], + env={**os.environ, "APT_CONFIG": str(config), "LC_ALL": "C"}, + capture_output=True, text=True, timeout=10, + ) + self.assertEqual(result.returncode, 100, result.stdout + result.stderr) + self.assertIn("Pre-Invoke", result.stderr) + def test_incomplete_index_is_a_real_apt_failure_not_a_warning_pass(self): # An unlistening reserved loopback port gives a definite transport # failure. No external request, privileged apt operation, host list, @@ -201,10 +271,12 @@ class PlaywrightAcquisitionTest(unittest.TestCase): 'Dir::State::status "/dev/null";\nDebug::NoLocking "true";\n' ) for strict, expected_exit in ((True, 100), (False, 0)): - policy = POLICY.read_text() + policy = POLICY.read_text().replace(UBUNTU_SOURCES, str(sources)) if not strict: policy = policy.replace('APT::Update::Error-Mode "any";', "") - config.write_text(prefix + policy) + # The controlled source overrides only the distribution path. + # Keep this a private loopback failure after policy is loaded. + config.write_text(policy + prefix) result = subprocess.run( ["apt-get", "update"], env={**os.environ, "APT_CONFIG": str(config), "LC_ALL": "C"},