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..e48cacf 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:677ad87c8c58bdb61e6fc8e54782b70a89533ac4916827414a7d639985949c6b" } } 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 70% rename from .github/workflows/maintenance-e2e.yml rename to .github/workflows/autorelease-e2e.yml index 70372ac..e33e8b4 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,11 +26,49 @@ permissions: contents: read concurrency: - group: maintenance-e2e-${{ inputs.suite }} + group: autorelease-e2e-${{ inputs.suite }} 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 }} + 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: + needs: preflight if: inputs.suite == 'production-parity' runs-on: macos-26 timeout-minutes: 45 @@ -44,14 +82,14 @@ 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 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 +97,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 @@ -75,37 +113,41 @@ jobs: test -z "${GH_TOKEN:-}" test -z "${GITHUB_TOKEN:-}" - name: Run stable cross-repository verifier + env: + PHP_BIN_SHA: ${{ needs.preflight.outputs.php_bin_sha }} + MISE_PHP_SHA: ${{ needs.preflight.outputs.mise_php_sha }} 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 }}" \ + --php-bin-sha "$PHP_BIN_SHA" \ + --mise-php-sha "$MISE_PHP_SHA" \ --output ./verification-results - name: Retain production-parity report 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 include-hidden-files: true agent-canary: + needs: preflight 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: - 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" @@ -152,12 +194,13 @@ 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 notification-canary: + needs: preflight if: inputs.suite == 'notification-canary' runs-on: ubuntu-latest timeout-minutes: 10 @@ -167,7 +210,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: @@ -178,44 +221,44 @@ 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 live-canary: + needs: preflight 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: 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 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%.*}" diff --git a/.github/workflows/maintenance-implementation.yml b/.github/workflows/autorelease-implement.yml similarity index 58% rename from .github/workflows/maintenance-implementation.yml rename to .github/workflows/autorelease-implement.yml index a0ee9ea..f6ec9d4 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: @@ -23,34 +23,68 @@ 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 "maintenance-investigation-${{ inputs.investigation_run_id }}" --dir maintenance-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/maintenance-operator.json)" = "enabled" + 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 maintenance-run/maintenance-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 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 +92,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 +109,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 @@ -85,26 +119,28 @@ 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-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 + --base "$BASE_SHA" \ + --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 validate: name: Clean deterministic validation - needs: implement + needs: [preflight, implement] outputs: passed: ${{ steps.checks.outputs.passed }} runs-on: ubuntu-latest @@ -115,22 +151,22 @@ 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 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 @@ -140,36 +176,38 @@ 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=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 "^$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 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,22 +217,22 @@ 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 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 +240,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 +265,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 @@ -237,26 +275,28 @@ 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 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 + --base "$BASE_SHA" \ + --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 validate-repair: name: Clean deterministic repair validation - needs: repair + needs: [preflight, repair] runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -265,43 +305,48 @@ 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 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 + env: + BASE_SHA: ${{ needs.preflight.outputs.base_sha }} 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 "^$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 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,17 +358,17 @@ 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 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 +376,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 +394,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 +435,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 +464,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 +498,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 70% rename from .github/workflows/maintenance-release.yml rename to .github/workflows/autorelease-publish.yml index a27f926..0799e79 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,18 +29,56 @@ permissions: statuses: write concurrency: - group: guarded-release-${{ inputs.version }} + group: autorelease-publish-${{ inputs.version }} 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-maintenance-release + 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,28 +91,22 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - gh run download "${{ inputs.investigation_run_id }}" \ - --name "maintenance-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: | - ./maintenance/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._:-]+$ ]] + ./autorelease/control.py validate-policy 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,24 +118,24 @@ 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" + 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 - ./scripts/validate-maintenance-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 + 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-$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,42 +170,42 @@ 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/validate-maintenance-archive \ - --archive ".artifacts/php-${{ inputs.version }}-cli-macos-aarch64.tar.gz" \ - --version "${{ inputs.version }}" + ./scripts/package.sh ".build/$VERSION/s4/buildroot/bin/php" "$VERSION" + ./scripts/validate-autorelease-archive \ + --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" - if [[ -f "maintenance-events/$filename" ]]; then - cp "maintenance-events/$filename" release-run/event.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" \ - && "$(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 commit "${{ inputs.exact_commit }}" \ + --arg actionKey "$ACTION_KEY" \ + --arg classification "$(jq -r .action admitted-run/autorelease-plan.json)" \ + --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/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,16 +213,16 @@ 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 \ - --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-maintenance-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 @@ -231,25 +263,25 @@ 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 \ - --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,31 +297,32 @@ 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/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 }}" \ + jq -n --arg version "$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"' \ + 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 @@ -297,20 +330,20 @@ 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 \ - commit -m "chore: complete ${{ inputs.action_key }}" + 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 $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 }}" \ - --title "chore: complete ${{ inputs.action_key }}" \ + url="$(gh pr create --base main --head "autorelease/event-${{ github.run_id }}" \ + --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" @@ -338,25 +371,16 @@ 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: - 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 +395,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,8 +405,11 @@ jobs: notify-failure: name: Notify actionable release failure - needs: [release] - if: always() && needs.release.result == 'failure' + needs: [preflight, release] + # 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: @@ -394,19 +421,13 @@ 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 "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 +435,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..bbb5ba9 100644 --- a/README.md +++ b/README.md @@ -13,118 +13,19 @@ 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. -## 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..9bcd8b6 --- /dev/null +++ b/autorelease/protected-paths.json @@ -0,0 +1,33 @@ +{ + "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/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", + "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 74% rename from scripts/notify-maintenance rename to scripts/notify-autorelease index cbd49f3..5fe2c67 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, @@ -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, @@ -77,10 +87,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 +109,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 +151,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 91% rename from tests/test_maintenance.py rename to tests/test_autorelease.py index d61cc71..1e3246b 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) @@ -334,9 +334,32 @@ 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_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"] + 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"} self.assertEqual(issue, retained_notification_issue({"issue": issue})) @@ -344,7 +367,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 +375,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 +392,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 +407,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 +419,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 +429,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)