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
6 changes: 3 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
/.github/workflows/ @loadinglucian
/.github/CODEOWNERS @loadinglucian
/.codex/ @loadinglucian
/maintenance/ @loadinglucian
/autorelease/ @loadinglucian
/schemas/ @loadinglucian
/scripts/admit-maintenance-plan @loadinglucian
/scripts/seal-maintenance-patch @loadinglucian
/scripts/admit-autorelease-plan @loadinglucian
/scripts/seal-autorelease-patch @loadinglucian
/scripts/validate-codex-action-inputs @loadinglucian
/scripts/validate-structured-output-schemas @loadinglucian
/scripts/verify-merge-admission @loadinglucian
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Guarded PHP maintenance agent instructions
# Autorelease agent instructions

The overarching goal is one production maintenance system across
The overarching goal is one production autorelease system across
`bigpixelrocket/php-bin` and `bigpixelrocket/mise-php` that detects upstream
PHP release or lifecycle changes, prepares bounded repository work, coordinates
both repositories, and permits deterministic controls to publish immutable,
Expand Down

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .github/workflows/protected-controls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
PROTECTED_REVIEWER: ${{ vars.MAINTENANCE_OWNER }}
PROTECTED_REVIEWER: ${{ vars.AUTORELEASE_OWNER }}
run: |
python3 - <<'PY'
import fnmatch
Expand All @@ -75,7 +75,7 @@ jobs:
number = os.environ["PR_NUMBER"]
head = os.environ["HEAD_SHA"]
reviewer = os.environ["PROTECTED_REVIEWER"].lower()
manifest = json.loads(pathlib.Path("maintenance/protected-paths.json").read_text())
manifest = json.loads(pathlib.Path("autorelease/protected-paths.json").read_text())
patterns = manifest["patterns"]
files = api(f"repos/{repo}/pulls/{number}/files")
protected = sorted(
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.DS_Store
__pycache__/
*.py[cod]
maintenance-run/
autorelease-run/
.test-tmp/
76 changes: 76 additions & 0 deletions AUTORELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Autorelease

How this repository consumes the accepted `php-bin` support policy,
prepares bounded repository work, and records the exact-commit readiness
that `php-bin` requires before it may publish a new branch.

The scheduled `php-bin policy consumer` captures the accepted public
`support-policy.json` and compares it with `support-snapshot.json`: the policy
digest, the invariants digest, the php-bin policy commit, the maintained
branches, and any locally incomplete event. It does not fetch or classify
upstream PHP lifecycle data. When the exact policy changes, the
repository-scoped pinned Codex Action produces an evidence-bound plan. Any
implementation runs offline, without a GitHub write credential, and only
against admitted paths.

```mermaid
flowchart TD
policy["Accepted php-bin policy commit and digest"] --> compare{"Snapshot differs?"}
compare -- "No" --> quiet["Quiet: no model call or mutation"]
compare -- "Yes" --> plan["Offline repository-scoped Codex plan"]
plan --> admit["Deterministic admission"]
admit --> patch["Offline admitted implementation"]
patch --> seal["Seal paths and digests"]
seal --> test["Clean macOS arm64 plugin tests"]
test --> ready["Commit exact mise_ready record"]
ready --> release["php-bin verifies both readiness records"]
```

Only maintained branches appear in `mise ls-remote` or resolve from a branch
shorthand. An exact historical stable version may still install when its
immutable `php-bin` release and checksum assets exist. New branch publication
waits for matching `php_bin_ready` and `mise_ready` records at exact commits.

Failures and lifecycle transitions use one deduplicated GitHub issue per action
key, assigned through `AUTORELEASE_OWNER`. Comments are added only for meaningful
changes, and GitHub Actions failure email remains an independent fallback.

```mermaid
flowchart TD
phase["Consumer, agent, sealing, test, or readiness phase"] --> result{"Result"}
result -- "Passed" --> state["Record exact evidence and state"]
result -- "Retryable" --> bounded["Bounded repair"]
result -- "Critical, repeated, or exhausted" --> stop["Stop mutation"]
bounded --> result
stop --> issue["Assigned autorelease issue"]
issue --> email["GitHub issue email"]
stop --> actions["Actions failure email"]
```

Pause unattended mutation in the reviewed
`php-bin/.github/autorelease-operator.json` control. Read-only capture and
investigation remain available while paused. Resume through a reviewed change;
partial events continue only through the deterministic next transition.

From a checkout containing both repositories:

```bash
(cd php-bin && ./scripts/test.sh)
(cd mise-php && ./scripts/test.sh)

./php-bin/scripts/verify-autorelease-system \
--mise-repo ./mise-php \
--php-bin-sha <exact-php-bin-sha> \
--mise-php-sha <exact-mise-php-sha> \
--output ./verification-results
```

Each repository's `scripts/test.sh` also validates every pinned Codex Action
invocation, exact CLI version, and canonical `config.toml` loading against the
reviewed offline contract in `.github/codex-action-contract.json` before
exercising autorelease behavior.

Inspect `support-snapshot.json`, `autorelease-events/`, `readiness/`, retained
workflow artifacts, and the event's GitHub issue. Recovery corrects the cause
and reruns the normal admitted path; it never disables checksum, policy,
sealing, exact-SHA, or publication gates.
74 changes: 6 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,76 +24,14 @@ that have reached end of life are intentionally not listed or installable.
Other operating systems and Intel Macs receive an explicit unsupported-target
error. Older macOS releases cannot load the published binaries.

## Guarded automatic maintenance

The scheduled `php-bin policy consumer` captures the accepted public
`support-policy.json` and compares only its digest and incomplete-event state
with `support-snapshot.json`. It does not fetch or classify upstream PHP
lifecycle data. When the exact policy changes, the repository-scoped pinned
Codex Action produces an evidence-bound plan. Any implementation runs offline,
without a GitHub write credential, and only against admitted paths.

```mermaid
flowchart TD
policy["Accepted php-bin policy commit and digest"] --> compare{"Snapshot differs?"}
compare -- "No" --> quiet["Quiet: no model call or mutation"]
compare -- "Yes" --> plan["Offline repository-scoped Codex plan"]
plan --> admit["Deterministic admission"]
admit --> patch["Offline admitted implementation"]
patch --> seal["Seal paths and digests"]
seal --> test["Clean macOS arm64 plugin tests"]
test --> ready["Commit exact mise_ready record"]
ready --> release["php-bin verifies both readiness records"]
```

Only maintained branches appear in `mise ls-remote` or resolve from a branch
shorthand. An exact historical stable version may still install when its
immutable `php-bin` release and checksum assets exist. New branch publication
waits for matching `php_bin_ready` and `mise_ready` records at exact commits.

Failures and lifecycle transitions use one deduplicated GitHub issue per action
key, assigned through `MAINTENANCE_OWNER`. Comments are added only for meaningful
changes, and GitHub Actions failure email remains an independent fallback.

```mermaid
flowchart TD
phase["Consumer, agent, sealing, test, or readiness phase"] --> result{"Result"}
result -- "Passed" --> state["Record exact evidence and state"]
result -- "Retryable" --> bounded["Bounded repair"]
result -- "Critical, repeated, or exhausted" --> stop["Stop mutation"]
bounded --> result
stop --> issue["Assigned maintenance issue"]
issue --> email["GitHub issue email"]
stop --> actions["Actions failure email"]
```

Pause unattended mutation in the reviewed
`php-bin/.github/maintenance-operator.json` control. Read-only capture and
investigation remain available while paused. Resume through a reviewed change;
partial events continue only through the deterministic next transition.

From a checkout containing both repositories:

```bash
(cd php-bin && ./scripts/test.sh)
(cd mise-php && ./scripts/test.sh)

./php-bin/scripts/verify-maintenance-system \
--mise-repo ./mise-php \
--php-bin-sha <exact-php-bin-sha> \
--mise-php-sha <exact-mise-php-sha> \
--output ./verification-results
```
## Autorelease

Each repository's `scripts/test.sh` also validates every pinned Codex Action
invocation, exact CLI version, and canonical `config.toml` loading against the
reviewed offline contract in `.github/codex-action-contract.json` before
exercising maintenance behavior.
This repository tracks `php-bin` automatically. A daily consumer captures the
accepted public support policy, and deterministic workflows admit, seal, and
merge any required change, then record exact-commit readiness.

Inspect `support-snapshot.json`, `maintenance-events/`, `readiness/`, retained
workflow artifacts, and the event's GitHub issue. Recovery corrects the cause
and reruns the normal admitted path; it never disables checksum, policy,
sealing, exact-SHA, or publication gates.
See [AUTORELEASE.md](AUTORELEASE.md) for the full contract, the operator
pause control, and maintainer commands.

## Install

Expand Down
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions maintenance/admission.py → autorelease/admission.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,12 @@ def admit(
"no_change", "new_patch", "new_branch", "branch_eol", "repair",
"reconcile_partial", "blocked", "needs_human",
}:
raise AdmissionError("invalid maintenance action")
raise AdmissionError("invalid autorelease action")
if plan.get("action") in {"blocked", "needs_human"}:
raise AdmissionError("no-go plan cannot advance")
action_key = plan.get("actionKey", "")
if not ACTION_KEY_RE.fullmatch(action_key):
raise AdmissionError("invalid maintenance action key")
raise AdmissionError("invalid autorelease action key")
if action_key != contract.get("actionKey"):
raise AdmissionError("plan action key changed from the event contract")
if action_key.startswith("new_branch:") and plan.get("action") != "new_branch":
Expand Down Expand Up @@ -495,7 +495,7 @@ def main() -> int:
print(json.dumps(value))
return 0
except (AdmissionError, OSError, subprocess.CalledProcessError) as error:
print(f"mise maintenance admission rejected: {error}", file=sys.stderr)
print(f"mise autorelease admission rejected: {error}", file=sys.stderr)
return 1


Expand Down
39 changes: 33 additions & 6 deletions maintenance/consumer.py → autorelease/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class ConsumerError(RuntimeError):
pass


class CaptureAbsent(ConsumerError):
"""The capture URL resolved but the document is not published at that path."""


class RestrictedRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req: Any, fp: Any, code: int, msg: str, headers: Any, newurl: str) -> Any:
old = urllib.parse.urlparse(req.full_url)
Expand Down Expand Up @@ -79,7 +83,7 @@ def fetch_url(url: str, output: pathlib.Path) -> dict[str, Any]:
raise ConsumerError("policy capture URL is outside the reviewed HTTPS allowlist")
request = urllib.request.Request(
url,
headers={"Accept": "application/json", "User-Agent": "bigpixelrocket-maintenance/1"},
headers={"Accept": "application/json", "User-Agent": "bigpixelrocket-autorelease/1"},
)
opener = urllib.request.build_opener(RestrictedRedirect)
last_error: Exception | None = None
Expand All @@ -102,17 +106,40 @@ def fetch_url(url: str, output: pathlib.Path) -> dict[str, Any]:
"digest": digest(body),
"bodyPath": output.name,
}
except urllib.error.HTTPError as error:
if error.code == 404:
raise CaptureAbsent(f"policy capture path is not published: {url}") from error
last_error = error
except (OSError, urllib.error.URLError, json.JSONDecodeError, ConsumerError) as error:
last_error = error
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.

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, ...]]:
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}/maintenance/policy-invariants.json",
(
f"{RAW_ROOT}/{commit_sha}/autorelease/policy-invariants.json",
f"{RAW_ROOT}/{commit_sha}/maintenance/policy-invariants.json",
),
)


Expand All @@ -130,7 +157,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 @@ -139,7 +166,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 Expand Up @@ -350,7 +377,7 @@ def main() -> int:
print(json.dumps(result))
return 0
except (ConsumerError, OSError, json.JSONDecodeError) as error:
print(f"maintenance consumer rejected input: {error}", file=sys.stderr)
print(f"autorelease consumer rejected input: {error}", file=sys.stderr)
return 1


Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"schemaVersion": 1,
"patterns": [
".github/codex/maintenance/*",
".github/codex/autorelease/*",
".github/codex-action-contract.json",
".github/workflows/*",
".codex/*",
"schemas/*",
"maintenance/*",
"scripts/admit-maintenance-plan",
"autorelease/*",
"scripts/admit-autorelease-plan",
"scripts/dispatch-pr-checks",
"scripts/seal-maintenance-patch",
"scripts/seal-autorelease-patch",
"scripts/validate-codex-action-inputs",
"scripts/validate-structured-output-schemas",
"scripts/verify-merge-admission",
"maintenance-events/*",
"autorelease-events/*",
"readiness/*",
".github/CODEOWNERS"
]
Expand Down
10 changes: 5 additions & 5 deletions docs/admin-state/mise-php-after.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@
"custom_branch_policies": false,
"protected_branches": true
},
"html_url": "https://github.com/Bigpixelrocket/mise-php/deployments/activity_log?environments_filter=php-maintenance-release",
"html_url": "https://github.com/Bigpixelrocket/mise-php/deployments/activity_log?environments_filter=php-autorelease-publish",
"id": 18804850284,
"name": "php-maintenance-release",
"name": "php-autorelease-publish",
"node_id": "EN_kwDOTj2ybM8AAAAEYNs-bA",
"protection_rules": [
{
Expand All @@ -93,7 +93,7 @@
}
],
"updated_at": "2026-07-27T10:08:07Z",
"url": "https://api.github.com/repos/Bigpixelrocket/mise-php/environments/php-maintenance-release"
"url": "https://api.github.com/repos/Bigpixelrocket/mise-php/environments/php-autorelease-publish"
}
],
"total_count": 1
Expand All @@ -113,7 +113,7 @@
"good first issue",
"help wanted",
"invalid",
"maintenance",
"autorelease",
"question",
"wontfix"
],
Expand Down Expand Up @@ -163,7 +163,7 @@
},
"snapshotDigest": "sha256:e50672433148e1054cd0436af11f47cbc7bf643db650eaed166ffcd1633fa632",
"variables": [
"MAINTENANCE_OWNER"
"AUTORELEASE_OWNER"
],
"workflowPermissions": {
"can_approve_pull_request_reviews": true,
Expand Down
Loading
Loading