diff --git a/.github/scripts/demo-runtime-transaction.py b/.github/scripts/demo-runtime-transaction.py index 60193e378e..5fa81dda04 100644 --- a/.github/scripts/demo-runtime-transaction.py +++ b/.github/scripts/demo-runtime-transaction.py @@ -275,6 +275,8 @@ class Host: pattern = re.compile(r"^[ \t]*" + re.escape(key) + r"=.*$", re.M) text = pattern.sub(key + "=" + value, text) if pattern.search(text) else text.rstrip("\n") + "\n" + key + "=" + value + "\n" env_file.write_text(text) + data_owner = PATHS["data"].stat() + os.chown(env_file, data_owner.st_uid, data_owner.st_gid) os.chmod(env_file, 0o600) billing = json.loads(billing_file.read_text()) if not isinstance(billing, dict) or not isinstance(billing.get("capabilities", []), list): @@ -282,6 +284,7 @@ class Host: billing["capabilities"] = sorted(set(billing.get("capabilities", []) + ["demo_fixtures"])) billing.pop("integrity", None) billing_file.write_text(json.dumps(billing)) + os.chown(billing_file, data_owner.st_uid, data_owner.st_gid) os.chmod(billing_file, 0o600) if unhealthy: for relative in DEMO_HISTORY: @@ -408,14 +411,14 @@ class Transaction: elif self.request["mode"] == "update": raise Failure("update-baseline-unhealthy") self.receipt["healthy_baseline"] = healthy_baseline + old_handlers = {sig: signal.getsignal(sig) for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)} + for sig in old_handlers: + signal.signal(sig, cancel) # The no-op is observed too; an immediate healthy sample is not success. if self.request["mode"] == "recover" and healthy_baseline and self.host.profile_matches(self.request): self.watch("forward", original_version, self.host.cursors()) self.save("healthy_noop") return 0 - old_handlers = {sig: signal.getsignal(sig) for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)} - for sig in old_handlers: - signal.signal(sig, cancel) self.save("capturing") stopped = True self.host.stop() diff --git a/.github/scripts/tests/test_demo_runtime_transaction.py b/.github/scripts/tests/test_demo_runtime_transaction.py index 36bb810f44..6050a380c8 100644 --- a/.github/scripts/tests/test_demo_runtime_transaction.py +++ b/.github/scripts/tests/test_demo_runtime_transaction.py @@ -1,4 +1,3 @@ -import copy import hashlib import importlib.util import json @@ -195,6 +194,16 @@ class TransactionTest(unittest.TestCase): self.assertFalse(receipt["mutated"]) self.assertEqual(self.host.stops, 0) + def test_noop_cancellation_retains_terminal_failure_without_stopping_service(self): + self.host.profile(request(), False) + self.host.cancel_at = 55 + code, receipt = self.run_transaction() + self.assertEqual(code, 1) + self.assertEqual(receipt["status"], "refused") + self.assertEqual(receipt["failure"], "cancelled-forward") + self.assertEqual(self.host.stops, 0) + self.assertFalse(receipt["mutated"]) + def test_update_restores_complete_changed_executable_unit_and_data_after_late_failure(self): self.host.fail_at = 55 code, receipt = self.run_transaction("update", "v6.4.6") @@ -404,8 +413,6 @@ class DispatchLifecycleTest(unittest.TestCase): for uncertain in (False, True): with self.subTest(uncertain=uncertain), tempfile.TemporaryDirectory() as directory: root = Path(directory) / "state" - source = dispatcher.BOOTSTRAP.replace("pathlib.Path('/var/lib/pulse-deploy/demo')", repr(root)) - # repr(Path) is not a Python literal: bind just the fixed path. source = dispatcher.BOOTSTRAP.replace("'/var/lib/pulse-deploy/demo'", repr(str(root))) source = source.replace("root.stat().st_uid == 0", "root.stat().st_uid == os.getuid()") clock = [0] diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 69c3b883c3..9cf02a0271 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -4751,8 +4751,6 @@ "match_files": [ ".github/ISSUE_TEMPLATE/v6_rc_feedback.yml", ".github/scripts/check-demo-reachability.sh", - ".github/scripts/demo-runtime-transaction.py", - ".github/scripts/dispatch-demo-runtime.py", ".github/scripts/recover-demo-runtime.sh", ".github/scripts/resolve-demo-runtime-profile.sh", ".github/scripts/setup-demo-ssh.sh", @@ -4813,13 +4811,11 @@ "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ - ".github/scripts/tests/test_demo_runtime_transaction.py", "frontend-modern/src/components/__tests__/whatsNewModel.test.ts", "frontend-modern/src/utils/__tests__/docsLinks.test.ts", "pulse-enterprise:scripts/validate-pro-release-line_test.sh", "pulse-pro:scripts/tests/test_validate_paid_runtime_distribution.py", "scripts/installtests/build_release_assets_test.go", - "scripts/installtests/demo_transaction_test.go", "scripts/release_control/internal/record_rc_to_ga_rehearsal_test.py", "scripts/release_control/internal/release_preflight_test.py", "scripts/release_control/mobile_release_gate_test.py", @@ -5173,6 +5169,21 @@ "scripts/installtests/install_sh_test.go", "scripts/installtests/safe_profile_migration_test.go" ] + }, + { + "id": "demo-runtime-transaction", + "label": "demo shared-host transaction and retained recovery proof", + "match_prefixes": [], + "match_files": [ + ".github/scripts/demo-runtime-transaction.py", + ".github/scripts/dispatch-demo-runtime.py" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "scripts/installtests/demo_transaction_test.go", + ".github/scripts/tests/test_demo_runtime_transaction.py" + ] } ], "match_files": null diff --git a/scripts/installtests/demo_transaction_test.go b/scripts/installtests/demo_transaction_test.go index f53ed0abc0..ae800da558 100644 --- a/scripts/installtests/demo_transaction_test.go +++ b/scripts/installtests/demo_transaction_test.go @@ -25,8 +25,11 @@ func TestDemoTransactionKeepsSignedAdmissionAndVerificationOnlyReadOnly(t *testi } text := string(workflow) start := strings.Index(text, "- name: Apply guarded demo transaction") + if start < 0 { + t.Fatal("missing retained demo transaction entry") + } end := strings.Index(text[start:], "- name: Retain guarded demo transaction evidence") - if start < 0 || end < 0 { + if end < 0 { t.Fatal("missing retained demo transaction entry") } entry := text[start : start+end] diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index a4dc01c378..d77aece65c 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -100,6 +100,18 @@ def _contract_reference(contract_path: str, needle: str, runtime_path: str) -> d class SubsystemLookupTest(unittest.TestCase): + + def test_demo_transaction_sources_have_connected_proof_without_widening_release_metadata(self): + for path in (".github/scripts/demo-runtime-transaction.py", ".github/scripts/dispatch-demo-runtime.py"): + with self.subTest(path=path): + result = lookup_paths([path]) + subsystem = next(item for item in result["subsystems"] if item["id"] == "deployment-installability") + self.assertEqual(subsystem["matched_verification_policies"][0]["id"], "demo-runtime-transaction") + self.assertEqual(sorted(subsystem["required_verification"]["exact_files"]), [ + ".github/scripts/tests/test_demo_runtime_transaction.py", + "scripts/installtests/demo_transaction_test.go", + ]) + def git(self, repo_root: Path, *args: str) -> None: subprocess.run( ["git", *args],