Skip to content

Add pr-ci-readiness helper to dedupe PR CI gating#4067

Merged
justin808 merged 1 commit into
mainfrom
jg/pr-ci-readiness-script
Jun 17, 2026
Merged

Add pr-ci-readiness helper to dedupe PR CI gating#4067
justin808 merged 1 commit into
mainfrom
jg/pr-ci-readiness-script

Conversation

@justin808

@justin808 justin808 commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Quick-win #4 of the skill-simplification series. The "is this PR's CI ready?" check was re-explained in prose across three places. This PR encapsulates it in a small CLI wrapper and rewires the consumers to call it.

The shared rule: run gh pr checks <PR> --required; if no required checks exist, fall back to the full gh pr checks <PR> list; an empty list ⇒ UNKNOWN/not-ready; ignore superseded/cancelled rows; otherwise READY only when no failing/pending checks remain.

What's new

.agents/skills/pr-batch/bin/pr-ci-readiness <PR> (--repo OWNER/REPO defaults to gh repo view):

  • Runs both gh pr checks forms with --json name,state,bucket,link. gh pr checks exits non-zero on failing/pending checks and when no required checks exist, so output is captured and rows are inspected rather than trusting the exit status.
  • --json (default): {pr, verdict: "READY"|"NOT_READY"|"UNKNOWN", required_used: bool, failing: [names], pending: [names]}
  • --text: prints the verdict plus the failing/pending names.
  • --self-check / --help flags; mirrors the namespaced-bash + ruby-for-JSON conventions of fetch-pr-review-data and post-merge-audit-scope.

Consumers rewired (algorithm restatement → helper call)

  • .agents/skills/pr-batch/SKILL.md
  • .agents/workflows/pr-processing.md (both the "Before merge" paragraph and the canonical CI Polling And Live State section)
  • .agents/workflows/adversarial-pr-review.md (the SKILL.md for this skill does not restate the algorithm; the prose lives in the workflow file)

Surrounding scheduling/gating prose left intact; edits are surgical.

Verification

  • shellcheck -x clean on both pr-ci-readiness and pr-ci-readiness-test.bash.
  • 10/10 unit tests pass (fake gh): all-passing→READY, failing→NOT_READY, empty-required-falls-back-to-full (required_used flips), totally-empty→UNKNOWN, cancelled/superseded rows ignored, plus --self-check, arg validation, and text mode.
  • Verified against real merged PRs #4057 and #4058 in shakacode/react_on_rails: required list is empty (rc 1) so the helper falls back; full list is all pass/skipping → READY, matching gh pr checks ground truth.

No CHANGELOG entry (internal tooling).

🤖 Generated with Claude Code


Note

Low Risk
Internal agent tooling only; behavior matches the prior documented gate and is covered by unit/integration tests, with no production runtime or auth changes.

Overview
Adds pr-ci-readiness, a Ruby CLI that centralizes the repeated “is this PR’s CI ready?” rule: try gh pr checks --required, fall back to the full list when none exist, drop cancelled rows, and emit READY / NOT_READY / UNKNOWN with failing and pending check names (stdout is parsed because gh exits non-zero on pending/fail and on empty required checks).

pr-ci-readiness-test.rb covers the pure verdict logic and CLI paths via a fake gh on PATH; the script also supports --json, --text, --self-check, and --repo.

Agent docs in pr-batch/SKILL.md, pr-processing.md, and adversarial-pr-review.md now call the helper for the readiness gate instead of restating the algorithm; gh pr checks remains documented for advisory review-agent status beyond that gate.

Reviewed by Cursor Bugbot for commit 03c1d8c. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Introduced pr-ci-readiness command-line tool to evaluate whether a PR's CI checks are ready for merging, providing clear READY/NOT_READY/UNKNOWN verdicts.
  • Documentation

    • Updated PR processing and code review workflows to use the new readiness tool as the authoritative source for CI merge validation.
  • Tests

    • Added test suite for readiness verdict computation and command-line interface validation.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Introduces a new Ruby script .agents/skills/pr-batch/bin/pr-ci-readiness that computes a READY/NOT_READY/UNKNOWN merge-readiness verdict from gh pr checks output using a required-then-full-list fallback strategy. Adds a Minitest test suite. Updates three agent workflow/skill documents to reference the script instead of inline gh pr checks instructions.

Changes

pr-ci-readiness script and workflow integration

Layer / File(s) Summary
Pure verdict helpers and module functions
.agents/skills/pr-batch/bin/pr-ci-readiness
Adds PrCiReadiness module with parse_rows, checks?, active_rows, failing_names, pending_names, verdict_for, assess, text_summary, and the USAGE constant defining the CLI/output JSON schema.
Runner CLI, two-phase fetch, and self-check
.agents/skills/pr-batch/bin/pr-ci-readiness
Implements PrCiReadiness::Runner#run with argument parsing, PR-number validation, repo resolution via gh repo view, Open3.capture2-based two-phase gh pr checks --required/full-list fetch, JSON/text output, --self-check in-process scenarios, gh smoke check, and __FILE__ execution guard.
Minitest unit and CLI integration tests
.agents/skills/pr-batch/bin/pr-ci-readiness-test.rb
Adds PrCiReadinessTest for pure verdict/parsing logic and PrCiReadinessCliTest for end-to-end CLI behavior via a fake gh executable, covering all verdict paths, fallback, cancellation, argument validation, --help, and --self-check.
Workflow and skill doc updates
.agents/skills/pr-batch/SKILL.md, .agents/workflows/adversarial-pr-review.md, .agents/workflows/pr-processing.md
Replaces inline gh pr checks readiness decision trees in all three documents with references to the pr-ci-readiness script, documenting its verdict semantics, required-vs-full fallback, cancelled-row filtering, and UNKNOWN-blocks-merge rule.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • shakacode/react_on_rails#3904: Introduces the same core rule now encapsulated by pr-ci-readiness—fall back from gh pr checks --required to the full list when no required checks exist—directly overlapping at the readiness-gating logic level.
  • shakacode/react_on_rails#3916: Tightens the same inline gh pr checks --required empty-required fallback and UNKNOWN handling in .agents/skills/pr-batch/SKILL.md and .agents/workflows/pr-processing.md that this PR replaces with the script contract.
  • shakacode/react_on_rails#3794: Updates .agents/workflows/adversarial-pr-review.md to switch CI readiness gating away from statusCheckRollup toward direct gh pr checks evaluation, the same file and direction extended by this PR.

Suggested labels

enhancement, documentation, codex

Suggested reviewers

  • alexeyr-ci2
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: introducing a new helper script to centralize and deduplicate PR CI readiness logic across multiple workflow files.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/pr-ci-readiness-script

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Reviewed by Cursor Bugbot for commit 065ebbb. Configure here.

pcr_fetch_checks "$repo" "$pr" full "$full_file"
checks_file="$full_file"
required_used="false"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cancel-only required skips full fallback

Medium Severity

When gh pr checks --required returns a non-empty JSON array whose rows are all in the cancel bucket, pcr_has_checks treats the required list as present, so the script never fetches the full check list. After cancelled rows are stripped, the verdict becomes UNKNOWN with required_used: true even if the full gh pr checks output would show passing or pending live CI.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 065ebbb. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 065ebbbf10

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +121 to +125
verdict =
if rows.empty?
"UNKNOWN"
elsif failing.empty? && pending.empty?
"READY"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require skipped-check evidence before READY

When gh pr checks returns any bucket: "skipping" row, this branch reports READY as long as there are no fail/pending rows. The new caller docs still say skipped checks need CI-selector or maintainer-waiver evidence, and the GitHub CLI manual for gh pr checks documents skipping as a separate JSON bucket, so this helper can now mark a PR merge-ready while hiding the exact skipped checks that still need evidence.

Useful? React with 👍 / 👎.

Comment on lines +113 to +114
# Ignore superseded/cancelled rows; they are not part of the current gate.
rows = rows.reject { |row| row["bucket"].to_s == "cancel" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not drop current cancelled required checks

This drops every bucket: "cancel" row before deciding readiness. In the updated workflow, cancelled rows are only safe to ignore when they are superseded; current required checks or configured review-agent checks must still block readiness, and the GitHub CLI manual says --required returns only required checks while cancel is a possible bucket. If a current required workflow is cancelled alongside passing checks, the helper can return READY even though the required gate has not completed.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
.agents/skills/pr-batch/bin/pr-ci-readiness-test.bash (1)

269-307: ⚡ Quick win

Add boundary tests for PR=0 and multi-slash repo values.

These two cases are close to the existing validation tests and will lock down the intended input contract more completely.

🤖 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 @.agents/skills/pr-batch/bin/pr-ci-readiness-test.bash around lines 269 -
307, Add two new boundary test functions to complement the existing
test_rejects_non_integer_pr and test_rejects_bad_repo_form tests. Create a
test_rejects_zero_pr function that validates the script rejects PR number zero
with an appropriate error message about requiring a positive integer, following
the same pattern as test_rejects_non_integer_pr. Create a
test_rejects_multislash_repo function that validates the script rejects
repository values containing multiple slashes like owner/repo/extra with an
appropriate error message about the OWNER/REPO form requirement, following the
same pattern as test_rejects_bad_repo_form. Both new tests should use the same
structure of setting up environment, executing the script with the boundary case
input, capturing return code and output, and asserting the expected error
conditions.
🤖 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 @.agents/skills/pr-batch/bin/pr-ci-readiness:
- Around line 302-325: The PR number validation allows zero as a valid value
when it should only accept positive integers (1 and above), and the repo
validation pattern only checks for the presence of at least one slash, allowing
malformed strings like owner/repo/extra instead of enforcing exactly OWNER/REPO
format. Modify the case statement for the PR variable to reject zero as a value
in addition to non-numeric input, and change the repo validation pattern to
match the exact OWNER/REPO format with exactly one slash and non-empty owner and
repo components rather than just checking for the presence of any slash.

---

Nitpick comments:
In @.agents/skills/pr-batch/bin/pr-ci-readiness-test.bash:
- Around line 269-307: Add two new boundary test functions to complement the
existing test_rejects_non_integer_pr and test_rejects_bad_repo_form tests.
Create a test_rejects_zero_pr function that validates the script rejects PR
number zero with an appropriate error message about requiring a positive
integer, following the same pattern as test_rejects_non_integer_pr. Create a
test_rejects_multislash_repo function that validates the script rejects
repository values containing multiple slashes like owner/repo/extra with an
appropriate error message about the OWNER/REPO form requirement, following the
same pattern as test_rejects_bad_repo_form. Both new tests should use the same
structure of setting up environment, executing the script with the boundary case
input, capturing return code and output, and asserting the expected error
conditions.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f5222091-59c6-4f8b-8a4c-064f22204040

📥 Commits

Reviewing files that changed from the base of the PR and between 1ba7386 and 065ebbb.

📒 Files selected for processing (5)
  • .agents/skills/pr-batch/SKILL.md
  • .agents/skills/pr-batch/bin/pr-ci-readiness
  • .agents/skills/pr-batch/bin/pr-ci-readiness-test.bash
  • .agents/workflows/adversarial-pr-review.md
  • .agents/workflows/pr-processing.md

Comment on lines +302 to +325
case "$pr" in
''|*[!0-9]*)
pcr_error "a positive integer PR number is required"
pcr_usage >&2
return 1
;;
esac

pcr_require_command gh || return 1
pcr_require_command ruby || return 1

if [ -z "$repo" ]; then
repo="$(gh repo view --json nameWithOwner -q .nameWithOwner)" || {
pcr_error "could not determine repo; pass --repo OWNER/REPO"
return 1
}
fi
case "$repo" in
*/*) ;;
*)
pcr_error "--repo must be in OWNER/REPO form (got: '$repo')"
return 1
;;
esac

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten PR/repo validation to match the CLI contract.

The current checks allow 0 as a PR number and accept malformed repo strings like owner/repo/extra. That conflicts with the documented “positive integer” and OWNER/REPO constraints.

Suggested patch
   case "$pr" in
-    ''|*[!0-9]*)
+    ''|*[!0-9]*|0)
       pcr_error "a positive integer PR number is required"
       pcr_usage >&2
       return 1
       ;;
   esac
@@
-  case "$repo" in
-    */*) ;;
-    *)
-      pcr_error "--repo must be in OWNER/REPO form (got: '$repo')"
-      return 1
-      ;;
-  esac
+  if [[ ! "$repo" =~ ^[^/[:space:]]+/[^/[:space:]]+$ ]]; then
+    pcr_error "--repo must be in OWNER/REPO form (got: '$repo')"
+    return 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 @.agents/skills/pr-batch/bin/pr-ci-readiness around lines 302 - 325, The PR
number validation allows zero as a valid value when it should only accept
positive integers (1 and above), and the repo validation pattern only checks for
the presence of at least one slash, allowing malformed strings like
owner/repo/extra instead of enforcing exactly OWNER/REPO format. Modify the case
statement for the PR variable to reject zero as a value in addition to
non-numeric input, and change the repo validation pattern to match the exact
OWNER/REPO format with exactly one slash and non-empty owner and repo components
rather than just checking for the presence of any slash.

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR extracts the repeated "is this PR's CI ready?" rule into a small standalone helper (pr-ci-readiness) and rewires three workflow/skill documents to call it instead of restating the algorithm in prose.

  • New binary (.agents/skills/pr-batch/bin/pr-ci-readiness): runs gh pr checks --required, falls back to the full check list when no required checks are configured, ignores cancel-bucket rows, and emits a structured JSON or text verdict (READY/NOT_READY/UNKNOWN). Ships with a 10-test suite using a fake-gh harness and passes shellcheck -x.
  • Documentation rewiring (SKILL.md, adversarial-pr-review.md, pr-processing.md): algorithm prose in all three files is replaced with a concise reference to the helper; the advisory gh pr checks call is retained as a comment for non-required reviewer visibility; surrounding scheduling and gating rules are left intact.

Confidence Score: 4/5

Safe to merge; the helper and documentation changes are conservative and well-tested for the common paths.

The core algorithm and all common code paths are correct and covered by tests. Two edge cases in the new helper are worth a follow-up: when gh pr checks --required returns a non-empty array consisting entirely of cancelled rows, pcr_has_checks treats it as has checks (no fallback), and pcr_verdict emits UNKNOWN but the output carries required_used: true, which is semantically misleading. Additionally, 2>/dev/null in pcr_fetch_checks discards gh error output, so an UNKNOWN verdict from a tool error looks identical to an UNKNOWN from an unconfigured repo. Neither issue causes incorrect merge decisions (UNKNOWN always blocks), but they reduce debuggability.

.agents/skills/pr-batch/bin/pr-ci-readiness — specifically the pcr_has_checks cancel-row interaction and the stderr suppression in pcr_fetch_checks.

Important Files Changed

Filename Overview
.agents/skills/pr-batch/bin/pr-ci-readiness New CLI helper encapsulating the required-vs-full CI readiness algorithm; logic is correct, two minor edge cases around cancel-row required_used semantics and silent gh stderr suppression are worth reviewing.
.agents/skills/pr-batch/bin/pr-ci-readiness-test.bash Well-structured unit test suite with fake-gh harness covering 10 scenarios; missing a test for the edge case where the required list contains only cancelled rows.
.agents/skills/pr-batch/SKILL.md Algorithm prose replaced with a concise reference to pr-ci-readiness; semantics preserved, surrounding gating rules unchanged.
.agents/workflows/adversarial-pr-review.md Initial-commands block and readiness description rewired to call the new helper; advisory gh pr checks comment retained; no behavioral loss.
.agents/workflows/pr-processing.md Both the Before-merge paragraph and the canonical CI Polling section updated to call pr-ci-readiness; #3844 context updated to reflect the helper's current fallback behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["pr-ci-readiness PR"] --> B["pcr_fetch_checks --required"]
    B --> C{"pcr_has_checks non-empty array?"}
    C -- "yes" --> E["checks_file = required_file, required_used = true"]
    C -- "no fallback" --> D["pcr_fetch_checks full list"]
    D --> F["checks_file = full_file, required_used = false"]
    E --> G["pcr_verdict"]
    F --> G
    G --> H["Filter out cancel-bucket rows"]
    H --> I{"effective rows empty?"}
    I -- yes --> J["verdict = UNKNOWN"]
    I -- no --> K{"fail/pending rows present?"}
    K -- yes --> L["verdict = NOT_READY"]
    K -- no --> M["verdict = READY"]
    J --> N["JSON or text output"]
    L --> N
    M --> N
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["pr-ci-readiness PR"] --> B["pcr_fetch_checks --required"]
    B --> C{"pcr_has_checks non-empty array?"}
    C -- "yes" --> E["checks_file = required_file, required_used = true"]
    C -- "no fallback" --> D["pcr_fetch_checks full list"]
    D --> F["checks_file = full_file, required_used = false"]
    E --> G["pcr_verdict"]
    F --> G
    G --> H["Filter out cancel-bucket rows"]
    H --> I{"effective rows empty?"}
    I -- yes --> J["verdict = UNKNOWN"]
    I -- no --> K{"fail/pending rows present?"}
    K -- yes --> L["verdict = NOT_READY"]
    K -- no --> M["verdict = READY"]
    J --> N["JSON or text output"]
    L --> N
    M --> N
Loading

Reviews (1): Last reviewed commit: "Add pr-ci-readiness helper to dedupe PR ..." | Re-trigger Greptile

Comment on lines +131 to +145
"pr" => pr.to_i,
"verdict" => verdict,
"required_used" => required_used == "true",
"failing" => failing,
"pending" => pending
}

if mode == "text"
puts verdict
puts "required_used: #{result['required_used']}"
puts "failing: #{failing.empty? ? '(none)' : failing.join(', ')}"
puts "pending: #{pending.empty? ? '(none)' : pending.join(', ')}"
else
puts JSON.pretty_generate(result)
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 pcr_has_checks does not filter cancel rows, causing misleading required_used: true

When gh pr checks --required returns a list that contains only cancelled rows (e.g. a superseded required CI run), pcr_has_checks evaluates the raw non-empty array and returns true, so the fallback to the full list is skipped. pcr_verdict then filters those rows out, ends up with an empty effective list, and emits UNKNOWN — but with required_used: true. That flag signals to callers that "required checks gated this verdict" when in fact zero non-cancelled required checks existed. The verdict itself is conservatively correct (UNKNOWN = not ready), but a consumer that keys off required_used: true to decide "required checks are configured and were checked" would draw the wrong conclusion. There is also no test covering this scenario in the suite.

Comment on lines +116 to +129
failing = rows.select { |row| row["bucket"].to_s == "fail" }
.map { |row| row["name"].to_s }
pending = rows.select { |row| row["bucket"].to_s == "pending" }
.map { |row| row["name"].to_s }

verdict =
if rows.empty?
"UNKNOWN"
elsif failing.empty? && pending.empty?
"READY"
else
"NOT_READY"
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 2>/dev/null silently discards gh diagnostic output

When gh fails due to an authentication error, API rate limit, or network issue it emits its error message to stderr, which is dropped here. The captured stdout file is then empty, pcr_has_checks returns false, and the fallback to the full list is triggered — which also fails silently — producing an UNKNOWN verdict. The caller cannot distinguish a genuine "no CI configured" UNKNOWN from a tooling-failure UNKNOWN, making silent auth failures particularly hard to debug in CI pipelines or agent sessions.

The "is this PR's CI ready?" rule was restated in prose across pr-batch,
adversarial-pr-review, and pr-processing: run `gh pr checks <PR> --required`,
fall back to the full `gh pr checks <PR>` list when no required checks exist,
ignore cancelled/superseded rows, and treat an empty list as UNKNOWN/not-ready.

Encapsulate that rule in `.agents/skills/pr-batch/bin/pr-ci-readiness`, written
as pure Ruby (stdlib json + open3, no bundler/gems) mirroring the
fetch-pr-review-data template: a PrCiReadiness module of pure module_function
verdict helpers plus a Runner class for the CLI. Subprocesses use Open3 array
args (no shell); `gh pr checks` stdout is captured despite its non-zero exit on
failing/pending checks, with stderr discarded, and the rows are inspected to
print a verdict of READY, NOT_READY, or UNKNOWN with failing/pending names and a
required_used flag. Flags: --json (default)/--text/--self-check/--help.

This replaces the original bash implementation. The helpers already hard-depended
on Ruby (the bash versions shelled out to `ruby -rjson`), so going pure Ruby
removes the bash<->Ruby seam at zero dependency cost. Output JSON shape, executable
path/name, and CLI flags are unchanged, so the SKILL.md / workflow references stay
valid and need no edits.

The companion test is now minitest (pr-ci-readiness-test.rb, replacing the
-test.bash): pure verdict logic is tested directly with canned check arrays, and
the CLI/Runner path is exercised through a fake `gh` on PATH. Verified output is
byte-for-byte identical to the previous bash version against merged PR #4058 and
in-progress PR #4063. rubocop and minitest both pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@justin808 justin808 force-pushed the jg/pr-ci-readiness-script branch from 065ebbb to 03c1d8c Compare June 17, 2026 03:26

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03c1d8c3b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

cmd += ["--json", "name,state,bucket,link"]
# gh writes "no required checks..." to stderr when it exits non-zero; we
# inspect stdout only, so discard stderr to keep our output clean.
out, _status = Open3.capture2(*cmd, err: File::NULL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat required-check lookup errors as UNKNOWN

When gh pr checks --required fails before emitting JSON, this discards both the exit status and stderr, so the caller falls back to the full check list and can report READY with required_used: false. That is only safe for the known "no required checks" case; for an older gh without the documented --required flag (the GitHub CLI manual describes it as “Only show checks that are required”) or a branch-protection/API permission error, the helper no longer verifies required checks even though the workflow text says unavailable/API-error live state must be UNKNOWN. Capture status/stderr here and only fall back on the explicit no-required-checks condition.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (1)
.agents/skills/pr-batch/bin/pr-ci-readiness-test.rb (1)

121-139: ⚡ Quick win

Cover non-zero gh pr checks exits in the fake harness.

Line 132 and Line 136 always return success, so the CLI tests never validate the runner’s key contract of ignoring gh pr checks exit status. Add a scenario where gh pr checks returns non-zero with JSON stdout (and optional stderr) and assert verdict parsing still works.

Proposed patch sketch
-  def with_fake_gh(required_json:, full_json:)
+  def with_fake_gh(required_json:, full_json:, required_status: 0, full_status: 0, required_stderr: "", full_stderr: "")
     Dir.mktmpdir("pr-ci-readiness-test") do |dir|
       gh = File.join(dir, "gh")
-      File.write(gh, fake_gh_script(required_json, full_json))
+      File.write(
+        gh,
+        fake_gh_script(
+          required_json, full_json,
+          required_status: required_status, full_status: full_status,
+          required_stderr: required_stderr, full_stderr: full_stderr
+        )
+      )
       FileUtils.chmod(0o755, gh)
       env = { "PATH" => "#{dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH')}" }
       yield env
     end
   end

-  def fake_gh_script(required_json, full_json)
+  def fake_gh_script(required_json, full_json, required_status:, full_status:, required_stderr:, full_stderr:)
     <<~SH
       #!/usr/bin/env bash
       if [ "$1" = "repo" ] && [ "$2" = "view" ]; then
         printf 'owner/repo'
         exit 0
       fi
       if [ "$1" = "pr" ] && [ "$2" = "checks" ]; then
         for arg in "$@"; do
           if [ "$arg" = "--required" ]; then
             printf '%s' #{required_json.inspect}
-            exit 0
+            printf '%s' #{required_stderr.inspect} >&2
+            exit #{required_status}
           fi
         done
         printf '%s' #{full_json.inspect}
-        exit 0
+        printf '%s' #{full_stderr.inspect} >&2
+        exit #{full_status}
       fi
       exit 1
     SH
   end
def test_nonzero_gh_exit_still_parses_stdout
  with_fake_gh(
    required_json: "",
    required_status: 1,
    required_stderr: "no required checks",
    full_json: '[{"name":"lint","bucket":"fail"}]',
    full_status: 1
  ) do |env|
    out, status = run_script(env, "123", "--repo", "owner/repo")
    assert status.success?, out
    assert_equal "NOT_READY", JSON.parse(out)["verdict"]
  end
end
🤖 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 @.agents/skills/pr-batch/bin/pr-ci-readiness-test.rb around lines 121 - 139,
The fake_gh_script method currently always returns success (exit 0) for gh pr
checks operations, which prevents tests from validating that the runner
correctly ignores the exit status while parsing JSON stdout. Modify the
fake_gh_script method signature to accept additional parameters for exit status
codes and stderr output (required_status, required_stderr, and full_status),
then update the bash script logic to conditionally output stderr messages and
exit with the specified non-zero status codes while still printing the JSON
output to stdout, allowing tests to verify the runner handles non-zero exits
from gh pr checks correctly.
🤖 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 @.agents/skills/pr-batch/bin/pr-ci-readiness:
- Around line 200-209: The assess method incorrectly treats a required response
containing only cancelled or superseded rows as valid, preventing a fallback to
the full check list. Modify the logic to verify that parse_rows actually returns
active rows from the required response before using it. If the parsed required
rows are empty or contain no active checks after processing required_raw, fall
back to fetching and parsing the full check list (required: false) instead of
relying solely on the checks? method condition.
- Around line 215-231: Both the fetch_checks and capture_required! methods call
Open3.capture2 which can raise SystemCallError exceptions (such as Errno::ENOENT
when the gh executable is missing) before returning a status, and these
unhandled exceptions currently escape as Ruby stack traces. Add rescue blocks to
both fetch_checks and capture_required! methods that catch SystemCallError
exceptions and re-raise them as PrCiReadiness::Error to ensure consistent error
handling and graceful error messages.

---

Nitpick comments:
In @.agents/skills/pr-batch/bin/pr-ci-readiness-test.rb:
- Around line 121-139: The fake_gh_script method currently always returns
success (exit 0) for gh pr checks operations, which prevents tests from
validating that the runner correctly ignores the exit status while parsing JSON
stdout. Modify the fake_gh_script method signature to accept additional
parameters for exit status codes and stderr output (required_status,
required_stderr, and full_status), then update the bash script logic to
conditionally output stderr messages and exit with the specified non-zero status
codes while still printing the JSON output to stdout, allowing tests to verify
the runner handles non-zero exits from gh pr checks correctly.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29dcb5d8-2dca-4487-a409-1ea0c8da366b

📥 Commits

Reviewing files that changed from the base of the PR and between 065ebbb and 03c1d8c.

📒 Files selected for processing (5)
  • .agents/skills/pr-batch/SKILL.md
  • .agents/skills/pr-batch/bin/pr-ci-readiness
  • .agents/skills/pr-batch/bin/pr-ci-readiness-test.rb
  • .agents/workflows/adversarial-pr-review.md
  • .agents/workflows/pr-processing.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • .agents/workflows/adversarial-pr-review.md
  • .agents/workflows/pr-processing.md

Comment on lines +200 to +209
def assess(repo, pr_number)
required_raw = fetch_checks(repo, pr_number, required: true)
if PrCiReadiness.checks?(required_raw)
rows = PrCiReadiness.parse_rows(required_raw)
required_used = true
else
rows = PrCiReadiness.parse_rows(fetch_checks(repo, pr_number, required: false))
required_used = false
end
PrCiReadiness.assess(pr_number:, required_used:, rows:)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fall back when the required list has no active rows.

Line 202 treats a required response containing only cancelled/superseded rows as “checks exist,” so the runner never inspects the full list. Since cancelled rows are ignored, this can incorrectly return UNKNOWN even when the full check list has active rows.

Suggested patch
     def assess(repo, pr_number)
       required_raw = fetch_checks(repo, pr_number, required: true)
-      if PrCiReadiness.checks?(required_raw)
-        rows = PrCiReadiness.parse_rows(required_raw)
+      required_rows = PrCiReadiness.parse_rows(required_raw)
+      if PrCiReadiness.active_rows(required_rows).any?
+        rows = required_rows
         required_used = true
       else
         rows = PrCiReadiness.parse_rows(fetch_checks(repo, pr_number, required: false))
         required_used = false
       end
🤖 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 @.agents/skills/pr-batch/bin/pr-ci-readiness around lines 200 - 209, The
assess method incorrectly treats a required response containing only cancelled
or superseded rows as valid, preventing a fallback to the full check list.
Modify the logic to verify that parse_rows actually returns active rows from the
required response before using it. If the parsed required rows are empty or
contain no active checks after processing required_raw, fall back to fetching
and parsing the full check list (required: false) instead of relying solely on
the checks? method condition.

Comment on lines +215 to +231
def fetch_checks(repo, pr_number, required:)
cmd = ["gh", "pr", "checks", pr_number.to_s, "--repo", repo]
cmd << "--required" if required
cmd += ["--json", "name,state,bucket,link"]
# gh writes "no required checks..." to stderr when it exits non-zero; we
# inspect stdout only, so discard stderr to keep our output clean.
out, _status = Open3.capture2(*cmd, err: File::NULL)
out.to_s.force_encoding("UTF-8")
end

# Run a command with no shell (array args) and return UTF-8 stdout, raising
# when the command fails. Used only for commands whose exit status matters.
def capture_required!(*cmd)
out, status = Open3.capture2(*cmd)
raise Error, "command failed: #{cmd.first(3).join(' ')}" unless status.success?

out.force_encoding("UTF-8")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the stdlib behavior without executing repository code.
# Expectation: This prints an exception class such as Errno::ENOENT, confirming capture2 can raise.

ruby -ropen3 -e 'begin; Open3.capture2("definitely-not-a-real-command-coderabbit"); rescue => e; puts e.class; puts e.message; end'

Repository: shakacode/react_on_rails

Length of output: 151


🏁 Script executed:

# Verify file exists and check the context around these methods
if [ -f ".agents/skills/pr-batch/bin/pr-ci-readiness" ]; then
  echo "File found. Checking lines 215-231 and surrounding context..."
  wc -l ".agents/skills/pr-batch/bin/pr-ci-readiness"
  sed -n '200,250p' ".agents/skills/pr-batch/bin/pr-ci-readiness" | cat -n
else
  echo "File not found at expected location"
  fd -type f -name "pr-ci-readiness" 2>/dev/null || echo "No matching files found"
fi

Repository: shakacode/react_on_rails

Length of output: 2432


🏁 Script executed:

# Search for the 'run' method to verify the error handling context
rg -A 10 "def run" ".agents/skills/pr-batch/bin/pr-ci-readiness" 2>/dev/null | head -30

Repository: shakacode/react_on_rails

Length of output: 370


🏁 Script executed:

# Search for rescue blocks and Error class in the file to understand error handling pattern
rg "rescue|class.*Error" ".agents/skills/pr-batch/bin/pr-ci-readiness" 2>/dev/null | head -20

Repository: shakacode/react_on_rails

Length of output: 152


🏁 Script executed:

# Check the main script entry point and overall error handling structure
rg -B 5 -A 15 "^begin|^rescue|def run" ".agents/skills/pr-batch/bin/pr-ci-readiness" | head -60

Repository: shakacode/react_on_rails

Length of output: 605


🏁 Script executed:

# Look for any try-catch patterns around the method calls to see if there's broader error handling
sed -n '1,50p' ".agents/skills/pr-batch/bin/pr-ci-readiness" | cat -n

Repository: shakacode/react_on_rails

Length of output: 2202


Add exception handling to Open3.capture2 calls for missing executable or spawn failures.

Open3.capture2 raises SystemCallError (e.g., Errno::ENOENT when gh is missing) before returning a status. Currently, both fetch_checks and capture_required! lack rescue blocks, so subprocess spawn failures escape the rescue Error path in run() and print a Ruby stack trace instead of a graceful error message.

Wrap both methods with rescue SystemCallError => e and re-raise as PrCiReadiness::Error.

Suggested patch
     def fetch_checks(repo, pr_number, required:)
       cmd = ["gh", "pr", "checks", pr_number.to_s, "--repo", repo]
       cmd << "--required" if required
       cmd += ["--json", "name,state,bucket,link"]
       # gh writes "no required checks..." to stderr when it exits non-zero; we
       # inspect stdout only, so discard stderr to keep our output clean.
       out, _status = Open3.capture2(*cmd, err: File::NULL)
       out.to_s.force_encoding("UTF-8")
+    rescue SystemCallError => e
+      raise Error, "failed to run gh pr checks: #{e.message}"
     end
 
     # Run a command with no shell (array args) and return UTF-8 stdout, raising
     # when the command fails. Used only for commands whose exit status matters.
     def capture_required!(*cmd)
       out, status = Open3.capture2(*cmd)
       raise Error, "command failed: #{cmd.first(3).join(' ')}" unless status.success?
 
       out.force_encoding("UTF-8")
+    rescue SystemCallError => e
+      raise Error, "failed to run #{cmd.first}: #{e.message}"
     end
🤖 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 @.agents/skills/pr-batch/bin/pr-ci-readiness around lines 215 - 231, Both the
fetch_checks and capture_required! methods call Open3.capture2 which can raise
SystemCallError exceptions (such as Errno::ENOENT when the gh executable is
missing) before returning a status, and these unhandled exceptions currently
escape as Ruby stack traces. Add rescue blocks to both fetch_checks and
capture_required! methods that catch SystemCallError exceptions and re-raise
them as PrCiReadiness::Error to ensure consistent error handling and graceful
error messages.

@justin808 justin808 merged commit 02d9bcb into main Jun 17, 2026
48 checks passed
@justin808 justin808 deleted the jg/pr-ci-readiness-script branch June 17, 2026 03:40
justin808 added a commit that referenced this pull request Jun 19, 2026
## Summary

Implements the curated, real, low-risk subset of deferred AI-reviewer
feedback on the internal PR-helper Ruby scripts from #4064-#4067. Each
change closes a genuine correctness or input-validation gap; cosmetic
and stale nits are intentionally skipped (see below).

## Changes (why each one)

1. **Reject PR number "0"** in `fetch-pr-review-data`,
`pr-file-touch-map`, and `pr-ci-readiness`. `validate_pr!` used
`/\A\d+\z/`, which accepts `0` — not a valid PR number. Now
`/\A[1-9]\d*\z/`, so it fails fast with the existing "positive integer
PR number is required" error instead of issuing a doomed `gh` call.
(`changelog-merged-prs` has no PR-number arg — it validates a
`BASE..TARGET` range — so it is unaffected.)

2. **Tighten `--repo` validation to strict `OWNER/REPO`** in all four
helpers. The old `repo.include?("/")` check accepted malformed values
like `a/b/c` and `/repo` (empty owner). All four now use
`%r{\A[^/\s]+/[^/\s]+\z}` (non-empty owner and name, exactly one slash,
no whitespace), keeping the same error message.

3. **`pr-file-touch-map`: make the reachable-SHA (`headRefOid`) fetch
independent of head-branch validity** (per #4065 review).
`fetch_head_from_repo` previously did `return false unless
valid_branch?(meta[:head_ref])`, so an invalid or missing head branch
silently gated the SHA fallback — even though the 40-hex OID is the only
untrusted input that path consumes. The branch fetch is now gated on
branch validity, but the OID fetch runs regardless. PR head code is
still never checked out.

4. **`pr-ci-readiness`: a cancel-only required-check result now falls
back to the full check list** (per #4067 review). When `gh pr checks
--required` returned only `cancel` rows, the old `checks?` saw a
non-empty array, skipped the full-list fallback, and then `active_rows`
dropped every row → spurious `UNKNOWN`. `checks?` is replaced by
`usable_checks?`, which counts only active (non-cancel) rows, so
cancel-only (and empty) required results correctly fall back to the full
list.

## Tests

Added focused unit tests for every change (zero-PR rejection,
extra-path-segment and empty-owner repo rejection,
OID-fetch-runs-despite-invalid-branch via a hermetic fake `git`, and
cancel-only required → full-list fallback) and extended the
`pr-ci-readiness` `--self-check`. Updated the one existing test that
asserted the old cancel-only-`UNKNOWN` behavior. Also updated
`pr-batch/SKILL.md` wording for the cancel-only fallback.

Validation (all run locally, all pass):
- `ruby <each>-test.rb` — 13 / 21 / 26 / 14 runs, 0 failures, 0 errors
- `<each> --self-check` — all pass
- `bundle exec rubocop <changed files>` — no offenses
- `npx prettier --check .agents/skills/pr-batch/SKILL.md` — clean

## Intentionally skipped (cosmetic / stale, so #4069 can close without
them)

stderr capture; `status == "copied"` (already done in #4074); re-reading
`changedFiles`; perf caching; the `--` arg-terminator no-op; and all
#4074-comment nits. These were reviewed and judged out of scope per the
issue.

## Codex Decision Log

- **Combined PR vs per-helper PRs:** chose one combined PR. The four
changes are tiny, share two near-identical edits (items 1 and 2 across
the helpers), and reviewing them together makes the shared `OWNER/REPO`
regex and PR-number tightening obvious. Per-helper PRs would add
overhead without improving reviewability.
- **`--no-verify` on commit:** the pre-commit `prettier` hook fails in
this worktree with `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL / Command
"prettier" not found` (pnpm deps not installed here). Verified the only
Markdown change passes `npx prettier --check` manually, and
rubocop/trailing-newline/markdown-link hooks all pass, so the bypass is
environmental, not a content issue.

Fixes #4069

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Changes are limited to read-only agent helper scripts and their tests;
behavior is stricter validation and corrected fallback logic with no
production runtime impact.
> 
> **Overview**
> Hardens **input validation** across internal PR-helper Ruby scripts:
PR numbers must be **positive integers** (rejecting `0`), and `--repo`
must match strict **`OWNER/REPO`** (exactly one slash, non-empty
owner/name).
> 
> **`pr-ci-readiness`** now treats a required-check list with **only
cancelled rows** as unusable and **falls back to the full** `gh pr
checks` list, replacing `checks?` with `usable_checks?` so merge
readiness does not spuriously report `UNKNOWN`.
> 
> **`pr-file-touch-map`** still fetches PR head by **reachable SHA**
when the head branch name is invalid, instead of skipping the OID path
entirely.
> 
> **`pr-batch/SKILL.md`** documents the cancel-only required-check
fallback. Focused unit/CLI tests cover the new validation and behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
7190952. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed CI-readiness logic to correctly fall back to full check list
when required checks contain only cancelled items, instead of
incorrectly reporting unknown status.
* Improved input validation for pull request numbers and repository
format to prevent invalid arguments.

* **Documentation**
* Updated skill documentation to clarify CI-readiness behavior for
cancelled and superseded checks.

* **Tests**
* Added comprehensive test coverage for input validation and
CI-readiness fallback scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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