From a9eb81455ca46af47a59cd3371b2f341087204f0 Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 2 Aug 2026 12:23:35 +0300 Subject: [PATCH 1/6] refactor!: rename the guarded maintenance system to autorelease Rename every "guarded maintenance" identifier to "autorelease" across directories, workflows, scripts, schemas, controls, and prose, and move the system documentation out of README.md into a dedicated AUTORELEASE.md. Release transaction vocabulary is deliberately untouched: release_transition, LEGAL_RELEASE_TRANSITIONS, and releaseIntent name the release object, not the system that produces it. scripts/release-maintenance becomes scripts/publish-release to keep the verb-first convention used by every other script in the directory. protected-paths.json, CODEOWNERS, and the retained event and evidence records move in lockstep. autorelease-state/last-evidence.json and the completed new_patch:8.5.9 event record contain no renamed text, so their digests remain valid and the watcher keeps its comparison baseline. BREAKING CHANGE: these GitHub-side names live outside the repository and must be migrated in the same window as this merge, otherwise the publish workflow loses access to its secrets: environment php-maintenance-release -> php-autorelease-publish environment php-maintenance-agent-canary -> php-autorelease-canary variable MAINTENANCE_OWNER -> AUTORELEASE_OWNER variable MAINTENANCE_ADMIN_CANARY -> AUTORELEASE_ADMIN_CANARY label maintenance -> autorelease Workflow filenames change, so any saved dispatch links must be updated: maintenance-watch.yml -> autorelease-watch.yml, maintenance-release.yml -> autorelease-publish.yml, maintenance-implementation.yml -> autorelease-implement.yml, maintenance-e2e.yml -> autorelease-e2e.yml. --- .github/CODEOWNERS | 26 +- ...perator.json => autorelease-operator.json} | 0 ...enance-pins.json => autorelease-pins.json} | 2 +- .../implementation.md | 0 .../investigation.md | 18 +- .../{maintenance => autorelease}/repair.md | 0 .../{maintenance => autorelease}/shared.md | 4 +- ...aintenance-e2e.yml => autorelease-e2e.yml} | 36 +-- ...entation.yml => autorelease-implement.yml} | 256 +++++++++--------- ...ce-release.yml => autorelease-publish.yml} | 90 +++--- ...enance-watch.yml => autorelease-watch.yml} | 246 ++++++++--------- .github/workflows/ci.yml | 4 +- .github/workflows/protected-controls.yml | 34 +-- .gitignore | 2 +- AUTORELEASE.md | 112 ++++++++ README.md | 115 +------- .../.gitkeep | 0 .../new_patch-8.5.9.json | 0 .../.gitkeep | 0 .../last-evidence.json | 0 autorelease/__init__.py | 3 + {maintenance => autorelease}/control.py | 44 +-- .../policy-invariants.json | 0 autorelease/protected-paths.json | 31 +++ {maintenance => autorelease}/verify.py | 77 +++--- docs/admin-state/php-bin-after.json | 16 +- ...e.json => autorelease-admin-evidence.json} | 8 +- docs/repository-settings.md | 16 +- maintenance/__init__.py | 3 - maintenance/protected-paths.json | 31 --- ...ema.json => autorelease-event.schema.json} | 2 +- ...hema.json => autorelease-plan.schema.json} | 2 +- ...aintenance-plan => admit-autorelease-plan} | 4 +- .../{maintenance-event => autorelease-event} | 2 +- ...-evidence => capture-autorelease-evidence} | 2 +- ...intenance => configure-github-autorelease} | 8 +- ...{notify-maintenance => notify-autorelease} | 20 +- scripts/prepare-agent-task | 2 +- .../{release-maintenance => publish-release} | 6 +- ...intenance-patch => seal-autorelease-patch} | 2 +- ...ce-artifact => serve-autorelease-artifact} | 0 scripts/test.sh | 2 +- ...e-archive => validate-autorelease-archive} | 2 +- scripts/validate-structured-output-schemas | 8 +- ...nance-system => verify-autorelease-system} | 2 +- scripts/verify-merge-admission | 2 +- ...ce-evidence => watch-autorelease-evidence} | 2 +- ...est_maintenance.py => test_autorelease.py} | 32 +-- 48 files changed, 646 insertions(+), 628 deletions(-) rename .github/{maintenance-operator.json => autorelease-operator.json} (100%) rename .github/{maintenance-pins.json => autorelease-pins.json} (82%) rename .github/codex/{maintenance => autorelease}/implementation.md (100%) rename .github/codex/{maintenance => autorelease}/investigation.md (87%) rename .github/codex/{maintenance => autorelease}/repair.md (100%) rename .github/codex/{maintenance => autorelease}/shared.md (94%) rename .github/workflows/{maintenance-e2e.yml => autorelease-e2e.yml} (88%) rename .github/workflows/{maintenance-implementation.yml => autorelease-implement.yml} (66%) rename .github/workflows/{maintenance-release.yml => autorelease-publish.yml} (88%) rename .github/workflows/{maintenance-watch.yml => autorelease-watch.yml} (64%) create mode 100644 AUTORELEASE.md rename {maintenance-events => autorelease-events}/.gitkeep (100%) rename {maintenance-events => autorelease-events}/new_patch-8.5.9.json (100%) rename {maintenance-state => autorelease-state}/.gitkeep (100%) rename {maintenance-state => autorelease-state}/last-evidence.json (100%) create mode 100644 autorelease/__init__.py rename {maintenance => autorelease}/control.py (97%) rename {maintenance => autorelease}/policy-invariants.json (100%) create mode 100644 autorelease/protected-paths.json rename {maintenance => autorelease}/verify.py (92%) rename docs/{maintenance-admin-evidence.json => autorelease-admin-evidence.json} (95%) delete mode 100644 maintenance/__init__.py delete mode 100644 maintenance/protected-paths.json rename schemas/{maintenance-event.schema.json => autorelease-event.schema.json} (87%) rename schemas/{maintenance-plan.schema.json => autorelease-plan.schema.json} (99%) rename scripts/{admit-maintenance-plan => admit-autorelease-plan} (93%) rename scripts/{maintenance-event => autorelease-event} (82%) rename scripts/{capture-maintenance-evidence => capture-autorelease-evidence} (82%) rename scripts/{configure-github-maintenance => configure-github-autorelease} (95%) rename scripts/{notify-maintenance => notify-autorelease} (90%) rename scripts/{release-maintenance => publish-release} (97%) rename scripts/{seal-maintenance-patch => seal-autorelease-patch} (94%) rename scripts/{serve-maintenance-artifact => serve-autorelease-artifact} (100%) rename scripts/{validate-maintenance-archive => validate-autorelease-archive} (82%) rename scripts/{verify-maintenance-system => verify-autorelease-system} (80%) rename scripts/{watch-maintenance-evidence => watch-autorelease-evidence} (97%) rename tests/{test_maintenance.py => test_autorelease.py} (96%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3f4d29b..2f7cf2f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,23 +2,23 @@ /.github/codex-action-contract.json @loadinglucian /.github/workflows/ @loadinglucian /.github/CODEOWNERS @loadinglucian -/.github/maintenance-operator.json @loadinglucian -/.github/maintenance-pins.json @loadinglucian +/.github/autorelease-operator.json @loadinglucian +/.github/autorelease-pins.json @loadinglucian /.codex/ @loadinglucian -/maintenance/ @loadinglucian +/autorelease/ @loadinglucian /schemas/ @loadinglucian -/scripts/admit-maintenance-plan @loadinglucian -/scripts/capture-maintenance-evidence @loadinglucian -/scripts/configure-github-maintenance @loadinglucian -/scripts/maintenance-event @loadinglucian -/scripts/notify-maintenance @loadinglucian +/scripts/admit-autorelease-plan @loadinglucian +/scripts/capture-autorelease-evidence @loadinglucian +/scripts/configure-github-autorelease @loadinglucian +/scripts/autorelease-event @loadinglucian +/scripts/notify-autorelease @loadinglucian /scripts/prepare-agent-task @loadinglucian -/scripts/seal-maintenance-patch @loadinglucian +/scripts/seal-autorelease-patch @loadinglucian /scripts/snapshot-github-admin-state @loadinglucian -/scripts/validate-maintenance-archive @loadinglucian +/scripts/validate-autorelease-archive @loadinglucian /scripts/validate-codex-action-inputs @loadinglucian /scripts/validate-structured-output-schemas @loadinglucian /scripts/verify-merge-admission @loadinglucian -/scripts/release-maintenance @loadinglucian -/scripts/watch-maintenance-evidence @loadinglucian -/maintenance/policy-invariants.json @loadinglucian +/scripts/publish-release @loadinglucian +/scripts/watch-autorelease-evidence @loadinglucian +/autorelease/policy-invariants.json @loadinglucian diff --git a/.github/maintenance-operator.json b/.github/autorelease-operator.json similarity index 100% rename from .github/maintenance-operator.json rename to .github/autorelease-operator.json diff --git a/.github/maintenance-pins.json b/.github/autorelease-pins.json similarity index 82% rename from .github/maintenance-pins.json rename to .github/autorelease-pins.json index 266170c..e999d40 100644 --- a/.github/maintenance-pins.json +++ b/.github/autorelease-pins.json @@ -10,6 +10,6 @@ "openai/codex-action": "52fe01ec70a42f454c9d2ebd47598f9fd6893d56" }, "workflows": { - ".github/workflows/maintenance-e2e.yml": "sha256:a495b86fb9c2b7477adc4e88b35db303a11193ab77096dee45865a8b2dd18f96" + ".github/workflows/autorelease-e2e.yml": "sha256:3a37af525d3144b8e815f26fff5f80af4680cb4686aef9552aebca9028d40a24" } } diff --git a/.github/codex/maintenance/implementation.md b/.github/codex/autorelease/implementation.md similarity index 100% rename from .github/codex/maintenance/implementation.md rename to .github/codex/autorelease/implementation.md diff --git a/.github/codex/maintenance/investigation.md b/.github/codex/autorelease/investigation.md similarity index 87% rename from .github/codex/maintenance/investigation.md rename to .github/codex/autorelease/investigation.md index 3075449..9714a08 100644 --- a/.github/codex/maintenance/investigation.md +++ b/.github/codex/autorelease/investigation.md @@ -1,7 +1,7 @@ # Investigation phase Observable goal: classify exactly one action key from retained evidence and -produce a schema-valid, evidence-bound maintenance plan without modifying a +produce a schema-valid, evidence-bound autorelease plan without modifying a repository or causing a GitHub mutation. For every material release or lifecycle claim, cite one captured body by @@ -14,18 +14,18 @@ captured evidence. Plan evidence `captureId` values may name only a capture in the evidence manifest or the two deterministic runtime inputs `evidence_manifest` and `watch_decision`. Those runtime IDs resolve only to -`maintenance-run/evidence/evidence-manifest.json` and -`maintenance-run/watch-decision.json`; no other runtime or repository file is +`autorelease-run/evidence/evidence-manifest.json` and +`autorelease-run/watch-decision.json`; no other runtime or repository file is admissible as plan evidence. The required runtime inputs are generated before this phase and are available at these exact paths: -- `maintenance-run/evidence/evidence-manifest.json` +- `autorelease-run/evidence/evidence-manifest.json` - the captured bodies named by each manifest entry, resolved relative to - `maintenance-run/evidence/` -- `maintenance-run/preconditions.json` -- `maintenance-run/watch-decision.json` + `autorelease-run/evidence/` +- `autorelease-run/preconditions.json` +- `autorelease-run/watch-decision.json` These runtime files are intentionally gitignored, so discovery commands that respect `.gitignore` (including `rg --files`) may omit them. Read the exact paths @@ -43,7 +43,7 @@ advisory checks. Declare them in the plan, but do not run them in this read-only phase or treat their not-yet-run status as unresolved; writable deterministic jobs execute them before merge. -If changed evidence has no maintenance consequence, use action `no_change` and +If changed evidence has no autorelease consequence, use action `no_change` and the key `no_change:` so the reviewed snapshot remains uniquely auditable. @@ -53,7 +53,7 @@ action, include a `php_release_feed` JSON-pointer evidence item whose resolved value is the exact `releaseIntent.version`; otherwise classify the tag-only change as `no_change` until the official feed publishes that version. -The plan `actionKey` identifies the classified maintenance action, not the +The plan `actionKey` identifies the classified autorelease action, not the phase-scoped action key in the event contract. It must use one of the reviewed forms enforced by the output schema: `no_change`, `new_patch`, `new_branch`, `branch_eol`, `recipe_rebuild`, `repair`, `source_unhealthy`, `health_failed`, diff --git a/.github/codex/maintenance/repair.md b/.github/codex/autorelease/repair.md similarity index 100% rename from .github/codex/maintenance/repair.md rename to .github/codex/autorelease/repair.md diff --git a/.github/codex/maintenance/shared.md b/.github/codex/autorelease/shared.md similarity index 94% rename from .github/codex/maintenance/shared.md rename to .github/codex/autorelease/shared.md index 4344d76..c34ad8b 100644 --- a/.github/codex/maintenance/shared.md +++ b/.github/codex/autorelease/shared.md @@ -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, diff --git a/.github/workflows/maintenance-e2e.yml b/.github/workflows/autorelease-e2e.yml similarity index 88% rename from .github/workflows/maintenance-e2e.yml rename to .github/workflows/autorelease-e2e.yml index 70372ac..9c9073b 100644 --- a/.github/workflows/maintenance-e2e.yml +++ b/.github/workflows/autorelease-e2e.yml @@ -1,4 +1,4 @@ -name: Maintenance end to end +name: Autorelease end to end on: workflow_dispatch: @@ -26,7 +26,7 @@ permissions: contents: read concurrency: - group: maintenance-e2e-${{ inputs.suite }} + group: autorelease-e2e-${{ inputs.suite }} cancel-in-progress: false jobs: @@ -44,8 +44,8 @@ jobs: working-directory: control run: | # This reviewed pin detects workflow edits that omit the corresponding pin update. - expected="$(jq -r '.workflows[".github/workflows/maintenance-e2e.yml"]' .github/maintenance-pins.json)" - actual="sha256:$(shasum -a 256 .github/workflows/maintenance-e2e.yml | awk '{print $1}')" + expected="$(jq -r '.workflows[".github/workflows/autorelease-e2e.yml"]' .github/autorelease-pins.json)" + actual="sha256:$(shasum -a 256 .github/workflows/autorelease-e2e.yml | awk '{print $1}')" test "$actual" = "$expected" - name: Checkout exact php-bin commit uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -76,7 +76,7 @@ jobs: test -z "${GITHUB_TOKEN:-}" - name: Run stable cross-repository verifier run: | - ./php-bin/scripts/verify-maintenance-system \ + ./php-bin/scripts/verify-autorelease-system \ --mise-repo ./mise-php \ --php-bin-sha "${{ inputs.php_bin_sha }}" \ --mise-php-sha "${{ inputs.mise_php_sha }}" \ @@ -85,7 +85,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: maintenance-production-parity-${{ github.run_id }} + name: autorelease-production-parity-${{ github.run_id }} path: verification-results/ if-no-files-found: error retention-days: 90 @@ -95,7 +95,7 @@ jobs: if: inputs.suite == 'agent-canary' runs-on: ubuntu-latest timeout-minutes: 15 - environment: php-maintenance-agent-canary + environment: php-autorelease-canary permissions: contents: read steps: @@ -152,7 +152,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: maintenance-agent-canary-${{ github.run_id }} + name: autorelease-agent-canary-${{ github.run_id }} path: canary/ if-no-files-found: error retention-days: 90 @@ -178,23 +178,23 @@ jobs: jq -n --arg actionKey "$action_key" \ '{actionKey:$actionKey,state:"detected",severity:"info",summary:"Harmless notification delivery canary."}' \ > canary/event.json - ./scripts/notify-maintenance --event canary/event.json --state canary/state.json --output canary/next.json \ - --backend github --repo "${{ github.repository }}" --owner "${{ vars.MAINTENANCE_OWNER }}" + ./scripts/notify-autorelease --event canary/event.json --state canary/state.json --output canary/next.json \ + --backend github --repo "${{ github.repository }}" --owner "${{ vars.AUTORELEASE_OWNER }}" mv canary/next.json canary/state.json - ./scripts/notify-maintenance --event canary/event.json --state canary/state.json --output canary/replay.json \ - --backend github --repo "${{ github.repository }}" --owner "${{ vars.MAINTENANCE_OWNER }}" + ./scripts/notify-autorelease --event canary/event.json --state canary/state.json --output canary/replay.json \ + --backend github --repo "${{ github.repository }}" --owner "${{ vars.AUTORELEASE_OWNER }}" test "$(jq -r .lastAction canary/replay.json)" = "none" jq '.state="php_bin_ready"' canary/event.json > canary/transition.json - ./scripts/notify-maintenance --event canary/transition.json --state canary/state.json --output canary/next.json \ - --backend github --repo "${{ github.repository }}" --owner "${{ vars.MAINTENANCE_OWNER }}" + ./scripts/notify-autorelease --event canary/transition.json --state canary/state.json --output canary/next.json \ + --backend github --repo "${{ github.repository }}" --owner "${{ vars.AUTORELEASE_OWNER }}" mv canary/next.json canary/state.json jq '.state="complete" | .finalResult="passed"' canary/event.json > canary/complete.json - ./scripts/notify-maintenance --event canary/complete.json --state canary/state.json --output canary/closed.json \ - --backend github --repo "${{ github.repository }}" --owner "${{ vars.MAINTENANCE_OWNER }}" + ./scripts/notify-autorelease --event canary/complete.json --state canary/state.json --output canary/closed.json \ + --backend github --repo "${{ github.repository }}" --owner "${{ vars.AUTORELEASE_OWNER }}" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: - name: maintenance-notification-canary-${{ github.run_id }} + name: autorelease-notification-canary-${{ github.run_id }} path: canary/ if-no-files-found: error retention-days: 90 @@ -203,7 +203,7 @@ jobs: if: inputs.suite == 'live-canary' runs-on: macos-26 timeout-minutes: 20 - environment: php-maintenance-release + environment: php-autorelease-publish steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: diff --git a/.github/workflows/maintenance-implementation.yml b/.github/workflows/autorelease-implement.yml similarity index 66% rename from .github/workflows/maintenance-implementation.yml rename to .github/workflows/autorelease-implement.yml index a0ee9ea..6519e0e 100644 --- a/.github/workflows/maintenance-implementation.yml +++ b/.github/workflows/autorelease-implement.yml @@ -1,10 +1,10 @@ -name: PHP maintenance implementation +name: PHP autorelease implementation on: workflow_dispatch: inputs: investigation_run_id: - description: Run containing an admitted maintenance plan artifact + description: Run containing an admitted autorelease plan artifact required: true type: string exact_base_sha: @@ -36,21 +36,21 @@ jobs: - name: Download admitted investigation env: GH_TOKEN: ${{ github.token }} - run: gh run download "${{ inputs.investigation_run_id }}" --name "maintenance-investigation-${{ inputs.investigation_run_id }}" --dir maintenance-run + run: gh run download "${{ inputs.investigation_run_id }}" --name "autorelease-investigation-${{ inputs.investigation_run_id }}" --dir autorelease-run - name: Enforce operator pause - run: test "$(jq -r .unattendedMutation .github/maintenance-operator.json)" = "enabled" + run: test "$(jq -r .unattendedMutation .github/autorelease-operator.json)" = "enabled" - name: Verify exact admitted base run: | test "$(git rev-parse HEAD)" = "${{ inputs.exact_base_sha }}" - test "$(jq -r .preconditions.phpBinHead maintenance-run/maintenance-plan.json)" = "${{ inputs.exact_base_sha }}" + test "$(jq -r .preconditions.phpBinHead autorelease-run/autorelease-plan.json)" = "${{ inputs.exact_base_sha }}" - name: Prepare offline phase contract run: | phase="${{ inputs.phase }}" ./scripts/prepare-agent-task \ --phase "$phase" \ - --action-key "$(jq -r .actionKey maintenance-run/maintenance-plan.json)" \ - --goal "Implement the exact admitted maintenance plan and leave a sealable diff." \ - --preconditions maintenance-run/preconditions.json \ + --action-key "$(jq -r .actionKey autorelease-run/autorelease-plan.json)" \ + --goal "Implement the exact admitted autorelease plan and leave a sealable diff." \ + --preconditions autorelease-run/preconditions.json \ --allowed-authority workspace_write_admitted_paths \ --allowed-authority local_advisory_checks \ --non-goal protected_control_change \ @@ -58,16 +58,16 @@ jobs: --stop-condition changed_precondition \ --stop-condition required_protected_or_unadmitted_change \ --stop-condition required_check_unavailable \ - --shared .github/codex/maintenance/shared.md \ - --phase-template ".github/codex/maintenance/$phase.md" \ - --contract-output maintenance-run/implementation-contract.json \ - --prompt-output maintenance-run/implementation-prompt.md - shared="$(./maintenance/control.py digest .github/codex/maintenance/shared.md)" - phase_digest="$(./maintenance/control.py digest ".github/codex/maintenance/$phase.md")" - event="$(./maintenance/control.py digest maintenance-run/implementation-contract.json)" + --shared .github/codex/autorelease/shared.md \ + --phase-template ".github/codex/autorelease/$phase.md" \ + --contract-output autorelease-run/implementation-contract.json \ + --prompt-output autorelease-run/implementation-prompt.md + shared="$(./autorelease/control.py digest .github/codex/autorelease/shared.md)" + phase_digest="$(./autorelease/control.py digest ".github/codex/autorelease/$phase.md")" + event="$(./autorelease/control.py digest autorelease-run/implementation-contract.json)" jq --arg shared "$shared" --arg phaseTemplate "$phase_digest" --arg eventContract "$event" \ '.agentContract.instructionDigests={shared:$shared,phaseTemplate:$phaseTemplate,eventContract:$eventContract}' \ - maintenance-run/maintenance-plan.json > maintenance-run/implementation-plan.json + autorelease-run/autorelease-plan.json > autorelease-run/implementation-plan.json mkdir -p "$RUNNER_TEMP/codex-home" cp ".codex/$phase.config.toml" "$RUNNER_TEMP/codex-home/config.toml" - name: Run offline Codex implementation @@ -75,8 +75,8 @@ jobs: with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} codex-version: 0.145.0 - prompt-file: maintenance-run/implementation-prompt.md - output-file: maintenance-run/implementation-result.json + prompt-file: autorelease-run/implementation-prompt.md + output-file: autorelease-run/implementation-result.json codex-home: ${{ runner.temp }}/codex-home sandbox: workspace-write safety-strategy: drop-sudo @@ -86,18 +86,18 @@ jobs: codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/agent-completion-assessment.schema.json"]' - name: Seal exact agent diff run: | - ./scripts/seal-maintenance-patch \ + ./scripts/seal-autorelease-patch \ --repo . \ --base "${{ inputs.exact_base_sha }}" \ - --plan maintenance-run/implementation-plan.json \ - --result maintenance-run/implementation-result.json \ - --contract maintenance-run/implementation-contract.json \ - --output maintenance-run/sealed + --plan autorelease-run/implementation-plan.json \ + --result autorelease-run/implementation-result.json \ + --contract autorelease-run/implementation-contract.json \ + --output autorelease-run/sealed - name: Retain sealed patch uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: sealed-maintenance-patch-${{ github.run_id }} - path: maintenance-run/ + name: sealed-autorelease-patch-${{ github.run_id }} + path: autorelease-run/ if-no-files-found: error retention-days: 90 include-hidden-files: true @@ -120,17 +120,17 @@ jobs: persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: sealed-maintenance-patch-${{ github.run_id }} - path: maintenance-run + name: sealed-autorelease-patch-${{ github.run_id }} + path: autorelease-run - name: Apply exact sealed bytes run: | - test "$(./maintenance/control.py digest maintenance-run/sealed/sealed.patch)" = "$(jq -r .patchDigest maintenance-run/sealed/patch-manifest.json)" - git apply --index maintenance-run/sealed/sealed.patch + test "$(./autorelease/control.py digest autorelease-run/sealed/sealed.patch)" = "$(jq -r .patchDigest autorelease-run/sealed/patch-manifest.json)" + git apply --index autorelease-run/sealed/sealed.patch - name: Run authoritative checks and retain failure logs id: checks run: | set +e - ./scripts/test.sh 2>&1 | tee maintenance-run/authoritative-checks.log + ./scripts/test.sh 2>&1 | tee autorelease-run/authoritative-checks.log status="${PIPESTATUS[0]}" set -e if [[ "$status" == "0" ]]; then @@ -141,28 +141,28 @@ jobs: - name: Record validated SHA and tree if: steps.checks.outputs.passed == 'true' run: | - export GIT_AUTHOR_NAME=maintenance-validator - export GIT_AUTHOR_EMAIL=maintenance@invalid - export GIT_COMMITTER_NAME=maintenance-validator - export GIT_COMMITTER_EMAIL=maintenance@invalid + export GIT_AUTHOR_NAME=autorelease-validator + export GIT_AUTHOR_EMAIL=autorelease@invalid + export GIT_COMMITTER_NAME=autorelease-validator + export GIT_COMMITTER_EMAIL=autorelease@invalid export GIT_AUTHOR_DATE=2000-01-01T00:00:00Z export GIT_COMMITTER_DATE=2000-01-01T00:00:00Z - git commit -m "chore: apply admitted maintenance patch" - jq -n --arg headSha "$(git rev-parse HEAD)" --arg tree "$(git rev-parse HEAD^{tree})" '{headSha:$headSha,tree:$tree,checks:{"Script checks":"success"}}' > maintenance-run/validation.json - git bundle create maintenance-run/validated.bundle HEAD "^${{ inputs.exact_base_sha }}" + git commit -m "chore: apply admitted autorelease patch" + jq -n --arg headSha "$(git rev-parse HEAD)" --arg tree "$(git rev-parse HEAD^{tree})" '{headSha:$headSha,tree:$tree,checks:{"Script checks":"success"}}' > autorelease-run/validation.json + git bundle create autorelease-run/validated.bundle HEAD "^${{ inputs.exact_base_sha }}" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: steps.checks.outputs.passed == 'true' with: - name: validated-maintenance-patch-${{ github.run_id }} - path: maintenance-run/ + name: validated-autorelease-patch-${{ github.run_id }} + path: autorelease-run/ if-no-files-found: error retention-days: 90 include-hidden-files: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: steps.checks.outputs.passed != 'true' with: - name: failed-maintenance-validation-${{ github.run_id }} - path: maintenance-run/ + name: failed-autorelease-validation-${{ github.run_id }} + path: autorelease-run/ if-no-files-found: error retention-days: 90 include-hidden-files: true @@ -184,17 +184,17 @@ jobs: persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: failed-maintenance-validation-${{ github.run_id }} - path: maintenance-run + name: failed-autorelease-validation-${{ github.run_id }} + path: autorelease-run - name: Restore failed patch and prepare exact repair contract run: | - git apply --index maintenance-run/sealed/sealed.patch - fingerprint="$(./maintenance/control.py digest maintenance-run/authoritative-checks.log)" + git apply --index autorelease-run/sealed/sealed.patch + fingerprint="$(./autorelease/control.py digest autorelease-run/authoritative-checks.log)" ./scripts/prepare-agent-task \ --phase repair \ - --action-key "$(jq -r .actionKey maintenance-run/implementation-plan.json)" \ + --action-key "$(jq -r .actionKey autorelease-run/implementation-plan.json)" \ --goal "Repair the retained authoritative failure $fingerprint without expanding the admitted diff." \ - --preconditions maintenance-run/preconditions.json \ + --preconditions autorelease-run/preconditions.json \ --allowed-authority workspace_write_admitted_paths \ --allowed-authority local_advisory_checks \ --non-goal protected_control_change \ @@ -202,24 +202,24 @@ jobs: --stop-condition changed_precondition \ --stop-condition repeated_or_unrelated_failure \ --stop-condition required_protected_or_unadmitted_change \ - --shared .github/codex/maintenance/shared.md \ - --phase-template .github/codex/maintenance/repair.md \ - --contract-output maintenance-run/repair-contract.json \ - --prompt-output maintenance-run/repair-prompt-base.md - shared="$(./maintenance/control.py digest .github/codex/maintenance/shared.md)" - phase_digest="$(./maintenance/control.py digest .github/codex/maintenance/repair.md)" - event="$(./maintenance/control.py digest maintenance-run/repair-contract.json)" + --shared .github/codex/autorelease/shared.md \ + --phase-template .github/codex/autorelease/repair.md \ + --contract-output autorelease-run/repair-contract.json \ + --prompt-output autorelease-run/repair-prompt-base.md + shared="$(./autorelease/control.py digest .github/codex/autorelease/shared.md)" + phase_digest="$(./autorelease/control.py digest .github/codex/autorelease/repair.md)" + event="$(./autorelease/control.py digest autorelease-run/repair-contract.json)" jq --arg shared "$shared" --arg phaseTemplate "$phase_digest" --arg eventContract "$event" \ '.agentContract.instructionDigests={shared:$shared,phaseTemplate:$phaseTemplate,eventContract:$eventContract}' \ - maintenance-run/implementation-plan.json > maintenance-run/repair-plan.json + autorelease-run/implementation-plan.json > autorelease-run/repair-plan.json { - cat maintenance-run/repair-prompt-base.md + cat autorelease-run/repair-prompt-base.md printf '\n\n# Retained authoritative failure log\n\n```text\n' - cat maintenance-run/authoritative-checks.log + cat autorelease-run/authoritative-checks.log printf '\n```\n\n# Exact admitted plan\n\n```json\n' - jq . maintenance-run/repair-plan.json + jq . autorelease-run/repair-plan.json printf '\n```\n' - } > maintenance-run/repair-prompt.md + } > autorelease-run/repair-prompt.md mkdir -p "$RUNNER_TEMP/codex-home" cp .codex/repair.config.toml "$RUNNER_TEMP/codex-home/config.toml" - name: Run one offline Codex repair @@ -227,8 +227,8 @@ jobs: with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} codex-version: 0.145.0 - prompt-file: maintenance-run/repair-prompt.md - output-file: maintenance-run/repair-result.json + prompt-file: autorelease-run/repair-prompt.md + output-file: autorelease-run/repair-result.json codex-home: ${{ runner.temp }}/codex-home sandbox: workspace-write safety-strategy: drop-sudo @@ -238,18 +238,18 @@ jobs: codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/agent-completion-assessment.schema.json"]' - name: Seal the combined repaired diff run: | - mv maintenance-run/repair-plan.json maintenance-run/implementation-plan.json - ./scripts/seal-maintenance-patch \ + mv autorelease-run/repair-plan.json autorelease-run/implementation-plan.json + ./scripts/seal-autorelease-patch \ --repo . \ --base "${{ inputs.exact_base_sha }}" \ - --plan maintenance-run/implementation-plan.json \ - --result maintenance-run/repair-result.json \ - --contract maintenance-run/repair-contract.json \ - --output maintenance-run/sealed-repair + --plan autorelease-run/implementation-plan.json \ + --result autorelease-run/repair-result.json \ + --contract autorelease-run/repair-contract.json \ + --output autorelease-run/sealed-repair - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: repaired-maintenance-patch-${{ github.run_id }} - path: maintenance-run/ + name: repaired-autorelease-patch-${{ github.run_id }} + path: autorelease-run/ if-no-files-found: error retention-days: 90 include-hidden-files: true @@ -270,30 +270,30 @@ jobs: persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: repaired-maintenance-patch-${{ github.run_id }} - path: maintenance-run + name: repaired-autorelease-patch-${{ github.run_id }} + path: autorelease-run - name: Apply and validate exact repaired bytes run: | - test "$(./maintenance/control.py digest maintenance-run/sealed-repair/sealed.patch)" = "$(jq -r .patchDigest maintenance-run/sealed-repair/patch-manifest.json)" - git apply --index maintenance-run/sealed-repair/sealed.patch + test "$(./autorelease/control.py digest autorelease-run/sealed-repair/sealed.patch)" = "$(jq -r .patchDigest autorelease-run/sealed-repair/patch-manifest.json)" + git apply --index autorelease-run/sealed-repair/sealed.patch ./scripts/test.sh - cp maintenance-run/sealed-repair/sealed.patch maintenance-run/sealed/sealed.patch - cp maintenance-run/sealed-repair/patch-manifest.json maintenance-run/sealed/patch-manifest.json + cp autorelease-run/sealed-repair/sealed.patch autorelease-run/sealed/sealed.patch + cp autorelease-run/sealed-repair/patch-manifest.json autorelease-run/sealed/patch-manifest.json - name: Record repaired validated SHA and tree run: | - export GIT_AUTHOR_NAME=maintenance-validator - export GIT_AUTHOR_EMAIL=maintenance@invalid - export GIT_COMMITTER_NAME=maintenance-validator - export GIT_COMMITTER_EMAIL=maintenance@invalid + export GIT_AUTHOR_NAME=autorelease-validator + export GIT_AUTHOR_EMAIL=autorelease@invalid + export GIT_COMMITTER_NAME=autorelease-validator + export GIT_COMMITTER_EMAIL=autorelease@invalid export GIT_AUTHOR_DATE=2000-01-01T00:00:00Z export GIT_COMMITTER_DATE=2000-01-01T00:00:00Z - git commit -m "chore: apply admitted maintenance repair" - jq -n --arg headSha "$(git rev-parse HEAD)" --arg tree "$(git rev-parse HEAD^{tree})" '{headSha:$headSha,tree:$tree,checks:{"Script checks":"success"}}' > maintenance-run/validation.json - git bundle create maintenance-run/validated.bundle HEAD "^${{ inputs.exact_base_sha }}" + git commit -m "chore: apply admitted autorelease repair" + jq -n --arg headSha "$(git rev-parse HEAD)" --arg tree "$(git rev-parse HEAD^{tree})" '{headSha:$headSha,tree:$tree,checks:{"Script checks":"success"}}' > autorelease-run/validation.json + git bundle create autorelease-run/validated.bundle HEAD "^${{ inputs.exact_base_sha }}" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: validated-maintenance-patch-${{ github.run_id }} - path: maintenance-run/ + name: validated-autorelease-patch-${{ github.run_id }} + path: autorelease-run/ if-no-files-found: error retention-days: 90 include-hidden-files: true @@ -318,12 +318,12 @@ jobs: persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: validated-maintenance-patch-${{ github.run_id }} - path: maintenance-run + name: validated-autorelease-patch-${{ github.run_id }} + path: autorelease-run - name: Restore exact validated commit run: | - validated="$(jq -r .headSha maintenance-run/validation.json)" - git fetch maintenance-run/validated.bundle HEAD + validated="$(jq -r .headSha autorelease-run/validation.json)" + git fetch autorelease-run/validated.bundle HEAD git checkout --detach "$validated" test "$(git rev-parse HEAD)" = "$validated" - name: Create or reuse automation PR @@ -331,14 +331,14 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - action_key="$(jq -r .actionKey maintenance-run/implementation-plan.json)" - branch="maintenance/$(printf '%s' "$action_key" | tr ':/' '--')" + action_key="$(jq -r .actionKey autorelease-run/implementation-plan.json)" + branch="autorelease/$(printf '%s' "$action_key" | tr ':/' '--')" gh auth setup-git git push origin "HEAD:refs/heads/$branch" existing="$(gh pr list --head "$branch" --state open --json number --jq '.[0].number // empty')" if [[ -z "$existing" ]]; then url="$(gh pr create --base main --head "$branch" --title "chore: $action_key" \ - --body "Deterministically sealed maintenance patch for \`$action_key\`.\n\nValidated commit: \`$(git rev-parse HEAD)\`.")" + --body "Deterministically sealed autorelease patch for \`$action_key\`.\n\nValidated commit: \`$(git rev-parse HEAD)\`.")" existing="${url##*/}" fi echo "number=$existing" >> "$GITHUB_OUTPUT" @@ -349,33 +349,33 @@ jobs: ./scripts/dispatch-pr-checks \ --pr "${{ steps.pr.outputs.number }}" \ --check "Script checks" \ - --output maintenance-run/pr-checks.json - jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' maintenance-run/pr-checks.json + --output autorelease-run/pr-checks.json + jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' autorelease-run/pr-checks.json - name: Re-verify exact SHA, sealed tree, and preconditions env: GH_TOKEN: ${{ github.token }} run: | - expected="$(jq -r .headSha maintenance-run/validation.json)" + expected="$(jq -r .headSha autorelease-run/validation.json)" actual="$(gh pr view "${{ steps.pr.outputs.number }}" --json headRefOid --jq .headRefOid)" test "$actual" = "$expected" test "$(git rev-parse HEAD)" = "$expected" - admitted_head="$(jq -r .preconditions.phpBinHead maintenance-run/implementation-plan.json)" - admitted_policy="$(jq -r .preconditions.supportPolicyDigest maintenance-run/implementation-plan.json)" + admitted_head="$(jq -r .preconditions.phpBinHead autorelease-run/implementation-plan.json)" + admitted_policy="$(jq -r .preconditions.supportPolicyDigest autorelease-run/implementation-plan.json)" git fetch origin main current_head="$(git rev-parse origin/main)" current_policy="sha256:$(git show origin/main:support-policy.json | shasum -a 256 | awk '{print $1}')" jq -n --arg phpBinHead "$admitted_head" --arg supportPolicyDigest "$admitted_policy" \ - '{phpBinHead:$phpBinHead,supportPolicyDigest:$supportPolicyDigest}' > maintenance-run/preconditions.json + '{phpBinHead:$phpBinHead,supportPolicyDigest:$supportPolicyDigest}' > autorelease-run/preconditions.json jq -n --arg phpBinHead "$current_head" --arg supportPolicyDigest "$current_policy" \ - '{phpBinHead:$phpBinHead,supportPolicyDigest:$supportPolicyDigest}' > maintenance-run/current.json - jq -n '{"Script checks":"success"}' > maintenance-run/checks.json + '{phpBinHead:$phpBinHead,supportPolicyDigest:$supportPolicyDigest}' > autorelease-run/current.json + jq -n '{"Script checks":"success"}' > autorelease-run/checks.json ./scripts/verify-merge-admission \ --repo . \ --head "$expected" \ - --manifest maintenance-run/sealed/patch-manifest.json \ - --checks maintenance-run/checks.json \ - --preconditions maintenance-run/preconditions.json \ - --current maintenance-run/current.json + --manifest autorelease-run/sealed/patch-manifest.json \ + --checks autorelease-run/checks.json \ + --preconditions autorelease-run/preconditions.json \ + --current autorelease-run/current.json - name: Merge admitted exact commit id: merged env: @@ -390,22 +390,22 @@ jobs: run: | git fetch origin main test "$(git rev-parse origin/main)" = "${{ steps.merged.outputs.commit }}" - test "$(git rev-parse "${{ steps.merged.outputs.commit }}^{tree}")" = "$(jq -r .tree maintenance-run/validation.json)" - git checkout -B "maintenance/readiness-${{ github.run_id }}" origin/main + test "$(git rev-parse "${{ steps.merged.outputs.commit }}^{tree}")" = "$(jq -r .tree autorelease-run/validation.json)" + git checkout -B "autorelease/readiness-${{ github.run_id }}" origin/main base="$(git rev-parse HEAD)" - action_key="$(jq -r .actionKey maintenance-run/implementation-plan.json)" + action_key="$(jq -r .actionKey autorelease-run/implementation-plan.json)" filename="$(printf '%s' "$action_key" | tr ':/' '--').json" - mkdir -p maintenance-events + mkdir -p autorelease-events jq -n \ --arg actionKey "$action_key" \ - --arg classification "$(jq -r .action maintenance-run/implementation-plan.json)" \ + --arg classification "$(jq -r .action autorelease-run/implementation-plan.json)" \ --arg phpBinCommit "${{ steps.merged.outputs.commit }}" \ - --arg planDigest "$(jq -r .planDigest maintenance-run/sealed/patch-manifest.json)" \ - --arg policyDigest "$(./maintenance/control.py digest support-policy.json)" \ - --arg policyInvariantsDigest "$(./maintenance/control.py digest maintenance/policy-invariants.json)" \ - --arg evidenceManifestDigest "$(jq -r .manifestDigest maintenance-run/evidence/evidence-manifest.json)" \ - --argjson evidenceDigests "$(jq '[.captures[].digest]' maintenance-run/evidence/evidence-manifest.json)" \ - --argjson instructionDigests "$(jq .agentContract.instructionDigests maintenance-run/implementation-plan.json)" \ + --arg planDigest "$(jq -r .planDigest autorelease-run/sealed/patch-manifest.json)" \ + --arg policyDigest "$(./autorelease/control.py digest support-policy.json)" \ + --arg policyInvariantsDigest "$(./autorelease/control.py digest autorelease/policy-invariants.json)" \ + --arg evidenceManifestDigest "$(jq -r .manifestDigest autorelease-run/evidence/evidence-manifest.json)" \ + --argjson evidenceDigests "$(jq '[.captures[].digest]' autorelease-run/evidence/evidence-manifest.json)" \ + --argjson instructionDigests "$(jq .agentContract.instructionDigests autorelease-run/implementation-plan.json)" \ '{ schemaVersion:1, actionKey:$actionKey, @@ -419,26 +419,26 @@ jobs: evidenceManifestDigest:$evidenceManifestDigest, evidenceDigests:$evidenceDigests, instructionDigests:$instructionDigests - }' > "maintenance-events/$filename" + }' > "autorelease-events/$filename" jq -n \ --arg commit "${{ steps.merged.outputs.commit }}" \ - --arg planDigest "$(jq -r .planDigest maintenance-run/sealed/patch-manifest.json)" \ - '[{kind:"validated_merge",commit:$commit,planDigest:$planDigest}]' > maintenance-run/readiness-evidence.json - ./scripts/maintenance-event \ - --event "maintenance-events/$filename" \ + --arg planDigest "$(jq -r .planDigest autorelease-run/sealed/patch-manifest.json)" \ + '[{kind:"validated_merge",commit:$commit,planDigest:$planDigest}]' > autorelease-run/readiness-evidence.json + ./scripts/autorelease-event \ + --event "autorelease-events/$filename" \ --target php_bin_ready \ - --evidence maintenance-run/readiness-evidence.json \ - --output "maintenance-events/$filename.next" - mv "maintenance-events/$filename.next" "maintenance-events/$filename" - git add "maintenance-events/$filename" - git -c user.name=maintenance-readiness -c user.email=maintenance@invalid \ + --evidence autorelease-run/readiness-evidence.json \ + --output "autorelease-events/$filename.next" + mv "autorelease-events/$filename.next" "autorelease-events/$filename" + git add "autorelease-events/$filename" + git -c user.name=autorelease-readiness -c user.email=autorelease@invalid \ commit -m "chore: record $action_key php-bin readiness" head="$(git rev-parse HEAD)" - record="maintenance-events/$filename" + record="autorelease-events/$filename" digest="sha256:$(shasum -a 256 "$record" | awk '{print $1}')" gh auth setup-git git push origin HEAD - url="$(gh pr create --base main --head "maintenance/readiness-${{ github.run_id }}" \ + url="$(gh pr create --base main --head "autorelease/readiness-${{ github.run_id }}" \ --title "chore: record $action_key php-bin readiness" \ --body "Deterministic event state for the exact merged implementation commit.")" echo "number=${url##*/}" >> "$GITHUB_OUTPUT" @@ -453,8 +453,8 @@ jobs: ./scripts/dispatch-pr-checks \ --pr "${{ steps.readiness.outputs.number }}" \ --check "Script checks" \ - --output maintenance-run/readiness-checks.json - jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' maintenance-run/readiness-checks.json + --output autorelease-run/readiness-checks.json + jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' autorelease-run/readiness-checks.json actual="$(gh pr view "${{ steps.readiness.outputs.number }}" --json headRefOid --jq .headRefOid)" test "$actual" = "${{ steps.readiness.outputs.head_sha }}" git fetch origin main diff --git a/.github/workflows/maintenance-release.yml b/.github/workflows/autorelease-publish.yml similarity index 88% rename from .github/workflows/maintenance-release.yml rename to .github/workflows/autorelease-publish.yml index a27f926..fcd75e9 100644 --- a/.github/workflows/maintenance-release.yml +++ b/.github/workflows/autorelease-publish.yml @@ -1,4 +1,4 @@ -name: Guarded PHP release transaction +name: Autorelease publish transaction on: workflow_dispatch: @@ -12,7 +12,7 @@ on: required: true type: string action_key: - description: Exact admitted maintenance action key + description: Exact admitted autorelease action key required: true type: string investigation_run_id: @@ -29,14 +29,14 @@ permissions: statuses: write concurrency: - group: guarded-release-${{ inputs.version }} + group: autorelease-publish-${{ inputs.version }} cancel-in-progress: false jobs: release: runs-on: macos-26 timeout-minutes: 150 - environment: php-maintenance-release + environment: php-autorelease-publish steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -54,7 +54,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | gh run download "${{ inputs.investigation_run_id }}" \ - --name "maintenance-investigation-${{ inputs.investigation_run_id }}" \ + --name "autorelease-investigation-${{ inputs.investigation_run_id }}" \ --dir admitted-run - name: Verify stable version and exact commit env: @@ -63,18 +63,18 @@ jobs: EXACT_COMMIT: ${{ inputs.exact_commit }} ACTION_KEY: ${{ inputs.action_key }} run: | - ./maintenance/control.py validate-policy + ./autorelease/control.py validate-policy [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?$ ]] [[ "$EXACT_COMMIT" =~ ^[0-9a-f]{40}$ ]] [[ "$ACTION_KEY" =~ ^[A-Za-z0-9._:-]+$ ]] test "$(git rev-parse HEAD)" = "$EXACT_COMMIT" - test "$(jq -r .actionKey admitted-run/maintenance-plan.json)" = "$ACTION_KEY" - test "$(jq -r .releaseIntent.version admitted-run/maintenance-plan.json)" = "$VERSION" - test "$(jq -r .preconditions.phpBinHead admitted-run/maintenance-plan.json)" = "$EXACT_COMMIT" - test "$(jq -r .preconditions.supportPolicyDigest admitted-run/maintenance-plan.json)" = "$(./maintenance/control.py digest support-policy.json)" - test "$(jq -r .unattendedMutation .github/maintenance-operator.json)" = "enabled" + test "$(jq -r .actionKey admitted-run/autorelease-plan.json)" = "$ACTION_KEY" + test "$(jq -r .releaseIntent.version admitted-run/autorelease-plan.json)" = "$VERSION" + test "$(jq -r .preconditions.phpBinHead admitted-run/autorelease-plan.json)" = "$EXACT_COMMIT" + test "$(jq -r .preconditions.supportPolicyDigest admitted-run/autorelease-plan.json)" = "$(./autorelease/control.py digest support-policy.json)" + test "$(jq -r .unattendedMutation .github/autorelease-operator.json)" = "enabled" mkdir -p release-run - gh api "repos/${{ github.repository }}/contents/.github/maintenance-operator.json?ref=main" \ + gh api "repos/${{ github.repository }}/contents/.github/autorelease-operator.json?ref=main" \ --jq .content | base64 --decode > release-run/current-operator.json test "$(jq -r .unattendedMutation release-run/current-operator.json)" = "enabled" mise_commit="$(jq -r .sha admitted-run/evidence/raw/mise_php_state.body)" @@ -86,19 +86,19 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} run: | - ./scripts/capture-maintenance-evidence --output release-run/evidence - ./maintenance/control.py validate-recaptured-evidence \ - --plan admitted-run/maintenance-plan.json \ + ./scripts/capture-autorelease-evidence --output release-run/evidence + ./autorelease/control.py validate-recaptured-evidence \ + --plan admitted-run/autorelease-plan.json \ --admitted-manifest admitted-run/evidence/evidence-manifest.json \ --current-manifest release-run/evidence/evidence-manifest.json - name: Verify cross-repository readiness when required env: GH_TOKEN: ${{ github.token }} run: | - action="$(jq -r .action admitted-run/maintenance-plan.json)" + action="$(jq -r .action admitted-run/autorelease-plan.json)" if [[ "$action" == "new_branch" ]]; then filename="$(printf '%s' "${{ inputs.action_key }}" | tr ':/' '--').json" - event="maintenance-events/$filename" + event="autorelease-events/$filename" test -f "$event" test "$(jq -r .state "$event")" = "php_bin_ready" gh api "repos/bigpixelrocket/mise-php/contents/readiness/$filename?ref=main" \ @@ -121,7 +121,7 @@ jobs: mkdir -p .artifacts if gh release view "${{ inputs.version }}" --repo "${{ github.repository }}" >/dev/null 2>&1; then gh release download "${{ inputs.version }}" --repo "${{ github.repository }}" --dir .artifacts - ./scripts/validate-maintenance-archive \ + ./scripts/validate-autorelease-archive \ --archive ".artifacts/php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" \ --version "${{ inputs.version }}" archive_digest="$(shasum -a 256 ".artifacts/php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" | awk '{print $1}')" @@ -143,7 +143,7 @@ jobs: if: steps.existing.outputs.reuse != 'true' run: | ./scripts/package.sh ".build/${{ inputs.version }}/s4/buildroot/bin/php" "${{ inputs.version }}" - ./scripts/validate-maintenance-archive \ + ./scripts/validate-autorelease-archive \ --archive ".artifacts/php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" \ --version "${{ inputs.version }}" - name: Initialize release transaction and event @@ -151,29 +151,29 @@ jobs: mkdir -p release-run printf '{"schemaVersion":1,"state":"requested","history":[]}\n' > release-run/transaction.json filename="$(printf '%s' "${{ inputs.action_key }}" | tr ':/' '--').json" - if [[ -f "maintenance-events/$filename" ]]; then - cp "maintenance-events/$filename" release-run/event.json + if [[ -f "autorelease-events/$filename" ]]; then + cp "autorelease-events/$filename" release-run/event.json if [[ "$(jq -r .state release-run/event.json)" == "php_bin_ready" \ - && "$(jq -r .action admitted-run/maintenance-plan.json)" == "new_branch" ]]; then + && "$(jq -r .action admitted-run/autorelease-plan.json)" == "new_branch" ]]; then jq -n --slurpfile ready release-run/mise-readiness.json \ '[{kind:"mise_readiness",record:$ready[0]}]' > release-run/evidence.json - ./scripts/maintenance-event --event release-run/event.json --target mise_ready \ + ./scripts/autorelease-event --event release-run/event.json --target mise_ready \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json fi jq -n --arg planDigest "$(jq -r .planDigest admitted-run/admission.json)" \ '[{kind:"admitted_release_intent",planDigest:$planDigest}]' > release-run/evidence.json - ./scripts/maintenance-event --event release-run/event.json --target release_requested \ + ./scripts/autorelease-event --event release-run/event.json --target release_requested \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json else jq -n \ --arg actionKey "${{ inputs.action_key }}" \ - --arg classification "$(jq -r .action admitted-run/maintenance-plan.json)" \ + --arg classification "$(jq -r .action admitted-run/autorelease-plan.json)" \ --arg commit "${{ inputs.exact_commit }}" \ --arg planDigest "$(jq -r .planDigest admitted-run/admission.json)" \ --arg evidenceManifestDigest "$(jq -r .manifestDigest admitted-run/evidence/evidence-manifest.json)" \ - --argjson evidenceDigests "$(jq '[.evidence[].digest]' admitted-run/maintenance-plan.json)" \ + --argjson evidenceDigests "$(jq '[.evidence[].digest]' admitted-run/autorelease-plan.json)" \ '{schemaVersion:1,actionKey:$actionKey,classification:$classification,state:"release_requested",history:[],phpBinCommit:$commit,planDigest:$planDigest,evidenceManifestDigest:$evidenceManifestDigest,evidenceDigests:$evidenceDigests}' \ > release-run/event.json fi @@ -181,11 +181,11 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh api "repos/${{ github.repository }}/contents/.github/maintenance-operator.json?ref=main" \ + gh api "repos/${{ github.repository }}/contents/.github/autorelease-operator.json?ref=main" \ --jq .content | base64 --decode > release-run/current-operator.json test "$(jq -r .unattendedMutation release-run/current-operator.json)" = "enabled" for target in built draft_created draft_verified; do - ./scripts/release-maintenance \ + ./scripts/publish-release \ --transaction release-run/transaction.json \ --target "$target" \ --assets .artifacts \ @@ -203,7 +203,7 @@ jobs: temporary="$(mktemp -d)" gh release download "${{ inputs.version }}" --repo "${{ github.repository }}" --dir "$temporary" port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')" - ./scripts/serve-maintenance-artifact --port "$port" --assets "$temporary" --version "${{ inputs.version }}" & + ./scripts/serve-autorelease-artifact --port "$port" --assets "$temporary" --version "${{ inputs.version }}" & server_pid="$!" trap 'kill "$server_pid" 2>/dev/null || true' EXIT ready=false @@ -231,11 +231,11 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh api "repos/${{ github.repository }}/contents/.github/maintenance-operator.json?ref=main" \ + gh api "repos/${{ github.repository }}/contents/.github/autorelease-operator.json?ref=main" \ --jq .content | base64 --decode > release-run/current-operator.json test "$(jq -r .unattendedMutation release-run/current-operator.json)" = "enabled" for target in published public_verified complete; do - ./scripts/release-maintenance \ + ./scripts/publish-release \ --transaction release-run/transaction.json \ --target "$target" \ --assets .artifacts \ @@ -277,16 +277,16 @@ jobs: run: | jq -n --arg version "${{ inputs.version }}" --argjson assets "$(jq .assetDigests release-run/transaction.json)" \ '[{kind:"published_release",version:$version,assetDigests:$assets}]' > release-run/evidence.json - ./scripts/maintenance-event --event release-run/event.json --target released \ + ./scripts/autorelease-event --event release-run/event.json --target released \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json jq -n --arg version "${{ inputs.version }}" \ '[{kind:"fresh_public_mise_installs",version:$version,modes:["exact","branch_shorthand"]}]' > release-run/evidence.json - ./scripts/maintenance-event --event release-run/event.json --target public_install_verified \ + ./scripts/autorelease-event --event release-run/event.json --target public_install_verified \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json jq -n '[{kind:"transaction_complete"}]' > release-run/evidence.json - ./scripts/maintenance-event --event release-run/event.json --target complete \ + ./scripts/autorelease-event --event release-run/event.json --target complete \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json jq '.severity="info" | .summary="PHP ${{ inputs.version }} was published and verified through fresh exact and branch-shorthand mise installs." | .finalResult="passed"' \ @@ -297,19 +297,19 @@ jobs: GH_TOKEN: ${{ github.token }} run: | git fetch origin main - git checkout -B "maintenance/event-${{ github.run_id }}" origin/main + git checkout -B "autorelease/event-${{ github.run_id }}" origin/main base="$(git rev-parse HEAD)" filename="$(printf '%s' "${{ inputs.action_key }}" | tr ':/' '--').json" - cp release-run/event.json "maintenance-events/$filename" - git add "maintenance-events/$filename" - git -c user.name=maintenance-release -c user.email=maintenance@invalid \ + cp release-run/event.json "autorelease-events/$filename" + git add "autorelease-events/$filename" + git -c user.name=autorelease -c user.email=autorelease@invalid \ commit -m "chore: complete ${{ inputs.action_key }}" head="$(git rev-parse HEAD)" - record="maintenance-events/$filename" + record="autorelease-events/$filename" digest="sha256:$(shasum -a 256 "$record" | awk '{print $1}')" gh auth setup-git git push origin HEAD - url="$(gh pr create --base main --head "maintenance/event-${{ github.run_id }}" \ + url="$(gh pr create --base main --head "autorelease/event-${{ github.run_id }}" \ --title "chore: complete ${{ inputs.action_key }}" \ --body "Durable event record for the immutable verified release transaction.")" echo "number=${url##*/}" >> "$GITHUB_OUTPUT" @@ -338,13 +338,13 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - ./scripts/notify-maintenance \ + ./scripts/notify-autorelease \ --event release-run/notification-event.json \ --state release-run/notification-state.json \ --output release-run/notification-next.json \ --backend github \ --repo "${{ github.repository }}" \ - --owner "${{ vars.MAINTENANCE_OWNER }}" + --owner "${{ vars.AUTORELEASE_OWNER }}" - name: Record release failure diagnostic if: failure() env: @@ -406,7 +406,7 @@ jobs: [[ "$INVESTIGATION_RUN_ID" =~ ^[1-9][0-9]*$ ]] jq -n \ --arg actionKey "$ACTION_KEY" \ - --arg summary "Guarded release failed for PHP $VERSION: $RUN_URL" \ + --arg summary "Autorelease failed for PHP $VERSION: $RUN_URL" \ --arg failureFingerprint "release-failure:$ACTION_KEY" \ '{actionKey:$actionKey,state:"blocked",severity:"critical",humanActionRequired:false,summary:$summary,failureFingerprint:$failureFingerprint}' \ > event.json @@ -414,10 +414,10 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - ./scripts/notify-maintenance \ + ./scripts/notify-autorelease \ --event event.json \ --state notification-state.json \ --output notification.json \ --backend github \ --repo "${{ github.repository }}" \ - --owner "${{ vars.MAINTENANCE_OWNER }}" + --owner "${{ vars.AUTORELEASE_OWNER }}" diff --git a/.github/workflows/maintenance-watch.yml b/.github/workflows/autorelease-watch.yml similarity index 64% rename from .github/workflows/maintenance-watch.yml rename to .github/workflows/autorelease-watch.yml index 8684eb6..42c2caa 100644 --- a/.github/workflows/maintenance-watch.yml +++ b/.github/workflows/autorelease-watch.yml @@ -1,4 +1,4 @@ -name: PHP maintenance watcher +name: PHP autorelease watcher on: schedule: @@ -9,7 +9,7 @@ permissions: contents: read concurrency: - group: php-maintenance-watcher + group: php-autorelease-watcher cancel-in-progress: false jobs: @@ -32,39 +32,39 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} run: | - ./maintenance/control.py validate-policy - ./scripts/capture-maintenance-evidence --output maintenance-run/evidence + ./autorelease/control.py validate-policy + ./scripts/capture-autorelease-evidence --output autorelease-run/evidence - name: Decide whether an agent call is required id: decision run: | self_update=() - if [[ "$(git diff-tree --no-commit-id --name-only -r HEAD)" == "maintenance-state/last-evidence.json" ]]; then + if [[ "$(git diff-tree --no-commit-id --name-only -r HEAD)" == "autorelease-state/last-evidence.json" ]]; then self_update=(--self-evidence-update) fi - ./scripts/watch-maintenance-evidence \ - --manifest maintenance-run/evidence/evidence-manifest.json \ - --previous maintenance-state/last-evidence.json \ - --events maintenance-events \ + ./scripts/watch-autorelease-evidence \ + --manifest autorelease-run/evidence/evidence-manifest.json \ + --previous autorelease-state/last-evidence.json \ + --events autorelease-events \ "${self_update[@]}" \ - --output maintenance-run/watch-decision.json - echo "trigger=$(jq -r .trigger maintenance-run/watch-decision.json)" >> "$GITHUB_OUTPUT" + --output autorelease-run/watch-decision.json + echo "trigger=$(jq -r .trigger autorelease-run/watch-decision.json)" >> "$GITHUB_OUTPUT" - name: Record exact preconditions if: steps.decision.outputs.trigger != 'quiet' run: | jq -n \ --arg phpBinHead "$(git rev-parse HEAD)" \ - --arg misePhpHead "$(jq -r .sha maintenance-run/evidence/raw/mise_php_state.body)" \ - --arg supportPolicyDigest "$(./maintenance/control.py digest support-policy.json)" \ + --arg misePhpHead "$(jq -r .sha autorelease-run/evidence/raw/mise_php_state.body)" \ + --arg supportPolicyDigest "$(./autorelease/control.py digest support-policy.json)" \ '{phpBinHead:$phpBinHead,misePhpHead:$misePhpHead,supportPolicyDigest:$supportPolicyDigest}' \ - > maintenance-run/preconditions.json - [[ "$(jq -r .misePhpHead maintenance-run/preconditions.json)" =~ ^[0-9a-f]{40}$ ]] + > autorelease-run/preconditions.json + [[ "$(jq -r .misePhpHead autorelease-run/preconditions.json)" =~ ^[0-9a-f]{40}$ ]] shopt -s nullglob - event_files=(maintenance-events/*.json) + event_files=(autorelease-events/*.json) if (( ${#event_files[@]} )); then jq -s '[.[] | select(.state=="complete") | .actionKey] | sort | unique' \ - "${event_files[@]}" > maintenance-run/completed-actions.json + "${event_files[@]}" > autorelease-run/completed-actions.json else - printf '[]\n' > maintenance-run/completed-actions.json + printf '[]\n' > autorelease-run/completed-actions.json fi - name: Prepare investigation contract if: steps.decision.outputs.trigger != 'quiet' @@ -72,8 +72,8 @@ jobs: ./scripts/prepare-agent-task \ --phase investigation \ --action-key "investigate:${{ steps.decision.outputs.trigger }}:${{ github.run_id }}" \ - --goal "Classify the captured maintenance trigger and emit one evidence-bound plan." \ - --preconditions maintenance-run/preconditions.json \ + --goal "Classify the captured autorelease trigger and emit one evidence-bound plan." \ + --preconditions autorelease-run/preconditions.json \ --allowed-authority read_repository \ --allowed-authority read_captured_evidence \ --allowed-authority allowlisted_web_search \ @@ -83,10 +83,10 @@ jobs: --stop-condition missing_or_contradictory_required_evidence \ --stop-condition changed_precondition \ --stop-condition required_protected_change \ - --shared .github/codex/maintenance/shared.md \ - --phase-template .github/codex/maintenance/investigation.md \ - --contract-output maintenance-run/event-contract.json \ - --prompt-output maintenance-run/investigation-prompt.md + --shared .github/codex/autorelease/shared.md \ + --phase-template .github/codex/autorelease/investigation.md \ + --contract-output autorelease-run/event-contract.json \ + --prompt-output autorelease-run/investigation-prompt.md mkdir -p "$RUNNER_TEMP/codex-home" cp .codex/investigation.config.toml "$RUNNER_TEMP/codex-home/config.toml" - name: Run read-only Codex investigation @@ -95,44 +95,44 @@ jobs: with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} codex-version: 0.145.0 - prompt-file: maintenance-run/investigation-prompt.md - output-file: maintenance-run/maintenance-plan.json + prompt-file: autorelease-run/investigation-prompt.md + output-file: autorelease-run/autorelease-plan.json codex-home: ${{ runner.temp }}/codex-home sandbox: read-only safety-strategy: drop-sudo allow-users: loadinglucian allow-bots: false allow-bot-users: github-actions[bot] - codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/maintenance-plan.schema.json"]' + codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/autorelease-plan.schema.json"]' - name: Admit plan against exact evidence if: steps.decision.outputs.trigger != 'quiet' run: | - ./scripts/admit-maintenance-plan \ - --plan maintenance-run/maintenance-plan.json \ - --manifest maintenance-run/evidence/evidence-manifest.json \ - --contract maintenance-run/event-contract.json \ - --shared .github/codex/maintenance/shared.md \ - --phase .github/codex/maintenance/investigation.md \ - --event-contract maintenance-run/event-contract.json \ + ./scripts/admit-autorelease-plan \ + --plan autorelease-run/autorelease-plan.json \ + --manifest autorelease-run/evidence/evidence-manifest.json \ + --contract autorelease-run/event-contract.json \ + --shared .github/codex/autorelease/shared.md \ + --phase .github/codex/autorelease/investigation.md \ + --event-contract autorelease-run/event-contract.json \ --php-bin-head "$(git rev-parse HEAD)" \ - --mise-php-head "$(jq -r .misePhpHead maintenance-run/preconditions.json)" \ - --policy-digest "$(./maintenance/control.py digest support-policy.json)" \ - --completed-actions maintenance-run/completed-actions.json \ - --output maintenance-run/admission.json + --mise-php-head "$(jq -r .misePhpHead autorelease-run/preconditions.json)" \ + --policy-digest "$(./autorelease/control.py digest support-policy.json)" \ + --completed-actions autorelease-run/completed-actions.json \ + --output autorelease-run/admission.json - name: Expose admitted plan id: plan if: steps.decision.outputs.trigger != 'quiet' run: | - echo "action_key=$(jq -r .actionKey maintenance-run/maintenance-plan.json)" >> "$GITHUB_OUTPUT" - echo "edits_required=$(jq -r .editsRequired maintenance-run/maintenance-plan.json)" >> "$GITHUB_OUTPUT" - echo "base_sha=$(jq -r .preconditions.phpBinHead maintenance-run/maintenance-plan.json)" >> "$GITHUB_OUTPUT" - echo "action=$(jq -r .action maintenance-run/maintenance-plan.json)" >> "$GITHUB_OUTPUT" + echo "action_key=$(jq -r .actionKey autorelease-run/autorelease-plan.json)" >> "$GITHUB_OUTPUT" + echo "edits_required=$(jq -r .editsRequired autorelease-run/autorelease-plan.json)" >> "$GITHUB_OUTPUT" + echo "base_sha=$(jq -r .preconditions.phpBinHead autorelease-run/autorelease-plan.json)" >> "$GITHUB_OUTPUT" + echo "action=$(jq -r .action autorelease-run/autorelease-plan.json)" >> "$GITHUB_OUTPUT" - name: Retain evidence and admitted plan if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: maintenance-investigation-${{ github.run_id }} - path: maintenance-run/ + name: autorelease-investigation-${{ github.run_id }} + path: autorelease-run/ if-no-files-found: error retention-days: 90 include-hidden-files: true @@ -165,27 +165,27 @@ jobs: run: | gh run download "${{ github.run_id }}" \ --repo "${{ github.repository }}" \ - --name "maintenance-investigation-${{ github.run_id }}" \ - --dir maintenance-plan-download + --name "autorelease-investigation-${{ github.run_id }}" \ + --dir autorelease-plan-download - name: Read unattended mutation state id: operator run: | - test "$(jq -r .unattendedMutation .github/maintenance-operator.json)" = "enabled" \ + test "$(jq -r .unattendedMutation .github/autorelease-operator.json)" = "enabled" \ && echo "enabled=true" >> "$GITHUB_OUTPUT" \ || echo "enabled=false" >> "$GITHUB_OUTPUT" - name: Prepare deterministic no-change evidence id: evidence if: needs.investigate.outputs.action == 'no_change' && steps.operator.outputs.enabled == 'true' run: | - git checkout -B "maintenance/evidence-${{ github.run_id }}" origin/main - mkdir -p maintenance-state + git checkout -B "autorelease/evidence-${{ github.run_id }}" origin/main + mkdir -p autorelease-state jq -n \ - --arg manifestDigest "$(jq -r .manifestDigest maintenance-plan-download/evidence/evidence-manifest.json)" \ - --arg planDigest "$(jq -r .planDigest maintenance-plan-download/admission.json)" \ - --argjson captureDigests "$(jq '[.captures[] | {captureId,digest,status}]' maintenance-plan-download/evidence/evidence-manifest.json)" \ + --arg manifestDigest "$(jq -r .manifestDigest autorelease-plan-download/evidence/evidence-manifest.json)" \ + --arg planDigest "$(jq -r .planDigest autorelease-plan-download/admission.json)" \ + --argjson captureDigests "$(jq '[.captures[] | {captureId,digest,status}]' autorelease-plan-download/evidence/evidence-manifest.json)" \ '{schemaVersion:1,manifestDigest:$manifestDigest,planDigest:$planDigest,captures:$captureDigests}' \ - > maintenance-state/last-evidence.json - if [[ -z "$(git status --porcelain -- maintenance-state/last-evidence.json)" ]]; then + > autorelease-state/last-evidence.json + if [[ -z "$(git status --porcelain -- autorelease-state/last-evidence.json)" ]]; then echo "already_recorded=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -194,22 +194,22 @@ jobs: --arg runId "${{ github.run_id }}" \ --arg sourceSha "${{ needs.investigate.outputs.base_sha }}" \ --arg actionKey "${{ needs.investigate.outputs.action_key }}" \ - --arg manifestDigest "$(jq -r .manifestDigest maintenance-plan-download/evidence/evidence-manifest.json)" \ + --arg manifestDigest "$(jq -r .manifestDigest autorelease-plan-download/evidence/evidence-manifest.json)" \ '{schemaVersion:1,runId:$runId,sourceSha:$sourceSha,actionKey:$actionKey,manifestDigest:$manifestDigest}' \ - > maintenance-plan-download/evidence-attestation-predicate.json + > autorelease-plan-download/evidence-attestation-predicate.json - name: Attest deterministic no-change evidence if: needs.investigate.outputs.action == 'no_change' && steps.operator.outputs.enabled == 'true' && steps.evidence.outputs.already_recorded == 'false' uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4 with: - subject-path: maintenance-state/last-evidence.json - predicate-type: https://bigpixelrocket.dev/maintenance/evidence-state/v1 - predicate-path: maintenance-plan-download/evidence-attestation-predicate.json + subject-path: autorelease-state/last-evidence.json + predicate-type: https://bigpixelrocket.dev/autorelease/evidence-state/v1 + predicate-path: autorelease-plan-download/evidence-attestation-predicate.json - name: Dispatch implementation or no-edit release env: EVIDENCE_ALREADY_RECORDED: ${{ steps.evidence.outputs.already_recorded }} GH_TOKEN: ${{ github.token }} run: | - if [[ "$(jq -r .unattendedMutation .github/maintenance-operator.json)" != "enabled" ]]; then + if [[ "$(jq -r .unattendedMutation .github/autorelease-operator.json)" != "enabled" ]]; then echo "Unattended mutation is paused; retained investigation remains read-only." exit 0 fi @@ -222,42 +222,42 @@ jobs: jq -n \ --arg actionKey "${{ needs.investigate.outputs.action_key }}" \ --arg state "$action" \ - --arg summary "$(jq -r .summary maintenance-plan-download/maintenance-plan.json)" \ - --arg evidenceDigest "$(jq -r .manifestDigest maintenance-plan-download/evidence/evidence-manifest.json)" \ + --arg summary "$(jq -r .summary autorelease-plan-download/autorelease-plan.json)" \ + --arg evidenceDigest "$(jq -r .manifestDigest autorelease-plan-download/evidence/evidence-manifest.json)" \ '{actionKey:$actionKey,state:$state,severity:"warning",humanActionRequired:($state=="needs_human"),summary:$summary,evidenceDigest:$evidenceDigest}' \ - > maintenance-plan-download/notification-event.json - ./scripts/notify-maintenance \ - --event maintenance-plan-download/notification-event.json \ - --state maintenance-plan-download/notification-state.json \ - --output maintenance-plan-download/notification-next.json \ + > autorelease-plan-download/notification-event.json + ./scripts/notify-autorelease \ + --event autorelease-plan-download/notification-event.json \ + --state autorelease-plan-download/notification-state.json \ + --output autorelease-plan-download/notification-next.json \ --backend github \ --repo "${{ github.repository }}" \ - --owner "${{ vars.MAINTENANCE_OWNER }}" + --owner "${{ vars.AUTORELEASE_OWNER }}" exit 0 elif [[ "$action" == "new_branch" || "$action" == "branch_eol" ]]; then jq -n \ --arg actionKey "${{ needs.investigate.outputs.action_key }}" \ - --arg summary "$(jq -r .notification.summary maintenance-plan-download/maintenance-plan.json)" \ - --arg evidenceDigest "$(jq -r .manifestDigest maintenance-plan-download/evidence/evidence-manifest.json)" \ + --arg summary "$(jq -r .notification.summary autorelease-plan-download/autorelease-plan.json)" \ + --arg evidenceDigest "$(jq -r .manifestDigest autorelease-plan-download/evidence/evidence-manifest.json)" \ '{actionKey:$actionKey,state:"detected",severity:"info",humanActionRequired:false,summary:$summary,evidenceDigest:$evidenceDigest}' \ - > maintenance-plan-download/lifecycle-notification.json - ./scripts/notify-maintenance \ - --event maintenance-plan-download/lifecycle-notification.json \ - --state maintenance-plan-download/lifecycle-notification-state.json \ - --output maintenance-plan-download/lifecycle-notification-next.json \ + > autorelease-plan-download/lifecycle-notification.json + ./scripts/notify-autorelease \ + --event autorelease-plan-download/lifecycle-notification.json \ + --state autorelease-plan-download/lifecycle-notification-state.json \ + --output autorelease-plan-download/lifecycle-notification-next.json \ --backend github \ --repo "${{ github.repository }}" \ - --owner "${{ vars.MAINTENANCE_OWNER }}" + --owner "${{ vars.AUTORELEASE_OWNER }}" fi if [[ "$action" == "no_change" ]]; then - branch="maintenance/evidence-${{ github.run_id }}" + branch="autorelease/evidence-${{ github.run_id }}" base="$(git rev-parse HEAD)" - git add maintenance-state/last-evidence.json - git -c user.name=maintenance-watcher -c user.email=maintenance@invalid \ - commit -m "chore: record reviewed maintenance evidence" + git add autorelease-state/last-evidence.json + git -c user.name=autorelease-watcher -c user.email=autorelease@invalid \ + commit -m "chore: record reviewed autorelease evidence" head="$(git rev-parse HEAD)" - record_digest="sha256:$(shasum -a 256 maintenance-state/last-evidence.json | awk '{print $1}')" + record_digest="sha256:$(shasum -a 256 autorelease-state/last-evidence.json | awk '{print $1}')" gh auth setup-git if git ls-remote --exit-code --heads origin "$branch" >/dev/null; then git fetch origin "$branch:refs/remotes/origin/$branch" @@ -266,26 +266,26 @@ jobs: number="$(gh pr list --state open --head "$branch" --json number --jq '.[0].number // empty')" if [[ -z "$number" ]]; then url="$(gh pr create --base main --head "$branch" \ - --title "chore: record reviewed maintenance evidence" \ + --title "chore: record reviewed autorelease evidence" \ --body "Opaque evidence state for a reviewed no-change result.")" number="${url##*/}" fi ./scripts/dispatch-pr-checks \ --pr "$number" \ --check "Script checks" \ - --output maintenance-plan-download/no-change-checks.json - jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' maintenance-plan-download/no-change-checks.json - jq -e '[.[] | select(.name=="Protected controls") | .bucket] == ["pass"]' maintenance-plan-download/no-change-checks.json + --output autorelease-plan-download/no-change-checks.json + jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' autorelease-plan-download/no-change-checks.json + jq -e '[.[] | select(.name=="Protected controls") | .bucket] == ["pass"]' autorelease-plan-download/no-change-checks.json test "$(gh pr view "$number" --json headRefOid --jq .headRefOid)" = "$head" git fetch origin main test "$(git rev-parse origin/main)" = "$base" test "$(git rev-list --parents -n 1 "$head")" = "$head $base" - test "$(git diff --name-only "$base" "$head")" = "maintenance-state/last-evidence.json" - test "sha256:$(git show "$head:maintenance-state/last-evidence.json" | shasum -a 256 | awk '{print $1}')" = "$record_digest" + test "$(git diff --name-only "$base" "$head")" = "autorelease-state/last-evidence.json" + test "sha256:$(git show "$head:autorelease-state/last-evidence.json" | shasum -a 256 | awk '{print $1}')" = "$record_digest" gh pr merge "$number" --squash --delete-branch exit 0 elif [[ "${{ needs.investigate.outputs.edits_required }}" == "true" ]]; then - gh workflow run maintenance-implementation.yml \ + gh workflow run autorelease-implement.yml \ --repo "${{ github.repository }}" \ --ref main \ -f investigation_run_id="${{ github.run_id }}" \ @@ -299,8 +299,8 @@ jobs: exit 0 fi fi - version="$(jq -r .releaseIntent.version maintenance-plan-download/maintenance-plan.json)" - gh workflow run maintenance-release.yml \ + version="$(jq -r .releaseIntent.version autorelease-plan-download/autorelease-plan.json)" + gh workflow run autorelease-publish.yml \ --repo "${{ github.repository }}" \ --ref main \ -f action_key="${{ needs.investigate.outputs.action_key }}" \ @@ -311,40 +311,40 @@ jobs: action_key="${{ needs.investigate.outputs.action_key }}" filename="$(printf '%s' "$action_key" | tr ':/' '--').json" if ! gh api "repos/bigpixelrocket/mise-php/contents/readiness/$filename?ref=main" \ - --jq .content > maintenance-plan-download/mise-readiness.b64; then + --jq .content > autorelease-plan-download/mise-readiness.b64; then echo "Waiting for exact mise-php EOL readiness for $filename." exit 0 fi - base64 --decode < maintenance-plan-download/mise-readiness.b64 \ - > maintenance-plan-download/mise-readiness.json - event="maintenance-events/$filename" + base64 --decode < autorelease-plan-download/mise-readiness.b64 \ + > autorelease-plan-download/mise-readiness.json + event="autorelease-events/$filename" test -f "$event" - test "$(jq -r .actionKey maintenance-plan-download/mise-readiness.json)" = "$action_key" - test "$(jq -r .ready maintenance-plan-download/mise-readiness.json)" = "true" - test "$(jq -r .supportPolicyDigest "$event")" = "$(jq -r .policyDigest maintenance-plan-download/mise-readiness.json)" - test "$(jq -r .policyInvariantsDigest "$event")" = "$(jq -r .policyInvariantsDigest maintenance-plan-download/mise-readiness.json)" - policy_commit="$(jq -r .phpBinPolicyCommit maintenance-plan-download/mise-readiness.json)" + test "$(jq -r .actionKey autorelease-plan-download/mise-readiness.json)" = "$action_key" + test "$(jq -r .ready autorelease-plan-download/mise-readiness.json)" = "true" + test "$(jq -r .supportPolicyDigest "$event")" = "$(jq -r .policyDigest autorelease-plan-download/mise-readiness.json)" + test "$(jq -r .policyInvariantsDigest "$event")" = "$(jq -r .policyInvariantsDigest autorelease-plan-download/mise-readiness.json)" + policy_commit="$(jq -r .phpBinPolicyCommit autorelease-plan-download/mise-readiness.json)" gh api "repos/${{ github.repository }}/contents/support-policy.json?ref=$policy_commit" \ - --jq .content | base64 --decode > maintenance-plan-download/readiness-policy.json - test "sha256:$(shasum -a 256 maintenance-plan-download/readiness-policy.json | awk '{print $1}')" = "$(jq -r .policyDigest maintenance-plan-download/mise-readiness.json)" - jq -n --slurpfile ready maintenance-plan-download/mise-readiness.json \ - '[{kind:"mise_eol_readiness",record:$ready[0]}]' > maintenance-plan-download/transition-evidence.json + --jq .content | base64 --decode > autorelease-plan-download/readiness-policy.json + test "sha256:$(shasum -a 256 autorelease-plan-download/readiness-policy.json | awk '{print $1}')" = "$(jq -r .policyDigest autorelease-plan-download/mise-readiness.json)" + jq -n --slurpfile ready autorelease-plan-download/mise-readiness.json \ + '[{kind:"mise_eol_readiness",record:$ready[0]}]' > autorelease-plan-download/transition-evidence.json if [[ "$(jq -r .state "$event")" == "php_bin_ready" ]]; then - ./scripts/maintenance-event --event "$event" --target mise_ready \ - --evidence maintenance-plan-download/transition-evidence.json --output "$event.next" + ./scripts/autorelease-event --event "$event" --target mise_ready \ + --evidence autorelease-plan-download/transition-evidence.json --output "$event.next" mv "$event.next" "$event" fi test "$(jq -r .state "$event")" = "mise_ready" jq -n '[{kind:"eol_support_and_publication_cleanup_verified"}]' \ - > maintenance-plan-download/transition-evidence.json - ./scripts/maintenance-event --event "$event" --target complete \ - --evidence maintenance-plan-download/transition-evidence.json --output "$event.next" + > autorelease-plan-download/transition-evidence.json + ./scripts/autorelease-event --event "$event" --target complete \ + --evidence autorelease-plan-download/transition-evidence.json --output "$event.next" mv "$event.next" "$event" base="$(git rev-parse HEAD)" - branch="maintenance/eol-complete-${{ github.run_id }}" + branch="autorelease/eol-complete-${{ github.run_id }}" git checkout -B "$branch" git add "$event" - git -c user.name=maintenance-lifecycle -c user.email=maintenance@invalid \ + git -c user.name=autorelease-lifecycle -c user.email=autorelease@invalid \ commit -m "chore: complete $action_key" head="$(git rev-parse HEAD)" digest="sha256:$(shasum -a 256 "$event" | awk '{print $1}')" @@ -356,9 +356,9 @@ jobs: ./scripts/dispatch-pr-checks \ --pr "$number" \ --check "Script checks" \ - --output maintenance-plan-download/eol-checks.json - jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' maintenance-plan-download/eol-checks.json - jq -e '[.[] | select(.name=="Protected controls") | .bucket] == ["pass"]' maintenance-plan-download/eol-checks.json + --output autorelease-plan-download/eol-checks.json + jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' autorelease-plan-download/eol-checks.json + jq -e '[.[] | select(.name=="Protected controls") | .bucket] == ["pass"]' autorelease-plan-download/eol-checks.json test "$(gh pr view "$number" --json headRefOid --jq .headRefOid)" = "$head" git fetch origin main test "$(git rev-parse origin/main)" = "$base" @@ -367,12 +367,12 @@ jobs: test "sha256:$(git show "$head:$event" | shasum -a 256 | awk '{print $1}')" = "$digest" gh pr merge "$number" --squash --delete-branch jq '.severity="info" | .summary="Branch EOL support cleanup completed at exact php-bin and mise-php commits." | .finalResult="passed"' \ - "$event" > maintenance-plan-download/eol-notification.json - ./scripts/notify-maintenance \ - --event maintenance-plan-download/eol-notification.json \ - --state maintenance-plan-download/eol-notification-state.json \ - --output maintenance-plan-download/eol-notification-next.json \ - --backend github --repo "${{ github.repository }}" --owner "${{ vars.MAINTENANCE_OWNER }}" + "$event" > autorelease-plan-download/eol-notification.json + ./scripts/notify-autorelease \ + --event autorelease-plan-download/eol-notification.json \ + --state autorelease-plan-download/eol-notification-state.json \ + --output autorelease-plan-download/eol-notification-next.json \ + --backend github --repo "${{ github.repository }}" --owner "${{ vars.AUTORELEASE_OWNER }}" else echo "Action $action requires repository readiness before release." >&2 fi @@ -394,17 +394,17 @@ jobs: run: | jq -n \ --arg actionKey "watcher-failure:${{ github.run_id }}" \ - --arg summary "Maintenance watcher failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + --arg summary "Autorelease watcher failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ '{actionKey:$actionKey,state:"blocked",severity:"critical",humanActionRequired:true,summary:$summary,failureFingerprint:$actionKey}' \ > event.json - name: Notify owner env: GH_TOKEN: ${{ github.token }} run: | - ./scripts/notify-maintenance \ + ./scripts/notify-autorelease \ --event event.json \ - --state "maintenance-events/notification-watcher-failure-${{ github.run_id }}.json" \ + --state "autorelease-events/notification-watcher-failure-${{ github.run_id }}.json" \ --output notification.json \ --backend github \ --repo "${{ github.repository }}" \ - --owner "${{ vars.MAINTENANCE_OWNER }}" + --owner "${{ vars.AUTORELEASE_OWNER }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5690ff7..f355c5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,8 @@ name: CI on: pull_request: paths-ignore: - - maintenance-state/** - - maintenance-events/** + - autorelease-state/** + - autorelease-events/** workflow_dispatch: push: branches: diff --git a/.github/workflows/protected-controls.yml b/.github/workflows/protected-controls.yml index 08ddf27..a6f5b89 100644 --- a/.github/workflows/protected-controls.yml +++ b/.github/workflows/protected-controls.yml @@ -3,8 +3,8 @@ name: Protected controls on: pull_request_target: paths-ignore: - - maintenance-state/** - - maintenance-events/** + - autorelease-state/** + - autorelease-events/** workflow_dispatch: inputs: pr_number: @@ -60,7 +60,7 @@ jobs: HEAD_REF: ${{ steps.pr.outputs.head_ref }} HEAD_REPOSITORY: ${{ steps.pr.outputs.head_repository }} PR_AUTHOR: ${{ steps.pr.outputs.author }} - PROTECTED_REVIEWER: ${{ vars.MAINTENANCE_OWNER }} + PROTECTED_REVIEWER: ${{ vars.AUTORELEASE_OWNER }} run: | python3 - <<'PY' import fnmatch @@ -74,7 +74,7 @@ jobs: import tempfile import time - from maintenance.control import ( + from autorelease.control import ( ControlError, validate_completed_event_record, validate_evidence_attestation_predicate, @@ -110,7 +110,7 @@ jobs: head_repo = os.environ["HEAD_REPOSITORY"] author = os.environ["PR_AUTHOR"] 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( @@ -122,9 +122,9 @@ jobs: print("No protected control path changed.") raise SystemExit(0) - evidence_run = re.fullmatch(r"maintenance/evidence-(\d+)", head_ref) + evidence_run = re.fullmatch(r"autorelease/evidence-(\d+)", head_ref) if ( - protected == ["maintenance-state/last-evidence.json"] + protected == ["autorelease-state/last-evidence.json"] and evidence_run and author == "github-actions[bot]" and head_repo.lower() == repo.lower() @@ -132,7 +132,7 @@ jobs: commit = api_one(f"repos/{repo}/commits/{head}") run = api_one(f"repos/{repo}/actions/runs/{evidence_run.group(1)}") content = api_one( - f"repos/{repo}/contents/maintenance-state/last-evidence.json?ref={head}" + f"repos/{repo}/contents/autorelease-state/last-evidence.json?ref={head}" ) try: encoded = content["content"].replace("\n", "") @@ -144,7 +144,7 @@ jobs: raise SystemExit(1) from error direct_parent = [parent.get("sha") for parent in commit.get("parents", [])] == [base] trusted_run = ( - run.get("path") == ".github/workflows/maintenance-watch.yml" + run.get("path") == ".github/workflows/autorelease-watch.yml" and run.get("event") in {"schedule", "workflow_dispatch"} and run.get("head_branch") == "main" and run.get("head_sha") == base @@ -163,13 +163,13 @@ jobs: "--repo", repo, "--signer-workflow", - f"{repo}/.github/workflows/maintenance-watch.yml", + f"{repo}/.github/workflows/autorelease-watch.yml", "--source-ref", "refs/heads/main", "--source-digest", base, "--predicate-type", - "https://bigpixelrocket.dev/maintenance/evidence-state/v1", + "https://bigpixelrocket.dev/autorelease/evidence-state/v1", "--deny-self-hosted-runners", "--format", "json", @@ -222,10 +222,10 @@ jobs: print("No attestation matched the exact watcher run and evidence state.", file=sys.stderr) raise SystemExit(1) - event_run = re.fullmatch(r"maintenance/(event|eol-complete)-(\d+)", head_ref) + event_run = re.fullmatch(r"autorelease/(event|eol-complete)-(\d+)", head_ref) if ( len(protected) == 1 - and re.fullmatch(r"maintenance-events/[A-Za-z0-9._-]+\.json", protected[0]) + and re.fullmatch(r"autorelease-events/[A-Za-z0-9._-]+\.json", protected[0]) and event_run and author == "github-actions[bot]" and head_repo.lower() == repo.lower() @@ -238,18 +238,18 @@ jobs: record = json.loads(decoded) validate_completed_event_record(record) except (KeyError, ValueError, json.JSONDecodeError, ControlError) as error: - print(f"Invalid completed maintenance event: {error}", file=sys.stderr) + print(f"Invalid completed autorelease event: {error}", file=sys.stderr) raise SystemExit(1) from error expected_filename = record["actionKey"].translate(str.maketrans({":": "-", "/": "-"})) + ".json" expected_workflow = ( - ".github/workflows/maintenance-release.yml" + ".github/workflows/autorelease-publish.yml" if event_run.group(1) == "event" - else ".github/workflows/maintenance-watch.yml" + else ".github/workflows/autorelease-watch.yml" ) allowed_events = {"workflow_dispatch"} if event_run.group(1) == "event" else {"schedule", "workflow_dispatch"} direct_parent = [parent.get("sha") for parent in commit.get("parents", [])] == [base] trusted_run = ( - protected[0] == f"maintenance-events/{expected_filename}" + protected[0] == f"autorelease-events/{expected_filename}" and run.get("path") == expected_workflow and run.get("event") in allowed_events and run.get("head_branch") == "main" diff --git a/.gitignore b/.gitignore index c3d0c75..a4caa10 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ __pycache__/ buildroot/ downloads/ log/ -maintenance-run/ +autorelease-run/ pkgroot/ source/ verification-results/ diff --git a/AUTORELEASE.md b/AUTORELEASE.md new file mode 100644 index 0000000..65264b4 --- /dev/null +++ b/AUTORELEASE.md @@ -0,0 +1,112 @@ +# Autorelease + +How this repository detects new upstream PHP releases and lifecycle +changes, prepares bounded repository work, and publishes immutable, +verified macOS 26 arm64 CLI binaries without a human in the loop. + +`PHP autorelease watcher` runs daily and can also be dispatched manually. It +captures the raw PHP lifecycle page, release feed, php-src tags, and public +state of both repositories, including response metadata and SHA-256 digests. +The watcher compares only opaque digests and incomplete-event state. An +unchanged healthy day is quiet: it makes no model call and causes no issue, +repository, tag, asset, or release mutation. + +When evidence changes, the pinned official Codex Action investigates from a +read-only checkout. Web search is limited to `php.net`, `github.com`, and +`docs.github.com`; material release and lifecycle claims must still resolve to +the retained raw captures. A separate offline Codex invocation may edit only +paths admitted by the evidence-bound plan. It has no GitHub write credential +and cannot change the prompts, contracts, workflows, policy, admission, +sealing, merge, or release controls. + +```mermaid +flowchart TD + capture["Capture fixed raw evidence"] --> changed{"Digest or health changed?"} + changed -- "No" --> quiet["Quiet: no model call or mutation"] + changed -- "Yes" --> investigate["Read-only Codex investigation"] + investigate --> admit["Deterministic plan admission"] + admit --> edit{"Repository edit required?"} + edit -- "Yes" --> implement["Offline Codex implementation"] + implement --> seal["Seal admitted paths and digests"] + seal --> validate["Clean checkout validation"] + validate --> merge["Exact-SHA PR and merge admission"] + edit -- "No" --> release + merge --> release["Immutable release transaction"] + release --> draft["Verify draft bytes and temporary install"] + draft --> public["Publish unchanged bytes and verify public installs"] +``` + +The release transaction is the only component allowed to create an annotated +tag, draft, assets, or publication. It advances one legal state at a time, +reconciles existing state before acting, never rebuilds under an existing tag, +and never overwrites, deletes, or retags a published release. A first release +on a new PHP branch also requires exact-commit `php_bin_ready` and `mise_ready` +records. + +Failures use one deduplicated issue per action key, assigned to the username in +`AUTORELEASE_OWNER`. Only a meaningful state, evidence, fingerprint, required +action, or final-result change adds a comment. Critical failures stop mutation. +GitHub Actions failure email is an independent fallback. + +```mermaid +flowchart TD + job["Any autorelease phase"] --> result{"Result"} + result -- "Success" --> transition["Record evidence-backed transition"] + result -- "Retryable failure" --> retry{"Bounded retry remains?"} + retry -- "Yes" --> repair["Offline Codex repair"] + retry -- "No" --> blocked["Stop as blocked or needs_human"] + result -- "Critical or policy failure" --> blocked + blocked --> issue["Create or update one assigned issue"] + issue --> email["GitHub inbox and email"] + issue --> actions["Actions failure email fallback"] +``` + +Unattended mutation is controlled by +`.github/autorelease-operator.json`. Set `unattendedMutation` to `paused` in a +reviewed protected-path PR to stop implementation, merge, and release while +leaving read-only evidence capture and investigation available. Re-enable it +through another reviewed PR; an incomplete event then resumes only through its +single legal next transition. + +Maintainer commands: + +```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 \ + --mise-php-sha \ + --output ./verification-results + +gh workflow run autorelease-e2e.yml \ + --repo bigpixelrocket/php-bin \ + --ref \ + -f php_bin_sha= \ + -f mise_php_sha= \ + -f suite=production-parity + +# After the reviewed php-bin commit is merged to main, exercise the actual +# pinned Codex Action and repository API key inside the protected canary environment. +gh workflow run autorelease-e2e.yml \ + --repo bigpixelrocket/php-bin \ + --ref main \ + -f php_bin_sha= \ + -f mise_php_sha= \ + -f suite=agent-canary +``` + +`scripts/test.sh` validates every Codex Action invocation, exact CLI version, +and canonical `config.toml` loading against the reviewed offline contract in +`.github/codex-action-contract.json`. The live agent canary must run from +protected `main`; feature-branch runs cannot enter its credentialed +environment. + +Inspect `autorelease-events/`, generated `support-policy.json`, the reviewed +`autorelease/policy-invariants.json`, retained workflow +artifacts, the event issue marker, and `docs/autorelease-verification.md` to +reconstruct a decision. `scripts/snapshot-github-admin-state` captures settings, +variables, and secret names without secret values. Recovery never skips +admission or a failed gate: correct the external dependency or submit a +reviewed protected-control change, then rerun the normal workflow. diff --git a/README.md b/README.md index 3ddcc86..68d3520 100644 --- a/README.md +++ b/README.md @@ -17,114 +17,15 @@ Public macOS arm64 releases are available for every maintained PHP branch: Each release is rebuilt on macOS 26 arm64 and published only after its exact module baseline and deployment target checks pass. -## Guarded automatic maintenance - -`PHP maintenance watcher` runs daily and can also be dispatched manually. It -captures the raw PHP lifecycle page, release feed, php-src tags, and public -state of both repositories, including response metadata and SHA-256 digests. -The watcher compares only opaque digests and incomplete-event state. An -unchanged healthy day is quiet: it makes no model call and causes no issue, -repository, tag, asset, or release mutation. - -When evidence changes, the pinned official Codex Action investigates from a -read-only checkout. Web search is limited to `php.net`, `github.com`, and -`docs.github.com`; material release and lifecycle claims must still resolve to -the retained raw captures. A separate offline Codex invocation may edit only -paths admitted by the evidence-bound plan. It has no GitHub write credential -and cannot change the prompts, contracts, workflows, policy, admission, -sealing, merge, or release controls. - -```mermaid -flowchart TD - capture["Capture fixed raw evidence"] --> changed{"Digest or health changed?"} - changed -- "No" --> quiet["Quiet: no model call or mutation"] - changed -- "Yes" --> investigate["Read-only Codex investigation"] - investigate --> admit["Deterministic plan admission"] - admit --> edit{"Repository edit required?"} - edit -- "Yes" --> implement["Offline Codex implementation"] - implement --> seal["Seal admitted paths and digests"] - seal --> validate["Clean checkout validation"] - validate --> merge["Exact-SHA PR and merge admission"] - edit -- "No" --> release - merge --> release["Immutable release transaction"] - release --> draft["Verify draft bytes and temporary install"] - draft --> public["Publish unchanged bytes and verify public installs"] -``` - -The release transaction is the only component allowed to create an annotated -tag, draft, assets, or publication. It advances one legal state at a time, -reconciles existing state before acting, never rebuilds under an existing tag, -and never overwrites, deletes, or retags a published release. A first release -on a new PHP branch also requires exact-commit `php_bin_ready` and `mise_ready` -records. - -Failures use one deduplicated issue per action key, assigned to the username in -`MAINTENANCE_OWNER`. Only a meaningful state, evidence, fingerprint, required -action, or final-result change adds a comment. Critical failures stop mutation. -GitHub Actions failure email is an independent fallback. - -```mermaid -flowchart TD - job["Any maintenance phase"] --> result{"Result"} - result -- "Success" --> transition["Record evidence-backed transition"] - result -- "Retryable failure" --> retry{"Bounded retry remains?"} - retry -- "Yes" --> repair["Offline Codex repair"] - retry -- "No" --> blocked["Stop as blocked or needs_human"] - result -- "Critical or policy failure" --> blocked - blocked --> issue["Create or update one assigned issue"] - issue --> email["GitHub inbox and email"] - issue --> actions["Actions failure email fallback"] -``` - -Unattended mutation is controlled by -`.github/maintenance-operator.json`. Set `unattendedMutation` to `paused` in a -reviewed protected-path PR to stop implementation, merge, and release while -leaving read-only evidence capture and investigation available. Re-enable it -through another reviewed PR; an incomplete event then resumes only through its -single legal next transition. - -Maintainer commands: - -```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 \ - --mise-php-sha \ - --output ./verification-results - -gh workflow run maintenance-e2e.yml \ - --repo bigpixelrocket/php-bin \ - --ref \ - -f php_bin_sha= \ - -f mise_php_sha= \ - -f suite=production-parity - -# After the reviewed php-bin commit is merged to main, exercise the actual -# pinned Codex Action and repository API key inside the protected canary environment. -gh workflow run maintenance-e2e.yml \ - --repo bigpixelrocket/php-bin \ - --ref main \ - -f php_bin_sha= \ - -f mise_php_sha= \ - -f suite=agent-canary -``` +## Autorelease -`scripts/test.sh` validates every Codex Action invocation, exact CLI version, -and canonical `config.toml` loading against the reviewed offline contract in -`.github/codex-action-contract.json`. The live agent canary must run from -protected `main`; feature-branch runs cannot enter its credentialed -environment. +Releases are produced automatically. A daily watcher detects upstream PHP +release and lifecycle changes, a read-only Codex agent investigates, and +deterministic workflows build, verify, and publish the binaries. The agent +holds no write credential and cannot tag, publish, or merge. -Inspect `maintenance-events/`, generated `support-policy.json`, the reviewed -`maintenance/policy-invariants.json`, retained workflow -artifacts, the event issue marker, and `docs/maintenance-verification.md` to -reconstruct a decision. `scripts/snapshot-github-admin-state` captures settings, -variables, and secret names without secret values. Recovery never skips -admission or a failed gate: correct the external dependency or submit a -reviewed protected-control change, then rerun the normal workflow. +See [AUTORELEASE.md](AUTORELEASE.md) for the full contract, the operator +pause control, and maintainer commands. ## Release contract @@ -181,7 +82,7 @@ requests. ### New patch on a supported branch For an ordinary stable patch, the admitted no-edit intent goes directly to -`Guarded PHP release transaction`; no implementation job or PR is created. A +`Autorelease publish transaction`; no implementation job or PR is created. A recipe change uses a sealed automation PR first. Never move an existing tag or replace a published asset. Use a rebuild tag such as `8.5.9-1` when the PHP patch is unchanged but the recipe changes the bytes. diff --git a/maintenance-events/.gitkeep b/autorelease-events/.gitkeep similarity index 100% rename from maintenance-events/.gitkeep rename to autorelease-events/.gitkeep diff --git a/maintenance-events/new_patch-8.5.9.json b/autorelease-events/new_patch-8.5.9.json similarity index 100% rename from maintenance-events/new_patch-8.5.9.json rename to autorelease-events/new_patch-8.5.9.json diff --git a/maintenance-state/.gitkeep b/autorelease-state/.gitkeep similarity index 100% rename from maintenance-state/.gitkeep rename to autorelease-state/.gitkeep diff --git a/maintenance-state/last-evidence.json b/autorelease-state/last-evidence.json similarity index 100% rename from maintenance-state/last-evidence.json rename to autorelease-state/last-evidence.json diff --git a/autorelease/__init__.py b/autorelease/__init__.py new file mode 100644 index 0000000..7696f69 --- /dev/null +++ b/autorelease/__init__.py @@ -0,0 +1,3 @@ +"""Deterministic controls for the PHP autorelease system.""" + +__version__ = "1.0.0" diff --git a/maintenance/control.py b/autorelease/control.py similarity index 97% rename from maintenance/control.py rename to autorelease/control.py index 1d090a1..b726724 100755 --- a/maintenance/control.py +++ b/autorelease/control.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Deterministic maintenance controls. +"""Deterministic autorelease controls. This module deliberately does not classify PHP releases or lifecycle state. It validates authority, evidence, state transitions, and immutable effects @@ -316,7 +316,7 @@ def indexed_captures(manifest: dict[str, Any], label: str) -> dict[str, dict[str admitted = indexed_captures(admitted_manifest, "admitted") current = indexed_captures(current_manifest, "current") evidence = plan.get("evidence") - require(isinstance(evidence, list) and bool(evidence), "maintenance plan has no evidence") + require(isinstance(evidence, list) and bool(evidence), "autorelease plan has no evidence") verified = [] for item in evidence: require(isinstance(item, dict), "plan evidence entry must be an object") @@ -329,43 +329,43 @@ def indexed_captures(manifest: dict[str, Any], label: str) -> dict[str, dict[str require(admitted[capture_id]["digest"] == digest, f"admitted evidence digest mismatch: {capture_id}") require(current[capture_id]["digest"] == digest, f"recaptured evidence changed: {capture_id}") verified.append(capture_id) - require(bool(verified), "maintenance plan cites no authoritative captured evidence") + require(bool(verified), "autorelease plan cites no authoritative captured evidence") return {"valid": True, "verifiedCaptureIds": sorted(verified)} def validate_completed_event_record(record: dict[str, Any]) -> None: """Validate a durable event as a complete, contiguous legal transition history.""" - require(isinstance(record, dict), "maintenance event must be an object") - require(record.get("schemaVersion") == 1, "maintenance event version is invalid") - require(bool(ACTION_KEY_RE.fullmatch(record.get("actionKey", ""))), "maintenance event action key is invalid") - require(record.get("state") == "complete", "maintenance event is not complete") + require(isinstance(record, dict), "autorelease event must be an object") + require(record.get("schemaVersion") == 1, "autorelease event version is invalid") + require(bool(ACTION_KEY_RE.fullmatch(record.get("actionKey", ""))), "autorelease event action key is invalid") + require(record.get("state") == "complete", "autorelease event is not complete") history = record.get("history") - require(isinstance(history, list) and bool(history), "maintenance event has no transition history") + require(isinstance(history, list) and bool(history), "autorelease event has no transition history") current = history[0].get("from") if isinstance(history[0], dict) else None for transition in history: - require(isinstance(transition, dict), "maintenance event transition must be an object") + require(isinstance(transition, dict), "autorelease event transition must be an object") require( set(transition) == {"from", "to", "at", "evidence"}, - "maintenance event transition fields changed", + "autorelease event transition fields changed", ) - require(transition.get("from") == current, "maintenance event history is not contiguous") + require(transition.get("from") == current, "autorelease event history is not contiguous") target = transition.get("to") - require(target in LEGAL_EVENT_TRANSITIONS.get(current, set()), "maintenance event transition is illegal") + require(target in LEGAL_EVENT_TRANSITIONS.get(current, set()), "autorelease event transition is illegal") timestamp = transition.get("at") require( isinstance(timestamp, str) and timestamp.endswith("Z"), - "maintenance event transition timestamp is invalid", + "autorelease event transition timestamp is invalid", ) evidence = transition.get("evidence") require( isinstance(evidence, list) and bool(evidence) and all(isinstance(item, dict) and bool(item) for item in evidence), - "maintenance event transition evidence is invalid", + "autorelease event transition evidence is invalid", ) current = target - require(current == record["state"], "maintenance event state does not match its history") + require(current == record["state"], "autorelease event state does not match its history") def validate_evidence_state_record(record: dict[str, Any]) -> None: @@ -500,7 +500,7 @@ def _validate_support_policy_document( def validate_support_policy(root: pathlib.Path = ROOT) -> dict[str, Any]: - invariants_path = root / "maintenance/policy-invariants.json" + invariants_path = root / "autorelease/policy-invariants.json" policy_path = root / "support-policy.json" invariants = load_json(invariants_path) policy = load_json(policy_path) @@ -554,7 +554,7 @@ def validate_plan( policy_digest: str | None = None, completed_actions: set[str] | None = None, ) -> dict[str, Any]: - require(plan.get("schemaVersion") == 1, "unsupported maintenance plan version") + require(plan.get("schemaVersion") == 1, "unsupported autorelease plan version") require( plan.get("action") in { @@ -567,7 +567,7 @@ def validate_plan( "blocked", "needs_human", }, - "invalid maintenance action", + "invalid autorelease action", ) action_key = plan.get("actionKey", "") require(bool(ACTION_KEY_RE.fullmatch(action_key)), "invalid action key") @@ -783,7 +783,7 @@ def seal_patch( raise ControlError("support policy is not valid JSON") from error _branches, policy_evidence = _validate_support_policy_document( policy, - repo / "maintenance/policy-invariants.json", + repo / "autorelease/policy-invariants.json", ) evidence_digests = sorted( {item.get("digest") for item in plan.get("evidence", []) if item.get("digest")} @@ -948,7 +948,7 @@ def notification_decision(event: dict[str, Any], prior: dict[str, Any] | None) - "action": action, "fingerprint": fingerprint, "critical": critical, - "labels": ["maintenance", *(["attention-required"] if critical or event.get("humanActionRequired") else [])], + "labels": ["autorelease", *(["attention-required"] if critical or event.get("humanActionRequired") else [])], } @@ -1087,7 +1087,7 @@ def capture_evidence( for source in sources: headers = { "Accept": "application/vnd.github+json, application/json, text/html", - "User-Agent": "bigpixelrocket-maintenance/1", + "User-Agent": "bigpixelrocket-autorelease/1", } if token and urllib.parse.urlparse(source.url).hostname == "api.github.com": headers["Authorization"] = f"Bearer {token}" @@ -1195,7 +1195,7 @@ def validate_archive(archive: pathlib.Path, version: str) -> None: def cli_error(error: Exception) -> int: - print(f"maintenance control rejected input: {error}", file=sys.stderr) + print(f"autorelease control rejected input: {error}", file=sys.stderr) return 1 diff --git a/maintenance/policy-invariants.json b/autorelease/policy-invariants.json similarity index 100% rename from maintenance/policy-invariants.json rename to autorelease/policy-invariants.json diff --git a/autorelease/protected-paths.json b/autorelease/protected-paths.json new file mode 100644 index 0000000..cbbe4c5 --- /dev/null +++ b/autorelease/protected-paths.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "patterns": [ + ".github/codex/autorelease/**", + ".github/codex-action-contract.json", + ".github/autorelease-operator.json", + ".github/autorelease-pins.json", + ".github/workflows/**", + ".codex/**", + "schemas/**", + "autorelease/**", + "scripts/admit-autorelease-plan", + "scripts/capture-autorelease-evidence", + "scripts/configure-github-autorelease", + "scripts/dispatch-pr-checks", + "scripts/autorelease-event", + "scripts/notify-autorelease", + "scripts/prepare-agent-task", + "scripts/seal-autorelease-patch", + "scripts/snapshot-github-admin-state", + "scripts/validate-autorelease-archive", + "scripts/validate-codex-action-inputs", + "scripts/validate-structured-output-schemas", + "scripts/verify-merge-admission", + "scripts/publish-release", + "scripts/watch-autorelease-evidence", + "autorelease-events/**", + "autorelease-state/**", + ".github/CODEOWNERS" + ] +} diff --git a/maintenance/verify.py b/autorelease/verify.py similarity index 92% rename from maintenance/verify.py rename to autorelease/verify.py index eae60e8..b188c2f 100755 --- a/maintenance/verify.py +++ b/autorelease/verify.py @@ -18,7 +18,7 @@ import traceback from typing import Any, Callable -from maintenance.control import ( +from autorelease.control import ( ControlError, audit_reconstruction, canonical_json, @@ -144,8 +144,8 @@ def assessment(contract: dict[str, Any], digests: dict[str, str], status: str = def fixture_admission_inputs(directory: pathlib.Path, action: str = "new_patch") -> dict[str, Any]: - shared = PHP_ROOT / ".github/codex/maintenance/shared.md" - phase = PHP_ROOT / ".github/codex/maintenance/investigation.md" + shared = PHP_ROOT / ".github/codex/autorelease/shared.md" + phase = PHP_ROOT / ".github/codex/autorelease/investigation.md" event_path = directory / "event-contract.json" contract = fixture_contract() event_path.write_bytes(canonical_json(contract)) @@ -336,7 +336,7 @@ def a04(self, directory: pathlib.Path) -> list[str]: inputs = fixture_admission_inputs(target, action) admit_fixture(inputs) actions[action] = inputs["plan"]["actionKey"] - source = (PHP_ROOT / "maintenance/control.py").read_text() + source = (PHP_ROOT / "autorelease/control.py").read_text() forbidden_classifier_markers = ("BeautifulSoup", "support_table_to_events", "classify_php_release") assert_true(not any(item in source for item in forbidden_classifier_markers), "deterministic control contains lifecycle classifier") (directory / "classifications.json").write_bytes(canonical_json(actions)) @@ -364,8 +364,8 @@ def a06(self, directory: pathlib.Path) -> list[str]: repeated = retry_decision({**event, "lastRejectionRepeated": True}, "fp", 2) assert_true(first["recallAgent"], "bounded repair was not allowed") assert_true(not exhausted["recallAgent"] and not repeated["recallAgent"], "exhausted identical failure recalled agent") - php_workflow = (PHP_ROOT / ".github/workflows/maintenance-implementation.yml").read_text() - mise_workflow = (self.mise_root / ".github/workflows/maintenance-consumer.yml").read_text() + php_workflow = (PHP_ROOT / ".github/workflows/autorelease-implement.yml").read_text() + mise_workflow = (self.mise_root / ".github/workflows/autorelease-consumer.yml").read_text() for name, workflow in {"php-bin": php_workflow, "mise-php": mise_workflow}.items(): assert_true("authoritative-checks.log" in workflow, f"{name} does not retain deterministic failure logs") assert_true("Run one offline Codex repair" in workflow, f"{name} has no bounded repair invocation") @@ -379,15 +379,15 @@ def a06(self, directory: pathlib.Path) -> list[str]: "first": first, "exhausted": exhausted, "repeated": repeated, - "phpWorkflowDigest": sha256_file(PHP_ROOT / ".github/workflows/maintenance-implementation.yml"), - "miseWorkflowDigest": sha256_file(self.mise_root / ".github/workflows/maintenance-consumer.yml"), + "phpWorkflowDigest": sha256_file(PHP_ROOT / ".github/workflows/autorelease-implement.yml"), + "miseWorkflowDigest": sha256_file(self.mise_root / ".github/workflows/autorelease-consumer.yml"), } (directory / "retry.json").write_bytes(canonical_json(evidence)) return ["retry.json"] def a07(self, directory: pathlib.Path) -> list[str]: - watch = (PHP_ROOT / ".github/workflows/maintenance-watch.yml").read_text() - implementation = (PHP_ROOT / ".github/workflows/maintenance-implementation.yml").read_text() + watch = (PHP_ROOT / ".github/workflows/autorelease-watch.yml").read_text() + implementation = (PHP_ROOT / ".github/workflows/autorelease-implement.yml").read_text() assert_true( "sandbox: read-only" in watch and 'cp .codex/investigation.config.toml "$RUNNER_TEMP/codex-home/config.toml"' in watch @@ -409,10 +409,10 @@ def a07(self, directory: pathlib.Path) -> list[str]: def a08(self, directory: pathlib.Path) -> list[str]: protected_classes = [ ".github/workflows/evil.yml", - ".github/codex/maintenance/shared.md", + ".github/codex/autorelease/shared.md", "schemas/agent-task-contract.schema.json", - "maintenance/control.py", - "maintenance/policy-invariants.json", + "autorelease/control.py", + "autorelease/policy-invariants.json", "unadmitted.txt", ] rejected = [] @@ -448,7 +448,7 @@ def a09(self, directory: pathlib.Path) -> list[str]: base = init_repo(repo) (repo / "src.txt").write_text("coordinated\n") run("git", "add", "src.txt", cwd=repo) - run("git", "commit", "-q", "-m", "validated maintenance", cwd=repo) + run("git", "commit", "-q", "-m", "validated autorelease", cwd=repo) head = run("git", "rev-parse", "HEAD", cwd=repo).stdout.strip() manifest = { "baseSha": base, @@ -484,7 +484,7 @@ def a11(self, directory: pathlib.Path) -> list[str]: inputs["manifestPath"].write_bytes(canonical_json(inputs["manifest"])) inputs["plan"]["evidence"][0]["digest"] = sha256_bytes(body) admit_fixture(inputs) - source = (PHP_ROOT / "maintenance/control.py").read_text() + source = (PHP_ROOT / "autorelease/control.py").read_text() assert_true("supported-versions.php" in source and "BeautifulSoup" not in source, "source-format handling became a lifecycle parser") return ["evidence-manifest.json"] @@ -493,7 +493,7 @@ def a12(self, directory: pathlib.Path) -> list[str]: base = init_repo(repo) (repo / "src.txt").write_text("validated\n") run("git", "add", "src.txt", cwd=repo) - run("git", "commit", "-q", "-m", "validated maintenance", cwd=repo) + run("git", "commit", "-q", "-m", "validated autorelease", cwd=repo) head = run("git", "rev-parse", "HEAD", cwd=repo).stdout.strip() manifest = { "baseSha": base, @@ -531,12 +531,12 @@ def a13(self, directory: pathlib.Path) -> list[str]: "repositories do not pin the same reviewed Codex CLI version", ) (directory / "codex-action-inputs.json").write_bytes(canonical_json(codex_contracts)) - pins = json.loads((PHP_ROOT / ".github/maintenance-pins.json").read_text()) + pins = json.loads((PHP_ROOT / ".github/autorelease-pins.json").read_text()) assert_true( pins["actions"]["openai/codex-action"] == codex_contracts["php-bin"]["commit"], "Codex Action pin is not bound to the reviewed input contract", ) - e2e = PHP_ROOT / ".github/workflows/maintenance-e2e.yml" + e2e = PHP_ROOT / ".github/workflows/autorelease-e2e.yml" e2e_text = e2e.read_text() assert_true( 'status:{type:"string",const:"passed"}' in e2e_text @@ -544,12 +544,12 @@ def a13(self, directory: pathlib.Path) -> list[str]: "credentialed agent canary schema does not declare string types", ) assert_true( - pins["workflows"][".github/workflows/maintenance-e2e.yml"] == sha256_file(e2e), + pins["workflows"][".github/workflows/autorelease-e2e.yml"] == sha256_file(e2e), "reviewed production-parity workflow digest changed", ) - watch_path = PHP_ROOT / ".github/workflows/maintenance-watch.yml" + watch_path = PHP_ROOT / ".github/workflows/autorelease-watch.yml" watch = watch_path.read_text() - release = (PHP_ROOT / ".github/workflows/maintenance-release.yml").read_text() + release = (PHP_ROOT / ".github/workflows/autorelease-publish.yml").read_text() watch_document = load_workflow(watch_path) workflow_permissions = watch_document.get("permissions", {}) investigate = watch_document.get("jobs", {}).get("investigate", {}) @@ -560,7 +560,7 @@ def a13(self, directory: pathlib.Path) -> list[str]: "runtime investigation does not have resolved read-only contents permission", ) assert_true("openai-api-key" not in release, "release job can read OpenAI credential") - admin = PHP_ROOT / "docs/maintenance-admin-evidence.json" + admin = PHP_ROOT / "docs/autorelease-admin-evidence.json" assert_true(admin.is_file(), "redacted administrator evidence is missing") evidence = json.loads(admin.read_text()) assert_true(evidence.get("canary", {}).get("removed") is True, "admin canary was not removed") @@ -568,7 +568,7 @@ def a13(self, directory: pathlib.Path) -> list[str]: assert_true(evidence.get("immutableReleasesEnabled") is True, "immutable releases were not enabled") agent_canary_environment = evidence.get("agentCanaryEnvironment", {}) assert_true( - agent_canary_environment.get("name") == "php-maintenance-agent-canary" + agent_canary_environment.get("name") == "php-autorelease-canary" and agent_canary_environment.get("protectedBranchesOnly") is True and agent_canary_environment.get("administratorBypass") is False, "credentialed agent canary environment is not protected", @@ -623,16 +623,16 @@ def a17(self, directory: pathlib.Path) -> list[str]: def a18(self, directory: pathlib.Path) -> list[str]: assert_true(not mutation_allowed({"unattendedMutation": "paused"}), "paused control allowed mutation") assert_true(mutation_allowed({"unattendedMutation": "enabled"}), "enabled control blocked mutation") - watch_workflow = (PHP_ROOT / ".github/workflows/maintenance-watch.yml").read_text() - release_workflow = (PHP_ROOT / ".github/workflows/maintenance-release.yml").read_text() - mise_workflow = (self.mise_root / ".github/workflows/maintenance-consumer.yml").read_text() + watch_workflow = (PHP_ROOT / ".github/workflows/autorelease-watch.yml").read_text() + release_workflow = (PHP_ROOT / ".github/workflows/autorelease-publish.yml").read_text() + mise_workflow = (self.mise_root / ".github/workflows/autorelease-consumer.yml").read_text() assert_true( "Unattended mutation is paused" in watch_workflow, "watcher pause does not stop downstream mutation", ) assert_true( release_workflow.count("current-operator.json") >= 3, - "release effects are not guarded by the live operator state", + "release effects are not gated by the live operator state", ) assert_true( "phpBinOperatorCommit" in mise_workflow and "operatorState" in mise_workflow, @@ -659,11 +659,16 @@ def a19(self, directory: pathlib.Path) -> list[str]: return ["audit.json", "evidence.json"] def a20(self, directory: pathlib.Path) -> list[str]: + # The system documentation lives in AUTORELEASE.md; each README only + # points at it. + for doc in (PHP_ROOT / "AUTORELEASE.md", self.mise_root / "AUTORELEASE.md"): + body = doc.read_text() + assert_true("```mermaid" in body, f"AUTORELEASE.md has no Mermaid flow: {doc}") + assert_true("verify-autorelease-system" in body, f"AUTORELEASE.md lacks verifier command: {doc}") + assert_true("AUTORELEASE_OWNER" in body, f"AUTORELEASE.md lacks notification configuration: {doc}") for readme in (PHP_ROOT / "README.md", self.mise_root / "README.md"): body = readme.read_text() - assert_true("```mermaid" in body, f"README has no Mermaid flow: {readme}") - assert_true("verify-maintenance-system" in body, f"README lacks verifier command: {readme}") - assert_true("MAINTENANCE_OWNER" in body, f"README lacks notification configuration: {readme}") + assert_true("AUTORELEASE.md" in body, f"README does not link AUTORELEASE.md: {readme}") run("./scripts/test.sh", cwd=PHP_ROOT) run("./scripts/test.sh", cwd=self.mise_root) (directory / "commands.txt").write_text("(cd php-bin && ./scripts/test.sh)\n(cd mise-php && ./scripts/test.sh)\nverification: passed\n") @@ -710,14 +715,14 @@ def execute(self) -> int: ) configuration_paths = [ PHP_ROOT / "support-policy.json", - PHP_ROOT / "maintenance/protected-paths.json", + PHP_ROOT / "autorelease/protected-paths.json", self.mise_root / "support-snapshot.json", ] instruction_roots = {"php-bin": PHP_ROOT, "mise-php": self.mise_root} instruction_names = ("shared.md", "investigation.md", "implementation.md", "repair.md") instruction_digests = { - f"{repo}/.github/codex/maintenance/{name}": sha256_file( - root / ".github/codex/maintenance" / name + f"{repo}/.github/codex/autorelease/{name}": sha256_file( + root / ".github/codex/autorelease" / name ) for repo, root in instruction_roots.items() for name in instruction_names @@ -734,11 +739,11 @@ def execute(self) -> int: "tests": self.results, "result": "passed" if all(item["result"] == "passed" for item in self.results) else "failed", } - report_path = self.output / "maintenance-verification.json" + report_path = self.output / "autorelease-verification.json" report_path.write_bytes(canonical_json(report)) report_digest = sha256_file(report_path) lines = [ - "# Maintenance verification", + "# Autorelease verification", "", f"- Result: **{report['result']}**", f"- php-bin: `{self.php_sha}`", @@ -749,7 +754,7 @@ def execute(self) -> int: "| --- | --- | --- |", ] lines.extend(f"| {item['id']} | {item['name']} | {item['result']} |" for item in self.results) - (self.output / "maintenance-verification.md").write_text("\n".join(lines) + "\n") + (self.output / "autorelease-verification.md").write_text("\n".join(lines) + "\n") print(json.dumps({"result": report["result"], "report": str(report_path), "digest": report_digest})) return 0 if report["result"] == "passed" else 1 diff --git a/docs/admin-state/php-bin-after.json b/docs/admin-state/php-bin-after.json index e5b9c02..f0dc2a6 100644 --- a/docs/admin-state/php-bin-after.json +++ b/docs/admin-state/php-bin-after.json @@ -81,9 +81,9 @@ "custom_branch_policies": false, "protected_branches": true }, - "html_url": "https://github.com/Bigpixelrocket/php-bin/deployments/activity_log?environments_filter=php-maintenance-agent-canary", + "html_url": "https://github.com/Bigpixelrocket/php-bin/deployments/activity_log?environments_filter=php-autorelease-canary", "id": 18869369898, - "name": "php-maintenance-agent-canary", + "name": "php-autorelease-canary", "node_id": "EN_kwDOTj2wQ88AAAAEZLO8Kg", "protection_rules": [ { @@ -93,7 +93,7 @@ } ], "updated_at": "2026-07-28T09:03:03Z", - "url": "https://api.github.com/repos/Bigpixelrocket/php-bin/environments/php-maintenance-agent-canary" + "url": "https://api.github.com/repos/Bigpixelrocket/php-bin/environments/php-autorelease-canary" }, { "can_admins_bypass": false, @@ -102,9 +102,9 @@ "custom_branch_policies": false, "protected_branches": true }, - "html_url": "https://github.com/Bigpixelrocket/php-bin/deployments/activity_log?environments_filter=php-maintenance-release", + "html_url": "https://github.com/Bigpixelrocket/php-bin/deployments/activity_log?environments_filter=php-autorelease-publish", "id": 18804833478, - "name": "php-maintenance-release", + "name": "php-autorelease-publish", "node_id": "EN_kwDOTj2wQ88AAAAEYNr8xg", "protection_rules": [ { @@ -114,7 +114,7 @@ } ], "updated_at": "2026-07-27T10:09:17Z", - "url": "https://api.github.com/repos/Bigpixelrocket/php-bin/environments/php-maintenance-release" + "url": "https://api.github.com/repos/Bigpixelrocket/php-bin/environments/php-autorelease-publish" } ], "total_count": 2 @@ -134,7 +134,7 @@ "good first issue", "help wanted", "invalid", - "maintenance", + "autorelease", "question", "wontfix" ], @@ -184,7 +184,7 @@ }, "snapshotDigest": "sha256:3d0ed7f751c408e71033bdc593f7eeba2161470658277281d1c409a7b0572797", "variables": [ - "MAINTENANCE_OWNER" + "AUTORELEASE_OWNER" ], "workflowPermissions": { "can_approve_pull_request_reviews": true, diff --git a/docs/maintenance-admin-evidence.json b/docs/autorelease-admin-evidence.json similarity index 95% rename from docs/maintenance-admin-evidence.json rename to docs/autorelease-admin-evidence.json index 016d673..bf5f76c 100644 --- a/docs/maintenance-admin-evidence.json +++ b/docs/autorelease-admin-evidence.json @@ -33,7 +33,7 @@ } }, "canary": { - "variableName": "MAINTENANCE_ADMIN_CANARY", + "variableName": "AUTORELEASE_ADMIN_CANARY", "repositories": [ "Bigpixelrocket/php-bin", "Bigpixelrocket/mise-php" @@ -44,7 +44,7 @@ "absenceVerified": true, "protectedPath": { "repository": "Bigpixelrocket/mise-php", - "path": "maintenance/MAINTENANCE_ADMIN_CANARY.json", + "path": "autorelease/AUTORELEASE_ADMIN_CANARY.json", "additionPullRequest": "https://github.com/Bigpixelrocket/mise-php/pull/10", "expectedGateFailure": "https://github.com/Bigpixelrocket/mise-php/actions/runs/30255504107/job/89943039916", "additionMergeCommit": "4160680862fe247addd4008b5fd71a7d8ff35019", @@ -76,7 +76,7 @@ "administratorBypass": false }, "agentCanaryEnvironment": { - "name": "php-maintenance-agent-canary", + "name": "php-autorelease-canary", "protectedBranchesOnly": true, "administratorBypass": false, "purpose": "Exercise the pinned Codex Action and repository API key only from reviewed main workflow bytes." @@ -101,7 +101,7 @@ "assignedIssue": "https://github.com/Bigpixelrocket/php-bin/issues/12", "assignedIssueRun": "https://github.com/Bigpixelrocket/php-bin/actions/runs/30263155155", "failedActionsRun": "https://github.com/Bigpixelrocket/php-bin/actions/runs/30263272377", - "evidenceDescription": "Maintainer-provided inbox screenshot shows the assigned issue #12 email at 14:46 and the isolated maintenance end-to-end failure email at 14:48." + "evidenceDescription": "Maintainer-provided inbox screenshot shows the assigned issue #12 email at 14:46 and the isolated autorelease end-to-end failure email at 14:48." } }, "secrets": { diff --git a/docs/repository-settings.md b/docs/repository-settings.md index cfa994f..db8e3d4 100644 --- a/docs/repository-settings.md +++ b/docs/repository-settings.md @@ -1,7 +1,7 @@ # Repository settings The plan executor installs these settings with -`scripts/configure-github-maintenance` and verifies them with +`scripts/configure-github-autorelease` and verifies them with `scripts/snapshot-github-admin-state`. Snapshots contain secret names, never secret values. @@ -11,8 +11,8 @@ Required repository state: - Require the `Script checks` status check. - Require the base-controlled `Protected controls` status check. It passes automatically for unprotected generated paths and requires an exact-head - `loadinglucian` approval for paths in `maintenance/protected-paths.json`. - The sole deterministic exception is `maintenance-state/last-evidence.json`: + `loadinglucian` approval for paths in `autorelease/protected-paths.json`. + The sole deterministic exception is `autorelease-state/last-evidence.json`: a same-repository `github-actions[bot]` PR may pass only when it is a direct child of the current base, is tied to the still-running protected watcher, changes exactly that file, and the record has the reviewed healthy-capture @@ -43,9 +43,9 @@ Required repository state: - Allow GitHub-owned Actions plus only `openai/codex-action` and `jdx/mise-action`, and require every Action reference to use a full commit SHA. -- Create the protected `php-maintenance-release` environment, limited to +- Create the protected `php-autorelease-publish` environment, limited to protected branches, and disable administrator bypass. -- Create the protected `php-maintenance-agent-canary` environment with the +- Create the protected `php-autorelease-canary` environment with the same protected-branch-only policy and disabled administrator bypass. - Enable Dependabot security updates, provider-pattern secret scanning, and secret-scanning push protection. Request validity checks and non-provider @@ -53,9 +53,9 @@ Required repository state: organization has GitHub Secret Protection. - Enable GitHub immutable releases so future published tags and assets cannot be moved, replaced, or deleted. -- Set `MAINTENANCE_OWNER=loadinglucian`. +- Set `AUTORELEASE_OWNER=loadinglucian`. - Keep distinct repository-scoped `OPENAI_API_KEY` secrets. -- Keep the `maintenance` and `attention-required` labels. +- Keep the `autorelease` and `attention-required` labels. CODEOWNERS covers prompts, contracts, workflows, policy invariants, authority controls, and release code. Deterministic event/state records and admitted @@ -71,7 +71,7 @@ The normal verification commands are: --repo bigpixelrocket/php-bin \ --output docs/admin-state/php-bin.json -./scripts/configure-github-maintenance \ +./scripts/configure-github-autorelease \ --repo bigpixelrocket/php-bin \ --owner loadinglucian \ --required-check "Script checks" diff --git a/maintenance/__init__.py b/maintenance/__init__.py deleted file mode 100644 index 6bfb333..0000000 --- a/maintenance/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Deterministic controls for the PHP maintenance system.""" - -__version__ = "1.0.0" diff --git a/maintenance/protected-paths.json b/maintenance/protected-paths.json deleted file mode 100644 index 2b7056f..0000000 --- a/maintenance/protected-paths.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "schemaVersion": 1, - "patterns": [ - ".github/codex/maintenance/**", - ".github/codex-action-contract.json", - ".github/maintenance-operator.json", - ".github/maintenance-pins.json", - ".github/workflows/**", - ".codex/**", - "schemas/**", - "maintenance/**", - "scripts/admit-maintenance-plan", - "scripts/capture-maintenance-evidence", - "scripts/configure-github-maintenance", - "scripts/dispatch-pr-checks", - "scripts/maintenance-event", - "scripts/notify-maintenance", - "scripts/prepare-agent-task", - "scripts/seal-maintenance-patch", - "scripts/snapshot-github-admin-state", - "scripts/validate-maintenance-archive", - "scripts/validate-codex-action-inputs", - "scripts/validate-structured-output-schemas", - "scripts/verify-merge-admission", - "scripts/release-maintenance", - "scripts/watch-maintenance-evidence", - "maintenance-events/**", - "maintenance-state/**", - ".github/CODEOWNERS" - ] -} diff --git a/schemas/maintenance-event.schema.json b/schemas/autorelease-event.schema.json similarity index 87% rename from schemas/maintenance-event.schema.json rename to schemas/autorelease-event.schema.json index 443b800..f737e04 100644 --- a/schemas/maintenance-event.schema.json +++ b/schemas/autorelease-event.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://bigpixelrocket.dev/schemas/maintenance-event.schema.json", + "$id": "https://bigpixelrocket.dev/schemas/autorelease-event.schema.json", "type": "object", "required": ["schemaVersion", "actionKey", "state", "history"], "properties": { diff --git a/schemas/maintenance-plan.schema.json b/schemas/autorelease-plan.schema.json similarity index 99% rename from schemas/maintenance-plan.schema.json rename to schemas/autorelease-plan.schema.json index 83dbf55..7158454 100644 --- a/schemas/maintenance-plan.schema.json +++ b/schemas/autorelease-plan.schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://bigpixelrocket.dev/schemas/maintenance-plan.schema.json", + "$id": "https://bigpixelrocket.dev/schemas/autorelease-plan.schema.json", "type": "object", "additionalProperties": false, "required": ["schemaVersion", "actionKey", "action", "agentContract", "evidence", "researchSources", "repositories", "preconditions", "editsRequired", "allowedPaths", "requiredChecks", "releaseIntent", "agentOperations", "budgets", "notification", "risk", "completionAssessment", "summary"], diff --git a/scripts/admit-maintenance-plan b/scripts/admit-autorelease-plan similarity index 93% rename from scripts/admit-maintenance-plan rename to scripts/admit-autorelease-plan index bb43075..ba2ca4d 100755 --- a/scripts/admit-maintenance-plan +++ b/scripts/admit-autorelease-plan @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Admit a Codex maintenance plan against exact evidence and preconditions.""" +"""Admit a Codex autorelease plan against exact evidence and preconditions.""" import argparse import json @@ -7,7 +7,7 @@ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import ControlError, load_json, validate_plan, write_json # noqa: E402 +from autorelease.control import ControlError, load_json, validate_plan, write_json # noqa: E402 parser = argparse.ArgumentParser() diff --git a/scripts/maintenance-event b/scripts/autorelease-event similarity index 82% rename from scripts/maintenance-event rename to scripts/autorelease-event index 4f07116..3af0b97 100755 --- a/scripts/maintenance-event +++ b/scripts/autorelease-event @@ -3,6 +3,6 @@ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import main +from autorelease.control import main raise SystemExit(main(["transition-event", *sys.argv[1:]])) diff --git a/scripts/capture-maintenance-evidence b/scripts/capture-autorelease-evidence similarity index 82% rename from scripts/capture-maintenance-evidence rename to scripts/capture-autorelease-evidence index 26827f6..314a4ce 100755 --- a/scripts/capture-maintenance-evidence +++ b/scripts/capture-autorelease-evidence @@ -3,6 +3,6 @@ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import main +from autorelease.control import main raise SystemExit(main(["capture-evidence", *sys.argv[1:]])) diff --git a/scripts/configure-github-maintenance b/scripts/configure-github-autorelease similarity index 95% rename from scripts/configure-github-maintenance rename to scripts/configure-github-autorelease index 8ca7399..5699ab3 100755 --- a/scripts/configure-github-maintenance +++ b/scripts/configure-github-autorelease @@ -113,12 +113,12 @@ try: }, ) gh("api", f"repos/{args.repo}/immutable-releases", "--method", "PUT") - gh("variable", "set", "MAINTENANCE_OWNER", "--repo", args.repo, "--body", args.owner) - ensure_label(args.repo, "maintenance", "1d76db", "Guarded PHP maintenance event") + gh("variable", "set", "AUTORELEASE_OWNER", "--repo", args.repo, "--body", args.owner) + ensure_label(args.repo, "autorelease", "1d76db", "Autorelease event") ensure_label(args.repo, "attention-required", "d73a4a", "Owner action is required") environment = json.loads(gh( "api", - f"repos/{args.repo}/environments/php-maintenance-release", + f"repos/{args.repo}/environments/php-autorelease-publish", "--method", "PUT", input_value={ @@ -178,5 +178,5 @@ try: ) except (subprocess.CalledProcessError, OSError, RuntimeError, json.JSONDecodeError) as error: detail = error.stderr.strip() if isinstance(error, subprocess.CalledProcessError) and error.stderr else str(error) - print(f"GitHub maintenance configuration failed: {detail}", file=sys.stderr) + print(f"GitHub autorelease configuration failed: {detail}", file=sys.stderr) raise SystemExit(1) diff --git a/scripts/notify-maintenance b/scripts/notify-autorelease similarity index 90% rename from scripts/notify-maintenance rename to scripts/notify-autorelease index cbd49f3..050cbed 100755 --- a/scripts/notify-maintenance +++ b/scripts/notify-autorelease @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Deduplicate maintenance issue creation and transition comments.""" +"""Deduplicate autorelease issue creation and transition comments.""" import argparse import json @@ -9,7 +9,7 @@ import sys import re sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import ( # noqa: E402 +from autorelease.control import ( # noqa: E402 ControlError, load_json, notification_decision, @@ -30,7 +30,7 @@ def gh(*arguments: str) -> str: def find_issue(repo: str, action_key: str) -> dict | None: - marker = f"" + marker = f"" issues = json.loads( gh( "issue", @@ -67,7 +67,7 @@ def discover_github_prior(repo: str, action_key: str) -> dict | None: fingerprints = [ match.group(1) for body in bodies - for match in re.finditer(r"", body) + for match in re.finditer(r"", body) ] return { "fingerprint": fingerprints[-1] if fingerprints else None, @@ -77,10 +77,10 @@ def discover_github_prior(repo: str, action_key: str) -> dict | None: def apply_github(repo: str, owner: str, event: dict, decision: dict, prior: dict | None) -> dict: action_key = event["actionKey"] - marker = f"" - summary = event.get("summary", f"Maintenance event {action_key} entered {event.get('state')}.") - fingerprint_marker = f"" - body = f"{marker}\n{fingerprint_marker}\n\n# PHP maintenance event\n\n{summary}\n" + marker = f"" + summary = event.get("summary", f"Autorelease event {action_key} entered {event.get('state')}.") + fingerprint_marker = f"" + body = f"{marker}\n{fingerprint_marker}\n\n# PHP autorelease event\n\n{summary}\n" comment = ( f"{fingerprint_marker}\n\nState: `{event.get('state')}`\n\n{summary}\n\n" f"Transition fingerprint: `{decision['fingerprint']}`" @@ -99,7 +99,7 @@ def apply_github(repo: str, owner: str, event: dict, decision: dict, prior: dict "--repo", repo, "--title", - f"[maintenance] {action_key}", + f"[autorelease] {action_key}", "--body", body, "--assignee", @@ -141,7 +141,7 @@ def main() -> int: issue = {} if args.backend == "github" and decision["action"] != "none": if not args.owner: - raise ControlError("MAINTENANCE_OWNER is required") + raise ControlError("AUTORELEASE_OWNER is required") issue = apply_github(args.repo, args.owner, event, decision, prior) updated = { "schemaVersion": 1, diff --git a/scripts/prepare-agent-task b/scripts/prepare-agent-task index 1ba53aa..0008b61 100755 --- a/scripts/prepare-agent-task +++ b/scripts/prepare-agent-task @@ -7,7 +7,7 @@ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import ControlError, instruction_digest, write_json # noqa: E402 +from autorelease.control import ControlError, instruction_digest, write_json # noqa: E402 CRITERIA = { diff --git a/scripts/release-maintenance b/scripts/publish-release similarity index 97% rename from scripts/release-maintenance rename to scripts/publish-release index b576a1a..922b509 100755 --- a/scripts/release-maintenance +++ b/scripts/publish-release @@ -9,7 +9,7 @@ import sys import tempfile sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import ( # noqa: E402 +from autorelease.control import ( # noqa: E402 ControlError, LEGAL_RELEASE_TRANSITIONS, load_json, @@ -121,13 +121,13 @@ def github_effect( "--title", f"PHP {version} for macOS arm64", "--notes", - "Guarded maintenance release. Asset digests are recorded in SHA256SUMS.", + "Autorelease publication. Asset digests are recorded in SHA256SUMS.", "--draft", "--verify-tag", capture=False, ) elif target in {"draft_verified", "public_verified"}: - with tempfile.TemporaryDirectory(prefix="maintenance-download-") as temporary: + with tempfile.TemporaryDirectory(prefix="autorelease-download-") as temporary: gh( "release", "download", diff --git a/scripts/seal-maintenance-patch b/scripts/seal-autorelease-patch similarity index 94% rename from scripts/seal-maintenance-patch rename to scripts/seal-autorelease-patch index 77c2d25..dd454ea 100755 --- a/scripts/seal-maintenance-patch +++ b/scripts/seal-autorelease-patch @@ -8,7 +8,7 @@ import subprocess import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import ControlError, load_json, seal_patch # noqa: E402 +from autorelease.control import ControlError, load_json, seal_patch # noqa: E402 parser = argparse.ArgumentParser() diff --git a/scripts/serve-maintenance-artifact b/scripts/serve-autorelease-artifact similarity index 100% rename from scripts/serve-maintenance-artifact rename to scripts/serve-autorelease-artifact diff --git a/scripts/test.sh b/scripts/test.sh index f8088ca..e0eb1f9 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -8,7 +8,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" "$SCRIPT_DIR/check-public-language.sh" "$SCRIPT_DIR/validate-codex-action-inputs" "$SCRIPT_DIR/validate-structured-output-schemas" -"$PROJECT_ROOT/maintenance/control.py" validate-policy +"$PROJECT_ROOT/autorelease/control.py" validate-policy "$SCRIPT_DIR/compare-modules.sh" \ "$PROJECT_ROOT/tests/fixtures/modules.txt" \ "$PROJECT_ROOT/tests/fixtures/expected-exact.txt" \ diff --git a/scripts/validate-maintenance-archive b/scripts/validate-autorelease-archive similarity index 82% rename from scripts/validate-maintenance-archive rename to scripts/validate-autorelease-archive index f3ab15f..b2011be 100755 --- a/scripts/validate-maintenance-archive +++ b/scripts/validate-autorelease-archive @@ -3,6 +3,6 @@ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import main +from autorelease.control import main raise SystemExit(main(["validate-archive", *sys.argv[1:]])) diff --git a/scripts/validate-structured-output-schemas b/scripts/validate-structured-output-schemas index de57218..f520fd2 100755 --- a/scripts/validate-structured-output-schemas +++ b/scripts/validate-structured-output-schemas @@ -15,7 +15,7 @@ OUTPUT_SCHEMA_RE = re.compile(r'--output-schema","([^"]+\.json)"') UNSUPPORTED_KEYWORDS = {"uniqueItems"} sys.path.insert(0, str(ROOT)) -from maintenance.control import ( # noqa: E402 +from autorelease.control import ( # noqa: E402 ACTION_KEY_RE, COMPLETION_EVIDENCE_REF_RE, REQUIRED_PLAN_CHECKS, @@ -79,10 +79,10 @@ def main() -> int: except json.JSONDecodeError as error: fail(f"{path.relative_to(ROOT)} is invalid JSON: {error}") validate_node(document, str(path.relative_to(ROOT))) - if path == ROOT / "schemas/maintenance-plan.schema.json": + if path == ROOT / "schemas/autorelease-plan.schema.json": schema_pattern = document.get("properties", {}).get("actionKey", {}).get("pattern") if schema_pattern != ACTION_KEY_RE.pattern: - fail("maintenance plan actionKey pattern must match deterministic admission") + fail("autorelease plan actionKey pattern must match deterministic admission") properties = document.get("properties", {}) required_checks = properties.get("requiredChecks", {}) if ( @@ -90,7 +90,7 @@ def main() -> int: or required_checks.get("minItems") != len(REQUIRED_PLAN_CHECKS) or required_checks.get("maxItems") != len(REQUIRED_PLAN_CHECKS) ): - fail("maintenance plan requiredChecks must match deterministic admission") + fail("autorelease plan requiredChecks must match deterministic admission") evidence_pattern = ( properties.get("completionAssessment", {}) .get("properties", {}) diff --git a/scripts/verify-maintenance-system b/scripts/verify-autorelease-system similarity index 80% rename from scripts/verify-maintenance-system rename to scripts/verify-autorelease-system index 5bdab37..4e9880d 100755 --- a/scripts/verify-maintenance-system +++ b/scripts/verify-autorelease-system @@ -3,6 +3,6 @@ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.verify import main +from autorelease.verify import main raise SystemExit(main()) diff --git a/scripts/verify-merge-admission b/scripts/verify-merge-admission index 46d4e74..5bbfe43 100755 --- a/scripts/verify-merge-admission +++ b/scripts/verify-merge-admission @@ -8,7 +8,7 @@ import subprocess import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import ControlError, load_json, verify_merge # noqa: E402 +from autorelease.control import ControlError, load_json, verify_merge # noqa: E402 parser = argparse.ArgumentParser() diff --git a/scripts/watch-maintenance-evidence b/scripts/watch-autorelease-evidence similarity index 97% rename from scripts/watch-maintenance-evidence rename to scripts/watch-autorelease-evidence index 8872f10..9791c70 100755 --- a/scripts/watch-maintenance-evidence +++ b/scripts/watch-autorelease-evidence @@ -7,7 +7,7 @@ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) -from maintenance.control import ( # noqa: E402 +from autorelease.control import ( # noqa: E402 ControlError, load_json, validate_evidence_state_record, diff --git a/tests/test_maintenance.py b/tests/test_autorelease.py similarity index 96% rename from tests/test_maintenance.py rename to tests/test_autorelease.py index d61cc71..5645b6b 100644 --- a/tests/test_maintenance.py +++ b/tests/test_autorelease.py @@ -8,7 +8,7 @@ import unittest from unittest import mock -from maintenance.control import ( +from autorelease.control import ( COMPLETION_EVIDENCE_REF_RE, ControlError, canonical_json, @@ -35,7 +35,7 @@ ) -class MaintenanceControlTests(unittest.TestCase): +class AutoreleaseControlTests(unittest.TestCase): @staticmethod def _contract(): return { @@ -151,8 +151,8 @@ def test_investigation_evidence_references_are_machine_resolvable(self): def test_investigation_defers_required_checks_to_writable_jobs(self): root = pathlib.Path(__file__).resolve().parents[1] - instructions = (root / ".github/codex/maintenance/investigation.md").read_text() - watcher = (root / ".github/workflows/maintenance-watch.yml").read_text() + instructions = (root / ".github/codex/autorelease/investigation.md").read_text() + watcher = (root / ".github/workflows/autorelease-watch.yml").read_text() self.assertIn("Treat `requiredChecks` as downstream exact-head gates", instructions) self.assertIn("do not run them in this read-only", instructions) self.assertIn("not-yet-run status as unresolved", instructions) @@ -344,7 +344,7 @@ def test_notification_transition_reuses_retained_issue_identity(self): self.assertIsNone(retained_notification_issue({"issue": {"number": True}})) namespace = runpy.run_path( - str(pathlib.Path(__file__).resolve().parents[1] / "scripts/notify-maintenance") + str(pathlib.Path(__file__).resolve().parents[1] / "scripts/notify-autorelease") ) apply_github = namespace["apply_github"] gh = mock.Mock(return_value="") @@ -352,7 +352,7 @@ def test_notification_transition_reuses_retained_issue_identity(self): decision = { "action": "comment_and_close", "fingerprint": "sha256:" + "a" * 64, - "labels": ["maintenance"], + "labels": ["autorelease"], } event = {"actionKey": "fixture", "state": "complete", "summary": "Done."} with mock.patch.dict(apply_github.__globals__, {"gh": gh, "find_issue": find_issue}): @@ -369,11 +369,11 @@ def test_retry_and_pause_bounds(self): def test_invariants_and_durable_state_are_protected(self): self.assertTrue(path_is_protected(".github/codex-action-contract.json")) - self.assertTrue(path_is_protected("maintenance/policy-invariants.json")) + self.assertTrue(path_is_protected("autorelease/policy-invariants.json")) self.assertTrue(path_is_protected("scripts/validate-codex-action-inputs")) self.assertTrue(path_is_protected("scripts/dispatch-pr-checks")) - self.assertTrue(path_is_protected("maintenance-events/new-branch.json")) - self.assertTrue(path_is_protected("maintenance-state/last-evidence.json")) + self.assertTrue(path_is_protected("autorelease-events/new-branch.json")) + self.assertTrue(path_is_protected("autorelease-state/last-evidence.json")) self.assertFalse(path_is_protected("support-policy.json")) def test_token_created_prs_explicitly_dispatch_required_checks(self): @@ -384,11 +384,11 @@ def test_token_created_prs_explicitly_dispatch_required_checks(self): self.assertIn("workflow_dispatch:", ci) self.assertIn("workflow_dispatch:", protected) self.assertIn("paths-ignore:", ci) - self.assertIn("maintenance-state/**", ci) + self.assertIn("autorelease-state/**", ci) self.assertIn("paths-ignore:", protected) - self.assertIn("maintenance-events/**", protected) + self.assertIn("autorelease-events/**", protected) self.assertIn("validate_completed_event_record", protected) - self.assertIn('maintenance/(event|eol-complete)-', protected) + self.assertIn('autorelease/(event|eol-complete)-', protected) self.assertIn("pr_number:", protected) self.assertIn("gh workflow run ci.yml", dispatcher) self.assertIn("gh workflow run protected-controls.yml", dispatcher) @@ -396,9 +396,9 @@ def test_token_created_prs_explicitly_dispatch_required_checks(self): self.assertIn('"repos/$repository/statuses/$head_sha"', dispatcher) self.assertIn("Exact-head validator passed", dispatcher) for workflow in ( - "maintenance-watch.yml", - "maintenance-implementation.yml", - "maintenance-release.yml", + "autorelease-watch.yml", + "autorelease-implement.yml", + "autorelease-publish.yml", ): body = (root / ".github/workflows" / workflow).read_text() self.assertIn("./scripts/dispatch-pr-checks", body) @@ -406,7 +406,7 @@ def test_token_created_prs_explicitly_dispatch_required_checks(self): self.assertIn("checks: write", body) self.assertIn("statuses: write", body) - release = (root / ".github/workflows/maintenance-release.yml").read_text() + release = (root / ".github/workflows/autorelease-publish.yml").read_text() self.assertIn("validate-recaptured-evidence", release) self.assertIn("Notify actionable release failure", release) self.assertIn("release-run/failure.json", release) From ff62a8e94ded388239ec7a77771783d9b2363ee8 Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 2 Aug 2026 13:09:05 +0300 Subject: [PATCH 2/6] fix: preserve pre-rename notification dedup and protect control scripts Issues opened before the autorelease rename carry maintenance-action-key and maintenance-fingerprint markers. notify-autorelease searched only the new prefix, so a recurring action key would have created a duplicate issue and the "issue already exists" guard would no longer have fired for those issues. Both prefixes are now searched and merged by issue number. scripts/serve-autorelease-artifact and scripts/verify-autorelease-system were absent from protected-paths.json before the rename as well. Adding them only narrows what an offline agent may edit, so the omission is closed here rather than left in place while the file is already being rewritten. Also points the README at 8.5.9, which the completed event record shows as published. --- README.md | 2 +- autorelease/protected-paths.json | 2 ++ scripts/notify-autorelease | 56 +++++++++++++++++++------------- tests/test_autorelease.py | 21 ++++++++++++ 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 68d3520..bbb5ba9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Public macOS arm64 releases are available for every maintained PHP branch: [8.2.32](https://github.com/bigpixelrocket/php-bin/releases/tag/8.2.32), [8.3.32](https://github.com/bigpixelrocket/php-bin/releases/tag/8.3.32), [8.4.23](https://github.com/bigpixelrocket/php-bin/releases/tag/8.4.23), and -[8.5.8](https://github.com/bigpixelrocket/php-bin/releases/tag/8.5.8). +[8.5.9](https://github.com/bigpixelrocket/php-bin/releases/tag/8.5.9). Each release is rebuilt on macOS 26 arm64 and published only after its exact module baseline and deployment target checks pass. diff --git a/autorelease/protected-paths.json b/autorelease/protected-paths.json index cbbe4c5..9bcd8b6 100644 --- a/autorelease/protected-paths.json +++ b/autorelease/protected-paths.json @@ -17,10 +17,12 @@ "scripts/notify-autorelease", "scripts/prepare-agent-task", "scripts/seal-autorelease-patch", + "scripts/serve-autorelease-artifact", "scripts/snapshot-github-admin-state", "scripts/validate-autorelease-archive", "scripts/validate-codex-action-inputs", "scripts/validate-structured-output-schemas", + "scripts/verify-autorelease-system", "scripts/verify-merge-admission", "scripts/publish-release", "scripts/watch-autorelease-evidence", diff --git a/scripts/notify-autorelease b/scripts/notify-autorelease index 050cbed..5fe2c67 100755 --- a/scripts/notify-autorelease +++ b/scripts/notify-autorelease @@ -29,30 +29,42 @@ def gh(*arguments: str) -> str: return result.stdout.strip() +# Issues opened before the autorelease rename carry "maintenance-" markers. +# Both prefixes are searched so those issues keep deduplicating, and so the +# "issue already exists" guard still fires for them. Drop the legacy prefix once +# no action key predating the rename can recur. +MARKER_PREFIXES = ("autorelease", "maintenance") +FINGERPRINT_RE = re.compile( + r"" +) + + def find_issue(repo: str, action_key: str) -> dict | None: - marker = f"" - issues = json.loads( - gh( - "issue", - "list", - "--repo", - repo, - "--state", - "all", - "--search", - f'"{marker}" in:body', - "--json", - "number,body,state,url", - "--limit", - "100", + matches: dict[int, dict] = {} + for prefix in MARKER_PREFIXES: + marker = f"" + issues = json.loads( + gh( + "issue", + "list", + "--repo", + repo, + "--state", + "all", + "--search", + f'"{marker}" in:body', + "--json", + "number,body,state,url", + "--limit", + "100", + ) ) - ) - matches = [issue for issue in issues if marker in issue.get("body", "")] + for issue in issues: + if marker in issue.get("body", ""): + matches[issue["number"]] = issue if len(matches) > 1: raise ControlError("multiple issues exist for one action key") - if not matches: - return None - return matches[0] + return next(iter(matches.values()), None) def discover_github_prior(repo: str, action_key: str) -> dict | None: @@ -65,9 +77,7 @@ def discover_github_prior(repo: str, action_key: str) -> dict | None: bodies = [detail.get("body", "")] bodies.extend(comment.get("body", "") for comment in detail.get("comments", [])) fingerprints = [ - match.group(1) - for body in bodies - for match in re.finditer(r"", body) + match.group(1) for body in bodies for match in FINGERPRINT_RE.finditer(body) ] return { "fingerprint": fingerprints[-1] if fingerprints else None, diff --git a/tests/test_autorelease.py b/tests/test_autorelease.py index 5645b6b..8c52929 100644 --- a/tests/test_autorelease.py +++ b/tests/test_autorelease.py @@ -334,9 +334,30 @@ def test_published_asset_mismatch_fails_closed(self): def test_notification_replay_is_deduplicated(self): event = {"actionKey": "new_patch:8.5.9", "state": "released"} first = notification_decision(event, None) + self.assertEqual(["autorelease"], first["labels"]) replay = notification_decision(event, {"fingerprint": first["fingerprint"]}) self.assertEqual("none", replay["action"]) + def test_notification_search_finds_pre_rename_marker_issues(self): + namespace = runpy.run_path( + str(pathlib.Path(__file__).resolve().parents[1] / "scripts/notify-autorelease") + ) + find_issue = namespace["find_issue"] + legacy = { + "number": 46, + "url": "https://example.invalid/issues/46", + "state": "CLOSED", + "body": "", + } + gh = mock.Mock( + side_effect=lambda *arguments: json.dumps([legacy]) + if "maintenance-action-key" in " ".join(arguments) + else "[]" + ) + with mock.patch.dict(find_issue.__globals__, {"gh": gh}): + found = find_issue("Bigpixelrocket/php-bin", "new_patch:8.5.9") + self.assertEqual(legacy, found) + def test_notification_transition_reuses_retained_issue_identity(self): issue = {"number": 10, "url": "https://example.invalid/issues/10", "state": "OPEN"} self.assertEqual(issue, retained_notification_issue({"issue": issue})) From 7ad9adfaf83c929ec725c4bb809b1a7d08e82519 Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 2 Aug 2026 13:15:14 +0300 Subject: [PATCH 3/6] test: assert both notification marker lookups independently The legacy fixture alone still passed when the autorelease prefix was removed from MARKER_PREFIXES. Each prefix now gets its own subtest and fixture, so dropping either lookup fails the test. --- tests/test_autorelease.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/test_autorelease.py b/tests/test_autorelease.py index 8c52929..1e3246b 100644 --- a/tests/test_autorelease.py +++ b/tests/test_autorelease.py @@ -338,25 +338,27 @@ def test_notification_replay_is_deduplicated(self): replay = notification_decision(event, {"fingerprint": first["fingerprint"]}) self.assertEqual("none", replay["action"]) - def test_notification_search_finds_pre_rename_marker_issues(self): + def test_notification_search_covers_current_and_pre_rename_markers(self): namespace = runpy.run_path( str(pathlib.Path(__file__).resolve().parents[1] / "scripts/notify-autorelease") ) find_issue = namespace["find_issue"] - legacy = { - "number": 46, - "url": "https://example.invalid/issues/46", - "state": "CLOSED", - "body": "", - } - gh = mock.Mock( - side_effect=lambda *arguments: json.dumps([legacy]) - if "maintenance-action-key" in " ".join(arguments) - else "[]" - ) - with mock.patch.dict(find_issue.__globals__, {"gh": gh}): - found = find_issue("Bigpixelrocket/php-bin", "new_patch:8.5.9") - self.assertEqual(legacy, found) + for prefix, number in (("autorelease", 47), ("maintenance", 46)): + with self.subTest(prefix=prefix): + issue = { + "number": number, + "url": f"https://example.invalid/issues/{number}", + "state": "CLOSED", + "body": f"", + } + gh = mock.Mock( + side_effect=lambda *arguments, issue=issue, prefix=prefix: json.dumps([issue]) + if f"{prefix}-action-key" in " ".join(arguments) + else "[]" + ) + with mock.patch.dict(find_issue.__globals__, {"gh": gh}): + found = find_issue("Bigpixelrocket/php-bin", "new_patch:8.5.9") + self.assertEqual(issue, found) def test_notification_transition_reuses_retained_issue_identity(self): issue = {"number": 10, "url": "https://example.invalid/issues/10", "state": "OPEN"} From 8f653bc56be06073c756d2834f8278247605ebcf Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 2 Aug 2026 13:36:11 +0300 Subject: [PATCH 4/6] fix(ci): stop expanding dispatch inputs into workflow shell text GitHub substitutes ${{ }} into run script text before the shell parses it, so shell quoting cannot contain a hostile value. Both dispatch workflows now validate their inputs in a preflight job and publish them as outputs, and every downstream job reads those outputs through an env map instead of expanding a raw inputs expression. The preflight job also runs ahead of every actions/checkout, so exact_base_sha and exact_commit are shape-checked before they can select a ref. The e2e verifier call gains the same env binding and SHA assertion. --- .github/autorelease-pins.json | 2 +- .github/workflows/autorelease-e2e.yml | 9 +- .github/workflows/autorelease-implement.yml | 81 ++++++++--- .github/workflows/autorelease-publish.yml | 143 +++++++++++--------- 4 files changed, 151 insertions(+), 84 deletions(-) diff --git a/.github/autorelease-pins.json b/.github/autorelease-pins.json index e999d40..95df49c 100644 --- a/.github/autorelease-pins.json +++ b/.github/autorelease-pins.json @@ -10,6 +10,6 @@ "openai/codex-action": "52fe01ec70a42f454c9d2ebd47598f9fd6893d56" }, "workflows": { - ".github/workflows/autorelease-e2e.yml": "sha256:3a37af525d3144b8e815f26fff5f80af4680cb4686aef9552aebca9028d40a24" + ".github/workflows/autorelease-e2e.yml": "sha256:995cdadf6de5b756202ea9780655c1985f370cc2eb5a2c99bd7b90b1b2ad3c93" } } diff --git a/.github/workflows/autorelease-e2e.yml b/.github/workflows/autorelease-e2e.yml index 9c9073b..f54b32c 100644 --- a/.github/workflows/autorelease-e2e.yml +++ b/.github/workflows/autorelease-e2e.yml @@ -75,11 +75,16 @@ jobs: test -z "${GH_TOKEN:-}" test -z "${GITHUB_TOKEN:-}" - name: Run stable cross-repository verifier + env: + PHP_BIN_SHA: ${{ inputs.php_bin_sha }} + MISE_PHP_SHA: ${{ inputs.mise_php_sha }} run: | + [[ "$PHP_BIN_SHA" =~ ^[0-9a-f]{40}$ ]] + [[ "$MISE_PHP_SHA" =~ ^[0-9a-f]{40}$ ]] ./php-bin/scripts/verify-autorelease-system \ --mise-repo ./mise-php \ - --php-bin-sha "${{ inputs.php_bin_sha }}" \ - --mise-php-sha "${{ inputs.mise_php_sha }}" \ + --php-bin-sha "$PHP_BIN_SHA" \ + --mise-php-sha "$MISE_PHP_SHA" \ --output ./verification-results - name: Retain production-parity report if: always() diff --git a/.github/workflows/autorelease-implement.yml b/.github/workflows/autorelease-implement.yml index 6519e0e..f6ec9d4 100644 --- a/.github/workflows/autorelease-implement.yml +++ b/.github/workflows/autorelease-implement.yml @@ -23,29 +23,63 @@ permissions: actions: read jobs: + # Dispatch inputs reach actions/checkout and several run scripts. They are + # validated once here, ahead of every other job, and republished as outputs so + # no raw inputs expression is expanded anywhere downstream. + preflight: + name: Validate dispatch inputs + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + run_id: ${{ steps.validated.outputs.run_id }} + base_sha: ${{ steps.validated.outputs.base_sha }} + phase: ${{ steps.validated.outputs.phase }} + steps: + - name: Enforce exact input shapes + id: validated + env: + RUN_ID: ${{ inputs.investigation_run_id }} + BASE_SHA: ${{ inputs.exact_base_sha }} + PHASE: ${{ inputs.phase }} + run: | + [[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]] + [[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]] + [[ "$PHASE" == "implementation" || "$PHASE" == "repair" ]] + { + echo "run_id=$RUN_ID" + echo "base_sha=$BASE_SHA" + echo "phase=$PHASE" + } >> "$GITHUB_OUTPUT" + implement: name: Offline agent and patch sealing + needs: preflight runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.exact_base_sha }} + ref: ${{ needs.preflight.outputs.base_sha }} fetch-depth: 0 persist-credentials: false - name: Download admitted investigation env: GH_TOKEN: ${{ github.token }} - run: gh run download "${{ inputs.investigation_run_id }}" --name "autorelease-investigation-${{ inputs.investigation_run_id }}" --dir autorelease-run + RUN_ID: ${{ needs.preflight.outputs.run_id }} + run: gh run download "$RUN_ID" --name "autorelease-investigation-$RUN_ID" --dir autorelease-run - name: Enforce operator pause run: test "$(jq -r .unattendedMutation .github/autorelease-operator.json)" = "enabled" - name: Verify exact admitted base + env: + BASE_SHA: ${{ needs.preflight.outputs.base_sha }} run: | - test "$(git rev-parse HEAD)" = "${{ inputs.exact_base_sha }}" - test "$(jq -r .preconditions.phpBinHead autorelease-run/autorelease-plan.json)" = "${{ inputs.exact_base_sha }}" + test "$(git rev-parse HEAD)" = "$BASE_SHA" + test "$(jq -r .preconditions.phpBinHead autorelease-run/autorelease-plan.json)" = "$BASE_SHA" - name: Prepare offline phase contract + env: + PHASE: ${{ needs.preflight.outputs.phase }} run: | - phase="${{ inputs.phase }}" + phase="$PHASE" ./scripts/prepare-agent-task \ --phase "$phase" \ --action-key "$(jq -r .actionKey autorelease-run/autorelease-plan.json)" \ @@ -85,10 +119,12 @@ jobs: allow-bot-users: github-actions[bot] codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/agent-completion-assessment.schema.json"]' - name: Seal exact agent diff + env: + BASE_SHA: ${{ needs.preflight.outputs.base_sha }} run: | ./scripts/seal-autorelease-patch \ --repo . \ - --base "${{ inputs.exact_base_sha }}" \ + --base "$BASE_SHA" \ --plan autorelease-run/implementation-plan.json \ --result autorelease-run/implementation-result.json \ --contract autorelease-run/implementation-contract.json \ @@ -104,7 +140,7 @@ jobs: validate: name: Clean deterministic validation - needs: implement + needs: [preflight, implement] outputs: passed: ${{ steps.checks.outputs.passed }} runs-on: ubuntu-latest @@ -115,7 +151,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.exact_base_sha }} + ref: ${{ needs.preflight.outputs.base_sha }} fetch-depth: 0 persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -140,6 +176,8 @@ jobs: fi - name: Record validated SHA and tree if: steps.checks.outputs.passed == 'true' + env: + BASE_SHA: ${{ needs.preflight.outputs.base_sha }} run: | export GIT_AUTHOR_NAME=autorelease-validator export GIT_AUTHOR_EMAIL=autorelease@invalid @@ -149,7 +187,7 @@ jobs: export GIT_COMMITTER_DATE=2000-01-01T00:00:00Z git commit -m "chore: apply admitted autorelease patch" jq -n --arg headSha "$(git rev-parse HEAD)" --arg tree "$(git rev-parse HEAD^{tree})" '{headSha:$headSha,tree:$tree,checks:{"Script checks":"success"}}' > autorelease-run/validation.json - git bundle create autorelease-run/validated.bundle HEAD "^${{ inputs.exact_base_sha }}" + git bundle create autorelease-run/validated.bundle HEAD "^$BASE_SHA" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: steps.checks.outputs.passed == 'true' with: @@ -169,7 +207,7 @@ jobs: repair: name: One bounded offline repair - needs: validate + needs: [preflight, validate] if: needs.validate.outputs.passed != 'true' runs-on: ubuntu-latest timeout-minutes: 30 @@ -179,7 +217,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.exact_base_sha }} + ref: ${{ needs.preflight.outputs.base_sha }} fetch-depth: 0 persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -237,11 +275,13 @@ jobs: allow-bot-users: github-actions[bot] codex-args: '["--strict-config","--ephemeral","--output-schema","schemas/agent-completion-assessment.schema.json"]' - name: Seal the combined repaired diff + env: + BASE_SHA: ${{ needs.preflight.outputs.base_sha }} run: | mv autorelease-run/repair-plan.json autorelease-run/implementation-plan.json ./scripts/seal-autorelease-patch \ --repo . \ - --base "${{ inputs.exact_base_sha }}" \ + --base "$BASE_SHA" \ --plan autorelease-run/implementation-plan.json \ --result autorelease-run/repair-result.json \ --contract autorelease-run/repair-contract.json \ @@ -256,7 +296,7 @@ jobs: validate-repair: name: Clean deterministic repair validation - needs: repair + needs: [preflight, repair] runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -265,7 +305,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.exact_base_sha }} + ref: ${{ needs.preflight.outputs.base_sha }} fetch-depth: 0 persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -280,6 +320,8 @@ jobs: cp autorelease-run/sealed-repair/sealed.patch autorelease-run/sealed/sealed.patch cp autorelease-run/sealed-repair/patch-manifest.json autorelease-run/sealed/patch-manifest.json - name: Record repaired validated SHA and tree + env: + BASE_SHA: ${{ needs.preflight.outputs.base_sha }} run: | export GIT_AUTHOR_NAME=autorelease-validator export GIT_AUTHOR_EMAIL=autorelease@invalid @@ -289,7 +331,7 @@ jobs: export GIT_COMMITTER_DATE=2000-01-01T00:00:00Z git commit -m "chore: apply admitted autorelease repair" jq -n --arg headSha "$(git rev-parse HEAD)" --arg tree "$(git rev-parse HEAD^{tree})" '{headSha:$headSha,tree:$tree,checks:{"Script checks":"success"}}' > autorelease-run/validation.json - git bundle create autorelease-run/validated.bundle HEAD "^${{ inputs.exact_base_sha }}" + git bundle create autorelease-run/validated.bundle HEAD "^$BASE_SHA" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: validated-autorelease-patch-${{ github.run_id }} @@ -300,8 +342,11 @@ jobs: merge: name: Exact-SHA PR and merge admission - needs: [validate, validate-repair] - if: always() && (needs.validate.outputs.passed == 'true' || needs['validate-repair'].result == 'success') + needs: [preflight, validate, validate-repair] + # always() keeps this job reachable when repair paths skip, so the preflight + # result is asserted explicitly. Without it a rejected dispatch input would + # still reach checkout, with an empty ref. + if: always() && needs.preflight.result == 'success' && (needs.validate.outputs.passed == 'true' || needs['validate-repair'].result == 'success') runs-on: ubuntu-latest timeout-minutes: 130 permissions: @@ -313,7 +358,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.exact_base_sha }} + ref: ${{ needs.preflight.outputs.base_sha }} fetch-depth: 0 persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 diff --git a/.github/workflows/autorelease-publish.yml b/.github/workflows/autorelease-publish.yml index fcd75e9..a089f63 100644 --- a/.github/workflows/autorelease-publish.yml +++ b/.github/workflows/autorelease-publish.yml @@ -33,14 +33,52 @@ concurrency: cancel-in-progress: false jobs: + # Dispatch inputs reach actions/checkout and many run scripts. They are shaped + # once here, ahead of every other job, and republished as outputs so no raw + # inputs expression is expanded anywhere downstream. + preflight: + name: Validate dispatch inputs + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + version: ${{ steps.validated.outputs.version }} + exact_commit: ${{ steps.validated.outputs.exact_commit }} + action_key: ${{ steps.validated.outputs.action_key }} + investigation_run_id: ${{ steps.validated.outputs.investigation_run_id }} + steps: + - name: Enforce exact input shapes + id: validated + env: + VERSION: ${{ inputs.version }} + EXACT_COMMIT: ${{ inputs.exact_commit }} + ACTION_KEY: ${{ inputs.action_key }} + INVESTIGATION_RUN_ID: ${{ inputs.investigation_run_id }} + run: | + [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?$ ]] + [[ "$EXACT_COMMIT" =~ ^[0-9a-f]{40}$ ]] + [[ "$ACTION_KEY" =~ ^[A-Za-z0-9._:-]+$ ]] + [[ "$INVESTIGATION_RUN_ID" =~ ^[1-9][0-9]*$ ]] + { + echo "version=$VERSION" + echo "exact_commit=$EXACT_COMMIT" + echo "action_key=$ACTION_KEY" + echo "investigation_run_id=$INVESTIGATION_RUN_ID" + } >> "$GITHUB_OUTPUT" + release: + needs: preflight runs-on: macos-26 timeout-minutes: 150 environment: php-autorelease-publish + env: + VERSION: ${{ needs.preflight.outputs.version }} + EXACT_COMMIT: ${{ needs.preflight.outputs.exact_commit }} + ACTION_KEY: ${{ needs.preflight.outputs.action_key }} + INVESTIGATION_RUN_ID: ${{ needs.preflight.outputs.investigation_run_id }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.exact_commit }} + ref: ${{ needs.preflight.outputs.exact_commit }} fetch-depth: 0 persist-credentials: false - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -53,20 +91,14 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh run download "${{ inputs.investigation_run_id }}" \ - --name "autorelease-investigation-${{ inputs.investigation_run_id }}" \ + gh run download "$INVESTIGATION_RUN_ID" \ + --name "autorelease-investigation-$INVESTIGATION_RUN_ID" \ --dir admitted-run - name: Verify stable version and exact commit env: GH_TOKEN: ${{ github.token }} - VERSION: ${{ inputs.version }} - EXACT_COMMIT: ${{ inputs.exact_commit }} - ACTION_KEY: ${{ inputs.action_key }} run: | ./autorelease/control.py validate-policy - [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?$ ]] - [[ "$EXACT_COMMIT" =~ ^[0-9a-f]{40}$ ]] - [[ "$ACTION_KEY" =~ ^[A-Za-z0-9._:-]+$ ]] test "$(git rev-parse HEAD)" = "$EXACT_COMMIT" test "$(jq -r .actionKey admitted-run/autorelease-plan.json)" = "$ACTION_KEY" test "$(jq -r .releaseIntent.version admitted-run/autorelease-plan.json)" = "$VERSION" @@ -97,13 +129,13 @@ jobs: run: | action="$(jq -r .action admitted-run/autorelease-plan.json)" if [[ "$action" == "new_branch" ]]; then - filename="$(printf '%s' "${{ inputs.action_key }}" | tr ':/' '--').json" + filename="$(printf '%s' "$ACTION_KEY" | tr ':/' '--').json" event="autorelease-events/$filename" test -f "$event" test "$(jq -r .state "$event")" = "php_bin_ready" gh api "repos/bigpixelrocket/mise-php/contents/readiness/$filename?ref=main" \ --jq .content | base64 --decode > release-run/mise-readiness.json - test "$(jq -r .actionKey release-run/mise-readiness.json)" = "${{ inputs.action_key }}" + test "$(jq -r .actionKey release-run/mise-readiness.json)" = "$ACTION_KEY" test "$(jq -r .ready release-run/mise-readiness.json)" = "true" test "$(jq -r .policyDigest release-run/mise-readiness.json)" = "$(jq -r .supportPolicyDigest "$event")" test "$(jq -r .policyInvariantsDigest release-run/mise-readiness.json)" = "$(jq -r .policyInvariantsDigest "$event")" @@ -119,13 +151,13 @@ jobs: GH_TOKEN: ${{ github.token }} run: | mkdir -p .artifacts - if gh release view "${{ inputs.version }}" --repo "${{ github.repository }}" >/dev/null 2>&1; then - gh release download "${{ inputs.version }}" --repo "${{ github.repository }}" --dir .artifacts + if gh release view "$VERSION" --repo "${{ github.repository }}" >/dev/null 2>&1; then + gh release download "$VERSION" --repo "${{ github.repository }}" --dir .artifacts ./scripts/validate-autorelease-archive \ - --archive ".artifacts/php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" \ - --version "${{ inputs.version }}" - archive_digest="$(shasum -a 256 ".artifacts/php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" | awk '{print $1}')" - grep -Fx "$archive_digest php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" .artifacts/SHA256SUMS + --archive ".artifacts/php-$VERSION-cli-macos-aarch64.tar.gz" \ + --version "$VERSION" + archive_digest="$(shasum -a 256 ".artifacts/php-$VERSION-cli-macos-aarch64.tar.gz" | awk '{print $1}')" + grep -Fx "$archive_digest php-$VERSION-cli-macos-aarch64.tar.gz" .artifacts/SHA256SUMS echo "reuse=true" >> "$GITHUB_OUTPUT" else echo "reuse=false" >> "$GITHUB_OUTPUT" @@ -138,19 +170,19 @@ jobs: run: ./scripts/install-spc.sh - name: Build exact patch if: steps.existing.outputs.reuse != 'true' - run: ./scripts/build.sh "${{ inputs.version }}" s4 + run: ./scripts/build.sh "$VERSION" s4 - name: Package and validate staged bytes if: steps.existing.outputs.reuse != 'true' run: | - ./scripts/package.sh ".build/${{ inputs.version }}/s4/buildroot/bin/php" "${{ inputs.version }}" + ./scripts/package.sh ".build/$VERSION/s4/buildroot/bin/php" "$VERSION" ./scripts/validate-autorelease-archive \ - --archive ".artifacts/php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" \ - --version "${{ inputs.version }}" + --archive ".artifacts/php-$VERSION-cli-macos-aarch64.tar.gz" \ + --version "$VERSION" - name: Initialize release transaction and event run: | mkdir -p release-run printf '{"schemaVersion":1,"state":"requested","history":[]}\n' > release-run/transaction.json - filename="$(printf '%s' "${{ inputs.action_key }}" | tr ':/' '--').json" + filename="$(printf '%s' "$ACTION_KEY" | tr ':/' '--').json" if [[ -f "autorelease-events/$filename" ]]; then cp "autorelease-events/$filename" release-run/event.json if [[ "$(jq -r .state release-run/event.json)" == "php_bin_ready" \ @@ -168,9 +200,9 @@ jobs: mv release-run/next-event.json release-run/event.json else jq -n \ - --arg actionKey "${{ inputs.action_key }}" \ + --arg actionKey "$ACTION_KEY" \ --arg classification "$(jq -r .action admitted-run/autorelease-plan.json)" \ - --arg commit "${{ inputs.exact_commit }}" \ + --arg commit "$EXACT_COMMIT" \ --arg planDigest "$(jq -r .planDigest admitted-run/admission.json)" \ --arg evidenceManifestDigest "$(jq -r .manifestDigest admitted-run/evidence/evidence-manifest.json)" \ --argjson evidenceDigests "$(jq '[.evidence[].digest]' admitted-run/autorelease-plan.json)" \ @@ -189,8 +221,8 @@ jobs: --transaction release-run/transaction.json \ --target "$target" \ --assets .artifacts \ - --version "${{ inputs.version }}" \ - --commit "${{ inputs.exact_commit }}" \ + --version "$VERSION" \ + --commit "$EXACT_COMMIT" \ --backend github \ --repo "${{ github.repository }}" \ --output release-run/next.json @@ -201,9 +233,9 @@ jobs: GH_TOKEN: ${{ github.token }} run: | temporary="$(mktemp -d)" - gh release download "${{ inputs.version }}" --repo "${{ github.repository }}" --dir "$temporary" + gh release download "$VERSION" --repo "${{ github.repository }}" --dir "$temporary" port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()')" - ./scripts/serve-autorelease-artifact --port "$port" --assets "$temporary" --version "${{ inputs.version }}" & + ./scripts/serve-autorelease-artifact --port "$port" --assets "$temporary" --version "$VERSION" & server_pid="$!" trap 'kill "$server_pid" 2>/dev/null || true' EXIT ready=false @@ -221,9 +253,9 @@ jobs: export MISE_CONFIG_DIR="$temporary/mise/config" export MISE_STATE_DIR="$temporary/mise/state" mise plugin link php "$GITHUB_WORKSPACE/mise-php" - mise install "php@${{ inputs.version }}" - mise exec "php@${{ inputs.version }}" -- php -v - mise exec "php@${{ inputs.version }}" -- php -m | grep -Fx Core + mise install "php@$VERSION" + mise exec "php@$VERSION" -- php -v + mise exec "php@$VERSION" -- php -m | grep -Fx Core kill "$server_pid" wait "$server_pid" || true trap - EXIT @@ -239,17 +271,17 @@ jobs: --transaction release-run/transaction.json \ --target "$target" \ --assets .artifacts \ - --version "${{ inputs.version }}" \ - --commit "${{ inputs.exact_commit }}" \ + --version "$VERSION" \ + --commit "$EXACT_COMMIT" \ --backend github \ --repo "${{ github.repository }}" \ --output release-run/next.json mv release-run/next.json release-run/transaction.json done - test "$(gh release view "${{ inputs.version }}" --repo "${{ github.repository }}" --json isImmutable --jq .isImmutable)" = "true" + test "$(gh release view "$VERSION" --repo "${{ github.repository }}" --json isImmutable --jq .isImmutable)" = "true" verified=false for _ in {1..12}; do - if gh release verify "${{ inputs.version }}" --repo "${{ github.repository }}" --format json \ + if gh release verify "$VERSION" --repo "${{ github.repository }}" --format json \ > release-run/release-attestation.json; then verified=true break @@ -265,22 +297,22 @@ jobs: export MISE_CACHE_DIR="$temporary/cache" export MISE_CONFIG_DIR="$temporary/config" export MISE_STATE_DIR="$temporary/state" - branch="${{ inputs.version }}" + branch="$VERSION" branch="${branch%%-*}" branch="${branch%.*}" mise plugin link php "$GITHUB_WORKSPACE/mise-php" - mise install "php@${{ inputs.version }}" + mise install "php@$VERSION" mise install "php@$branch" - mise exec "php@${{ inputs.version }}" -- php -v + mise exec "php@$VERSION" -- php -v mise exec "php@$branch" -- php -v - name: Complete durable event run: | - jq -n --arg version "${{ inputs.version }}" --argjson assets "$(jq .assetDigests release-run/transaction.json)" \ + jq -n --arg version "$VERSION" --argjson assets "$(jq .assetDigests release-run/transaction.json)" \ '[{kind:"published_release",version:$version,assetDigests:$assets}]' > release-run/evidence.json ./scripts/autorelease-event --event release-run/event.json --target released \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json - jq -n --arg version "${{ inputs.version }}" \ + jq -n --arg version "$VERSION" \ '[{kind:"fresh_public_mise_installs",version:$version,modes:["exact","branch_shorthand"]}]' > release-run/evidence.json ./scripts/autorelease-event --event release-run/event.json --target public_install_verified \ --evidence release-run/evidence.json --output release-run/next-event.json @@ -289,7 +321,7 @@ jobs: ./scripts/autorelease-event --event release-run/event.json --target complete \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json - jq '.severity="info" | .summary="PHP ${{ inputs.version }} was published and verified through fresh exact and branch-shorthand mise installs." | .finalResult="passed"' \ + jq '.severity="info" | .summary="PHP $VERSION was published and verified through fresh exact and branch-shorthand mise installs." | .finalResult="passed"' \ release-run/event.json > release-run/notification-event.json - name: Commit final event record through a checked PR id: event_pr @@ -299,18 +331,18 @@ jobs: git fetch origin main git checkout -B "autorelease/event-${{ github.run_id }}" origin/main base="$(git rev-parse HEAD)" - filename="$(printf '%s' "${{ inputs.action_key }}" | tr ':/' '--').json" + filename="$(printf '%s' "$ACTION_KEY" | tr ':/' '--').json" cp release-run/event.json "autorelease-events/$filename" git add "autorelease-events/$filename" git -c user.name=autorelease -c user.email=autorelease@invalid \ - commit -m "chore: complete ${{ inputs.action_key }}" + commit -m "chore: complete $ACTION_KEY" head="$(git rev-parse HEAD)" record="autorelease-events/$filename" digest="sha256:$(shasum -a 256 "$record" | awk '{print $1}')" gh auth setup-git git push origin HEAD url="$(gh pr create --base main --head "autorelease/event-${{ github.run_id }}" \ - --title "chore: complete ${{ inputs.action_key }}" \ + --title "chore: complete $ACTION_KEY" \ --body "Durable event record for the immutable verified release transaction.")" echo "number=${url##*/}" >> "$GITHUB_OUTPUT" echo "base_sha=$base" >> "$GITHUB_OUTPUT" @@ -347,16 +379,7 @@ jobs: --owner "${{ vars.AUTORELEASE_OWNER }}" - name: Record release failure diagnostic if: failure() - env: - ACTION_KEY: ${{ inputs.action_key }} - VERSION: ${{ inputs.version }} - EXACT_COMMIT: ${{ inputs.exact_commit }} - INVESTIGATION_RUN_ID: ${{ inputs.investigation_run_id }} run: | - [[ "$ACTION_KEY" =~ ^[A-Za-z0-9._:-]+$ ]] - [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?$ ]] - [[ "$EXACT_COMMIT" =~ ^[0-9a-f]{40}$ ]] - [[ "$INVESTIGATION_RUN_ID" =~ ^[1-9][0-9]*$ ]] mkdir -p release-run jq -n \ --arg actionKey "$ACTION_KEY" \ @@ -371,7 +394,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: release-transaction-${{ inputs.version }}-${{ github.run_id }} + name: release-transaction-${{ needs.preflight.outputs.version }}-${{ github.run_id }} path: | .artifacts/ release-run/ @@ -381,7 +404,7 @@ jobs: notify-failure: name: Notify actionable release failure - needs: [release] + needs: [preflight, release] if: always() && needs.release.result == 'failure' runs-on: ubuntu-latest timeout-minutes: 5 @@ -394,16 +417,10 @@ jobs: persist-credentials: false - name: Create structured critical event env: - ACTION_KEY: ${{ inputs.action_key }} - VERSION: ${{ inputs.version }} - EXACT_COMMIT: ${{ inputs.exact_commit }} - INVESTIGATION_RUN_ID: ${{ inputs.investigation_run_id }} + ACTION_KEY: ${{ needs.preflight.outputs.action_key }} + VERSION: ${{ needs.preflight.outputs.version }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - [[ "$ACTION_KEY" =~ ^[A-Za-z0-9._:-]+$ ]] - [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?$ ]] - [[ "$EXACT_COMMIT" =~ ^[0-9a-f]{40}$ ]] - [[ "$INVESTIGATION_RUN_ID" =~ ^[1-9][0-9]*$ ]] jq -n \ --arg actionKey "$ACTION_KEY" \ --arg summary "Autorelease failed for PHP $VERSION: $RUN_URL" \ From 0a7ea3d7e1aac77830c8af5ed0bb666c5fdf2d22 Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 2 Aug 2026 13:41:18 +0300 Subject: [PATCH 5/6] fix(ci): restore the version in the completion summary and gate e2e inputs The publish summary built its jq program in single quotes, so moving the version off a ${{ }} expansion left jq resolving an undefined $VERSION variable instead of the released version. It is passed with --arg, matching the two jq calls directly above it. autorelease-e2e.yml gains the same preflight job as the other two dispatch workflows, so all four suites check out validated SHAs. notify-failure asserts the preflight result alongside always(). --- .github/autorelease-pins.json | 2 +- .github/workflows/autorelease-e2e.yml | 46 ++++++++++++++++++----- .github/workflows/autorelease-publish.yml | 8 +++- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.github/autorelease-pins.json b/.github/autorelease-pins.json index 95df49c..80f49f5 100644 --- a/.github/autorelease-pins.json +++ b/.github/autorelease-pins.json @@ -10,6 +10,6 @@ "openai/codex-action": "52fe01ec70a42f454c9d2ebd47598f9fd6893d56" }, "workflows": { - ".github/workflows/autorelease-e2e.yml": "sha256:995cdadf6de5b756202ea9780655c1985f370cc2eb5a2c99bd7b90b1b2ad3c93" + ".github/workflows/autorelease-e2e.yml": "sha256:8de5aed90a14a55eb8fcfa07e765ac44a2114e3dcf46f5ffbf61228afc05a25c" } } diff --git a/.github/workflows/autorelease-e2e.yml b/.github/workflows/autorelease-e2e.yml index f54b32c..eba0bd1 100644 --- a/.github/workflows/autorelease-e2e.yml +++ b/.github/workflows/autorelease-e2e.yml @@ -30,7 +30,32 @@ concurrency: cancel-in-progress: false jobs: + # Dispatch inputs select the refs every suite checks out. They are shaped once + # here, ahead of every other job, and republished as outputs so no raw inputs + # expression reaches a checkout or a run script. + preflight: + name: Validate dispatch inputs + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + php_bin_sha: ${{ steps.validated.outputs.php_bin_sha }} + mise_php_sha: ${{ steps.validated.outputs.mise_php_sha }} + steps: + - name: Enforce exact input shapes + id: validated + env: + PHP_BIN_SHA: ${{ inputs.php_bin_sha }} + MISE_PHP_SHA: ${{ inputs.mise_php_sha }} + run: | + [[ "$PHP_BIN_SHA" =~ ^[0-9a-f]{40}$ ]] + [[ "$MISE_PHP_SHA" =~ ^[0-9a-f]{40}$ ]] + { + echo "php_bin_sha=$PHP_BIN_SHA" + echo "mise_php_sha=$MISE_PHP_SHA" + } >> "$GITHUB_OUTPUT" + production-parity: + needs: preflight if: inputs.suite == 'production-parity' runs-on: macos-26 timeout-minutes: 45 @@ -51,7 +76,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: bigpixelrocket/php-bin - ref: ${{ inputs.php_bin_sha }} + ref: ${{ needs.preflight.outputs.php_bin_sha }} path: php-bin fetch-depth: 0 persist-credentials: false @@ -59,7 +84,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: bigpixelrocket/mise-php - ref: ${{ inputs.mise_php_sha }} + ref: ${{ needs.preflight.outputs.mise_php_sha }} path: mise-php fetch-depth: 0 persist-credentials: false @@ -76,11 +101,9 @@ jobs: test -z "${GITHUB_TOKEN:-}" - name: Run stable cross-repository verifier env: - PHP_BIN_SHA: ${{ inputs.php_bin_sha }} - MISE_PHP_SHA: ${{ inputs.mise_php_sha }} + PHP_BIN_SHA: ${{ needs.preflight.outputs.php_bin_sha }} + MISE_PHP_SHA: ${{ needs.preflight.outputs.mise_php_sha }} run: | - [[ "$PHP_BIN_SHA" =~ ^[0-9a-f]{40}$ ]] - [[ "$MISE_PHP_SHA" =~ ^[0-9a-f]{40}$ ]] ./php-bin/scripts/verify-autorelease-system \ --mise-repo ./mise-php \ --php-bin-sha "$PHP_BIN_SHA" \ @@ -97,6 +120,7 @@ jobs: include-hidden-files: true agent-canary: + needs: preflight if: inputs.suite == 'agent-canary' runs-on: ubuntu-latest timeout-minutes: 15 @@ -106,11 +130,11 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.php_bin_sha }} + ref: ${{ needs.preflight.outputs.php_bin_sha }} persist-credentials: false - name: Bind the credentialed canary to reviewed main env: - EXPECTED_SHA: ${{ inputs.php_bin_sha }} + EXPECTED_SHA: ${{ needs.preflight.outputs.php_bin_sha }} run: | test "$GITHUB_REF" = "refs/heads/main" test "$GITHUB_SHA" = "$EXPECTED_SHA" @@ -163,6 +187,7 @@ jobs: retention-days: 90 notification-canary: + needs: preflight if: inputs.suite == 'notification-canary' runs-on: ubuntu-latest timeout-minutes: 10 @@ -172,7 +197,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - ref: ${{ inputs.php_bin_sha }} + ref: ${{ needs.preflight.outputs.php_bin_sha }} persist-credentials: false - name: Create, replay, transition, and close one namespaced issue env: @@ -205,6 +230,7 @@ jobs: retention-days: 90 live-canary: + needs: preflight if: inputs.suite == 'live-canary' runs-on: macos-26 timeout-minutes: 20 @@ -213,7 +239,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: bigpixelrocket/mise-php - ref: ${{ inputs.mise_php_sha }} + ref: ${{ needs.preflight.outputs.mise_php_sha }} persist-credentials: false - uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4 - name: Verify exact and branch-shorthand public installs diff --git a/.github/workflows/autorelease-publish.yml b/.github/workflows/autorelease-publish.yml index a089f63..0799e79 100644 --- a/.github/workflows/autorelease-publish.yml +++ b/.github/workflows/autorelease-publish.yml @@ -321,7 +321,8 @@ jobs: ./scripts/autorelease-event --event release-run/event.json --target complete \ --evidence release-run/evidence.json --output release-run/next-event.json mv release-run/next-event.json release-run/event.json - jq '.severity="info" | .summary="PHP $VERSION was published and verified through fresh exact and branch-shorthand mise installs." | .finalResult="passed"' \ + jq --arg version "$VERSION" \ + '.severity="info" | .summary="PHP \($version) was published and verified through fresh exact and branch-shorthand mise installs." | .finalResult="passed"' \ release-run/event.json > release-run/notification-event.json - name: Commit final event record through a checked PR id: event_pr @@ -405,7 +406,10 @@ jobs: notify-failure: name: Notify actionable release failure needs: [preflight, release] - if: always() && needs.release.result == 'failure' + # A skipped release already keeps this job quiet when preflight rejects the + # dispatch, but the preflight result is asserted anyway so the gate survives + # a later change to the release job's own condition. + if: always() && needs.preflight.result == 'success' && needs.release.result == 'failure' runs-on: ubuntu-latest timeout-minutes: 5 permissions: From 3f2f45f06e8536d1e2bd4d6d183ddbbc38f8eb13 Mon Sep 17 00:00:00 2001 From: Lucian Behind The Scenes Date: Sun, 2 Aug 2026 13:47:14 +0300 Subject: [PATCH 6/6] ci: validate the e2e live version in preflight live_version was the last free-form dispatch input still shaped inside the job that consumed it. Preflight now shapes it for the live-canary suite and republishes it blank for the others, so the file has one validation point and a malformed version fails before a macOS runner is provisioned. --- .github/autorelease-pins.json | 2 +- .github/workflows/autorelease-e2e.yml | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/autorelease-pins.json b/.github/autorelease-pins.json index 80f49f5..e48cacf 100644 --- a/.github/autorelease-pins.json +++ b/.github/autorelease-pins.json @@ -10,6 +10,6 @@ "openai/codex-action": "52fe01ec70a42f454c9d2ebd47598f9fd6893d56" }, "workflows": { - ".github/workflows/autorelease-e2e.yml": "sha256:8de5aed90a14a55eb8fcfa07e765ac44a2114e3dcf46f5ffbf61228afc05a25c" + ".github/workflows/autorelease-e2e.yml": "sha256:677ad87c8c58bdb61e6fc8e54782b70a89533ac4916827414a7d639985949c6b" } } diff --git a/.github/workflows/autorelease-e2e.yml b/.github/workflows/autorelease-e2e.yml index eba0bd1..e33e8b4 100644 --- a/.github/workflows/autorelease-e2e.yml +++ b/.github/workflows/autorelease-e2e.yml @@ -40,18 +40,31 @@ jobs: outputs: php_bin_sha: ${{ steps.validated.outputs.php_bin_sha }} mise_php_sha: ${{ steps.validated.outputs.mise_php_sha }} + live_version: ${{ steps.validated.outputs.live_version }} steps: - name: Enforce exact input shapes id: validated env: PHP_BIN_SHA: ${{ inputs.php_bin_sha }} MISE_PHP_SHA: ${{ inputs.mise_php_sha }} + SUITE: ${{ inputs.suite }} + LIVE_VERSION: ${{ inputs.live_version }} run: | [[ "$PHP_BIN_SHA" =~ ^[0-9a-f]{40}$ ]] [[ "$MISE_PHP_SHA" =~ ^[0-9a-f]{40}$ ]] + # live_version is optional and only meaningful for live-canary, so it is + # shaped for that suite and republished empty for every other one. The + # blanking matters: an unshaped value reaching $GITHUB_OUTPUT could set + # arbitrary outputs. + if [[ "$SUITE" == "live-canary" ]]; then + [[ "$LIVE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?$ ]] + else + LIVE_VERSION= + fi { echo "php_bin_sha=$PHP_BIN_SHA" echo "mise_php_sha=$MISE_PHP_SHA" + echo "live_version=$LIVE_VERSION" } >> "$GITHUB_OUTPUT" production-parity: @@ -244,9 +257,8 @@ jobs: - uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4 - name: Verify exact and branch-shorthand public installs env: - LIVE_VERSION: ${{ inputs.live_version }} + LIVE_VERSION: ${{ needs.preflight.outputs.live_version }} run: | - [[ "$LIVE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?$ ]] branch="$LIVE_VERSION" branch="${branch%%-*}" branch="${branch%.*}"