Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8427c3c
docs: add autorelease unattended hardening plan
loadinglucian Aug 3, 2026
fba2b25
fix: accept any maintained branch in stage-4 module comparison
loadinglucian Aug 3, 2026
145211a
fix: validate version shape only in build and package gates
loadinglucian Aug 3, 2026
a1375a1
fix: protect gate harness scripts and tests from admitted patches
loadinglucian Aug 3, 2026
12fa3f4
fix: protect toolchain pins and the shared shell library from admitte…
loadinglucian Aug 3, 2026
368dbee
refactor: unify merge admission check assertions in one script
loadinglucian Aug 3, 2026
9a899ea
fix: correct admission assert rationale and cover check-name path
loadinglucian Aug 3, 2026
eb91008
refactor: assert workflow structure instead of source text in verifier
loadinglucian Aug 3, 2026
b907f5d
fix: assert the whole release workflow is free of the OpenAI credential
loadinglucian Aug 3, 2026
7c9c0c0
feat: decide to recover a published release with no event record
loadinglucian Aug 3, 2026
d174ea8
feat: recover missing event records from the watcher
loadinglucian Aug 3, 2026
0f641aa
fix: stop a blocked record recovery from starving the watcher
loadinglucian Aug 3, 2026
e78e2f9
fix: bind a recovered record to an attested release on main
loadinglucian Aug 3, 2026
8b0c869
fix: keep record recovery out of the shared checkout and branch name
loadinglucian Aug 3, 2026
243329f
fix: keep the recovery merge from deleting a branch in use
loadinglucian Aug 3, 2026
78cbe67
refactor: route watch actions through deterministic control table
loadinglucian Aug 3, 2026
9c632b8
fix: route the recovery action and assert the hard operator gates
loadinglucian Aug 3, 2026
9e82fd1
chore: delete unreferenced schemas
loadinglucian Aug 3, 2026
ae0454a
refactor: align the completion assessment schema with admission
loadinglucian Aug 3, 2026
952ed15
refactor: dedupe digest and sha validation helpers
loadinglucian Aug 3, 2026
47f1123
chore: enforce bash defaults and least privilege in workflows
loadinglucian Aug 3, 2026
4075fbf
fix: bind trusted automation exemptions to single-file records
loadinglucian Aug 3, 2026
b104cf6
test: fold line continuations before checking gh calls name the repo
loadinglucian Aug 3, 2026
d3df414
docs: record why the recovery withdraw path traps only EXIT
loadinglucian Aug 3, 2026
3cf020c
chore: scope the public-language check to tracked files
loadinglucian Aug 3, 2026
bec6c5e
chore: keep the packaging test artifacts out of the working tree
loadinglucian Aug 3, 2026
8dc474b
refactor: reuse canonical digest helpers in the admin snapshot
loadinglucian Aug 3, 2026
abd64cc
fix: shut the release fixture server down cleanly
loadinglucian Aug 3, 2026
723dc81
chore: narrow the shellcheck suppression and cover every bash script
loadinglucian Aug 3, 2026
ea543de
test: assert both repositories name event records identically
loadinglucian Aug 3, 2026
8f5baf6
fix: shield the recovery withdraw from cancellation escalation
loadinglucian Aug 3, 2026
9ef9071
fix: fail the public-language check when it lists no files
loadinglucian Aug 3, 2026
49092ac
refactor: split control module behind stable facade
loadinglucian Aug 3, 2026
192074a
refactor: split plan admission into its three gates
loadinglucian Aug 3, 2026
6f7b221
docs: state the watcher routing invariant the workflow depends on
loadinglucian Aug 3, 2026
06cb37d
test: scan the whole control package for classifier markers
loadinglucian Aug 3, 2026
2958457
docs: correct autorelease references and document unattended lifecycle
loadinglucian Aug 3, 2026
0964951
fix: set the validate output only after its artifact uploads
loadinglucian Aug 3, 2026
c8589aa
test: scan nested control modules for classifier markers
loadinglucian Aug 3, 2026
09a03af
docs: make the automation the subject of the rebuild revision
loadinglucian Aug 3, 2026
e34872c
fix(autorelease): defer the no-change evidence record after a recover…
loadinglucian Aug 3, 2026
2b7d9be
refactor(autorelease): read the missing record after the health guards
loadinglucian Aug 3, 2026
b32afa0
fix(autorelease): protect the shared dependabot configuration in php-bin
loadinglucian Aug 3, 2026
bb27c0d
docs: drop the committed hardening plan from the release repository
loadinglucian Aug 3, 2026
6f59ab3
docs(autorelease): bind an in-flight action key across watcher runs
loadinglucian Aug 3, 2026
ca9cd9b
test(autorelease): validate the jq-built recovery record as a complet…
loadinglucian Aug 3, 2026
5e5d99f
fix: group the grep tolerance so the listing failure stays fatal
loadinglucian Aug 3, 2026
2b22061
test: anchor codeowners pairing on the repo root and tolerate tabs
loadinglucian Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.github/codex/ @loadinglucian
/.github/codex-action-contract.json @loadinglucian
/.github/dependabot.yml @loadinglucian
/.github/workflows/ @loadinglucian
/.github/CODEOWNERS @loadinglucian
/.github/autorelease-operator.json @loadinglucian
Expand All @@ -8,17 +9,32 @@
/autorelease/ @loadinglucian
/schemas/ @loadinglucian
/scripts/admit-autorelease-plan @loadinglucian
/scripts/assert-admission-checks @loadinglucian
/scripts/capture-autorelease-evidence @loadinglucian
/scripts/configure-github-autorelease @loadinglucian
/scripts/dispatch-pr-checks @loadinglucian
/scripts/autorelease-event @loadinglucian
/scripts/notify-autorelease @loadinglucian
/scripts/prepare-agent-task @loadinglucian
/scripts/seal-autorelease-patch @loadinglucian
/scripts/serve-autorelease-artifact @loadinglucian
/scripts/snapshot-github-admin-state @loadinglucian
/scripts/validate-autorelease-archive @loadinglucian
/scripts/validate-codex-action-inputs @loadinglucian
/scripts/validate-structured-output-schemas @loadinglucian
/scripts/verify-autorelease-system @loadinglucian
/scripts/verify-merge-admission @loadinglucian
/scripts/publish-release @loadinglucian
/scripts/watch-autorelease-evidence @loadinglucian
/autorelease/policy-invariants.json @loadinglucian
/scripts/test.sh @loadinglucian
/scripts/build.sh @loadinglucian
/scripts/package.sh @loadinglucian
/scripts/compare-modules.sh @loadinglucian
/scripts/check-public-language.sh @loadinglucian
/tests/ @loadinglucian
/scripts/lib.sh @loadinglucian
/scripts/install-spc.sh @loadinglucian
/scripts/install-build-deps.sh @loadinglucian
/.spc-version @loadinglucian
/.spc-sha256 @loadinglucian
2 changes: 1 addition & 1 deletion .github/autorelease-pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"openai/codex-action": "52fe01ec70a42f454c9d2ebd47598f9fd6893d56"
},
"workflows": {
".github/workflows/autorelease-e2e.yml": "sha256:677ad87c8c58bdb61e6fc8e54782b70a89533ac4916827414a7d639985949c6b"
".github/workflows/autorelease-e2e.yml": "sha256:5ae830a817f55657a6583dd3ed121c1156de1ce843a23e0b7ed8fa56a0f0f274"
}
}
5 changes: 4 additions & 1 deletion .github/codex/autorelease/investigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ 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`,
`policy_failure`, or `auth_failure` with the required version, date, attempt,
or lowercase hexadecimal evidence suffix.
or lowercase hexadecimal evidence suffix. When `autorelease-events/` already
holds an incomplete record for the same branch, reuse that record's `actionKey`
verbatim instead of re-deriving its date, attempt, or evidence suffix, so the
run that completes the action names the file the earlier run opened.

Every `completionAssessment.criteria[].evidence` entry is a machine-resolved
reference, never explanatory prose. Use only `evidence[N]` for an item in the
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/autorelease-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ concurrency:
group: autorelease-e2e-${{ inputs.suite }}
cancel-in-progress: false

defaults:
run:
shell: bash

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
Expand Down Expand Up @@ -138,8 +142,6 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
environment: php-autorelease-canary
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand Down
45 changes: 25 additions & 20 deletions .github/workflows/autorelease-implement.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ permissions:
contents: read
actions: read

defaults:
run:
shell: bash

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
Expand Down Expand Up @@ -68,7 +72,7 @@ jobs:
RUN_ID: ${{ needs.preflight.outputs.run_id }}
run: gh run download "$RUN_ID" --name "autorelease-investigation-$RUN_ID" --dir autorelease-run
- name: Enforce operator pause
run: test "$(jq -r .unattendedMutation .github/autorelease-operator.json)" = "enabled"
run: ./autorelease/control.py operator-gate --operator-file .github/autorelease-operator.json --require-enabled
- name: Verify exact admitted base
env:
BASE_SHA: ${{ needs.preflight.outputs.base_sha }}
Expand Down Expand Up @@ -145,9 +149,6 @@ jobs:
passed: ${{ steps.checks.outputs.passed }}
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand All @@ -163,19 +164,19 @@ jobs:
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
id: run-checks
run: |
set +e
./scripts/test.sh 2>&1 | tee autorelease-run/authoritative-checks.log
status="${PIPESTATUS[0]}"
set -e
if [[ "$status" == "0" ]]; then
echo "passed=true" >> "$GITHUB_OUTPUT"
echo "status=passed" >> "$GITHUB_OUTPUT"
else
echo "passed=false" >> "$GITHUB_OUTPUT"
echo "status=failed" >> "$GITHUB_OUTPUT"
fi
- name: Record validated SHA and tree
if: steps.checks.outputs.passed == 'true'
if: steps.run-checks.outputs.status == 'passed'
env:
BASE_SHA: ${{ needs.preflight.outputs.base_sha }}
run: |
Expand All @@ -189,31 +190,38 @@ jobs:
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'
if: steps.run-checks.outputs.status == 'passed'
with:
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'
if: steps.run-checks.outputs.status != 'passed'
with:
name: failed-autorelease-validation-${{ github.run_id }}
path: autorelease-run/
if-no-files-found: error
retention-days: 90
include-hidden-files: true
# Last step on purpose: the merge job keys on this output and then downloads
# the validated artifact, so a failed bundle or upload must leave `passed`
# unset rather than advertise a patch that is missing or incomplete. An unset
# output reads as not-passed to both merge and repair. validate-repair needs
# no equivalent: merge gates on that job's own result, and its upload is
# already the last step.
- name: Record that the patch validated
id: checks
if: steps.run-checks.outputs.status == 'passed'
run: echo "passed=true" >> "$GITHUB_OUTPUT"

repair:
name: One bounded offline repair
needs: [preflight, validate]
if: needs.validate.outputs.passed != 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand Down Expand Up @@ -299,9 +307,6 @@ jobs:
needs: [preflight, repair]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand Down Expand Up @@ -377,7 +382,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
action_key="$(jq -r .actionKey autorelease-run/implementation-plan.json)"
branch="autorelease/$(printf '%s' "$action_key" | tr ':/' '--')"
branch="autorelease/$(./autorelease/control.py action-filename "$action_key" --suffix '')"
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')"
Expand All @@ -395,7 +400,7 @@ jobs:
--pr "${{ steps.pr.outputs.number }}" \
--check "Script checks" \
--output autorelease-run/pr-checks.json
jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' autorelease-run/pr-checks.json
./scripts/assert-admission-checks --checks autorelease-run/pr-checks.json
- name: Re-verify exact SHA, sealed tree, and preconditions
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -439,7 +444,7 @@ jobs:
git checkout -B "autorelease/readiness-${{ github.run_id }}" origin/main
base="$(git rev-parse HEAD)"
action_key="$(jq -r .actionKey autorelease-run/implementation-plan.json)"
filename="$(printf '%s' "$action_key" | tr ':/' '--').json"
filename="$(./autorelease/control.py action-filename "$action_key")"
mkdir -p autorelease-events
jq -n \
--arg actionKey "$action_key" \
Expand Down Expand Up @@ -499,7 +504,7 @@ jobs:
--pr "${{ steps.readiness.outputs.number }}" \
--check "Script checks" \
--output autorelease-run/readiness-checks.json
jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' autorelease-run/readiness-checks.json
./scripts/assert-admission-checks --checks 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
Expand Down
77 changes: 65 additions & 12 deletions .github/workflows/autorelease-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ concurrency:
group: autorelease-publish-${{ inputs.version }}
cancel-in-progress: false

defaults:
run:
shell: bash

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
Expand All @@ -40,6 +44,8 @@ jobs:
name: Validate dispatch inputs
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
version: ${{ steps.validated.outputs.version }}
exact_commit: ${{ steps.validated.outputs.exact_commit }}
Expand Down Expand Up @@ -104,11 +110,11 @@ jobs:
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"
./autorelease/control.py operator-gate --operator-file .github/autorelease-operator.json --require-enabled
mkdir -p release-run
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"
./autorelease/control.py operator-gate --operator-file release-run/current-operator.json --require-enabled
mise_commit="$(jq -r .sha admitted-run/evidence/raw/mise_php_state.body)"
[[ "$mise_commit" =~ ^[0-9a-f]{40}$ ]]
git -C mise-php fetch origin "$mise_commit"
Expand All @@ -129,7 +135,7 @@ jobs:
run: |
action="$(jq -r .action admitted-run/autorelease-plan.json)"
if [[ "$action" == "new_branch" ]]; then
filename="$(printf '%s' "$ACTION_KEY" | tr ':/' '--').json"
filename="$(./autorelease/control.py action-filename "$ACTION_KEY")"
event="autorelease-events/$filename"
test -f "$event"
test "$(jq -r .state "$event")" = "php_bin_ready"
Expand Down Expand Up @@ -182,7 +188,7 @@ jobs:
run: |
mkdir -p release-run
printf '{"schemaVersion":1,"state":"requested","history":[]}\n' > release-run/transaction.json
filename="$(printf '%s' "$ACTION_KEY" | tr ':/' '--').json"
filename="$(./autorelease/control.py action-filename "$ACTION_KEY")"
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" \
Expand Down Expand Up @@ -215,7 +221,7 @@ jobs:
run: |
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"
./autorelease/control.py operator-gate --operator-file release-run/current-operator.json --require-enabled
for target in built draft_created draft_verified; do
./scripts/publish-release \
--transaction release-run/transaction.json \
Expand Down Expand Up @@ -265,7 +271,7 @@ jobs:
run: |
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"
./autorelease/control.py operator-gate --operator-file release-run/current-operator.json --require-enabled
for target in published public_verified complete; do
./scripts/publish-release \
--transaction release-run/transaction.json \
Expand All @@ -289,6 +295,26 @@ jobs:
sleep 5
done
test "$verified" = "true"
- name: Record whether the immutable release is live
if: always()
run: |
mkdir -p release-run
released=false
if [[ -f release-run/transaction.json ]]; then
case "$(jq -r .state release-run/transaction.json)" in
published|public_verified|complete) released=true ;;
esac
fi
jq -n --argjson released "$released" --arg version "$VERSION" \
'{schemaVersion:1,released:$released,version:$version}' > release-run/transaction-state.json
- name: Retain the transaction state for triage
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-transaction-state-${{ github.run_id }}
path: release-run/transaction-state.json
if-no-files-found: error
retention-days: 90
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Verify fresh public exact-version and branch-shorthand installs
run: |
unset MISE_PHP_API_BASE_URL
Expand Down Expand Up @@ -332,7 +358,7 @@ jobs:
git fetch origin main
git checkout -B "autorelease/event-${{ github.run_id }}" origin/main
base="$(git rev-parse HEAD)"
filename="$(printf '%s' "$ACTION_KEY" | tr ':/' '--').json"
filename="$(./autorelease/control.py action-filename "$ACTION_KEY")"
cp release-run/event.json "autorelease-events/$filename"
git add "autorelease-events/$filename"
git -c user.name=autorelease -c user.email=autorelease@invalid \
Expand All @@ -358,7 +384,7 @@ jobs:
--pr "${{ steps.event_pr.outputs.number }}" \
--check "Script checks" \
--output release-run/event-checks.json
jq -e '[.[] | select(.name=="Script checks") | .bucket] == ["pass"]' release-run/event-checks.json
./scripts/assert-admission-checks --checks release-run/event-checks.json
actual="$(gh pr view "${{ steps.event_pr.outputs.number }}" --json headRefOid --jq .headRefOid)"
test "$actual" = "${{ steps.event_pr.outputs.head_sha }}"
git fetch origin main
Expand Down Expand Up @@ -413,23 +439,50 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: read
contents: read
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Create structured critical event
# A run that fails before the state is retained keeps the critical default below.
- name: Download the retained transaction state
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-transaction-state-${{ github.run_id }}
path: release-state
- name: Create a structured event keyed on the transaction state
env:
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: |
# A live release is not a critical failure: the watcher files the missing
# event record on its own. Only an unpublished release stops the pipeline.
released=false
if [[ -f release-state/transaction-state.json ]]; then
released="$(jq -r .released release-state/transaction-state.json)"
fi
if [[ "$released" == "true" ]]; then
state=released
severity=warning
summary="PHP $VERSION was published; its event record is pending and the watcher will recover it: $RUN_URL"
fingerprint="release-record-pending:$ACTION_KEY"
else
state=blocked
severity=critical
summary="Autorelease failed for PHP $VERSION: $RUN_URL"
fingerprint="release-failure:$ACTION_KEY"
fi
jq -n \
--arg actionKey "$ACTION_KEY" \
--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}' \
--arg state "$state" \
--arg severity "$severity" \
--arg summary "$summary" \
--arg failureFingerprint "$fingerprint" \
'{actionKey:$actionKey,state:$state,severity:$severity,humanActionRequired:false,summary:$summary,failureFingerprint:$failureFingerprint}' \
> event.json
- name: Notify owner
env:
Expand Down
Loading
Loading