diff --git a/deploy/fixtures/node-lifecycle/service.go b/deploy/fixtures/node-lifecycle/service.go index 918781f..b228a6c 100644 --- a/deploy/fixtures/node-lifecycle/service.go +++ b/deploy/fixtures/node-lifecycle/service.go @@ -15,21 +15,52 @@ const version = "fixture-old" const unhealthy = false func main() { + args := os.Args[1:] + hasArg := func(want string) bool { + for _, arg := range args { + if arg == want { + return true + } + } + return false + } + validateConfig := func() bool { + for i, arg := range args { + if arg == "--config" && i+1 < len(args) { + data, err := os.ReadFile(args[i+1]) + return err == nil && strings.Contains(string(data), "data_dir:") + } + } + return false + } + // Match the production node's read-only maintenance contract so lifecycle + // tests exercise the installer's pre-stop guard instead of rejecting the + // synthetic candidate as an unsupported legacy binary. + if hasArg("--sale-upgrade-preflight-capability") { + if !hasArg("--validate-config") || !validateConfig() { + os.Exit(3) + } + fmt.Fprintln(os.Stdout, `{"schema":1,"command":"sale-upgrade-preflight"}`) + return + } + if hasArg("--sale-upgrade-preflight") { + if !validateConfig() { + os.Exit(3) + } + fmt.Fprintf(os.Stdout, + `{"schema":1,"status":"ok","mode":"inactive","tip_height":100,"tip_hash":%q}`+"\n", + strings.Repeat("a", 64)) + return + } for i, arg := range os.Args[1:] { if arg == "--version" { fmt.Println(version) return } if arg == "--validate-config" { - for j, value := range os.Args { - if value == "--config" && j+1 < len(os.Args) { - data, err := os.ReadFile(os.Args[j+1]) - if err != nil || !strings.Contains(string(data), "data_dir:") { - os.Exit(3) - } - fmt.Println("fixture config accepted") - return - } + if validateConfig() { + fmt.Println("fixture config accepted") + return } fmt.Fprintln(os.Stderr, "missing fixture config", i) os.Exit(3) diff --git a/deploy/publish_public_repo.py b/deploy/publish_public_repo.py index 824c740..a76cf8b 100644 --- a/deploy/publish_public_repo.py +++ b/deploy/publish_public_repo.py @@ -39,6 +39,14 @@ """ MESSAGE = ("chore: maintain verified source distribution\n\n" "Co-authored-by: aperod-network ") +REQUIRED_CHECKS = { + "Whole-tree publication security", + "go build & vet & test", + "install-node.sh e2e smoke test (install)", + "install-node.sh e2e smoke test (validator)", + "install-node.sh e2e smoke test (update)", + "install-node.sh e2e smoke test (upgrade)", +} def run(*args, cwd=None, test_python_userbase=None): @@ -223,7 +231,7 @@ def publish(candidate, tree, expected_head, request, with_tests=False): def await_merge(pr, request, timeout=900): deadline = time.monotonic() + timeout - required = {"Whole-tree publication security", "go build & vet & test"} + required = REQUIRED_CHECKS while time.monotonic() < deadline: pull = request("GET", f"{API}/pulls/{pr['number']}") if pull.get("merged"): diff --git a/deploy/test_publish_public_repo.py b/deploy/test_publish_public_repo.py index a1da9ac..69853cd 100644 --- a/deploy/test_publish_public_repo.py +++ b/deploy/test_publish_public_repo.py @@ -97,6 +97,25 @@ def request(method, url, data=None): publisher.await_merge({"number": 1, "html_url": "test"}, request) self.assertNotIn("PUT", methods) + def test_failed_lifecycle_matrix_check_never_merges(self): + methods = [] + failed = "install-node.sh e2e smoke test (update)" + + def request(method, url, data=None): + methods.append(method) + if url.endswith("/pulls/1"): + return {"state": "open", "head": {"sha": "reviewed"}} + return {"check_runs": [ + {"name": name, "status": "completed", + "conclusion": "failure" if name == failed else "success", + "app": {"id": 15368}} + for name in publisher.REQUIRED_CHECKS + ]} + + with self.assertRaisesRegex(RuntimeError, "check failed"): + publisher.await_merge({"number": 1, "html_url": "test"}, request) + self.assertNotIn("PUT", methods) + def test_untrusted_status_provider_cannot_authorize_merge(self): methods = [] def request(method, url, data=None): @@ -122,7 +141,7 @@ def request(method, url, data=None): return {"state": "open", "head": {"sha": "reviewed"}} return {"check_runs": [{"name": name, "status": "completed", "conclusion": "success", "app": {"id": 15368}} - for name in ("Whole-tree publication security", "go build & vet & test")]} + for name in publisher.REQUIRED_CHECKS]} publisher.await_merge({"number": 1, "html_url": "test"}, request) self.assertEqual(bodies[0]["sha"], "reviewed") self.assertNotIn("force", bodies[0])