Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b121f0c
feat: derive maintained branches from support snapshot in plugin
loadinglucian Aug 3, 2026
ec66bfd
fix: restore policy.lua via cleanup trap in plugin test
loadinglucian Aug 3, 2026
06fc525
feat: reject snapshot diffs with stale policy.lua
loadinglucian Aug 3, 2026
8e30907
fix: require the generated policy.lua path in admitted plans
loadinglucian Aug 3, 2026
eb188df
fix: check snapshot and policy.lua agreement for either diff path
loadinglucian Aug 3, 2026
ae1b3a5
feat: admit trusted automation readiness records without owner review
loadinglucian Aug 3, 2026
f32a6c4
fix: bind readiness exemption to the merged commit and a single-file …
loadinglucian Aug 3, 2026
438a701
fix: match php-bin secret patterns in admission diff scanning
loadinglucian Aug 3, 2026
066c424
fix: protect gate harness scripts and tests from admitted patches
loadinglucian Aug 3, 2026
fdfde26
fix: restore schema validator parity with php-bin
loadinglucian Aug 3, 2026
c70b01e
refactor: unify merge admission check assertions in one script
loadinglucian Aug 3, 2026
0ee8d82
fix: correct admission assert rationale and cover check-name path
loadinglucian Aug 3, 2026
9c3a36b
refactor: emit agent task criteria from one script
loadinglucian Aug 3, 2026
39816b0
feat: gate consumer runs on shared-file parity with php-bin
loadinglucian Aug 3, 2026
7b57c35
fix: fail closed when the shared-file manifest is empty or unreadable
loadinglucian Aug 3, 2026
90e4f08
refactor: reuse canonical action filename helper
loadinglucian Aug 3, 2026
962ceb4
chore: run every workflow step under bash with pipefail
loadinglucian Aug 3, 2026
10ff764
fix: name the shared file php-bin returned unencoded
loadinglucian Aug 3, 2026
4e228be
refactor: drop the local event scan from the policy comparison
loadinglucian Aug 3, 2026
99baf61
refactor: check public language over tracked files with one tool
loadinglucian Aug 3, 2026
a6bafa2
fix: compare admission paths case-sensitively
loadinglucian Aug 3, 2026
87f3088
refactor: drop the plan field no consumer step reads
loadinglucian Aug 3, 2026
5d3fa6c
fix: gate the merge job on a validation outcome, not a job result
loadinglucian Aug 3, 2026
d2d7b30
fix: keep the rejected seal when a repair replaces it
loadinglucian Aug 3, 2026
fd3d9cd
test: cover the merge admission entry point end to end
loadinglucian Aug 3, 2026
96779e0
fix: fail the public-language check when it lists no files
loadinglucian Aug 3, 2026
1c48c2f
fix: set the validate output only after its artifact uploads
loadinglucian Aug 3, 2026
3c0a825
docs: scope the notification claim and document unattended lifecycle
loadinglucian Aug 3, 2026
d82c70c
docs: name a published version in the still-installs promise
loadinglucian Aug 3, 2026
55ba456
fix: group the grep tolerance so the listing failure stays fatal
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
9 changes: 9 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
/.github/codex/ @loadinglucian
/.github/codex-action-contract.json @loadinglucian
/.github/dependabot.yml @loadinglucian
/.github/workflows/ @loadinglucian
/.github/CODEOWNERS @loadinglucian
/.codex/ @loadinglucian
/autorelease/ @loadinglucian
/schemas/ @loadinglucian
/scripts/admit-autorelease-plan @loadinglucian
/scripts/assert-admission-checks @loadinglucian
/scripts/dispatch-pr-checks @loadinglucian
/scripts/prepare-agent-task @loadinglucian
/scripts/seal-autorelease-patch @loadinglucian
/scripts/validate-codex-action-inputs @loadinglucian
/scripts/validate-structured-output-schemas @loadinglucian
/scripts/verify-merge-admission @loadinglucian
/scripts/test.sh @loadinglucian
/scripts/check-public-language.sh @loadinglucian
/scripts/consume-php-policy @loadinglucian
/scripts/generate-policy-lua @loadinglucian
/test/ @loadinglucian
6 changes: 4 additions & 2 deletions .github/codex/autorelease/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ inside only admitted paths, and leave a diff ready for deterministic sealing
and clean validation.

Use no web or shell network. Run and record all advisory checks. Do not change
protected or unadmitted paths. Return GO only when all criteria pass, the local
support behavior matches the accepted php-bin policy, and unresolved is empty.
protected or unadmitted paths. When the edit changes `support-snapshot.json`,
run `scripts/generate-policy-lua` and include the regenerated `lib/policy.lua`
in the same diff. Return GO only when all criteria pass, the local support
behavior matches the accepted php-bin policy, and unresolved is empty.
Do not commit, push, merge, tag, publish, or record readiness yourself.
6 changes: 4 additions & 2 deletions .github/codex/autorelease/investigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ with the exact local support snapshot, then produce one evidence-bound plan
without modifying the repository.

Identify whether local parsing, filtering, fixtures, documentation, temporary
artifact installation, or readiness state must change. Cite exact public policy
commit and digests. Do not independently fetch or classify upstream PHP data.
artifact installation, or readiness state must change. A `support-snapshot.json`
edit also regenerates `lib/policy.lua`, so admit both paths in the same plan.
Cite exact public policy commit and digests. Do not independently fetch or
classify upstream PHP data.
Return GO only when every criterion passes and unresolved is empty.

Treat `requiredChecks` as downstream exact-head gates, not investigation-phase
Expand Down
105 changes: 71 additions & 34 deletions .github/workflows/autorelease-consumer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ concurrency:
group: mise-php-autorelease-consumer
cancel-in-progress: false

defaults:
run:
shell: bash

jobs:
investigate:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -40,6 +44,33 @@ jobs:
test "$state" = "paused" || test "$state" = "enabled"
echo "state=$state" >> "$GITHUB_OUTPUT"
echo "commit=$(git -C php-operator-control rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Gate the run on shared-file parity with php-bin
env:
GH_TOKEN: ${{ github.token }}
PHP_BIN_COMMIT: ${{ steps.operator.outputs.commit }}
run: |
mapfile -t shared < <(jq -r '.paths[]' autorelease/shared-files.json)
# mapfile reports success even when the substitution failed, so an
# absent, unparseable, or empty manifest would disable the gate.
if [[ "${#shared[@]}" -eq 0 ]]; then
echo "Shared-file manifest is missing, unparseable, or empty: autorelease/shared-files.json" >&2
exit 1
fi
for path in "${shared[@]}"; do
gh api "repos/Bigpixelrocket/php-bin/contents/$path?ref=$PHP_BIN_COMMIT" \
> "$RUNNER_TEMP/shared-file.json"
# An oversized or symlinked entry comes back unencoded, which would
# otherwise decode to nothing and be reported as drift.
if [[ "$(jq -r .encoding "$RUNNER_TEMP/shared-file.json")" != "base64" ]]; then
echo "Shared file was not returned base64-encoded by php-bin at $PHP_BIN_COMMIT: $path" >&2
exit 1
fi
jq -r .content "$RUNNER_TEMP/shared-file.json" | base64 --decode > "$RUNNER_TEMP/shared-file"
if ! cmp -s "$RUNNER_TEMP/shared-file" "$path"; then
echo "Shared file drifted from php-bin at $PHP_BIN_COMMIT: $path" >&2
exit 1
fi
done
- name: Capture accepted public php-bin policy
run: |
mkdir -p autorelease-run
Expand All @@ -48,15 +79,14 @@ jobs:
--invariants-output autorelease-run/policy-invariants.json \
--commit-output autorelease-run/php-bin-main.json \
--manifest autorelease-run/policy-capture.json
- name: Compare only opaque policy and event digests
- name: Compare only opaque policy digests
id: compare
run: |
./scripts/consume-php-policy compare \
--policy autorelease-run/support-policy.json \
--invariants autorelease-run/policy-invariants.json \
--policy-commit autorelease-run/php-bin-main.json \
--snapshot support-snapshot.json \
--events autorelease-events \
--output autorelease-run/decision.json
echo "trigger=$(jq -r .trigger autorelease-run/decision.json)" >> "$GITHUB_OUTPUT"
- name: Prepare exact investigation contract
Expand All @@ -70,6 +100,7 @@ jobs:
--arg policyInvariantsDigest "$(jq -r .policyInvariantsDigest autorelease-run/decision.json)" \
--arg phpBinOperatorCommit "${{ steps.operator.outputs.commit }}" \
--arg operatorState "${{ steps.operator.outputs.state }}" \
--argjson completionCriteria "$(./scripts/prepare-agent-task --phase investigation)" \
'{
contractVersion:1,
phase:"investigation",
Expand All @@ -78,12 +109,7 @@ jobs:
preconditions:{misePhpHead:$misePhpHead,phpBinPolicyCommit:$phpBinPolicyCommit,supportPolicyDigest:$supportPolicyDigest,policyInvariantsDigest:$policyInvariantsDigest,phpBinOperatorCommit:$phpBinOperatorCommit,operatorState:$operatorState},
allowedAuthority:["read_repository","read_captured_policy"],
nonGoals:["upstream_php_classification","repository_mutation","required_check_execution","irreversible_github_effect"],
completionCriteria:[
{id:"phase-goal-correct",requirement:"The goal matches exact inputs.",evidenceRequired:"Exact preconditions."},
{id:"policy-difference-explained",requirement:"Every required local change is bound to captured policy.",evidenceRequired:"Policy digest and JSON locator."},
{id:"authority-explicit",requirement:"Paths and checks are explicit.",evidenceRequired:"Allowed paths and required checks."},
{id:"no-unresolved-work",requirement:"No contradiction or stop condition remains.",evidenceRequired:"Empty unresolved list."}
],
completionCriteria:$completionCriteria,
stopConditions:["missing_or_contradictory_policy","changed_precondition","required_protected_change"]
}' > autorelease-run/event-contract.json
shared="sha256:$(shasum -a 256 .github/codex/autorelease/shared.md | awk '{print $1}')"
Expand Down Expand Up @@ -166,17 +192,12 @@ jobs:
path: autorelease-run
- name: Prepare implementation contract and prompt
run: |
jq \
jq --argjson completionCriteria "$(./scripts/prepare-agent-task --phase implementation)" \
'.phase="implementation"
| .goal="Implement the admitted mise-php policy synchronization at the exact base."
| .allowedAuthority=["workspace_write_admitted_paths","local_advisory_checks"]
| .nonGoals=["protected_control_change","irreversible_github_effect"]
| .completionCriteria=[
{id:"phase-goal-correct",requirement:"Goal and preconditions remain exact.",evidenceRequired:"Plan digest and base SHA."},
{id:"admitted-diff-complete",requirement:"Diff is complete and admitted.",evidenceRequired:"Final diff and path comparison."},
{id:"advisory-checks-recorded",requirement:"All checks ran.",evidenceRequired:"Commands and results."},
{id:"no-unresolved-work",requirement:"No in-scope work remains.",evidenceRequired:"Empty unresolved list."}
]' \
| .completionCriteria=$completionCriteria' \
autorelease-run/event-contract.json > autorelease-run/implementation-contract.json
shared="sha256:$(shasum -a 256 .github/codex/autorelease/shared.md | awk '{print $1}')"
phase="sha256:$(shasum -a 256 .github/codex/autorelease/implementation.md | awk '{print $1}')"
Expand Down Expand Up @@ -256,20 +277,20 @@ jobs:
test "$actual" = "$expected"
git apply --index autorelease-run/sealed/sealed.patch
- name: Run authoritative plugin checks without OpenAI credential
id: checks
id: run-checks
run: |
test -z "${OPENAI_API_KEY:-}"
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: Create reproducible validated commit bundle
if: steps.checks.outputs.passed == 'true'
if: steps.run-checks.outputs.status == 'passed'
env:
BASE_SHA: ${{ needs.investigate.outputs.base_sha }}
run: |
Expand All @@ -284,21 +305,29 @@ jobs:
'{headSha:$headSha,tree:$tree,checks:{"Plugin contract":"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: mise-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: mise-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, exactly as in validate-repair: the merge job keys on
# this output and downloads the validated artifact, so a failed upload must
# leave `passed` unset rather than claim a validated patch is waiting. An
# unset output reads as not-passed to both the merge job and repair.
- name: Record that the patch validated
id: checks
if: steps.run-checks.outputs.status == 'passed'
run: echo "passed=true" >> "$GITHUB_OUTPUT"

repair:
needs: [investigate, validate]
Expand All @@ -323,17 +352,14 @@ jobs:
git apply --index autorelease-run/sealed/sealed.patch
fingerprint="sha256:$(shasum -a 256 autorelease-run/authoritative-checks.log | awk '{print $1}')"
jq --arg goal "Repair the retained authoritative failure $fingerprint without expanding the admitted diff." \
--argjson completionCriteria "$(./scripts/prepare-agent-task --phase repair)" \
'.phase="repair"
| .goal=$goal
| .allowedAuthority=["workspace_write_admitted_paths","local_advisory_checks"]
| .nonGoals=["protected_control_change","irreversible_github_effect"]
| .stopConditions=["changed_precondition","repeated_or_unrelated_failure","required_protected_change"]
| .completionCriteria=[
{id:"phase-goal-correct",requirement:"The repair goal matches the exact failure and inputs.",evidenceRequired:"Failure fingerprint and exact base."},
{id:"failure-cause-removed",requirement:"The named failure is removed without weakening a gate.",evidenceRequired:"Minimal admitted diff and reproduction evidence."},
{id:"advisory-checks-recorded",requirement:"All advisory checks ran.",evidenceRequired:"Commands and results."},
{id:"no-unresolved-work",requirement:"No repeated or unrelated failure remains.",evidenceRequired:"Empty unresolved list."}
]' autorelease-run/implementation-contract.json > autorelease-run/repair-contract.json
| .completionCriteria=$completionCriteria' \
autorelease-run/implementation-contract.json > autorelease-run/repair-contract.json
shared="sha256:$(shasum -a 256 .github/codex/autorelease/shared.md | awk '{print $1}')"
phase="sha256:$(shasum -a 256 .github/codex/autorelease/repair.md | awk '{print $1}')"
event="sha256:$(shasum -a 256 autorelease-run/repair-contract.json | awk '{print $1}')"
Expand Down Expand Up @@ -389,6 +415,8 @@ jobs:

validate-repair:
needs: [investigate, repair]
outputs:
passed: ${{ steps.checks.outputs.passed }}
runs-on: macos-26
timeout-minutes: 25
permissions:
Expand All @@ -412,8 +440,12 @@ jobs:
git apply --index autorelease-run/sealed-repair/sealed.patch
test -z "${OPENAI_API_KEY:-}"
./scripts/test.sh
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
# The merge job reads autorelease-run/sealed, so the repaired seal has to
# arrive under that name. The rejected seal is renamed rather than
# overwritten: it is the evidence of what the repair replaced, and it ships
# in the same artifact.
mv autorelease-run/sealed autorelease-run/sealed-failed
cp -R autorelease-run/sealed-repair autorelease-run/sealed
- name: Create repaired validated commit bundle
env:
BASE_SHA: ${{ needs.investigate.outputs.base_sha }}
Expand All @@ -435,10 +467,15 @@ jobs:
if-no-files-found: error
retention-days: 90
include-hidden-files: true
# Last step on purpose: the merge job keys on this output, so it must not be
# set until the validated artifact it downloads has actually been uploaded.
- name: Record that the repaired patch validated
id: checks
run: echo "passed=true" >> "$GITHUB_OUTPUT"

merge-and-record-readiness:
needs: [investigate, validate, validate-repair]
if: always() && (needs.validate.outputs.passed == 'true' || needs['validate-repair'].result == 'success')
if: ${{ !cancelled() && (needs.validate.outputs.passed == 'true' || needs['validate-repair'].outputs.passed == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
Expand Down Expand Up @@ -471,7 +508,7 @@ jobs:
# action key, but this value reaches $GITHUB_OUTPUT and later shell
# steps, so its alphabet is re-asserted at the boundary.
[[ "$action_key" =~ ^[A-Za-z0-9._:-]+$ ]]
branch="autorelease/$(printf '%s' "$action_key" | tr ':/' '--')"
branch="autorelease/$(./scripts/consume-php-policy action-filename "$action_key" --suffix '')"
gh auth setup-git
git push origin "HEAD:refs/heads/$branch"
number="$(gh pr list --head "$branch" --state open --json number --jq '.[0].number // empty')"
Expand All @@ -491,7 +528,7 @@ jobs:
--pr "$PR_NUMBER" \
--check "Plugin contract" \
--output autorelease-run/pr-checks.json
jq -e '[.[] | select(.name=="Plugin contract") | .bucket] == ["pass"]' autorelease-run/pr-checks.json
./scripts/assert-admission-checks --check-name "Plugin contract" --checks autorelease-run/pr-checks.json
expected="$(jq -r .headSha autorelease-run/validation.json)"
actual="$(gh pr view "$PR_NUMBER" --json headRefOid --jq .headRefOid)"
test "$actual" = "$expected"
Expand Down Expand Up @@ -556,7 +593,7 @@ jobs:
git checkout -B autorelease/readiness-${{ github.run_id }} origin/main
base="$(git rev-parse HEAD)"
mkdir -p readiness
filename="$(printf '%s' "$ACTION_KEY" | tr ':/' '--').json"
filename="$(./scripts/consume-php-policy action-filename "$ACTION_KEY")"
mapfile -t digests < <(jq -r '.captures[].digest' autorelease-run/policy-capture.json)
args=()
for digest in "${digests[@]}"; do args+=(--evidence-digest "$digest"); done
Expand Down Expand Up @@ -589,7 +626,7 @@ jobs:
--pr "${{ steps.readiness.outputs.number }}" \
--check "Plugin contract" \
--output autorelease-run/readiness-checks.json
jq -e '[.[] | select(.name=="Plugin contract") | .bucket] == ["pass"]' autorelease-run/readiness-checks.json
./scripts/assert-admission-checks --check-name "Plugin contract" --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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ on:
permissions:
contents: read

defaults:
run:
shell: bash

jobs:
contract:
name: Plugin contract
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
permissions:
contents: read

defaults:
run:
shell: bash

jobs:
install:
name: Install PHP ${{ inputs.version }}
Expand Down
Loading
Loading