feat(merge-queue): add await-and-enqueue script - #2400
Conversation
Polls a PR until all required checks pass and approvals are present, then enqueues it in the merge queue. Cross-references required checks from branch rulesets against the actual check rollup so missing checks (not yet reported) are treated as pending. Exits early if any check fails. GitHub's auto-merge API (gh pr merge --auto) does not work with merge queues, so this script fills that gap. Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
… associative arrays Associative arrays with declare -A are fragile across shell contexts. Move all check analysis into a single jq pass. Assisted-by: Claude claude-opus-4-6 <noreply@anthropic.com> Signed-off-by: Ralph Bean <rbean@redhat.com>
Site previewPreview: https://d3becbe8-site.fullsend-ai.workers.dev Commit: |
|
🤖 Finished Review · ✅ Success · Started 9:34 PM UTC · Completed 9:46 PM UTC |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
ReviewFindingsHigh
Medium
Low
Info
Previous runReviewFindingsHigh
Medium
Low
Info
|
| ($checks | map({(.name): (.conclusion // .status // "PENDING")}) | add // {}) as $map | | ||
| # Check for failures | ||
| [$map | to_entries[] | select(.value | test("FAILURE|ERROR|CANCELLED|TIMED_OUT|STARTUP_FAILURE|ACTION_REQUIRED")) | .key + " (" + .value + ")"] as $failures | | ||
| # Check for pending |
There was a problem hiding this comment.
[high] logic-error
The jq expression uses .name to identify checks and .conclusion // .status for their state, but GitHub statusCheckRollup contains two types of objects: CheckRun (with .name, .status, .conclusion) and StatusContext (with .context, .state). StatusContext entries will be keyed as null in the map, will not match required check names, and their state will not be correctly read — causing the script to loop forever or incorrectly treat them as pending.
Suggested fix: Use (.name // .context) for the key and (.conclusion // .state // .status // PENDING) for the value in the jq map construction.
| echo "Waiting for checks and approvals on: $pr_url" | ||
|
|
||
| while true; do | ||
| # Get check rollup and review decision in one call |
There was a problem hiding this comment.
[medium] edge-case
The script has no maximum iteration/timeout limit. If checks remain in a perpetually pending state, the script will poll indefinitely. For an automation script invoked by agents, this could consume resources and block the agent forever.
Suggested fix: Add a MAX_ATTEMPTS or TIMEOUT parameter (defaulting to ~60 attempts / 30 minutes) and exit with a clear error when exceeded.
| # Extract owner/repo from the PR URL | ||
| repo_nwo="$(echo "$pr_url" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/.*|\1|')" | ||
|
|
||
| # Fetch required status checks from branch rulesets as a JSON array |
There was a problem hiding this comment.
[low] logic-error
The branch rulesets API only returns required checks configured via the newer rulesets feature. Legacy branch protection rules will return an empty array. The script degrades gracefully (still checks overall rollup), but could attempt to enqueue before slow-to-report checks appear.
Suggested fix: Document this limitation in SKILL.md or add a fallback to the legacy branch protection API.
| [$map | to_entries[] | select(.value | test("SUCCESS|NEUTRAL|SKIPPED|COMPLETED|FAILURE|ERROR|CANCELLED|TIMED_OUT|STARTUP_FAILURE|ACTION_REQUIRED") | not) | .key] as $pending | | ||
| # Check for missing required checks | ||
| [$required[] | select(. as $r | $map | has($r) | not)] as $missing | | ||
| {failures: $failures, pending: $pending, missing: $missing} |
There was a problem hiding this comment.
[low] edge-case
ACTION_REQUIRED is treated as a failure state, but it typically indicates a check needing manual intervention. Users may expect the script to wait rather than abort.
Suggested fix: Consider treating ACTION_REQUIRED as pending with a user-facing message, or document this behavior.
| @@ -37,6 +40,18 @@ Run `bash skills/merge-queue/scripts/dequeue-reason.sh <PR_NUMBER_OR_URL>` to fi | |||
|
|
|||
There was a problem hiding this comment.
[info] documentation-order
The Await and enqueue section is placed after Investigate dequeue reasons but is closely related to Enqueue a PR. Consider placing it immediately after that section.
| - **"Pull request is not mergeable"** — the PR may need approvals, passing checks, or conflict resolution before it can be enqueued. | ||
| - **"Resource not accessible by integration"** — the `gh` token lacks sufficient permissions. | ||
| - **"status checks are expected"** — required checks haven't finished yet. Use `await-and-enqueue.sh` to poll and enqueue once they pass. | ||
| - **`gh pr merge --auto` fails with merge queues** — GitHub's auto-merge API does not support merge queues. Use `await-and-enqueue.sh` instead. |
There was a problem hiding this comment.
[info] common-errors-consistency
The new error entry starts with a backtick-wrapped command rather than a quoted error message in bold, which differs from the pattern of other entries in the Common errors section.
|
🤖 Finished Review · ✅ Success · Started 3:28 PM UTC · Completed 3:39 PM UTC |
| ($checks | map({(.name): (.conclusion // .status // "PENDING")}) | add // {}) as $map | | ||
| # Check for failures | ||
| [$map | to_entries[] | select(.value | test("FAILURE|ERROR|CANCELLED|TIMED_OUT|STARTUP_FAILURE|ACTION_REQUIRED")) | .key + " (" + .value + ")"] as $failures | | ||
| # Check for pending |
There was a problem hiding this comment.
[high] logic-error
The jq expression uses .name to identify checks and .conclusion // .status for their state, but GitHub's statusCheckRollup contains two object types: CheckRun (with .name, .status, .conclusion) and StatusContext (with .context, .state). StatusContext objects will be keyed as null, won't match required check names, and their state won't be correctly read — causing infinite loops or silently ignored failures.
Suggested fix: Use (.name // .context) for the key and (.conclusion // .state // .status // "PENDING") for the value in the jq map construction.
| echo "Waiting for checks and approvals on: $pr_url" | ||
|
|
||
| while true; do | ||
| # Get check rollup and review decision in one call |
There was a problem hiding this comment.
[medium] edge-case
The script has no maximum iteration or timeout limit. If checks remain perpetually pending, the script will poll indefinitely, blocking the invoking agent and consuming resources without bound.
Suggested fix: Add a MAX_ATTEMPTS or TIMEOUT environment variable (defaulting to ~60 attempts / 30 minutes) and exit with a clear error when exceeded.
| # Extract owner/repo from the PR URL | ||
| repo_nwo="$(echo "$pr_url" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/.*|\1|')" | ||
|
|
||
| # Fetch required status checks from branch rulesets as a JSON array |
There was a problem hiding this comment.
[low] logic-error
The branch rulesets API only returns required checks from the newer rulesets feature. Legacy branch protection rules return an empty array. The script degrades gracefully but this limitation is undocumented.
Suggested fix: Document this limitation in SKILL.md or add a fallback to the legacy branch protection API.
| [$map | to_entries[] | select(.value | test("SUCCESS|NEUTRAL|SKIPPED|COMPLETED|FAILURE|ERROR|CANCELLED|TIMED_OUT|STARTUP_FAILURE|ACTION_REQUIRED") | not) | .key] as $pending | | ||
| # Check for missing required checks | ||
| [$required[] | select(. as $r | $map | has($r) | not)] as $missing | | ||
| {failures: $failures, pending: $pending, missing: $missing} |
There was a problem hiding this comment.
[info] edge-case
ACTION_REQUIRED is treated as a failure state but typically indicates a check needing manual intervention. Consider documenting this behavior.
|
|
||
| Shows each removal event's timestamp, reason (e.g. `failed_checks`, `merge_conflict`), and the commit SHA at the time of removal. | ||
|
|
||
| ## Await and enqueue |
There was a problem hiding this comment.
[info] documentation-order
The Await and enqueue section is placed after Investigate dequeue reasons but is closely related to Enqueue a PR. Consider placing it immediately after that section.
| - **"Pull request is already in the merge queue"** — the PR was previously enqueued; no action needed. | ||
| - **"Pull request is not mergeable"** — the PR may need approvals, passing checks, or conflict resolution before it can be enqueued. | ||
| - **"Resource not accessible by integration"** — the `gh` token lacks sufficient permissions. | ||
| - **"status checks are expected"** — required checks haven't finished yet. Use `await-and-enqueue.sh` to poll and enqueue once they pass. |
There was a problem hiding this comment.
[info] common-errors-consistency
The new error entry starts with a backtick-wrapped command rather than a quoted error message in bold, differing from the pattern of other entries in the Common errors section.
|
🤖 Finished Retro · ✅ Success · Started 4:31 PM UTC · Completed 4:38 PM UTC |
Retro: PR #2400 — feat(merge-queue): add await-and-enqueue scriptTimeline
Review quality assessmentThe review agent performed well. The high-severity finding about StatusContext handling is a confirmed real bug: GitHub's Issues identified (all already tracked)All workflow improvement opportunities found are covered by existing open issues:
ConclusionNo new proposals — all improvement opportunities are well-covered by existing issues. The review agent's signal quality was high (caught a real bug), but the duplicate run after a merge-from-main commit wasted tokens. Prioritizing #1282/#1896 (skip re-review on merge-only commits) and #1956 (auto-file tracking issues for unresolved findings) would have the highest impact for workflows like this one. |
Summary
await-and-enqueue.shthat polls a PR until required checks pass and approvals are present, then enqueues itgh pr merge --autodoes not work with merge queuesGitHub's auto-merge API doesn't support merge queues, so this script fills that gap.
Test plan
e2echeck as pending🤖 Generated with Claude Code