Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions autorelease/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
)


Expand All @@ -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),
Expand All @@ -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)},
]


Expand Down
31 changes: 29 additions & 2 deletions test/test_autorelease.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading