From c6f26496b1756d6aabf0d473d992e62b217d9efb Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 2 Aug 2026 22:41:45 +0300 Subject: [PATCH] fix: restore the pinned policy invariants path fallback --- autorelease/consumer.py | 29 +++++++++++++++++++++++++---- test/test_autorelease.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/autorelease/consumer.py b/autorelease/consumer.py index 20bde7c..15c3ce6 100755 --- a/autorelease/consumer.py +++ b/autorelease/consumer.py @@ -115,12 +115,33 @@ def fetch_url(url: str, output: pathlib.Path) -> dict[str, Any]: raise ConsumerError(f"policy capture failed after bounded retries: {type(last_error).__name__}") -def pinned_policy_urls(commit_sha: str) -> tuple[str, str]: +def fetch_first_url(urls: tuple[str, ...], output: pathlib.Path) -> dict[str, Any]: + """Capture the first published path, recording which one supplied the bytes. + + Policy captures pin to the commit that last touched `support-policy.json`, + not to php-bin main. That commit still predates the maintenance-to-autorelease + rename, so the invariants it publishes remain at the pre-rename path. Only a + 404 falls through, so a transport failure still raises instead of silently + reaching for the older document. Drop every path but the first once a commit + that touches `support-policy.json` has landed after the rename. + """ + for url in urls[:-1]: + try: + return fetch_url(url, output) + except CaptureAbsent: + continue + return fetch_url(urls[-1], output) + + +def pinned_policy_urls(commit_sha: str) -> tuple[str, tuple[str, ...]]: if not re.fullmatch(r"[0-9a-f]{40}", commit_sha): raise ConsumerError("php-bin main state has no exact commit") return ( f"{RAW_ROOT}/{commit_sha}/support-policy.json", - f"{RAW_ROOT}/{commit_sha}/autorelease/policy-invariants.json", + ( + f"{RAW_ROOT}/{commit_sha}/autorelease/policy-invariants.json", + f"{RAW_ROOT}/{commit_sha}/maintenance/policy-invariants.json", + ), ) @@ -138,7 +159,7 @@ def fetch_policy_set( if not isinstance(selected, list) or len(selected) != 1: raise ConsumerError("php-bin policy commit selector is empty or ambiguous") commit_sha = selected[0].get("sha", "") - policy_url, invariants_url = pinned_policy_urls(commit_sha) + policy_url, invariants_urls = pinned_policy_urls(commit_sha) commit_capture = { "captureId": "php_bin_state", **fetch_url(f"{POLICY_COMMIT_ROOT}/{commit_sha}", commit_output), @@ -147,7 +168,7 @@ def fetch_policy_set( selector_capture, commit_capture, {"captureId": "support_policy", **fetch_url(policy_url, policy_output)}, - {"captureId": "policy_invariants", **fetch_url(invariants_url, invariants_output)}, + {"captureId": "policy_invariants", **fetch_first_url(invariants_urls, invariants_output)}, ] diff --git a/test/test_autorelease.py b/test/test_autorelease.py index 55c9876..b37ea57 100644 --- a/test/test_autorelease.py +++ b/test/test_autorelease.py @@ -3,12 +3,16 @@ import tempfile import unittest import json +from unittest import mock +from autorelease import consumer from autorelease.admission import AdmissionError, admit, digest_file, protected, verify_merge from autorelease.consumer import ( + CaptureAbsent, ConsumerError, compare, digest, + fetch_first_url, pinned_policy_urls, readiness, write, @@ -95,12 +99,35 @@ def test_policy_capture_urls_are_commit_pinned(self): policy, invariants = pinned_policy_urls(sha) self.assertIn(f"/{sha}/support-policy.json", policy) self.assertEqual( - f"/{sha}/autorelease/policy-invariants.json", - invariants.split("/php-bin")[-1], + [ + f"/{sha}/autorelease/policy-invariants.json", + f"/{sha}/maintenance/policy-invariants.json", + ], + [url.split("/php-bin")[-1] for url in invariants], ) with self.assertRaises(ConsumerError): pinned_policy_urls("main") + def test_policy_invariants_capture_prefers_the_current_path(self): + urls = ("https://example.invalid/new.json", "https://example.invalid/old.json") + output = pathlib.Path("unused.json") + + with mock.patch.object(consumer, "fetch_url", return_value={"url": urls[0]}) as fetch: + self.assertEqual(urls[0], fetch_first_url(urls, output)["url"]) + fetch.assert_called_once_with(urls[0], output) + + # The pinned policy commit predates the rename, so the older path must + # still resolve rather than fail the capture. + absent = [CaptureAbsent("absent"), {"url": urls[1]}] + with mock.patch.object(consumer, "fetch_url", side_effect=absent) as fetch: + self.assertEqual(urls[1], fetch_first_url(urls, output)["url"]) + self.assertEqual(2, fetch.call_count) + + # A transport failure must surface rather than reach for the older path. + with mock.patch.object(consumer, "fetch_url", side_effect=ConsumerError("timeout")): + with self.assertRaises(ConsumerError): + fetch_first_url(urls, output) + def test_merge_gate_binds_single_commit_diff_and_preconditions(self): with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary)