Refuse releases on red main and surface main CI status#3407
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a release-time gate that validates origin/main GitHub check runs (with override/dry-run support) and session-time Bash hooks that display cached origin/main CI status before PR creation or pushes; includes Rake wiring, RSpec coverage, docs, and a 5-minute fail-open cache. ChangesCI Status Guard: Release Gate and Monitoring
Sequence Diagram(s) sequenceDiagram
participant ClaudeSession as Claude Session
participant Wrapper as .claude/hooks/main-ci-status-on-push.sh
participant Hook as .claude/hooks/main-ci-status.sh
participant CacheFile as .claude/.main-ci-status.cache
participant ghCLI as gh CLI
participant ChecksAPI as GitHub Checks API
ClaudeSession->>Wrapper: PreToolUse JSON (stdin)
Wrapper->>Wrapper: extract .tool_input.command (jq)
Wrapper->>Wrapper: match gh pr create / git push origin main/HEAD
alt match
Wrapper->>Hook: exec main-ci-status.sh
Hook->>CacheFile: check TTL (stat)
alt cache hit
CacheFile-->>Hook: cached output
Hook-->>ClaudeSession: print cached status
else cache miss
Hook->>ghCLI: resolve repo / git ls-remote origin main
ghCLI-->>Hook: repo slug / head_sha
Hook->>ChecksAPI: gh api --paginate /repos/:owner/:repo/commits/:sha/check-runs
ChecksAPI-->>Hook: paged JSONL check-runs
Hook->>Hook: dedupe & aggregate with jq
Hook->>CacheFile: write atomically (tmp + mv)
Hook-->>ClaudeSession: print status
end
else no match
Wrapper-->>ClaudeSession: exit silently (0)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 666f5d32fe
ℹ️ 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".
Greptile SummaryThis PR adds two guardrails against releasing over a broken
Confidence Score: 4/5Safe to merge; the release gate and session hooks work correctly for the common case and all error paths are fail-open or have a documented override. The core logic is well-tested and the fail-open design prevents hooks from breaking sessions. The two issues in release.rake — the deprecated contexts field potentially returning [] instead of nil for newer branch-protection setups, and the check-runs endpoint returning stale historical runs after a re-run — could cause false-positive blocks, but both have a clear documented escape hatch via RELEASE_CI_STATUS_OVERRIDE=true. The shell hook substring match on origin main is harmless extra output. None of these affect correctness of the shipped release artifacts. rakelib/release.rake — the branch-protection query and check-run deduplication logic deserve a second look before the next release cycle. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[rake release called] --> B[resolve version]
B --> C{stable branch check}
C -- fail --> ABORT1[abort: must run from main]
C -- pass --> D[validate_main_ci_status!]
D --> E[git fetch origin main]
E --> F[gh api check-runs for HEAD SHA]
F --> G{check_runs empty?}
G -- yes --> H{override or dry-run?}
G -- no --> I{is_prerelease?}
I -- yes --> J[required_check_names_for_main]
J -- nil fallback --> K[evaluate ALL checks]
J -- names list --> L[filter to required checks only]
I -- no --> K
K --> M{any in_progress?}
L --> M
M -- yes --> H
M -- no --> N{any failed?}
N -- yes --> H
N -- no --> SUCCESS[CI healthy - continue release]
H -- dry-run --> WARN[DRY RUN warning, continue]
H -- override=true --> WARN2[OVERRIDE warning, continue]
H -- no override --> ABORT2[abort with failing check URLs]
|
Code ReviewThis PR adds two well-motivated guardrails against releasing over a broken What works well
Issues Medium: in_progress checked before failures may mislead the operator — see inline comment on rakelib/release.rake. When origin/main has both already-failed and still-in-progress checks, the current ordering reports only "CI is in progress — wait or override." The operator waits, re-runs rake release, and only then sees the failures. Checking failed first (or reporting both together) gives the full picture in one shot. Minor: redundant --force case in main-ci-status-on-push.sh — see inline comment. The fourth case pattern is fully covered by the second pattern. It also widens the false-positive surface by omitting origin. Dropping it tightens the match without losing intended coverage. Minor nits
Summary Well-structured and ready to merge with small adjustments. The in_progress/failed ordering is the only issue worth fixing before merge; the rest are low-stakes polish. The fail-open design means the worst-case outcome of any future hook bug is a missing status line, not a blocked session. |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@docs/superpowers/specs/2026-05-25-main-ci-release-guard-design.md`:
- Around line 54-56: The fenced code blocks around the release task and examples
lack language identifiers; update the blocks shown (the one containing "task
:release, %i[version dry_run override_version_policy override_ci_status]" -> add
ruby, the block with "RELEASE_CI_STATUS_OVERRIDE=true # bypass the CI status
check" -> add bash, and the example output blocks containing "❌ CI on main is
not healthy — refusing to release." and "Main CI status (3103496d, pushed 7h
ago):" -> add text) so each fenced block has an appropriate language tag (e.g.,
```ruby, ```bash, ```text) for lint MD040 and correct rendering.
In `@rakelib/release.rake`:
- Around line 386-389: Update the user-facing release command examples to use
bundle exec by replacing references to "rake" with "bundle exec rake" in the
override instructions; specifically change the examples that read
'RELEASE_CI_STATUS_OVERRIDE=true rake release[...]' and 'rake
"release[VERSION,false,false,true]"' so they become
'RELEASE_CI_STATUS_OVERRIDE=true bundle exec rake ...' and 'bundle exec rake
"release[VERSION,false,false,true]"' to ensure consistency with the repo's Ruby
workflow.
- Around line 318-326: The gh API invocations in fetch_main_ci_checks /
required_check_names_for_main currently call Open3.capture2e directly and can
raise Errno::ENOENT when the gh CLI is missing; replace those Open3.capture2e
calls that run "gh api --paginate --jq '.check_runs[]' #{api_path}" with the
existing helper capture_gh_output so the friendly abort path is used (keep the
current parsing logic that maps lines to JSON and the rescue that aborts on
JSON::ParserError; do not change to --slurp/page flattening). Ensure you call
capture_gh_output with the same gh arguments and preserve the subsequent
status/output handling and abort messages.
🪄 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: bad39209-4302-4f7a-b7f6-43fe37ceebe5
📒 Files selected for processing (9)
.claude/hooks/main-ci-status-on-push.sh.claude/hooks/main-ci-status.sh.claude/settings.json.gitignoreAGENTS.mdCLAUDE.mddocs/superpowers/specs/2026-05-25-main-ci-release-guard-design.mdrakelib/release.rakereact_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb
Applies must-fix correctness improvements plus optional polish surfaced
by cursor, codex, greptile, claude, and coderabbit bot reviews. None of
these change the high-level shape of the release gate or session hooks
documented in the design spec; they harden the edges.
Release gate (rakelib/release.rake):
- Combine .contexts + .checks[].context in the branch-protection query so
newer `checks`-based protection rules don't return an empty array and
trip the :no_required_checks path (cursor, greptile).
- Treat an empty result from that query as nil so the caller falls back
to "evaluate every check" rather than aborting (fail-safe).
- Dedupe check_runs by name, keeping the highest id (= latest attempt),
so an old failed run that was re-run and passed no longer blocks the
release (codex P1, greptile).
- Plumb dry_run/allow_override into fetch_main_ci_checks so dry-run on
a machine without gh/network warns instead of aborting (codex P2).
- Use capture_gh_output for all `gh api` calls so a missing gh binary
surfaces the friendly install hint instead of crashing with
Errno::ENOENT (coderabbit).
- Check failed runs before in-progress runs. If both are present, the
operator now sees the real blocker immediately instead of waiting for
the in-progress run to finish before discovering the failure (claude).
- Fix grammar of the success message ("(2 checks)" / "(1 required
check)") and switch override instructions to `bundle exec rake`
(claude, coderabbit).
Session hooks (.claude/hooks/):
- main-ci-status.sh: resolve origin/main HEAD via `git ls-remote origin
main` instead of the latest push-workflow run's headSha so the SHA
matches the release gate and doesn't lag right after a push or for
paths-ignored pushes (cursor).
- main-ci-status.sh: when no check runs are visible for the commit,
print "no check runs visible yet" instead of `Total: 0 | ... | 0`,
which an agent could misread as "all green" (cursor).
- main-ci-status.sh: write the cache via mktemp + mv so a concurrent
reader never sees a half-written file (claude).
- main-ci-status-on-push.sh: replace substring case-globs with anchored
bash regexes so `git push origin main-feature` / `maintenance` /
`main-ci-fix` no longer match. Drop the redundant `--force` case
(greptile P2, claude).
Tests:
- Add specs for the dedupe path, the failed-before-in-progress order,
the dry-run fetch-failure path, the empty-array branch-protection
fallback, and the missing-gh Errno::ENOENT path.
- Update existing assertions for the new success-message grammar.
Docs:
- docs/superpowers/specs/2026-05-25-...: mark Implemented, add language
identifiers to bare fenced code blocks (MD040), update override
instructions to use `bundle exec rake` (coderabbit).
|
Thanks for the review — addressed the inline items plus the three polish nits from your review summary in commit ccb3a87. From the summary body:
And the two inline must-fixes from your review: the Full per-comment replies are on the individual threads. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ccb3a87852
ℹ️ 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".
Code ReviewOverall: Well-designed and carefully implemented. Fail-open throughout, good test coverage, clear operator UX. A few minor issues worth addressing before merge. What's Good
IssuesMinor:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@rakelib/release.rake`:
- Around line 356-361: The current jq_query flattens required checks to bare
context strings losing app identity; change the query so it returns objects of
the form {context, app_id} (and convert legacy `contexts` strings to {context:
<string>, app_id: null}) when calling capture_gh_output, and then update the
later dedupe/matching (the code that currently collapses by run["name"] around
the dedupe block) to key and compare by both context and app_id (e.g. a
composite key like "#{context}|#{app_id}" or equivalent) so required checks are
matched and deduped only when both context and app_id match (preserving legacy
behavior when app_id is null).
🪄 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: ba505e48-4a0d-4f91-a6bf-87d2115f1c0f
📒 Files selected for processing (5)
.claude/hooks/main-ci-status-on-push.sh.claude/hooks/main-ci-status.shdocs/superpowers/specs/2026-05-25-main-ci-release-guard-design.mdrakelib/release.rakereact_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb
✅ Files skipped from review due to trivial changes (1)
- docs/superpowers/specs/2026-05-25-main-ci-release-guard-design.md
Hook (.claude/hooks/main-ci-status.sh): - write_cache_atomic prints from $1 instead of re-reading via cat (claude[bot], 2 inline duplicates + summary) - jq slurp now dedupes reruns by max_by(id), mirroring the Ruby gate in release.rake — keeps session-time and release-time CI views in sync when a check has been re-run (cursor bugbot) - Defensive ":=0" defaults for failed_count/in_progress_count guard the partial-output edge case (claude[bot]) rakelib/release.rake: - validate_main_ci_status! now flags partial-missing required checks with a new :missing_required_checks violation kind. Branch protection would refuse this merge state, so a release shouldn't ship through it. The all-missing case keeps :no_required_checks for clarity (chatgpt-codex-connector P1) - Comment in run_release_preflight_checks! explaining why the main-CI status check is wired separately in with_release_checkout (claude[bot] summary) react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb: - passing_run/failing_run/in_progress_run helpers now include "id" defaults to match real GitHub API responses (claude[bot]) - New test: aborts and lists missing required check names when some required checks are absent on a prerelease (chatgpt-codex-connector P1) - New test: warns instead of aborting when git fetch fails with allow_override, matching the dry-run symmetric case (claude[bot] summary) 63 examples, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-2 review feedback — addressed (commit
|
| Source | Where | What changed |
|---|---|---|
@chatgpt-codex-connector P1 |
rakelib/release.rake |
validate_main_ci_status! now flags partial-missing required checks (:missing_required_checks) — branch protection would refuse the merge, so a release shouldn't ship through it. The all-missing case keeps :no_required_checks for clarity. New regression test included. |
@cursor (Bugbot) |
.claude/hooks/main-ci-status.sh |
jq slurp now dedupes reruns by max_by(.id), mirroring the Ruby gate at release.rake:451-453. Session-time and release-time CI views now stay in sync when a check has been re-run. |
@claude (×2 inline) |
.claude/hooks/main-ci-status.sh:50 |
write_cache_atomic prints from $1 instead of re-reading via cat — closes a TOCTOU window that produced a misleading fail_open "cache write failed". |
@claude |
.claude/hooks/main-ci-status.sh |
Defensive : "${failed_count:=0}" "${in_progress_count:=0}" guard before the -gt 0 comparisons. |
@claude |
release_rake_helpers_spec.rb |
passing_run / failing_run / in_progress_run helpers now include id: defaults to match real GitHub API responses. |
From @claude's review summary
- ✅ Missing test —
allow_override: trueongit fetchfailure. Added a symmetric test to the existing dry-run+git-fetch-failure test. - ✅ Design spec ↔ implementation divergence note. Added a brief comment in
run_release_preflight_checks!explaining why the main-CI status check is wired separately insidewith_release_checkout(so it still fires underdry_run: true). The spec itself is already labeled stale at merge time.
Acknowledged (no separate action)
@chatgpt-codex-connectorreview summary: boilerplate-only review wrapper — no actionable body. Inline P1 + P2 addressed/deferred as noted.@coderabbitaireview summary: "Actionable comments posted: 1" pointing at the inlineapp_idfinding tracked under the deferred item below.
Deferred (pending review decision) — app_id matching for required checks
The Codex P2 / CodeRabbit duplicate finding is technically correct: required_status_checks.checks[].app_id lets branch protection pin a required check to a specific GitHub App, and the current code flattens that information away.
Why deferred rather than fixed in this PR:
- Practical risk in this repo is essentially zero. Required CI checks come from
github-actions[bot]. Other bots in this repo (CodeRabbit, Claude, Codex, Cursor) emit reviews/checks with distinct names (CodeRabbit,Codex review, etc.). For a collision to fire the gap, an unrelated app would have to emit a check whose name matches a required CI workflow. - Fix is non-trivial. It touches the branch-protection jq query (return
[{context, app_id}]instead of[string]), the Checks API projection infetch_main_ci_checks(carryapp.id), the dedup key (compose[name, app_id]), and the matching predicate (withapp_id == nilandapp_id == -1wildcards). Plus new tests for each branch.
Happy to fold it in here if you'd rather, or open a follow-up issue (Harden release gate against app_id collisions on required-check names) — let me know which you prefer and I'll close out the two threads with the chosen disposition.
Posted by I (Claude Code).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d620c631ff
ℹ️ 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".
|
On the deferred
If you'd like, I can open the issue now titled "Harden release gate against With that settled, this PR looks good to me — all the substantive concerns from both rounds are resolved. 🐇 ✨ ✅ Actions performedComments resolved and changes approved. |
`rake release` now checks origin/main HEAD's CI status before any
destructive operation:
- Stable releases require every check run on the commit to have
conclusion in {success, skipped, neutral}.
- Pre-releases require only GitHub-branch-protection-required checks
to pass; non-required checks are advisory.
- In-progress checks block (no polling).
- Override via env var RELEASE_CI_STATUS_OVERRIDE=true or the new 4th
positional argument, mirroring the existing version-policy override.
Failure messages name the failing checks and link to them, and explain
exactly how to override.
The check falls back safely:
- If branch protection isn't queryable, treat all checks as required.
- If gh fails (auth/network), abort with the underlying error rather
than silently overriding.
Tests cover all-pass, single failure (stable/prerelease),
required vs non-required checks, in-progress, zero checks visible,
override env/arg, branch protection fallback, and gh failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two hook scripts under .claude/hooks/: - main-ci-status.sh: fetches the latest origin/main push commit's check runs via `gh api` and prints a compact status block. 5-minute cache in .claude/.main-ci-status.cache. Fail-open: any tooling failure (no gh, no auth, no network) prints a one-line "unavailable" message and exits 0. - main-ci-status-on-push.sh: PreToolUse:Bash wrapper that runs the status script only when the next Bash command matches `gh pr create` or `git push origin main|HEAD`. Silently exits for any other Bash call so we don't spam the agent context. Wired into .claude/settings.json on SessionStart (always) and PreToolUse:Bash (filtered by the wrapper). AGENTS.md gets a new "Main branch health" section describing the release gate, the hook signal, the decision framework when main is red, and the rule that the release CI override must be documented in the PR / release notes. CLAUDE.md gets one line under "Behavioral Defaults" pointing at the new section. .claude/.main-ci-status.cache is gitignored (per-machine state). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies must-fix correctness improvements plus optional polish surfaced
by cursor, codex, greptile, claude, and coderabbit bot reviews. None of
these change the high-level shape of the release gate or session hooks
documented in the design spec; they harden the edges.
Release gate (rakelib/release.rake):
- Combine .contexts + .checks[].context in the branch-protection query so
newer `checks`-based protection rules don't return an empty array and
trip the :no_required_checks path (cursor, greptile).
- Treat an empty result from that query as nil so the caller falls back
to "evaluate every check" rather than aborting (fail-safe).
- Dedupe check_runs by name, keeping the highest id (= latest attempt),
so an old failed run that was re-run and passed no longer blocks the
release (codex P1, greptile).
- Plumb dry_run/allow_override into fetch_main_ci_checks so dry-run on
a machine without gh/network warns instead of aborting (codex P2).
- Use capture_gh_output for all `gh api` calls so a missing gh binary
surfaces the friendly install hint instead of crashing with
Errno::ENOENT (coderabbit).
- Check failed runs before in-progress runs. If both are present, the
operator now sees the real blocker immediately instead of waiting for
the in-progress run to finish before discovering the failure (claude).
- Fix grammar of the success message ("(2 checks)" / "(1 required
check)") and switch override instructions to `bundle exec rake`
(claude, coderabbit).
Session hooks (.claude/hooks/):
- main-ci-status.sh: resolve origin/main HEAD via `git ls-remote origin
main` instead of the latest push-workflow run's headSha so the SHA
matches the release gate and doesn't lag right after a push or for
paths-ignored pushes (cursor).
- main-ci-status.sh: when no check runs are visible for the commit,
print "no check runs visible yet" instead of `Total: 0 | ... | 0`,
which an agent could misread as "all green" (cursor).
- main-ci-status.sh: write the cache via mktemp + mv so a concurrent
reader never sees a half-written file (claude).
- main-ci-status-on-push.sh: replace substring case-globs with anchored
bash regexes so `git push origin main-feature` / `maintenance` /
`main-ci-fix` no longer match. Drop the redundant `--force` case
(greptile P2, claude).
Tests:
- Add specs for the dedupe path, the failed-before-in-progress order,
the dry-run fetch-failure path, the empty-array branch-protection
fallback, and the missing-gh Errno::ENOENT path.
- Update existing assertions for the new success-message grammar.
Docs:
- docs/superpowers/specs/2026-05-25-...: mark Implemented, add language
identifiers to bare fenced code blocks (MD040), update override
instructions to use `bundle exec rake` (coderabbit).
Hook (.claude/hooks/main-ci-status.sh): - write_cache_atomic prints from $1 instead of re-reading via cat (claude[bot], 2 inline duplicates + summary) - jq slurp now dedupes reruns by max_by(id), mirroring the Ruby gate in release.rake — keeps session-time and release-time CI views in sync when a check has been re-run (cursor bugbot) - Defensive ":=0" defaults for failed_count/in_progress_count guard the partial-output edge case (claude[bot]) rakelib/release.rake: - validate_main_ci_status! now flags partial-missing required checks with a new :missing_required_checks violation kind. Branch protection would refuse this merge state, so a release shouldn't ship through it. The all-missing case keeps :no_required_checks for clarity (chatgpt-codex-connector P1) - Comment in run_release_preflight_checks! explaining why the main-CI status check is wired separately in with_release_checkout (claude[bot] summary) react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb: - passing_run/failing_run/in_progress_run helpers now include "id" defaults to match real GitHub API responses (claude[bot]) - New test: aborts and lists missing required check names when some required checks are absent on a prerelease (chatgpt-codex-connector P1) - New test: warns instead of aborting when git fetch fails with allow_override, matching the dry-run symmetric case (claude[bot] summary) 63 examples, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hook (.claude/hooks/main-ci-status.sh): - SHA-key the CI status cache. The hook now resolves origin/main HEAD via git ls-remote BEFORE the cache lookup and keys the cache file by short SHA (.main-ci-status.cache.<sha8>). The 5-minute mtime TTL falls back to its original role of rate-limiting API calls when the same SHA is checked many times; cross-SHA staleness can no longer happen because a new commit gets its own cache file. Older SHA-keyed files are pruned on a successful cache write, with a -mmin +1 guard to avoid racing concurrent sessions that may be writing for a different SHA. Legacy unkeyed cache name is retained as a last-resort fallback when ls-remote fails so a single failed call doesn't force a full live re-fetch (cursor bugbot, medium-severity). rakelib/release.rake: - fetch_main_ci_checks now routes the Errno::ENOENT path (missing `gh` binary) and JSON::ParserError path through handle_main_ci_status_violation! so they respect dry_run / allow_override the same way a git-fetch failure does. Previously both paths called abort unconditionally, letting a dry-run release exit hard before showing the intended CI diagnostics (cursor bugbot, medium-severity). - Added the rubocop:disable Metrics/MethodLength on fetch_main_ci_checks since the rescue blocks pushed the method over 41 lines; the method is intentionally linear (fetch → resolve sha → call gh → parse → return) and extracting helpers would obscure the flow. react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb: - 2 new tests for the dry-run paths through Errno::ENOENT and JSON::ParserError. .gitignore: - Widened .claude/.main-ci-status.cache → .main-ci-status.cache* to cover the new SHA-keyed cache files. 65 examples, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rakelib/release.rake: - validate_main_ci_status! now catches check_runs whose `status` falls outside both "completed" and CI_INCOMPLETE_STATUSES (e.g. a future GitHub status value not yet in our allowlist, or a nil from a malformed response). Previously such runs slipped through silently and the gate would report main as healthy. Defensive: a last-line release gate should err toward blocking the ambiguous case (claude[bot] on commit f5a9476). react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb: - Two new tests for the unknown-status path: one for a literal "scheduled" status (a hypothetical future GitHub value) and one for nil. 67 examples, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(2 items) This pre-existing dedup was a real bug — both the original Ruby code AND my round-2 mirror-in-the-hook extended it: grouping by name alone collapses unrelated workflows that happen to share a job name. This repo has multiple workflows that each define a `detect-changes` job, so a passing run from one workflow was masking failing runs from others. Running the updated hook on the current origin/main shows 66 checks total, 11 failed — the prior dedup reported 26 / 1 (10 of the failures were silently dropped). rakelib/release.rake: - validate_main_ci_status! dedup key is now `[check_suite.id, name]`, not just `name`. True reruns (same suite, same name) still collapse to the latest attempt; cross-workflow same-name jobs stay distinct. .claude/hooks/main-ci-status.sh: - Matching jq aggregation now keys by `[suite_id, name]` so session-time output matches release-time output. The previous round-2 change brought the hook and Ruby code into sync — this round fixes the underlying shared bug in both. .claude/hooks/main-ci-status-on-push.sh: - Push matcher used to only fire on explicit `git push ... origin main`. Bare `git push` and `git push origin` (common shortcuts while on `main` with upstream tracking) were silently skipped. Added a third clause that catches any `git push` invocation when the current branch is `main`, preserving the stated guardrail without over-triggering on feature-branch work. react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb: - New test: two workflows emitting the same-named job on the same commit stay distinct (failing one is still reported). - New test: rerun-within-suite collapses while cross-suite same-name runs are preserved. Both findings from chatgpt-codex-connector on commit ffcb344. 69 examples, 0 failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rakelib/release.rake:
- validate_main_ci_status! now applies the missing-required-check gate
to STABLE releases too. Branch protection would refuse a merge when
required checks are missing, so a stable release shouldn't ship past
that gap either. Prerelease mode still uses the strict required-only
subset for the failed/in_progress gates; stable still evaluates all
visible check_runs for those (cursor bugbot).
.claude/hooks/main-ci-status.sh:
- write_cache_atomic failure no longer discards the computed status.
If the cache write fails (disk full, .claude/ read-only), we still
print the output so the agent sees the useful information we already
paid for the API round-trip to compute (claude[bot]).
.claude/hooks/main-ci-status-on-push.sh:
- Pre-push matcher now also fires on refspec-form pushes
(`git push origin HEAD:main`, `git push origin feature:main`, etc.).
Combined with the round-5 "on main + any push" fallback, all common
forms of pushing to main now surface the CI status block
(cursor bugbot).
docs/superpowers/specs/2026-05-25-main-ci-release-guard-design.md:
- Corrected the hook command description: the actual implementation
uses `gh api repos/{slug}/commits/{sha}/check-runs` (the Checks API),
not `gh run list`. The Checks API approach was the right call — it
gives per-check-run deduplication that matches the release gate
(claude[bot]).
react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb:
- Replaced `rand(1_000_000)` default `id` with a fixed sentinel 9999
so test data stays deterministic. Tests that rely on `max_by { id }`
ordering supply explicit ids (claude[bot]).
- Added a `before` block stubbing `required_check_names_for_main` to
return nil by default, accommodating the new unconditional call from
`validate_main_ci_status!`.
- New test: stable releases also abort when a required check is missing.
70 examples, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.claude/hooks/main-ci-status.sh:
- Add explicit `\n` to the printf in write_cache_atomic (and in the
fail-soft fallback). The surrounding `output=$(...)` substitution
strips trailing newlines, so without re-adding one here the cached
file and stdout print both ended mid-line and glommed onto the next
shell prompt (claude[bot]).
- Add a comment above the `git ls-remote` call clarifying that it runs
even on a cache hit (we need the SHA to key the cache correctly).
The 5-min TTL only eliminates the more expensive `gh api` call
(claude[bot]).
rakelib/release.rake:
- Add a comment in required_check_names_for_main explaining the
deliberate use of capture_gh_output (fail-safe nil) vs the direct
Open3.capture2e in fetch_main_ci_checks (routes through
handle_main_ci_status_violation! for dry_run/override semantics).
Same wrapper choice, different rationale — comment so future
readers don't see it as inconsistency (claude[bot]).
.claude/settings.json:
- Reference the two new hook scripts via ${CLAUDE_PROJECT_DIR} so they
resolve correctly when a session is launched from a subdirectory
(e.g. `react_on_rails/spec/dummy`). The existing
`bin/claude-hooks/autofix-file` entry is pre-existing and left
as-is — out of scope for this PR (chatgpt-codex-connector P2).
70 examples, 0 failures. Shellcheck + RuboCop clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (2 items)
.claude/hooks/main-ci-status.sh:
- `git ls-remote origin main` → `git ls-remote origin refs/heads/main`.
The bare-name form is a glob that would match `refs/tags/main` too if
such a tag existed on the remote, with `awk 'NR==1'` returning whichever
ref the server listed first. `refs/heads/main` is unambiguous and
always resolves to the branch tip (claude[bot]).
- jq dedup falls back to the run's own `id` when `check_suite.id` is
absent, so nil-suite runs from different workflows don't collapse:
`suite_id: (.check_suite.id // .id)`.
rakelib/release.rake:
- Same nil-suite guard in `validate_main_ci_status!`:
`[run.dig("check_suite", "id") || run["id"], run["name"]]`.
GitHub Actions check_runs always populate `check_suite`, so this only
affects external/third-party check integrations (claude[bot]).
react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb:
- Updated the existing rerun test to specify `check_suite => {"id" => 100}`
on both attempts. Real reruns from the GitHub API preserve the suite
across attempts; the previous test was simplified to omit it, which
worked under the old name-only dedup but would now incorrectly treat
the two attempts as distinct workflows under the suite-aware dedup
with the new nil-fallback. The test now models reality.
70 examples, 0 failures. ShellCheck + RuboCop clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- release.rake: only label success-message count as "required" when `evaluated` was actually filtered to the required subset. On stable releases the filter is skipped, so reporting "(N required checks)" misrepresented the count when branch protection was visible. - main-ci-status.sh: replace stale `release.rake:451-455` line-number pointer (which now lands inside an error heredoc) with a stable reference to `validate_main_ci_status!` so the "keep in sync" note does not rot on the next edit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e7f89cd to
3076a51
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Reviewed by Cursor Bugbot for commit 3076a51. Configure here.
| matched=0 | ||
| if [[ "${cmd}" =~ (^|[[:space:]])gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$) ]]; then | ||
| matched=1 | ||
| elif [[ "${cmd}" =~ (^|[[:space:]])git([[:space:]]+[^[:space:]]+)*[[:space:]]+push([[:space:]]|$) ]]; then |
There was a problem hiding this comment.
Wrapped bash commands skip hook
Medium Severity
PreToolUse matching requires gh or git at the start of the command string or after whitespace. Invocations such as bash -c "git push origin main" or bash -c "gh pr create" place the tool name immediately after a quote, so the hook exits without emitting main CI status.
Reviewed by Cursor Bugbot for commit 3076a51. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3076a5139a
ℹ️ 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".
| local_head_sha=$(git -C "${REPO_ROOT}" rev-parse --verify origin/main 2>/dev/null || echo "") | ||
| if [ -n "${local_head_sha}" ]; then | ||
| CACHE_FILE="${CACHE_DIR}/${CACHE_PREFIX}.${local_head_sha:0:12}" | ||
| read_fresh_cache "${CACHE_FILE}" |
There was a problem hiding this comment.
Refresh remote SHA before trusting cached status
When a fresh cache exists for the local origin/main, the hook exits here before git ls-remote checks the actual remote tip. If another commit lands on main within the 5-minute TTL after this checkout last updated origin/main, session start / pre-PR / pre-push output can report the old cached green commit while current main is red, undermining the guardrail this hook adds. Consider resolving the remote SHA first, then using the SHA-keyed cache for that current tip.
Useful? React with 👍 / 👎.
## Summary Adds the design doc for the benchmark **summary-table improvements** shipped in **#3627** (issue **#3601**). The doc was written during that PR's design phase but accidentally left untracked, so it never landed. This matches the established `docs/superpowers/specs/` convention — prior features committed their design docs the same way (e.g. `2026-05-21-otel-node-renderer-design.md` from #3382, `2026-05-25-main-ci-release-guard-design.md` from #3407). Status header updated to `Implemented (PR #3627)` to mirror those. ## Why keep it The merged code + comments capture the *what*; this doc captures the *why these and not the alternatives* — the decision rationale that code comments don't: - inline baseline + delta **in the same cell** (vs column-doubling) - **one perf link per benchmark name** (vs per-metric-cell) - send `p90_latency` to Bencher **boundary-less** (recorded for a baseline, never alerted) - **drop the Fail% column but keep the `failed_pct` threshold** (alert safety net) …plus the CI-only verification risks that were called out before the first live benchmark run. ## Notes - **Docs-only.** No code, no behavior change. - Base: freshly fetched `main`. Not auto-closing anything — issue #3601 was already closed by #3627; this just backfills the record. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Documentation-only addition with no code or CI behavior changes. > > **Overview** > Adds **`docs/superpowers/specs/2026-06-04-benchmark-table-improvements-design.md`**, backfilling the design record for issue **#3601** / implementation **#3627** under the existing `docs/superpowers/specs/` pattern. > > The doc is marked **Implemented (PR #3627)** and spells out the product choices that drove the merged benchmark table work: inline baseline + delta in metric cells, one Bencher perf link per benchmark name, boundary-less `p90_latency` with value-only fallback, and hiding Fail% while keeping `failed_pct` alerting. It also documents cell rendering rules, the intended code touchpoints, and CI-only verification risks. > > **Docs-only** — no application or benchmark runtime behavior changes in this PR. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 08448d7. 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 * **Documentation** * Marked the benchmark table spec as implemented and linked related issues. * Defined four visible behaviors: inline baseline+delta in metric cells; one performance link per benchmark; p90 shown from a boundary-less p90 latency measure with value-only fallback when no baseline; removed visible Fail% column (thresholds still trigger alerts). * Clarified cell rendering rules (nil/non-numeric handling, rounding, significance markers, arrows vs red/green indicators) and included a final example table. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>


Summary
This PR adds guardrails so releases and Claude Code sessions notice when
mainCI is unhealthy.Release-time gate in
rake release: checks CI onorigin/mainHEAD before publishing. Stable releases require every visible check to pass; prereleases gate on branch-protection-required checks, with fail-safe fallback to all checks when required checks cannot be read. Failed, in-progress, missing-required, unknown-status, and no-checks states block unless explicitly overridden.Session-time signal for Claude Code: hooks print a compact, cached
origin/mainCI summary at session start and before PR creation or push-to-main commands. The hooks are fail-open, SHA-keyed, origin-derived, and surface missing required checks as well as failed/in-progress checks.Agent guidance and design notes:
AGENTS.md,CLAUDE.md, and the design spec document when to stop, wait, override, or proceed whenmainis red.Review Follow-Up Included
main; the previous CI install failure came from the old merge base and is fixed upstream by the OpenTelemetry syntax fix now included here.HEAD:main,HEAD:refs/heads/main, fully qualified main refspecs,git -C ... push,--all, and--mirror.origin, matching the release gate.ls-remotecall when localorigin/mainalready has a fresh SHA-keyed cache.check_suitebehavior.Test Plan
bundle exec rspec react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb(74 examples, 0 failures)bundle exec rubocop rakelib/release.rake react_on_rails/spec/react_on_rails/release_rake_helpers_spec.rb(cd react_on_rails && bundle exec rubocop)bash -n .claude/hooks/main-ci-status.sh && bash -n .claude/hooks/main-ci-status-on-push.shshellcheck .claude/hooks/main-ci-status.sh .claude/hooks/main-ci-status-on-push.shpnpm install --frozen-lockfile(confirms the previous Pro node-renderer prepare failure is gone after rebase)Follow-Ups
loadRouteChunkcalled twice under StrictMode hydration replaybench.rbdoes not produce a summary and port 3001 is not freedNote
Medium Risk
Changes release publishing behavior (can block releases) and adds GitHub API-dependent hooks; mistakes in CI policy or hook matching could block legitimate releases or add noise, but dry-run/override paths and fail-open hooks limit blast radius.
Overview
Adds main-branch CI guardrails so releases and Claude Code sessions see when
origin/mainis unhealthy before shipping or opening PRs.rake releasenow evaluates GitHub Checks onorigin/mainHEAD (aftergit pull --rebasein the release checkout, including dry runs). Stable releases require every visible check to pass; prereleases gate on branch-protection-required checks, with fail-safe “treat all as required” when protection cannot be read. Failed, in-progress, missing-required, unknown-status, and zero-check cases block unlessRELEASE_CI_STATUS_OVERRIDEor the new 4th positional arg is set. Logic mirrors the hook (dedup bycheck_suite+ name, combinedcontexts/checksfor required names).Claude Code hooks inject a compact, SHA-keyed 5-minute cache of main CI at SessionStart and before
gh pr create/ push-to-main (broadgit pushdetection, fail-open).AGENTS.md,CLAUDE.md,.gitignore, and a design spec document the policy;release_rake_helpers_spec.rbcovers the new release and fetch paths extensively.Reviewed by Cursor Bugbot for commit 3076a51. Bugbot is set up for automated code reviews on this repo. Configure here.