Skip to content

fix: autorelease unattended hardening - #27

Merged
loadinglucian merged 30 commits into
mainfrom
fix/autorelease-unattended-hardening
Aug 3, 2026
Merged

fix: autorelease unattended hardening#27
loadinglucian merged 30 commits into
mainfrom
fix/autorelease-unattended-hardening

Conversation

@loadinglucian

@loadinglucian loadinglucian commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Hardens the consumer side of the autorelease system so new PHP branches, patch releases, and EOL delisting flow through with zero maintainer input, staying in lockstep with php-bin.

Unattended functional gaps (Phase 1)

  • lib/policy.lua maintained branches are snapshot-driven from php-bin's support policy instead of hardcoded; admission requires the regenerated policy.lua to match the captured snapshot.
  • Readiness records merge unattended through the trusted-automation exemption (record-anchored base binding, single-file diff guard).
  • Fixed the dead sk- secret-scanner arm and restored validator parity with php-bin.

Cross-repo integrity (Phases 3-5)

  • autorelease/shared-files.json declares the 7 byte-identical shared files; the consumer workflow verifies parity against php-bin at the pinned commit on every run, failing closed on a missing or empty manifest.
  • Gate harness protected and code-owned; product code (hooks, patches, stages, craft.yml) remains agent-admissible.
  • ACTION_KEY_RE and the action-filename mapping are single-sourced from consumer.py; verify-merge-admission gained a subprocess smoke test.
  • Repair validation writes artifacts to a fresh path, the merge condition survives a skipped validate job, and the passed output is only set after the artifact upload it advertises.

Docs (Phase 6)

  • AUTORELEASE.md documents the unattended lifecycle and scopes the notification description to php-bin (consumer failures surface via Actions email).

All gates green: 22 unit tests, structural workflow assertions, markdownlint.

Summary by CodeRabbit

  • New Features

    • PHP version availability is now driven by the maintained support policy.
    • Exact immutable PHP versions remain installable, while end-of-life branches are removed from listings.
    • Version parsing supports future major and minor releases without hardcoded limits.
    • Automated release workflows now provide clearer validation status and artifact handling.
  • Bug Fixes

    • Improved validation prevents invalid release metadata, readiness records, and unsupported action configurations.
    • Enhanced checks ensure generated policy data stays synchronized with supported versions.
  • Documentation

    • Updated guidance explains version availability, automated releases, recovery, and repository settings.

The action-key-to-filename mapping was written out inline in the consumer
workflow, and ACTION_KEY_RE was compiled a second time in admission.py.
consumer.py now owns action_filename() beside the regex it validates against,
and exposes it as the action-filename subcommand the workflow calls.

verify-merge-admission ran admission.py as a script, which puts autorelease/
rather than the repository root on sys.path and would break the new import; it
becomes the same shim its three siblings already use, and admission.py drops
its now-dead __main__ block.
GitHub runs `run:` blocks with `bash -e {0}` unless a shell is named, so a
failing command in the middle of a pipeline was silently ignored. Naming bash
explicitly switches the runner to `bash --noprofile --norc -eo pipefail {0}`,
matching what the scripts already assume.

Every pipeline in these workflows was walked first: the remaining ones either
feed a command substitution whose exit status was already checked, or read
PIPESTATUS under `set +e` and are unaffected by the new setting.
The parity gate failed on a bare `test`, which prints nothing, so an oversized
or symlinked entry in the manifest produced a red run with no indication of
which path caused it. The neighbouring drift failure already names the path.
mise-php never writes an event record: `autorelease-events/` held only a
.gitkeep, last touched by a repository rename, so the scan could only ever find
nothing and the `event_incomplete` trigger was unreachable. `incompleteActions`
was written into decision.json and read by nobody.

`autorelease/protected-paths.json` keeps its `autorelease-events/*` entry and
its test on purpose: it is a fail-closed guard, and it should keep refusing
writes under that prefix whether or not the directory exists.

Zero-reference proof after the change, over the whole worktree excluding .git:

    $ grep -rn "event_incomplete\|incompleteActions\|autorelease-events\|--events" .
    autorelease/protected-paths.json:19:    "autorelease-events/*",
    test/test_autorelease.py:133:        self.assertTrue(protected("autorelease-events/new-patch.json"))

php-bin has its own `event_incomplete` in `control.py`'s watch decision; that
one is live, backed by a real `autorelease-events/` tree, and is untouched.
The script picked ripgrep when it was installed and grep otherwise, and the two
branches disagreed about hidden files, ignore rules, and build output, so the
runner's tool inventory decided what was actually checked. Scanning the tracked
file list makes the scope the same everywhere.

This path is in autorelease/shared-files.json, so the file is byte-identical to
php-bin's copy: sha256
143cd773132b1a760da86b6fba6989fe23bd0e1d9d0b64f8a98f5b64a3cea630.
fnmatch.fnmatch runs os.path.normcase on both sides, so on a case-folding
platform the protected-path and allowed-path gates would answer differently from
the Linux runner that enforces them. Git tracks paths as case-sensitive bytes,
so fnmatchcase is the comparison these gates meant all along.
The investigation agent was required to produce a `notification` object that
mise-php never opens: there is no notify script, no `issues: write` grant, and
no step that reads the field out of the plan. Failure notification for both
repositories is raised by php-bin's watcher, which reads its own plan.

Zero-reference proof after the change, over the whole worktree excluding .git:

    $ grep -rn "notification\|suggestedSeverity\|humanActionRequired" .
    (no output)

php-bin keeps its own `notification` block; autorelease-watch.yml reads
`.notification.summary` from it.
The merge job ran under `always()`, so a cancelled run still entered the job
that pushes the branch, opens the pull request, and merges it. `!cancelled()`
keeps the "run even though validate failed and the repair path took over"
behaviour without that.

It also keyed the repair path on `needs['validate-repair'].result`, which is
`success` for any green job, including one that went green without producing the
validated artifact this job downloads. `validate-repair` now publishes the same
named `passed` output as `validate`, set by a final step that only runs after
the artifact upload succeeded.
The repair validation overwrote autorelease-run/sealed in place, so the uploaded
artifact ended up claiming the repaired patch had been the sealed one all along
and the patch that actually failed validation was gone. Renaming it to
sealed-failed keeps both in the evidence while the merge job still finds the
bytes it verifies under autorelease-run/sealed.

seal() writes exactly sealed.patch and patch-manifest.json, so copying the
directory carries the same files the two per-file copies did.
verify_merge was tested in process, but the merge job calls it through
scripts/verify-merge-admission and reads only the exit status. The script fixes
the subcommand by editing sys.argv, so a wiring mistake there would let a
rejected patch merge with nobody noticing. consume-php-policy already has the
same kind of check.
`git ls-files -z | xargs -0 grep ... || true` needed the tolerance for xargs
exit 123, which grep produces whenever a batch matches nothing, but the same
tolerance covered a failing listing. Run outside a repository the check printed
"fatal: not a git repository" and then "Public-language check passed." with exit
0, having scanned nothing.

The listing is now produced and checked before the grep runs, in a file because
command substitution drops the NUL separators.

Probes, in order: outside a repository, a clean repository, a tracked violation,
a repository with no tracked files.

    $ ./scripts/check-public-language.sh        # not a repository
    fatal: not a git repository (or any of the parent directories): .git
    Public-language check could not list the tracked files of /tmp/probe/nogit.
    rc=1

    $ ./scripts/check-public-language.sh        # clean
    Public-language check passed.
    rc=0

    $ ./scripts/check-public-language.sh        # tracked violation
    doc.txt:1:a HERD of cows
    Public-language check failed.
    rc=1

    $ ./scripts/check-public-language.sh        # empty repository
    Public-language check found no tracked files in /tmp/probe/empty.
    rc=1

The same file before this change answered rc 0 with "passed" to the first probe.

This path is in autorelease/shared-files.json, so the file is byte-identical to
php-bin's copy: sha256
280d32fb10e58baa0b6b364f6e5cc1d122528a8cb7a0f4aa4e504eeb0ba76ea8.
validate-repair records passed=true as its last step on purpose: the merge job
keys on that output and downloads the artifact, so claiming success before the
upload would point merge at something that may not exist. validate set the same
output before its uploads, so the stated invariant only held in one of the two
jobs.

The check step now reports a separate status output that gates the bundle and
both uploads, and passed=true is recorded last. A failed run leaves passed
unset, which merge and repair already read as not passed.
AUTORELEASE.md described deduplicated assigned issues as if this repository
raised them. It does not: php-bin owns notify-autorelease and the jobs holding
issues: write, and this consumer requests no issue permission at all, so a
failure confined to it reaches the owner through Actions email until php-bin
records it. Say so.

The supported-branch range was hardcoded as "8.2 through 8.5"; it now points at
the snapshot that tracks the accepted policy automatically, and states that EOL
delists a branch without making an already-published version less installable.

Also record which paths are protected harness and which stay agent-admissible,
add an "Unattended lifecycle" section covering both directions, and correct the
admin-state snapshot filename to the -after convention actually committed.
The sentence promising that an already-published version keeps installing used
8.2.29, which was never released. The branch's published tag is 8.2.32.
@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_aa0078b3-37a7-4cc3-9869-585747cc7cb5)

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b415ab1-bc1d-4710-92a1-b9fb3fa855c3

📥 Commits

Reviewing files that changed from the base of the PR and between d82c70c and 55ba456.

📒 Files selected for processing (1)
  • scripts/check-public-language.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/check-public-language.sh

📝 Walkthrough

Walkthrough

The change centralizes PHP support policy, strengthens autorelease admission and readiness validation, adds shared-file parity checks, and requires explicit successful workflow validation before merge readiness.

Changes

Policy-driven version support

Layer / File(s) Summary
Generated policy and version discovery
README.md, docs/repository-settings.md, hooks/parse_legacy_file.lua, lib/*, scripts/generate-policy-lua, scripts/test.sh, test/mock_server.py
Supported PHP branches now come from generated policy data. Version parsing and tests cover future releases and temporary policy updates.

Admission contracts and validation

Layer / File(s) Summary
Admission rules and schemas
autorelease/admission.py, schemas/*, autorelease/protected-paths.json, autorelease/shared-files.json, .github/CODEOWNERS
Admission validates readiness records, action keys, protected paths, secrets, required checks, and synchronized lib/policy.lua output.
Validation tools and coverage
scripts/assert-admission-checks, scripts/check-public-language.sh, scripts/validate-structured-output-schemas, scripts/verify-merge-admission, test/test_autorelease.py
Validation scripts enforce deterministic schema and check rules. Tests cover admission, merge verification, shared files, readiness records, and policy synchronization.

Workflow enforcement and lifecycle

Layer / File(s) Summary
Consumer and protected-control workflows
.github/workflows/autorelease-consumer.yml, .github/workflows/protected-controls.yml, .github/workflows/ci.yml, .github/workflows/e2e.yml
Workflows use Bash, verify shared-file parity, generate phase criteria, upload validation artifacts, validate readiness records, and require explicit successful validation before merge readiness.
Autorelease contracts and consumer commands
.github/codex/autorelease/*, autorelease/consumer.py, scripts/prepare-agent-task, scripts/validate-codex-action-inputs, AUTORELEASE.md
Investigation and implementation criteria are generated centrally. Action filenames use validated keys. Event inspection is removed from comparison, and lifecycle rules are documented.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant ConsumerWorkflow
  participant PhpBin
  participant PrepareAgentTask
  participant AdmissionChecks
  participant ProtectedControls
  participant MergeReadiness
  ConsumerWorkflow->>PhpBin: Fetch shared files
  ConsumerWorkflow->>PrepareAgentTask: Request phase criteria
  ConsumerWorkflow->>AdmissionChecks: Validate checks and artifacts
  ProtectedControls->>AdmissionChecks: Validate readiness record
  ConsumerWorkflow->>MergeReadiness: Publish passed validation
  MergeReadiness->>ConsumerWorkflow: Permit merge readiness
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: hardening unattended autorelease behavior.
Description check ✅ Passed The description explains the changes, reports verification results, and addresses security controls, although it does not use the template headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/autorelease-unattended-hardening

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (10)
.github/CODEOWNERS (1)

3-21: 🧹 Nitpick | 🔵 Trivial

Every protected control now has one owner.

All 21 rules name @loadinglucian. The autorelease design requires owner review for each protected path, so this single account becomes a required reviewer for every admission, workflow, schema, and gate-harness change. When that account is unavailable, protected-control changes cannot merge.

Consider a team handle, for example @org/autorelease-owners, with the same effective membership. A team handle keeps the same review requirement and lets membership change without editing a protected file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/CODEOWNERS around lines 3 - 21, Replace the individual
`@loadinglucian` owner entries in CODEOWNERS with a team handle representing the
autorelease/control owners, such as `@org/autorelease-owners`, while preserving
coverage for every listed protected path.
test/test_autorelease.py (3)

240-253: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for --require-protected-controls.

The test covers only the --check-name path. scripts/assert-admission-checks lines 22-24 add a second jq assertion on the Protected controls bucket when --require-protected-controls is set. That branch decides whether a flow may touch protected paths, and no test exercises it. Two more fixtures cover it: one where Protected controls is pass, and one where it is absent or another bucket.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_autorelease.py` around lines 240 - 253, Extend
test_assert_admission_checks_covers_the_plugin_contract_bucket to exercise
--require-protected-controls: add a passing fixture containing a pass-valued
Protected controls bucket and a failing fixture where that bucket is absent or
replaced by another bucket, asserting the script succeeds and fails
respectively.

103-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the repository root the way the rest of the file does.

Line 108 reads autorelease/admission.py through a cwd-relative path. Lines 156 and 193 resolve the root with pathlib.Path(__file__).resolve().parents[1]. If the suite runs from any directory other than the repository root, line 108 raises FileNotFoundError and the test errors instead of reporting a duplicated action-key definition.

♻️ Proposed fix
-        source = pathlib.Path("autorelease/admission.py").read_text()
+        root = pathlib.Path(__file__).resolve().parents[1]
+        source = (root / "autorelease/admission.py").read_text()

The same applies to the relative paths at lines 121 and 125.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_autorelease.py` around lines 103 - 111, Update
test_action_key_alphabet_and_filename_have_one_definition to resolve the
repository root from pathlib.Path(__file__).resolve().parents[1], matching the
existing approach elsewhere in the file, and use that root for all admission.py
and related file reads currently using cwd-relative paths. Preserve the existing
assertions and test behavior.

405-439: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Line 411 writes event.json twice with the same content.

The loop at lines 420-431 mutates preconditions in place, and contract holds a reference to that same dict. Line 432 therefore writes the final contract, and line 411 is redundant. The digests at lines 435-439 read the file after line 432, so behavior is correct today.

Remove line 411 and add a short note that the event file must be written after the capture loop settles the precondition digests. Without that note, a later reader may remove line 432 instead and silently break digests["eventContract"].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_autorelease.py` around lines 405 - 439, Remove the initial
redundant event.write_text call before the captures loop, and retain the later
write after the loop that finalizes preconditions. Add a brief comment near the
retained write stating that event.json must be written after capture-derived
precondition digests are settled, before eventContract is digested.
scripts/validate-structured-output-schemas (1)

81-91: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

The plan-schema parity check is skipped silently when the path does not match.

The checks at lines 82-91 run only when a discovered path equals ROOT / "schemas/autorelease-plan.schema.json". schema_paths is built from --output-schema strings found in workflow files at line 68. If a workflow stops referencing the plan schema, or references it under a non-identical spelling such as schemas/../schemas/autorelease-plan.schema.json, then the loop never enters this branch. The script still prints a success line, so the action-key and required-check parity is no longer enforced and nothing reports the gap.

Assert that the plan schema is present in schema_paths before the loop.

🛡️ Proposed assertion
     if not schema_paths:
         fail("no static Codex output schemas were discovered")
+
+    plan_schema = ROOT / "schemas/autorelease-plan.schema.json"
+    if plan_schema not in schema_paths:
+        fail("the autorelease plan schema was not discovered in any workflow")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/validate-structured-output-schemas` around lines 81 - 91, Before
iterating over schema_paths, assert that the expected autorelease plan schema
path is present, using the same normalized path representation as schema_paths.
Fail with a clear validation message when it is absent, then retain the existing
actionKey and requiredChecks parity checks in the loop.
scripts/check-public-language.sh (1)

29-31: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

|| true masks xargs and grep failures, not only status 123.

The comment names status 123, but || true discards every non-zero status. xargs returns 126 when it cannot run grep and 127 when grep is absent. grep returns 2 on a read error, and xargs maps that to 123 as well. In each case matches is empty and the script prints "Public-language check passed." The gate then reports a pass without scanning anything.

Accept only the statuses that mean "ran correctly and found nothing".

🛡️ Proposed status allowlist
-# xargs reports 123 when any grep batch matches nothing, so the finding is read
-# from the output rather than from the exit status.
-matches="$(cd "$PROJECT_ROOT" && xargs -0 grep -HIFni -e "$REJECTED_TERM" < "$tracked" || true)"
+# xargs reports 123 when any grep batch matches nothing, so the finding is read
+# from the output rather than from the exit status. Every other non-zero status
+# means grep did not run correctly, so the scan is not treated as a pass.
+status=0
+matches="$(cd "$PROJECT_ROOT" && xargs -0 grep -HIFni -e "$REJECTED_TERM" < "$tracked")" || status=$?
+if [[ "$status" -ne 0 && "$status" -ne 123 ]]; then
+  echo "Public-language check could not scan the tracked files (xargs status $status)." >&2
+  exit 1
+fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-public-language.sh` around lines 29 - 31, Update the xargs/grep
invocation assigning matches in the public-language check to tolerate only the
expected no-match status while propagating execution, missing-command, and read
errors. Capture the command’s exit status explicitly and allow only successful
scanning outcomes, including xargs status 123 when it represents completed grep
batches, before reporting the check as passed.
autorelease/admission.py (2)

111-118: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Check element types before you sort evidenceDigests.

If evidenceDigests contains mixed types, for example ["sha256:...", 1], then sorted(digests) raises TypeError before the isinstance check runs. The result is still fail-closed, but the caller receives TypeError instead of AdmissionError, so the rejection message is lost.

♻️ Proposed reordering
     digests = record["evidenceDigests"]
     if (
         not isinstance(digests, list)
         or not digests
+        or not all(isinstance(item, str) and SHA256_RE.fullmatch(item) for item in digests)
         or digests != sorted(digests)
-        or not all(isinstance(item, str) and SHA256_RE.fullmatch(item) for item in digests)
     ):
         raise AdmissionError("readiness record evidence digests are invalid")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autorelease/admission.py` around lines 111 - 118, Update the evidenceDigests
validation in the admission record handling so the all-elements string/SHA-256
format check runs before comparing digests with sorted(digests). Preserve the
existing non-list and empty-list rejection behavior, and ensure mixed-type input
raises the established AdmissionError rather than allowing sorted to raise
TypeError.

417-432: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The generated lib/policy.lua layout is defined independently in the gate and in the test. Both sites spell out the same header comments, return {, maintained = {, and eight-space quoted branch lines. scripts/generate-policy-lua owns the same layout. Because the test does not read the gate's definition, a change to either copy alone leaves the test green while the gate rejects every real patch.

  • autorelease/admission.py#L417-L432: replace expected_policy_lines with a call to one shared renderer, for example render_policy_lua(maintained).splitlines(), and compare the full text so a missing trailing newline is also rejected.
  • test/test_autorelease.py#L490-L499: delete the local generated_policy_lua body and return the shared renderer's output, so seal_fixture and the gate cannot disagree about the layout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autorelease/admission.py` around lines 417 - 432, Centralize generated policy
rendering so the admission gate and tests use the same layout. In
autorelease/admission.py lines 417-432, replace the local expected_policy_lines
construction with the shared renderer (such as render_policy_lua(maintained))
and compare complete text, including the trailing newline. In
test/test_autorelease.py lines 490-499, remove the local generated_policy_lua
body and return the shared renderer output so seal_fixture and the gate cannot
diverge.
scripts/assert-admission-checks (1)

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

--checks omission exits without a diagnostic, and this file is parity-gated.

Line 20 is a bare [[ -n "$checks_file" ]]. If a caller omits --checks, the script exits 1 under set -e and prints nothing, so the workflow log shows a failed step with no cause. Add an explicit message.

autorelease/shared-files.json lists this script, so the consumer compares it byte-for-byte against php-bin at the pinned commit. Land the same edit in php-bin, or the next shared-file gate run fails.

♻️ Proposed diagnostic
-[[ -n "$checks_file" ]]
+if [[ -z "$checks_file" ]]; then
+  echo "--checks is required" >&2
+  exit 2
+fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/assert-admission-checks` around lines 20 - 21, Replace the bare
checks_file assertion in the argument-validation flow with an explicit
diagnostic before exiting when --checks is omitted, while preserving the
existing failure behavior. Apply the identical byte-for-byte change to the
corresponding php-bin copy because this script is parity-gated through
autorelease/shared-files.json.
scripts/validate-codex-action-inputs (1)

18-21: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make destination-path quoting optional in both regexes.

Current workflows use quoted destinations, but unquoted equivalent copies are not detected. Add "? around both destination paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/validate-codex-action-inputs` around lines 18 - 21, Update
CANONICAL_CONFIG_RE and UNLOADED_CONFIG_RE to make the destination paths
optionally double-quoted, adding optional-quote matching around each destination
while preserving the existing source and destination patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/protected-controls.yml:
- Around line 120-154: Replace the inline actionKey-to-filename translation in
the trusted-readiness block with the shared autorelease.consumer.action_filename
helper and import its associated error type. Update the existing exception
handler around validate_readiness_record to catch that error alongside the
current exceptions, while preserving the expected_filename comparison and
approval flow.

In `@scripts/test.sh`:
- Around line 11-12: Update the policy parity-check flow around
generate-policy-lua and the lib/policy.lua diff check so the existing policy
file is preserved when generation or comparison fails. Generate into a temporary
file for comparison, or capture and restore the original content before any
failure exit; ensure cleanup occurs without overwriting uncommitted
lib/policy.lua changes.

---

Nitpick comments:
In @.github/CODEOWNERS:
- Around line 3-21: Replace the individual `@loadinglucian` owner entries in
CODEOWNERS with a team handle representing the autorelease/control owners, such
as `@org/autorelease-owners`, while preserving coverage for every listed protected
path.

In `@autorelease/admission.py`:
- Around line 111-118: Update the evidenceDigests validation in the admission
record handling so the all-elements string/SHA-256 format check runs before
comparing digests with sorted(digests). Preserve the existing non-list and
empty-list rejection behavior, and ensure mixed-type input raises the
established AdmissionError rather than allowing sorted to raise TypeError.
- Around line 417-432: Centralize generated policy rendering so the admission
gate and tests use the same layout. In autorelease/admission.py lines 417-432,
replace the local expected_policy_lines construction with the shared renderer
(such as render_policy_lua(maintained)) and compare complete text, including the
trailing newline. In test/test_autorelease.py lines 490-499, remove the local
generated_policy_lua body and return the shared renderer output so seal_fixture
and the gate cannot diverge.

In `@scripts/assert-admission-checks`:
- Around line 20-21: Replace the bare checks_file assertion in the
argument-validation flow with an explicit diagnostic before exiting when
--checks is omitted, while preserving the existing failure behavior. Apply the
identical byte-for-byte change to the corresponding php-bin copy because this
script is parity-gated through autorelease/shared-files.json.

In `@scripts/check-public-language.sh`:
- Around line 29-31: Update the xargs/grep invocation assigning matches in the
public-language check to tolerate only the expected no-match status while
propagating execution, missing-command, and read errors. Capture the command’s
exit status explicitly and allow only successful scanning outcomes, including
xargs status 123 when it represents completed grep batches, before reporting the
check as passed.

In `@scripts/validate-codex-action-inputs`:
- Around line 18-21: Update CANONICAL_CONFIG_RE and UNLOADED_CONFIG_RE to make
the destination paths optionally double-quoted, adding optional-quote matching
around each destination while preserving the existing source and destination
patterns.

In `@scripts/validate-structured-output-schemas`:
- Around line 81-91: Before iterating over schema_paths, assert that the
expected autorelease plan schema path is present, using the same normalized path
representation as schema_paths. Fail with a clear validation message when it is
absent, then retain the existing actionKey and requiredChecks parity checks in
the loop.

In `@test/test_autorelease.py`:
- Around line 240-253: Extend
test_assert_admission_checks_covers_the_plugin_contract_bucket to exercise
--require-protected-controls: add a passing fixture containing a pass-valued
Protected controls bucket and a failing fixture where that bucket is absent or
replaced by another bucket, asserting the script succeeds and fails
respectively.
- Around line 103-111: Update
test_action_key_alphabet_and_filename_have_one_definition to resolve the
repository root from pathlib.Path(__file__).resolve().parents[1], matching the
existing approach elsewhere in the file, and use that root for all admission.py
and related file reads currently using cwd-relative paths. Preserve the existing
assertions and test behavior.
- Around line 405-439: Remove the initial redundant event.write_text call before
the captures loop, and retain the later write after the loop that finalizes
preconditions. Add a brief comment near the retained write stating that
event.json must be written after capture-derived precondition digests are
settled, before eventContract is digested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26bba10c-ae4a-47b6-90b1-3d613c17a43d

📥 Commits

Reviewing files that changed from the base of the PR and between aa92bc2 and d82c70c.

📒 Files selected for processing (29)
  • .github/CODEOWNERS
  • .github/codex/autorelease/implementation.md
  • .github/codex/autorelease/investigation.md
  • .github/workflows/autorelease-consumer.yml
  • .github/workflows/ci.yml
  • .github/workflows/e2e.yml
  • .github/workflows/protected-controls.yml
  • AUTORELEASE.md
  • README.md
  • autorelease-events/.gitkeep
  • autorelease/admission.py
  • autorelease/consumer.py
  • autorelease/protected-paths.json
  • autorelease/shared-files.json
  • docs/repository-settings.md
  • hooks/parse_legacy_file.lua
  • lib/policy.lua
  • lib/releases.lua
  • schemas/autorelease-plan.schema.json
  • scripts/assert-admission-checks
  • scripts/check-public-language.sh
  • scripts/generate-policy-lua
  • scripts/prepare-agent-task
  • scripts/test.sh
  • scripts/validate-codex-action-inputs
  • scripts/validate-structured-output-schemas
  • scripts/verify-merge-admission
  • test/mock_server.py
  • test/test_autorelease.py
💤 Files with no reviewable changes (1)
  • autorelease-events/.gitkeep

Comment thread .github/workflows/protected-controls.yml
Comment thread scripts/test.sh
@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_77b8a822-9587-4153-9cb2-e80433b0c6b1)

@loadinglucian
loadinglucian merged commit 7263fa0 into main Aug 3, 2026
4 of 5 checks passed
@loadinglucian
loadinglucian deleted the fix/autorelease-unattended-hardening branch August 3, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant