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
17 changes: 1 addition & 16 deletions .github/workflows/autorelease-consumer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,11 @@ jobs:
path: php-operator-control
sparse-checkout: |
.github/autorelease-operator.json
.github/maintenance-operator.json
persist-credentials: false
- name: Bind investigation to the live operator control
id: operator
run: |
# php-bin main still carries the pre-rename filename until its own
# autorelease PR merges. Drop this fallback once that has landed.
operator="php-operator-control/.github/autorelease-operator.json"
test -f "$operator" || operator="php-operator-control/.github/maintenance-operator.json"
state="$(jq -r .unattendedMutation "$operator")"
test "$state" = "paused" || test "$state" = "enabled"
echo "state=$state" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -513,18 +509,7 @@ jobs:
admitted_operator_commit="$(jq -r .preconditions.phpBinOperatorCommit autorelease-run/implementation-plan.json)"
admitted_operator_state="$(jq -r .preconditions.operatorState autorelease-run/implementation-plan.json)"
current_operator_commit="$(gh api repos/Bigpixelrocket/php-bin/commits/main --jq .sha)"
# Same pre-rename fallback as the operator bind step above, decided by
# one directory listing rather than a probe. A probe's non-zero exit
# cannot separate "the file is absent" from "the API call failed", and
# a transient error must not select the legacy path.
operator_names="$(gh api "repos/Bigpixelrocket/php-bin/contents/.github?ref=$current_operator_commit" --jq '.[].name')"
if printf '%s\n' "$operator_names" | grep -qx 'autorelease-operator.json'; then
operator_path=".github/autorelease-operator.json"
else
printf '%s\n' "$operator_names" | grep -qx 'maintenance-operator.json'
operator_path=".github/maintenance-operator.json"
fi
gh api "repos/Bigpixelrocket/php-bin/contents/$operator_path?ref=$current_operator_commit" \
gh api "repos/Bigpixelrocket/php-bin/contents/.github/autorelease-operator.json?ref=$current_operator_commit" \
--jq .content | base64 --decode > autorelease-run/current-operator.json
jq -n \
--arg misePhpHead "$admitted_mise" \
Expand Down
27 changes: 4 additions & 23 deletions autorelease/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,31 +115,12 @@ 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 fetch_first_url(urls: tuple[str, ...], output: pathlib.Path) -> dict[str, Any]:
"""Capture the first published path, recording which one supplied the bytes.

php-bin main keeps the pre-rename `maintenance/` path until its own
autorelease change merges. 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 php-bin main has landed.
"""
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, ...]]:
def pinned_policy_urls(commit_sha: str) -> tuple[str, 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}/maintenance/policy-invariants.json",
),
f"{RAW_ROOT}/{commit_sha}/autorelease/policy-invariants.json",
)


Expand All @@ -157,7 +138,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_urls = pinned_policy_urls(commit_sha)
policy_url, invariants_url = pinned_policy_urls(commit_sha)
commit_capture = {
"captureId": "php_bin_state",
**fetch_url(f"{POLICY_COMMIT_ROOT}/{commit_sha}", commit_output),
Expand All @@ -166,7 +147,7 @@ def fetch_policy_set(
selector_capture,
commit_capture,
{"captureId": "support_policy", **fetch_url(policy_url, policy_output)},
{"captureId": "policy_invariants", **fetch_first_url(invariants_urls, invariants_output)},
{"captureId": "policy_invariants", **fetch_url(invariants_url, invariants_output)},
]


Expand Down
29 changes: 2 additions & 27 deletions test/test_autorelease.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@
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 @@ -99,33 +95,12 @@ 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",
f"/{sha}/maintenance/policy-invariants.json",
],
[url.split("/php-bin")[-1] for url in invariants],
f"/{sha}/autorelease/policy-invariants.json",
invariants.split("/php-bin")[-1],
)
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)

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