Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: CI

on:
pull_request:
workflow_dispatch:
push:
branches:
- main
Expand Down
14 changes: 9 additions & 5 deletions .github/workflows/maintenance-implementation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ jobs:
contents: write
pull-requests: write
checks: read
actions: read
actions: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand Down Expand Up @@ -345,8 +345,10 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr checks "${{ steps.pr.outputs.number }}" --watch --fail-fast --interval 10
gh pr checks "${{ steps.pr.outputs.number }}" --json name,bucket,link > maintenance-run/pr-checks.json
./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
- name: Re-verify exact SHA, sealed tree, and preconditions
env:
Expand Down Expand Up @@ -447,8 +449,10 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr checks "${{ steps.readiness.outputs.number }}" --watch --fail-fast --interval 10
gh pr checks "${{ steps.readiness.outputs.number }}" --json name,bucket,link > maintenance-run/readiness-checks.json
./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
actual="$(gh pr view "${{ steps.readiness.outputs.number }}" --json headRefOid --jq .headRefOid)"
test "$actual" = "${{ steps.readiness.outputs.head_sha }}"
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/maintenance-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ on:
permissions:
contents: write
issues: write
actions: read
actions: write
checks: read
pull-requests: write

concurrency:
Expand Down Expand Up @@ -330,8 +331,10 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr checks "${{ steps.event_pr.outputs.number }}" --watch --fail-fast --interval 10
gh pr checks "${{ steps.event_pr.outputs.number }}" --json name,bucket,link > release-run/event-checks.json
./scripts/dispatch-pr-checks \
--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
actual="$(gh pr view "${{ steps.event_pr.outputs.number }}" --json headRefOid --jq .headRefOid)"
test "$actual" = "${{ steps.event_pr.outputs.head_sha }}"
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/maintenance-watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ jobs:
actions: write
artifact-metadata: write
attestations: write
checks: read
contents: write
id-token: write
pull-requests: write
Expand Down Expand Up @@ -267,8 +268,10 @@ jobs:
--body "Opaque evidence state for a reviewed no-change result.")"
number="${url##*/}"
fi
gh pr checks "$number" --required --watch --fail-fast --interval 10
gh pr checks "$number" --required --json name,bucket,link > maintenance-plan-download/no-change-checks.json
./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
test "$(gh pr view "$number" --json headRefOid --jq .headRefOid)" = "$head"
Expand Down Expand Up @@ -348,8 +351,10 @@ jobs:
url="$(gh pr create --base main --head "$branch" --title "chore: complete $action_key" \
--body "Deterministic EOL completion bound to exact cross-repository readiness.")"
number="${url##*/}"
gh pr checks "$number" --required --watch --fail-fast --interval 10
gh pr checks "$number" --required --json name,bucket,link > maintenance-plan-download/eol-checks.json
./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
test "$(gh pr view "$number" --json headRefOid --jq .headRefOid)" = "$head"
Expand Down
43 changes: 36 additions & 7 deletions .github/workflows/protected-controls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ name: Protected controls

on:
pull_request_target:
workflow_dispatch:
inputs:
pr_number:
description: Pull request to validate at the dispatched branch head
required: true
type: string

permissions:
attestations: read
Expand All @@ -14,20 +20,43 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Resolve exact pull request state
id: pr
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: |
[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]
gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" > "$RUNNER_TEMP/pr.json"
test "$(jq -r .state "$RUNNER_TEMP/pr.json")" = "open"
test "$(jq -r .base.ref "$RUNNER_TEMP/pr.json")" = "main"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
test "$(jq -r .head.sha "$RUNNER_TEMP/pr.json")" = "${{ github.sha }}"
test "refs/heads/$(jq -r .head.ref "$RUNNER_TEMP/pr.json")" = "${{ github.ref }}"
fi
{
echo "number=$PR_NUMBER"
echo "head_sha=$(jq -r .head.sha "$RUNNER_TEMP/pr.json")"
echo "base_sha=$(jq -r .base.sha "$RUNNER_TEMP/pr.json")"
echo "head_ref=$(jq -r .head.ref "$RUNNER_TEMP/pr.json")"
echo "head_repository=$(jq -r .head.repo.full_name "$RUNNER_TEMP/pr.json")"
echo "author=$(jq -r .user.login "$RUNNER_TEMP/pr.json")"
} >> "$GITHUB_OUTPUT"
Comment on lines +23 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files .github/workflows/protected-controls.yml

echo
echo "== targeted workflow excerpt =="
if [ -f .github/workflows/protected-controls.yml ]; then
  sed -n '1,120p' .github/workflows/protected-controls.yml | cat -n
fi

echo
echo "== occurrences in workflow =="
rg -n 'github\.(ref|sha|event_name)|PR_NUMBER|pr_number|workflow_dispatch|pull_request' .github/workflows/protected-controls.yml || true

Repository: Bigpixelrocket/php-bin

Length of output: 6379


🌐 Web query:

GitHub Actions protected branch rules enforce pull requests created on untrusted forks cannot be built with workflow_dispatch

💡 Result:

The statement that GitHub Actions protected branch rules specifically enforce that pull requests from untrusted forks cannot be built with workflow_dispatch is not accurate as a general rule, but it aligns with the broader security mechanisms GitHub employs to manage manual triggers and untrusted code [1][2]. Workflow_dispatch is a manual trigger that requires the workflow file to exist on the repository's default branch [3]. Because it is a manual trigger, it is not tied to the automated pull request lifecycle in the way events like pull_request or pull_request_target are [3]. Key security context: 1. Manual Trigger Controls: GitHub provides enterprise-level and repository-level policies that allow administrators to limit workflow_dispatch triggers specifically to maintainers or users with specific permissions [1][2]. This is designed to prevent manual-trigger abuse, where untrusted identities might otherwise attempt to initiate workflows [1][2]. 2. Fork Pull Request Protections: The primary concern with untrusted forks is the "pwn request" pattern, where malicious code from a fork is executed in a privileged context (e.g., using pull_request_target) [4]. GitHub mitigates this by restricting pull_request_target and by ensuring that actions/checkout v7 and later refuses to check out fork pull request code in privileged contexts by default [5][4]. 3. Event Restrictions: GitHub allows organizations to enforce event rules that control which triggers are permitted [1][2]. While you can restrict workflow_dispatch globally or by user, the restriction is not inherently linked to "pull requests created on untrusted forks" by default; rather, it is a policy-driven control to prevent unauthorized users from executing workflows [1][2]. In summary, while you cannot "pull" a workflow_dispatch trigger from a fork (since it must reside on the default branch), you can and should use GitHub Actions policies to restrict who is authorized to manually trigger workflows, thereby preventing untrusted actors from leveraging this capability [1][2][3].

Citations:


🌐 Web query:

GitHub Pull Request API head repo full_name base sha head sha fields documentation

💡 Result:

The GitHub REST API provides detailed information about pull requests, including objects for head and base that contain specific branch and commit data [1][2]. When retrieving a single pull request via the GET /repos/{owner}/{repo}/pulls/{pull_number} endpoint, the response object includes nested head and base objects [1][2]. The relevant fields within these objects are: base object: - base.ref: The name of the base branch. - base.sha: The SHA of the latest commit on the base branch at the time the pull request was created or last updated [2]. - base.repo.full_name: The full name of the repository where the base branch resides (e.g., octocat/Hello-World) [2]. head object: - head.ref: The name of the head branch (the branch being merged). - head.sha: The SHA of the latest commit on the head branch [2]. - head.repo.full_name: The full name of the repository where the head branch resides [2]. These fields are essential for identifying the exact state and source of the branches involved in the pull request [2]. Users should consult the official GitHub REST API documentation for the most current schema definitions, as fields may evolve [1][3].

Citations:


🌐 Web query:

GitHub Actions branch name special characters backticks semicolon shell command injection github.ref

💡 Result:

Using branch names or tag names that contain special characters (such as backticks, semicolons, quotes, or dollar signs) in GitHub Actions workflows creates a severe security risk known as shell command injection [1][2][3]. This vulnerability occurs when untrusted context variables—such as github.ref, github.head_ref, or github.event.pull_request.head.ref—are directly interpolated into shell scripts within a run: block [1][4][5]. Because GitHub Actions evaluates the ${{ }} expression and substitutes the raw string before the shell executes the command, a malicious branch name (e.g., a branch named test"; ls $GITHUB_WORKSPACE #) can break out of intended quotes or command structures to execute arbitrary code on the runner [1][4][5]. Key Vulnerability Mechanisms: - Interpolation: The ${{ }} syntax automatically expands before the shell processes the script. If the value is not treated as a safe variable, special characters can be used to terminate the intended command and inject new ones [1][6][5]. - Shell Execution: Commands in run: steps are executed by a shell (like bash). If the input is not properly escaped or passed via an environment variable, the shell interprets injected characters (like backticks or semicolons) as control operators [1][4][7]. Recommended Hardening Strategies: - Never interpolate untrusted data directly into run: steps: Always pass untrusted inputs (like branch names, PR titles, or commit messages) through an env: block [4][5]. This ensures the value is passed to the shell as an environment variable rather than as part of the script itself [4][5]. - Use Environment Variables: Inside the run: script, reference the environment variable using double quotes (e.g., "$BRANCH_NAME") to prevent the shell from re-evaluating or expanding the content [4][5]. - Sanitize Inputs: If you must process data, avoid using dangerous shell functions like eval or unsanitized shell interpolation [3]. Use tools like printf %q or strictly validate that input matches an expected format (e.g., regex checks for alphanumeric branch names). Example of Secure Implementation: Instead of: - run: echo "Branch name: ${{ github.head_ref }}" Use: - name: Echo branch name safely env: BRANCH_NAME: ${{ github.head_ref }} run: | echo "Branch name: $BRANCH_NAME"

Citations:


Pass github context values through env before using them in the runner script.

${{ github.ref }}, ${{ github.sha }}, and ${{ github.event_name }} are expanded before the bash script runs, so a malicious branch/ref payload can break out of shell syntax when used directly inside run:. Define these as env vars from the expression and reference the env vars in the script, as already done for PR_NUMBER.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 36-36: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 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/workflows/protected-controls.yml around lines 23 - 45, Update the
“Resolve exact pull request state” step to pass github.ref, github.sha, and
github.event_name through its env block, then reference those environment
variables in the bash conditionals instead of embedding expressions directly in
run. Preserve the existing workflow_dispatch validation behavior.

Source: Linters/SAST tools

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event.pull_request.base.sha }}
ref: ${{ steps.pr.outputs.base_sha }}
persist-credentials: false
- name: Require exact-head owner approval for protected paths
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
BASE_SHA: ${{ steps.pr.outputs.base_sha }}
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 }}
run: |
python3 - <<'PY'
Expand Down
4 changes: 4 additions & 0 deletions docs/repository-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Required repository state:
- Enable the organization setting that permits Actions to create pull requests;
runtime workflows do not submit approving reviews. Protected-control approval
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.
- Allow GitHub-owned Actions plus only `openai/codex-action` and
`jdx/mise-action`, and require every Action reference to use a full commit
SHA.
Expand Down
1 change: 1 addition & 0 deletions maintenance/protected-paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"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",
Expand Down
93 changes: 93 additions & 0 deletions scripts/dispatch-pr-checks
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env bash

set -euo pipefail

usage() {
echo "Usage: $0 --pr <number> --check <required-check> --output <path>" >&2
exit 2
}

pr_number=""
primary_check=""
output=""

while (($#)); do
case "$1" in
--pr)
pr_number="${2:-}"
shift 2
;;
--check)
primary_check="${2:-}"
shift 2
;;
--output)
output="${2:-}"
shift 2
;;
*)
usage
;;
esac
done

[[ "$pr_number" =~ ^[1-9][0-9]*$ ]] || usage
[[ -n "$primary_check" && -n "$output" ]] || usage
repository="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"

pr="$(gh api "repos/$repository/pulls/$pr_number")"
state="$(jq -r .state <<<"$pr")"
base_ref="$(jq -r .base.ref <<<"$pr")"
head_ref="$(jq -r .head.ref <<<"$pr")"
head_sha="$(jq -r .head.sha <<<"$pr")"
head_repository="$(jq -r .head.repo.full_name <<<"$pr")"

[[ "$state" == "open" ]]
[[ "$base_ref" == "main" ]]
[[ "$head_repository" == "$repository" ]]
[[ "$head_ref" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]
[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]]
Comment on lines +45 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add diagnostics to the hard assertions.

These bare [[ ]] checks rely on set -e to abort on failure, but produce no error message — a failed dispatch here just dies with no explanation in the CI log.

🩹 Proposed fix
-[[ "$state" == "open" ]]
-[[ "$base_ref" == "main" ]]
-[[ "$head_repository" == "$repository" ]]
-[[ "$head_ref" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]
-[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]]
+[[ "$state" == "open" ]] || { echo "PR #$pr_number is not open." >&2; exit 1; }
+[[ "$base_ref" == "main" ]] || { echo "PR #$pr_number does not target main." >&2; exit 1; }
+[[ "$head_repository" == "$repository" ]] || { echo "PR #$pr_number head is not same-repository." >&2; exit 1; }
+[[ "$head_ref" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] || { echo "PR #$pr_number head ref has an unexpected shape." >&2; exit 1; }
+[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "PR #$pr_number head sha has an unexpected shape." >&2; exit 1; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[[ "$state" == "open" ]]
[[ "$base_ref" == "main" ]]
[[ "$head_repository" == "$repository" ]]
[[ "$head_ref" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]
[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]]
[[ "$state" == "open" ]] || { echo "PR #$pr_number is not open." >&2; exit 1; }
[[ "$base_ref" == "main" ]] || { echo "PR #$pr_number does not target main." >&2; exit 1; }
[[ "$head_repository" == "$repository" ]] || { echo "PR #$pr_number head is not same-repository." >&2; exit 1; }
[[ "$head_ref" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] || { echo "PR #$pr_number head ref has an unexpected shape." >&2; exit 1; }
[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "PR #$pr_number head sha has an unexpected shape." >&2; exit 1; }
🤖 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/dispatch-pr-checks` around lines 45 - 49, Replace the bare assertions
in the dispatch validation block with checks that emit a clear diagnostic
identifying the failed condition before exiting. Preserve the existing
validation rules for state, base_ref, head_repository, head_ref, and head_sha,
and ensure failures remain fatal under the script’s current control flow.


checks_url="repos/$repository/commits/$head_sha/check-runs?per_page=100&filter=latest"
before="$(gh api -H "Accept: application/vnd.github+json" "$checks_url")"
primary_before="$(jq --arg name "$primary_check" '[.check_runs[] | select(.name==$name) | .id] | max // 0' <<<"$before")"
protected_before="$(jq '[.check_runs[] | select(.name=="Protected controls") | .id] | max // 0' <<<"$before")"

# Events created with GITHUB_TOKEN do not recursively start pull_request
# workflows. workflow_dispatch is the supported exception, so run the two
# base-reviewed validators explicitly at the exact PR branch.
gh workflow run ci.yml --repo "$repository" --ref "$head_ref"
gh workflow run protected-controls.yml --repo "$repository" --ref "$head_ref" \
-f "pr_number=$pr_number"

deadline=$((SECONDS + 900))
latest=""
while ((SECONDS < deadline)); do
latest="$(gh api -H "Accept: application/vnd.github+json" "$checks_url")"
primary="$(jq -c --arg name "$primary_check" --argjson floor "$primary_before" \
'[.check_runs[] | select(.name==$name and .id>$floor)] | max_by(.id) // empty' <<<"$latest")"
protected="$(jq -c --argjson floor "$protected_before" \
'[.check_runs[] | select(.name=="Protected controls" and .id>$floor)] | max_by(.id) // empty' <<<"$latest")"

if [[ -n "$primary" && -n "$protected" ]]; then
failed="$(jq -r -s '[.[] | select(.status=="completed" and .conclusion!="success") | .details_url] | join("\n")' \
<<<"$primary"$'\n'"$protected")"
if [[ -n "$failed" ]]; then
echo "A dispatched required check failed:" >&2
echo "$failed" >&2
exit 1
fi
if jq -e -s 'all(.[]; .status=="completed" and .conclusion=="success")' \
<<<"$primary"$'\n'"$protected" >/dev/null; then
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"
exit 0
fi
fi
sleep 5
done
Comment on lines +63 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Failure detection is gated behind both checks appearing, masking/delaying real failures.

The failed/success evaluation at lines 72-88 only runs if [[ -n "$primary" && -n "$protected" ]]. If one check-run fails or errors out quickly while the other hasn't been created yet (e.g. protected-controls.yml never starts due to a permissions/infra issue), this script silently ignores the already-known failure and spins for the full 900s before reporting a generic timeout — hiding the real cause and wasting up to 15 minutes on every one of the 5 call sites (maintenance-implementation.yml x2, maintenance-release.yml, maintenance-watch.yml x2).

🐛 Proposed fix: evaluate failure independently of whether both checks have appeared
-  if [[ -n "$primary" && -n "$protected" ]]; then
-    failed="$(jq -r -s '[.[] | select(.status=="completed" and .conclusion!="success") | .details_url] | join("\n")' \
-      <<<"$primary"$'\n'"$protected")"
-    if [[ -n "$failed" ]]; then
-      echo "A dispatched required check failed:" >&2
-      echo "$failed" >&2
-      exit 1
-    fi
-    if jq -e -s 'all(.[]; .status=="completed" and .conclusion=="success")' \
-      <<<"$primary"$'\n'"$protected" >/dev/null; then
+  present=()
+  [[ -n "$primary" ]] && present+=("$primary")
+  [[ -n "$protected" ]] && present+=("$protected")
+  if ((${`#present`[@]})); then
+    failed="$(printf '%s\n' "${present[@]}" | jq -r -s \
+      '[.[] | select(.status=="completed" and .conclusion!="success") | .details_url] | join("\n")')"
+    if [[ -n "$failed" ]]; then
+      echo "A dispatched required check failed:" >&2
+      echo "$failed" >&2
+      exit 1
+    fi
+  fi
+  if [[ -n "$primary" && -n "$protected" ]]; then
+    if jq -e -s 'all(.[]; .status=="completed" and .conclusion=="success")' \
+      <<<"$primary"$'\n'"$protected" >/dev/null; then
       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"
       exit 0
     fi
   fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
deadline=$((SECONDS + 900))
latest=""
while ((SECONDS < deadline)); do
latest="$(gh api -H "Accept: application/vnd.github+json" "$checks_url")"
primary="$(jq -c --arg name "$primary_check" --argjson floor "$primary_before" \
'[.check_runs[] | select(.name==$name and .id>$floor)] | max_by(.id) // empty' <<<"$latest")"
protected="$(jq -c --argjson floor "$protected_before" \
'[.check_runs[] | select(.name=="Protected controls" and .id>$floor)] | max_by(.id) // empty' <<<"$latest")"
if [[ -n "$primary" && -n "$protected" ]]; then
failed="$(jq -r -s '[.[] | select(.status=="completed" and .conclusion!="success") | .details_url] | join("\n")' \
<<<"$primary"$'\n'"$protected")"
if [[ -n "$failed" ]]; then
echo "A dispatched required check failed:" >&2
echo "$failed" >&2
exit 1
fi
if jq -e -s 'all(.[]; .status=="completed" and .conclusion=="success")' \
<<<"$primary"$'\n'"$protected" >/dev/null; then
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"
exit 0
fi
fi
sleep 5
done
deadline=$((SECONDS + 900))
latest=""
while ((SECONDS < deadline)); do
latest="$(gh api -H "Accept: application/vnd.github+json" "$checks_url")"
primary="$(jq -c --arg name "$primary_check" --argjson floor "$primary_before" \
'[.check_runs[] | select(.name==$name and .id>$floor)] | max_by(.id) // empty' <<<"$latest")"
protected="$(jq -c --argjson floor "$protected_before" \
'[.check_runs[] | select(.name=="Protected controls" and .id>$floor)] | max_by(.id) // empty' <<<"$latest")"
present=()
[[ -n "$primary" ]] && present+=("$primary")
[[ -n "$protected" ]] && present+=("$protected")
if ((${`#present`[@]})); then
failed="$(printf '%s\n' "${present[@]}" | jq -r -s \
'[.[] | select(.status=="completed" and .conclusion!="success") | .details_url] | join("\n")')"
if [[ -n "$failed" ]]; then
echo "A dispatched required check failed:" >&2
echo "$failed" >&2
exit 1
fi
fi
if [[ -n "$primary" && -n "$protected" ]]; then
if jq -e -s 'all(.[]; .status=="completed" and .conclusion=="success")' \
<<<"$primary"$'\n'"$protected" >/dev/null; then
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"
exit 0
fi
fi
sleep 5
done
🤖 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/dispatch-pr-checks` around lines 63 - 90, Update the polling loop
around the primary and protected check-run evaluations so each non-success
completed check is detected and reported independently, without requiring both
$primary and $protected to exist. Preserve the success/output path only when
both checks are present and completed successfully, while continuing to wait for
a missing check unless the existing deadline is reached.


echo "Timed out waiting for newly dispatched required checks at $head_sha." >&2
exit 1
20 changes: 20 additions & 0 deletions tests/test_maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,30 @@ 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("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.assertFalse(path_is_protected("support-policy.json"))

def test_token_created_prs_explicitly_dispatch_required_checks(self):
root = pathlib.Path(__file__).resolve().parents[1]
ci = (root / ".github/workflows/ci.yml").read_text()
protected = (root / ".github/workflows/protected-controls.yml").read_text()
dispatcher = (root / "scripts/dispatch-pr-checks").read_text()
self.assertIn("workflow_dispatch:", ci)
self.assertIn("workflow_dispatch:", protected)
self.assertIn("pr_number:", protected)
self.assertIn("gh workflow run ci.yml", dispatcher)
self.assertIn("gh workflow run protected-controls.yml", dispatcher)
for workflow in (
"maintenance-watch.yml",
"maintenance-implementation.yml",
"maintenance-release.yml",
):
body = (root / ".github/workflows" / workflow).read_text()
self.assertIn("./scripts/dispatch-pr-checks", body)
self.assertNotIn("gh pr checks", body)

def test_malformed_contract_shapes_fail_closed(self):
contract = self._contract()
contract["allowedAuthority"] = [[]]
Expand Down
Loading