Skip to content

fix: merge gate scan every 10 min after PR is merge-ready#2379

Merged
MervinPraison merged 3 commits into
mainfrom
fix/merge-gate-10min-scan
Jun 27, 2026
Merged

fix: merge gate scan every 10 min after PR is merge-ready#2379
MervinPraison merged 3 commits into
mainfrom
fix/merge-gate-10min-scan

Conversation

@MervinPraison

@MervinPraison MervinPraison commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Scan every 10 minutes instead of 4× daily (02/08/14/20 UTC)
  • Skip 35min @claude cooldown when finalClaudeCompletedOnSha is already true (FINAL done on HEAD — no need to wait after the trigger that finished review)
  • cancel-in-progress: false so an in-flight assess/merge is not killed by the next scan

Expected timing after merge-ready

Step Delay
Post-push buffer up to 5 min after last commit
Next scan up to 10 min
Claude assess + merge ~10–20 min
Total ~15–35 min per batch of up to 5 PRs (FIFO)

Test plan

  • node .github/scripts/merge-gate-selftest.js (new cooldown test passes)

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes

    • Refined merge-gate cooldown logic so completed FINAL Claude reviews no longer cause additional cooldown skips.
    • Adjusted pipeline readiness evaluation to remove overly strict timing gates and ensure CI readiness is checked earlier.
  • Chores

    • Increased scan frequency to every 10 minutes, reduced merge candidates to 1, and updated workflow concurrency to avoid canceling in-progress runs.
    • Added up to 3 merge retries for certain base-branch modification failures.
    • Pipeline sync now dispatches merge-gate for only the oldest ready pull request.

Replace 4x-daily schedule with */10 cron so merge-ready PRs are picked up
within ~10 minutes. Do not block on 35min @claude cooldown once FINAL
review is complete on current HEAD. Disable cancel-in-progress so assess
jobs are not aborted by the next scan.

Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The merge-gate cooldown now checks for a verdict on the head SHA before applying the recent @claude skip reason. Pipeline-status sync can dispatch merge-gate for the oldest ready PR, and the workflows now run more frequently with updated merge retry behavior.

Changes

Merge-gate cooldown, dispatch, and workflow cadence

Layer / File(s) Summary
Cooldown gate check
.github/scripts/merge-gate.js, .github/scripts/merge-gate-selftest.js
The recent @claude skip reason is added only when final completion on the head SHA is not already present, and the self-test covers that FINAL-on-head case.
Pipeline label sync and dispatch
.github/scripts/pipeline-status.js, .github/workflows/pipeline-status-sync.yml
syncPipelineLabels returns detailed readiness data, syncOpenPullRequests collects ready candidates, and the oldest ready PR can trigger a claude-merge-gate repository dispatch.
Workflow schedule and concurrency
.github/workflows/claude-merge-gate.yml
The merge-gate workflow runs every 10 minutes, keeps concurrent runs active, and lowers the candidate cap to 1.
Merge retry loop
.github/workflows/claude-merge-gate.yml
The merge step retries up to three times on base-branch-modified failures, waits 8 seconds between retries, and rechecks merge readiness before retrying.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • MervinPraison/PraisonAI#2316: Updates merge-gate.js verdict detection and evaluation logic, which is directly related to the cooldown and verdict-handling changes here.

Poem

A bunny hops where gates turn wise,
With verdicts bright beneath the skies.
Every ten minutes, paws on time,
I twitch my nose at merge-and-climb. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 title clearly summarizes the main change: making merge-gate scans run every 10 minutes after a PR is ready to merge.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/merge-gate-10min-scan

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.

@MervinPraison

Copy link
Copy Markdown
Owner Author

@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings.

Review areas:

  1. Bloat check: Are changes minimal and focused? Any unnecessary code or scope creep?
  2. Security: Any hardcoded secrets, unsafe eval/exec, missing input validation?
  3. Performance: Any module-level heavy imports? Hot-path regressions?
  4. Tests: Are tests included? Do they cover the changes adequately?
  5. Backward compat: Any public API changes without deprecation?
  6. Code quality: DRY violations, naming conventions, error handling?
  7. Address reviewer feedback: If Qodo, Coderabbit, or Gemini flagged valid issues, include them in your review
  8. Suggest specific improvements with code examples where possible

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a mechanism to skip the 35-minute cooldown gate if a final Claude review has been completed on the current HEAD, adding a check in merge-gate.js and a corresponding test in merge-gate-selftest.js. The reviewer identified a critical issue where the implementation checks for the presence of the trigger comment (finalClaudeCompletedOnSha) rather than an actual completed verdict, which would prematurely bypass the cooldown. The feedback suggests using findMergeGateVerdict instead to ensure a verdict has been posted, and provides updated test assertions to match this corrected behavior.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread .github/scripts/merge-gate.js Outdated
Comment on lines +606 to +607
const finalDone = finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt);
if (!finalDone) reasons.push('recent @claude within 35min');

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.

high

Using finalClaudeCompletedOnSha to skip the 35-minute cooldown is problematic because finalClaudeCompletedOnSha only checks if the trigger comment (e.g., @claude You are the FINAL architecture reviewer.) has been posted and is not stale. It does not check if Claude has actually completed its review and posted a verdict.

As a result, the 35-minute cooldown is bypassed immediately after the trigger comment is posted (even before Claude starts or finishes its run). This defeats the purpose of the cooldown, which is to prevent concurrent/rapid triggers or race conditions while Claude is running or queued.

To correctly skip the cooldown only when Claude has actually completed its review and posted a verdict, you should check if a verdict comment exists on the current HEAD using findMergeGateVerdict.

Suggested change
const finalDone = finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt);
if (!finalDone) reasons.push('recent @claude within 35min');
const finalDone = findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt) !== null;
if (!finalDone) reasons.push('recent @claude within 35min');

Comment on lines +54 to +64
const recentFinalOnHead = [
{
user: { login: 'MervinPraison' },
body: '@claude You are the FINAL architecture reviewer.',
created_at: '2026-06-27T10:00:00Z',
},
];
assert(
'final on head skips cooldown gate',
mg.finalClaudeCompletedOnSha(recentFinalOnHead, '2026-06-27T09:55:00Z')
);

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.

medium

The test name 'final on head skips cooldown gate' is misleading because finalClaudeCompletedOnSha only checks if the trigger comment is present and not stale, rather than verifying if the cooldown gate is skipped.

Since we should use findMergeGateVerdict to determine if the final review is actually complete (and thus safe to skip the cooldown), we should update this test to assert the correct behavior of findMergeGateVerdict or the actual cooldown skipping logic.

const recentFinalOnHead = [
  {
    user: { login: 'MervinPraison' },
    body: '@claude You are the FINAL architecture reviewer.',
    created_at: '2026-06-27T10:00:00Z',
  },
];
assert(
  'final trigger on head does not skip cooldown without verdict',
  mg.findMergeGateVerdict(recentFinalOnHead, null, '2026-06-27T09:55:00Z') === null
);

const recentVerdictOnHead = [
  ...recentFinalOnHead,
  {
    user: { login: 'github-actions[bot]' },
    body: 'MERGE_GATE_VERDICT: APPROVE',
    created_at: '2026-06-27T10:05:00Z',
  },
];
assert(
  'final verdict on head skips cooldown gate',
  mg.findMergeGateVerdict(recentVerdictOnHead, null, '2026-06-27T09:55:00Z') === 'APPROVE'
);

@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

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

Inline comments:
In @.github/scripts/merge-gate.js:
- Around line 605-607: The cooldown check in merge-gate.js is skipping too early
when finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt) is true. Update
the logic around hasRecentClaudeTrigger and finalClaudeCompletedOnSha so the
“recent `@claude` within 35min” reason is only suppressed when the latest FINAL
completion on HEAD is at least as recent as the latest qualifying `@claude`
trigger; otherwise keep the cooldown reason and let hasInProgressClaudeAssistant
handle any newer run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7a84a9b-858a-4021-9d1c-2359f4471cd5

📥 Commits

Reviewing files that changed from the base of the PR and between bd6b1fb and 424e622.

📒 Files selected for processing (3)
  • .github/scripts/merge-gate-selftest.js
  • .github/scripts/merge-gate.js
  • .github/workflows/claude-merge-gate.yml

Comment thread .github/scripts/merge-gate.js Outdated
Comment on lines +605 to +607
if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
reasons.push('recent @claude within 35min');
const finalDone = finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt);
if (!finalDone) reasons.push('recent @claude within 35min');

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't bypass cooldown for newer non-FINAL @claude comments.

finalClaudeCompletedOnSha(...) only proves that some FINAL review completed on the current HEAD. It does not prove that the FINAL is newer than the most recent @claude trigger. If someone posts another @claude comment after that FINAL, this branch suppresses the 35-minute guard before hasInProgressClaudeAssistant(...) can see the new run, so the PR can be treated as merge-ready while a fresh Claude review is still being kicked off.

Only skip this reason when the latest qualifying FINAL completion on HEAD is at least as new as the latest qualifying @claude trigger; otherwise keep recent @claude within 35min.

Suggested direction
-  if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
-    const finalDone = finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt);
-    if (!finalDone) reasons.push('recent `@claude` within 35min');
-  }
+  if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
+    const finalDone = finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt);
+    const latestClaudeTriggerAt = getLatestClaudeTriggerTime(ctx.comments);
+    const latestFinalOnHeadAt = getLatestFinalCompletionOnHeadTime(ctx.comments, ctx.headPushedAt);
+    if (!finalDone || latestFinalOnHeadAt < latestClaudeTriggerAt) {
+      reasons.push('recent `@claude` within 35min');
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/merge-gate.js around lines 605 - 607, The cooldown check in
merge-gate.js is skipping too early when finalClaudeCompletedOnSha(ctx.comments,
ctx.headPushedAt) is true. Update the logic around hasRecentClaudeTrigger and
finalClaudeCompletedOnSha so the “recent `@claude` within 35min” reason is only
suppressed when the latest FINAL completion on HEAD is at least as recent as the
latest qualifying `@claude` trigger; otherwise keep the cooldown reason and let
hasInProgressClaudeAssistant handle any newer run.

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates the merge-gate workflow timing and readiness checks.

  • Runs merge-gate and pipeline sync scans every 10 minutes.
  • Skips the Claude cooldown only after a merge-gate verdict exists on HEAD.
  • Dispatches the oldest ready PR from pipeline status sync.
  • Keeps in-flight merge-gate runs active instead of cancelling them.
  • Retries merges when the base branch moves during the merge step.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
.github/scripts/merge-gate.js Updates cooldown and readiness checks for the merge-gate decision path.
.github/scripts/merge-gate-selftest.js Adds coverage for distinguishing a FINAL trigger from a HEAD verdict.
.github/scripts/pipeline-status.js Adds oldest-ready merge-gate dispatch from pipeline label sync.
.github/workflows/claude-merge-gate.yml Updates scan cadence, concurrency behavior, candidate count, and merge retry handling.
.github/workflows/pipeline-status-sync.yml Updates pipeline sync cadence and enables merge-gate dispatch.

Reviews (2): Last reviewed commit: "fix: require verdict on HEAD before skip..." | Re-trigger Greptile

Comment thread .github/scripts/merge-gate.js Outdated
- Remove redundant post-push buffer (CI green on HEAD is the gate)
- MAX_CANDIDATES=1 to avoid parallel merge race on main
- Retry merge up to 3x when base branch was modified
- Pipeline status sync dispatches merge gate for oldest merge-ready PR
- Align pipeline sync cron to every 10 minutes

Co-authored-by: Cursor <cursoragent@cursor.com>
@MervinPraison

Copy link
Copy Markdown
Owner Author

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions
  4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.

Phase 2: FIX Valid Issues
5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
6. Push all code fixes directly to THIS branch (do NOT create a new PR)
7. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
8. If all issues are resolved, approve the PR / close the Issue
9. If blocking issues remain, request changes / leave clear action items

@MervinPraison MervinPraison added pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:manual-review Blocked: requires manual review pipeline/reviews-pending Waiting for CodeRabbit/Qodo/Copilot reviews labels Jun 27, 2026
@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MervinPraison's task —— View job


FINAL Architecture Review — PR #2379

  • Read current merge-gate.js cooldown logic & self-test
  • Analyze reviewer findings (Gemini, CodeRabbit, Greptile — all flag line 606)
  • Implement fix for premature cooldown bypass
  • Update self-test
  • Commit & push

Scope note

This PR only touches CI infrastructure (.github/scripts/*.js, .github/workflows/*.yml) — no Python SDK changes (praisonaiagents / praisonai untouched). So AGENTS.md SDK-bloat/protocol checks don't apply. I can edit the .js scripts but cannot modify .github/workflows/ files (GitHub App permission); those workflow changes (10-min cron, cancel-in-progress: false) look correct and were left as authored.

Valid issue fixed (flagged by Gemini, CodeRabbit, and Greptile — same root cause)

.github/scripts/merge-gate.js:605

The cooldown bypass used finalClaudeCompletedOnSha(...), which only verifies the FINAL trigger comment exists and isn't stale — it does not prove Claude actually finished and posted a verdict. Consequences:

  • The 35-min cooldown was bypassed the instant the @claude FINAL comment was posted — before the run even started.
  • hasInProgressClaudeAssistant may not yet see a queued run, so the gate could treat the PR as merge-ready mid-review (the race CodeRabbit/Greptile described).

Fix: skip the cooldown only when a real MERGE_GATE_VERDICT exists on HEAD via findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt). A verdict is posted only after the Claude run completes, so a newer bare @claude trigger without a verdict correctly keeps the cooldown — addressing the "FINAL newer than trigger" concern too.

-    const finalDone = finalClaudeCompletedOnSha(ctx.comments, ctx.headPushedAt);
-    if (!finalDone) reasons.push('recent @claude within 35min');
+    const verdictOnHead = findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt) !== null;
+    if (!verdictOnHead) reasons.push('recent @claude within 35min');

Self-test updated: the misleading final on head skips cooldown gate (which asserted on the trigger-only helper) is replaced with two assertions — a FINAL trigger alone yields no verdict, and a verdict on HEAD returns APPROVE.

Files modified

  • .github/scripts/merge-gate.js — verdict-on-HEAD check for cooldown skip
  • .github/scripts/merge-gate-selftest.js — corrected coverage for the fix

Skipped

  • CodeRabbit "Docstring Coverage" warning — not applicable (JS workflow scripts, not Python SDK).
  • Qodo review — paused (no actionable findings).

Verdict

MERGE_GATE_VERDICT: APPROVE — the one valid blocking issue (premature cooldown bypass) is resolved; changes are minimal, backward-compatible, and make the gate strictly more conservative.

Note: I can't run node in this environment to execute the self-test, but the verdict-filter logic (findMergeGateVerdict) is exercised by existing passing assertions above and traced by hand. Please confirm node .github/scripts/merge-gate-selftest.js is green in CI.

Branch fix/merge-gate-10min-scan · Job run

The cooldown bypass used finalClaudeCompletedOnSha which only checks the
FINAL trigger comment is present and not stale — not that Claude actually
finished and posted a verdict. This let the gate skip the 35-min cooldown
before the FINAL run was even visible. Switch to findMergeGateVerdict so the
cooldown is only skipped once a real MERGE_GATE_VERDICT exists on HEAD.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison

Copy link
Copy Markdown
Owner Author

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions
  4. SDK value: review in depth whether the change genuinely adds value to the SDK — never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params — only if absolutely required; we already support many params.

Phase 2: FIX Valid Issues
5. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
6. Push all code fixes directly to THIS branch (do NOT create a new PR)
7. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
8. If all issues are resolved, approve the PR / close the Issue
9. If blocking issues remain, request changes / leave clear action items

@MervinPraison MervinPraison added the pipeline/blocked:cooldown Blocked: post-push or @claude cooldown label Jun 27, 2026
@praisonai-triage-agent

praisonai-triage-agent Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MervinPraison's task —— View job


FINAL Architecture Review — PR #2379

  • Verify cooldown-bypass fix is applied on HEAD
  • Read & validate self-test coverage
  • Run self-test (could not exec node in this env — traced by hand)
  • Final verdict

Scope note

This PR touches only CI infrastructure (.github/scripts/*.js, .github/workflows/*.yml). No Python SDK changes (praisonaiagents / praisonai untouched), so AGENTS.md SDK-bloat/protocol/Agent-param checks don't apply. I also cannot modify .github/workflows/ (GitHub App permission) — the workflow changes (10-min cron, cancel-in-progress: false, retry-3×) were left as authored and look correct.

Valid issue — already FIXED on HEAD ✅

All three bots (Gemini 🔴, CodeRabbit 🟠, Greptile P1) flagged the same root cause at merge-gate.js:604-607: the cooldown bypass used finalClaudeCompletedOnSha(...), which only checks that a FINAL trigger comment exists and isn't stale — it does not prove Claude finished and posted a verdict. That bypassed the 35-min guard the instant @claude FINAL was posted, before hasInProgressClaudeAssistant could see the queued run (the race condition).

The fix (commit 698ed7c0) is present on HEAD:

if (!skipRecentClaudeCooldown && hasRecentClaudeTrigger(ctx.comments, 35)) {
  const verdictOnHead = findMergeGateVerdict(ctx.comments, null, ctx.headPushedAt) !== null;
  if (!verdictOnHead) reasons.push('recent @claude within 35min');
}

Why this resolves every reviewer's concern (traced via findMergeGateVerdict, lines 678-695):

  • A MERGE_GATE_VERDICT: comment is posted only after a Claude run completes → a trigger-only FINAL no longer bypasses the cooldown (Gemini/Greptile "trigger ≠ completion").
  • The verdict must be created after headPushedAt - 60s → a verdict for an older HEAD is rejected.
  • A newer bare @claude trigger without a fresh verdict keeps the cooldown, and hasInProgressClaudeAssistant (line 609) catches the new run → addresses CodeRabbit's "FINAL not newer than latest trigger" case. Strictly more conservative than the suggested latestFinal >= latestTrigger comparison.

Self-test coverage ✅

merge-gate-selftest.js:54-77 correctly covers the fix:

  • findMergeGateVerdict(recentFinalOnHead, …)null (trigger alone ≠ verdict)
  • findMergeGateVerdict(recentVerdictOnHead, …)'APPROVE' (real verdict on HEAD)

Plus existing assertions at lines 44-49 exercise the headPushedAt time-filter. Logic verified by hand.

Skipped

  • CodeRabbit "Docstring Coverage 0%" — N/A (JS workflow scripts, not Python SDK).
  • Qodo — reviews paused, no actionable findings.

Verdict

MERGE_GATE_VERDICT: APPROVE — the one valid blocking issue (premature cooldown bypass) is already resolved on HEAD; changes are minimal, backward-compatible, and make the gate strictly more conservative.

⚠️ I couldn't exec node in this environment to run the self-test. Logic is traced by hand above and exercised by existing passing assertions. Please confirm node .github/scripts/merge-gate-selftest.js is green in CI before merge.


Job run · Branch fix/merge-gate-10min-scan

@MervinPraison MervinPraison added pipeline/blocked:cooldown Blocked: post-push or @claude cooldown and removed pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:cooldown Blocked: post-push or @claude cooldown labels Jun 27, 2026

@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

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

Inline comments:
In @.github/scripts/pipeline-status.js:
- Around line 158-165: Refresh the label check immediately before dispatch in
syncPipelineLabels so cand.labels is not based only on the stale loadPrContext
snapshot. Re-read the current PR labels from GitHub right before deciding to
trigger the merge-gate workflow, and use that fresh state to confirm
claude-merge-gate-active is still absent. Update the candidate handling around
the dispatch path so concurrent runs cannot enqueue a duplicate merge-gate
workflow for the same PR.

In @.github/workflows/claude-merge-gate.yml:
- Around line 456-475: The merge retry path in the pull-request merge loop needs
to re-check that the current PR head still matches the scanned SHA before
attempting the second merge. After the 8s backoff and the call to
mergeGate.evaluatePipelineQuiescent, rerun the same scanned-SHA guard used in
the initial approval path (the check comparing check.headSha and scanHeadSha)
inside this retry branch, and abort/remove labels if it no longer matches. Keep
this logic in the merge attempt loop around github.rest.pulls.merge and
mergeGate.evaluatePipelineQuiescent so retries cannot merge a different commit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32227927-33b2-4698-85d8-e2085bbee809

📥 Commits

Reviewing files that changed from the base of the PR and between 424e622 and 4c0026d.

📒 Files selected for processing (4)
  • .github/scripts/merge-gate.js
  • .github/scripts/pipeline-status.js
  • .github/workflows/claude-merge-gate.yml
  • .github/workflows/pipeline-status-sync.yml

Comment on lines +158 to +165
return {
synced: true,
stage,
blockers,
ready: evalResult.ready,
reasons: evalResult.reasons,
labels: ctx.labels,
createdAt: new Date(ctx.pr.created_at).getTime(),

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Refresh the active-label state immediately before dispatch.

cand.labels comes from syncPipelineLabels()'s earlier loadPrContext() snapshot. If another run adds claude-merge-gate-active before this loop reaches the candidate, we still dispatch a second merge-gate workflow for the same PR.

Suggested fix
 async function dispatchMergeGateForOldestReady(github, owner, repo, readyCandidates, core) {
   if (!readyCandidates.length) return 0;
   readyCandidates.sort((a, b) => a.createdAt - b.createdAt);
   for (const cand of readyCandidates) {
-    if ((cand.labels || []).includes('claude-merge-gate-active')) {
+    const { data: labels } = await github.rest.issues.listLabelsOnIssue({
+      owner,
+      repo,
+      issue_number: cand.prNumber,
+      per_page: 100,
+    });
+    if (labels.some((label) => label.name === 'claude-merge-gate-active')) {
       core?.info?.(`Skip dispatch PR #${cand.prNumber}: merge gate already active`);
       continue;
     }
     await github.rest.repos.createDispatchEvent({

Also applies to: 169-183

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

In @.github/scripts/pipeline-status.js around lines 158 - 165, Refresh the label
check immediately before dispatch in syncPipelineLabels so cand.labels is not
based only on the stale loadPrContext snapshot. Re-read the current PR labels
from GitHub right before deciding to trigger the merge-gate workflow, and use
that fresh state to confirm claude-merge-gate-active is still absent. Update the
candidate handling around the dispatch path so concurrent runs cannot enqueue a
duplicate merge-gate workflow for the same PR.

Comment on lines +456 to +475
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
merged = true;
break;
} catch (e) {
const msg = (e.message || '').toLowerCase();
if (attempt < 3 && msg.includes('base branch was modified')) {
core.info(`Merge attempt ${attempt} failed (base moved), retrying in 8s...`);
await sleep(8000);
const recheck = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!recheck.ready) {
core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
await removeLabels();
return;
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Re-apply the scanned SHA guard on merge retries.

After the 8s backoff, evaluatePipelineQuiescent() only proves the current HEAD is mergeable. It does not prove this workflow's APPROVE verdict was produced for that SHA. If the PR head changes during the wait, attempt 2 can merge a different commit because the original check.headSha !== scanHeadSha guard is never rerun inside the retry path.

Suggested fix
                     const recheck = await mergeGate.evaluatePipelineQuiescent(
                       github, owner, repo, prNumber, core, { forMergeStep: true }
                     );
                     if (!recheck.ready) {
                       core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
                       await removeLabels();
                       return;
                     }
+                    if (recheck.headSha !== scanHeadSha) {
+                      core.info(`Skip merge retry: HEAD moved ${scanHeadSha.slice(0, 7)} → ${recheck.headSha.slice(0, 7)}`);
+                      await removeLabels();
+                      return;
+                    }
                     continue;
📝 Committable suggestion

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

Suggested change
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
merged = true;
break;
} catch (e) {
const msg = (e.message || '').toLowerCase();
if (attempt < 3 && msg.includes('base branch was modified')) {
core.info(`Merge attempt ${attempt} failed (base moved), retrying in 8s...`);
await sleep(8000);
const recheck = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!recheck.ready) {
core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
await removeLabels();
return;
}
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await github.rest.pulls.merge({
owner, repo, pull_number: prNumber, merge_method: method,
});
merged = true;
break;
} catch (e) {
const msg = (e.message || '').toLowerCase();
if (attempt < 3 && msg.includes('base branch was modified')) {
core.info(`Merge attempt ${attempt} failed (base moved), retrying in 8s...`);
await sleep(8000);
const recheck = await mergeGate.evaluatePipelineQuiescent(
github, owner, repo, prNumber, core, { forMergeStep: true }
);
if (!recheck.ready) {
core.info(`Skip merge retry: ${recheck.reasons.join(', ')}`);
await removeLabels();
return;
}
if (recheck.headSha !== scanHeadSha) {
core.info(`Skip merge retry: HEAD moved ${scanHeadSha.slice(0, 7)} → ${recheck.headSha.slice(0, 7)}`);
await removeLabels();
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/claude-merge-gate.yml around lines 456 - 475, The merge
retry path in the pull-request merge loop needs to re-check that the current PR
head still matches the scanned SHA before attempting the second merge. After the
8s backoff and the call to mergeGate.evaluatePipelineQuiescent, rerun the same
scanned-SHA guard used in the initial approval path (the check comparing
check.headSha and scanHeadSha) inside this retry branch, and abort/remove labels
if it no longer matches. Keep this logic in the merge attempt loop around
github.rest.pulls.merge and mergeGate.evaluatePipelineQuiescent so retries
cannot merge a different commit.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/scripts/merge-gate.js (1)

609-614: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the post-push buffer before trusting empty check results.

allChecksGreenOnSha() explicitly allows runs.length === 0, so after this removal a freshly pushed SHA can be treated as CI-green before GitHub has even created its check runs. That makes the gate eligible for dispatch/merge too early.

Suggested fix
+  const headAgeMs = Date.now() - new Date(ctx.headPushedAt).getTime();
+  if (headAgeMs < 5 * 60 * 1000) {
+    reasons.push('post-push buffer (<5min)');
+  }
+
   const checksOk = await allChecksGreenOnSha(github, owner, repo, ctx.headSha, core);
   if (!checksOk) reasons.push('CI not green on HEAD');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/merge-gate.js around lines 609 - 614, The merge gate
currently trusts allChecksGreenOnSha() even when it returns an empty result set,
which can let a just-pushed HEAD look CI-green too early. Reintroduce the
post-push buffer logic in merge-gate.js around the checksOk decision, using the
existing ctx.headSha / prNumber flow so empty runs from allChecksGreenOnSha() do
not immediately allow dispatch or merge.
🤖 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.

Outside diff comments:
In @.github/scripts/merge-gate.js:
- Around line 609-614: The merge gate currently trusts allChecksGreenOnSha()
even when it returns an empty result set, which can let a just-pushed HEAD look
CI-green too early. Reintroduce the post-push buffer logic in merge-gate.js
around the checksOk decision, using the existing ctx.headSha / prNumber flow so
empty runs from allChecksGreenOnSha() do not immediately allow dispatch or
merge.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4e8a299-4b50-4b80-8552-0dc3d39b0e4d

📥 Commits

Reviewing files that changed from the base of the PR and between 4c0026d and 698ed7c.

📒 Files selected for processing (2)
  • .github/scripts/merge-gate-selftest.js
  • .github/scripts/merge-gate.js

@MervinPraison MervinPraison removed the pipeline/blocked:cooldown Blocked: post-push or @claude cooldown label Jun 27, 2026
@MervinPraison MervinPraison merged commit 9a771b2 into main Jun 27, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pipeline/blocked:manual-review Blocked: requires manual review pipeline/reviews-pending Waiting for CodeRabbit/Qodo/Copilot reviews

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant