diff --git a/.github/workflows/maintenance-implementation.yml b/.github/workflows/maintenance-implementation.yml index 324824f..c6e3c9f 100644 --- a/.github/workflows/maintenance-implementation.yml +++ b/.github/workflows/maintenance-implementation.yml @@ -307,7 +307,7 @@ jobs: permissions: contents: write pull-requests: write - checks: read + checks: write actions: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 diff --git a/.github/workflows/maintenance-release.yml b/.github/workflows/maintenance-release.yml index 050cd82..0147633 100644 --- a/.github/workflows/maintenance-release.yml +++ b/.github/workflows/maintenance-release.yml @@ -24,7 +24,7 @@ permissions: contents: write issues: write actions: write - checks: read + checks: write pull-requests: write concurrency: diff --git a/.github/workflows/maintenance-watch.yml b/.github/workflows/maintenance-watch.yml index 376d265..7593d42 100644 --- a/.github/workflows/maintenance-watch.yml +++ b/.github/workflows/maintenance-watch.yml @@ -146,7 +146,7 @@ jobs: actions: write artifact-metadata: write attestations: write - checks: read + checks: write contents: write id-token: write pull-requests: write diff --git a/docs/repository-settings.md b/docs/repository-settings.md index ca97e70..4ce3010 100644 --- a/docs/repository-settings.md +++ b/docs/repository-settings.md @@ -36,8 +36,9 @@ Required repository state: remains owner-only except for the deterministic evidence-state proof above. - Because GitHub suppresses ordinary PR events created by `GITHUB_TOKEN`, each deterministic PR coordinator explicitly dispatches `ci.yml` and - `protected-controls.yml` at the exact PR branch, then accepts only newly - created successful check runs for that head SHA. + `protected-controls.yml` at the exact PR branch, accepts only newly created + successful validator runs for that head SHA, and only then publishes the + Actions-owned required checks onto the PR rollup with the validator URLs. - Allow GitHub-owned Actions plus only `openai/codex-action` and `jdx/mise-action`, and require every Action reference to use a full commit SHA. diff --git a/scripts/dispatch-pr-checks b/scripts/dispatch-pr-checks index 70960de..3c1b182 100755 --- a/scripts/dispatch-pr-checks +++ b/scripts/dispatch-pr-checks @@ -79,10 +79,46 @@ while ((SECONDS < deadline)); do fi if jq -e -s 'all(.[]; .status=="completed" and .conclusion=="success")' \ <<<"$primary"$'\n'"$protected" >/dev/null; then + publish_gate() { + local validator="$1" + local name details request + name="$(jq -r .name <<<"$validator")" + details="$(jq -r .details_url <<<"$validator")" + request="$(mktemp)" + jq -n \ + --arg name "$name" \ + --arg head "$head_sha" \ + --arg details "$details" \ + --arg summary "Exact-head validator passed: $details" \ + '{name:$name,head_sha:$head,status:"completed",conclusion:"success",details_url:$details,output:{title:"Dispatched validator passed",summary:$summary}}' \ + > "$request" + gh api --method POST -H "Accept: application/vnd.github+json" \ + "repos/$repository/check-runs" --input "$request" + rm -f "$request" + } + + # workflow_dispatch runs validate the exact commit but GitHub does not + # attach them to a PR rollup. Publish Actions-owned gate check runs only + # after both new validator runs pass, retaining their URLs as evidence. + primary_gate="$(publish_gate "$primary")" + protected_gate="$(publish_gate "$protected")" + for _ in {1..24}; do + rollup="$(gh pr checks "$pr_number" --repo "$repository" --required \ + --json name,bucket,link 2>/dev/null || true)" + if jq -e --arg primary "$primary_check" \ + '([.[] | select(.name==$primary) | .bucket] | last) == "pass" and ([.[] | select(.name=="Protected controls") | .bucket] | last) == "pass"' \ + <<<"$rollup" >/dev/null 2>&1; then + break + fi + sleep 5 + done + jq -e --arg primary "$primary_check" \ + '([.[] | select(.name==$primary) | .bucket] | last) == "pass" and ([.[] | select(.name=="Protected controls") | .bucket] | last) == "pass"' \ + <<<"${rollup:-[]}" >/dev/null mkdir -p "$(dirname "$output")" jq -s --arg head "$head_sha" \ 'map({name, bucket:"pass", link:.details_url, headSha:$head, checkRunId:.id})' \ - <<<"$primary"$'\n'"$protected" > "$output" + <<<"$primary_gate"$'\n'"$protected_gate" > "$output" exit 0 fi fi diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index 375a3e6..354c323 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -286,6 +286,8 @@ def test_token_created_prs_explicitly_dispatch_required_checks(self): self.assertIn("pr_number:", protected) self.assertIn("gh workflow run ci.yml", dispatcher) self.assertIn("gh workflow run protected-controls.yml", dispatcher) + self.assertIn('"repos/$repository/check-runs"', dispatcher) + self.assertIn("Exact-head validator passed", dispatcher) for workflow in ( "maintenance-watch.yml", "maintenance-implementation.yml", @@ -294,6 +296,7 @@ def test_token_created_prs_explicitly_dispatch_required_checks(self): body = (root / ".github/workflows" / workflow).read_text() self.assertIn("./scripts/dispatch-pr-checks", body) self.assertNotIn("gh pr checks", body) + self.assertIn("checks: write", body) def test_malformed_contract_shapes_fail_closed(self): contract = self._contract()