Skip to content

Instantly share code, notes, and snippets.

@svallory
Created March 17, 2026 01:18
Show Gist options
  • Select an option

  • Save svallory/f99cffb9fd5782d28f3c47330cb3386d to your computer and use it in GitHub Desktop.

Select an option

Save svallory/f99cffb9fd5782d28f3c47330cb3386d to your computer and use it in GitHub Desktop.
Skill to monitor Github CI on a PR
name monitor-ci
description Use when waiting for CI checks to complete on a PR, polling for new review comments after a push, or monitoring GitHub Actions status. Triggers on "wait for CI", "check bugbot", "monitor PR", "poll for comments", "wait for checks".

Monitor CI

Poll a GitHub PR for new review comments and check completion. Reports as soon as new threads appear or all checks finish — distinguishing between passed and failed checks.

Usage

Invoke with: /monitor-ci <owner/repo> <pr-number>

If owner/repo is omitted, infer from the current git remote. If pr-number is omitted, infer from the current branch.

How It Works

Run a background Bash command that polls every 30 seconds:

  1. Snapshot the current review thread count and commit SHA
  2. Every 30s, check for new unresolved review threads
  3. Also check if all CI checks on the HEAD commit have completed
  4. Exit and notify as soon as either:
    • New review threads are detected
    • All checks complete with no new threads

Implementation

Run this as a background Bash command so the user can keep working:

# Resolve owner/repo and PR number
REPO="<owner/repo>"
PR=<number>
HEAD_SHA=$(gh pr view $PR --repo "$REPO" --json headRefOid --jq '.headRefOid')

# Snapshot current state
INITIAL_THREADS=$(gh api graphql -f query="
{
  repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
    pullRequest(number: $PR) {
      reviewThreads(first: 100) { totalCount }
    }
  }
}" --jq '.data.repository.pullRequest.reviewThreads.totalCount')

echo "Monitoring PR #$PR ($REPO) | commit: ${HEAD_SHA:0:7} | threads: $INITIAL_THREADS"

while true; do
  sleep 30

  # Check for new threads
  NEW_THREADS=$(gh api graphql -f query="
  {
    repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
      pullRequest(number: $PR) {
        reviewThreads(first: 100) { totalCount }
      }
    }
  }" --jq '.data.repository.pullRequest.reviewThreads.totalCount' 2>/dev/null)

  if [ -n "$NEW_THREADS" ] && [ "$NEW_THREADS" != "$INITIAL_THREADS" ]; then
    DIFF=$((NEW_THREADS - INITIAL_THREADS))
    echo "NEW COMMENTS: $DIFF new thread(s) (was $INITIAL_THREADS, now $NEW_THREADS)"
    break
  fi

  # Check if all checks completed — use gh pr checks which handles encoding safely
  CHECK_STATUS=$(gh pr checks $PR --repo "$REPO" --json name,state,bucket 2>/dev/null)
  if [ -z "$CHECK_STATUS" ]; then
    continue
  fi
  TOTAL=$(echo "$CHECK_STATUS" | jq 'length')
  PENDING=$(echo "$CHECK_STATUS" | jq '[.[] | select(.bucket == "pending")] | length')

  if [ "$TOTAL" -gt 0 ] && [ "$PENDING" = "0" ]; then
    FAILED=$(echo "$CHECK_STATUS" | jq -r '[.[] | select(.bucket == "fail")] | .[] | "  FAILED: \(.name)"')
    FAILED_COUNT=$(echo "$CHECK_STATUS" | jq '[.[] | select(.bucket == "fail")] | length')
    PASSED_COUNT=$(echo "$CHECK_STATUS" | jq '[.[] | select(.bucket == "pass")] | length')

    # Check for new threads one final time
    FINAL_THREADS=$(gh api graphql -f query="
    {
      repository(owner: \"${REPO%%/*}\", name: \"${REPO##*/}\") {
        pullRequest(number: $PR) {
          reviewThreads(first: 100) { totalCount }
        }
      }
    }" --jq '.data.repository.pullRequest.reviewThreads.totalCount' 2>/dev/null)

    NEW_THREAD_MSG=""
    if [ "$FINAL_THREADS" != "$INITIAL_THREADS" ]; then
      DIFF=$((FINAL_THREADS - INITIAL_THREADS))
      NEW_THREAD_MSG=" | $DIFF new comment thread(s)"
    fi

    if [ "$FAILED_COUNT" -gt 0 ]; then
      echo "CHECKS FAILED: $FAILED_COUNT failed, $PASSED_COUNT passed out of $TOTAL${NEW_THREAD_MSG}"
      echo "$FAILED"
      echo ""
      # Fetch logs for failed GitHub Actions runs
      FAILED_RUNS=$(gh run list --repo "$REPO" --commit "$HEAD_SHA" --status failure --json databaseId,name --jq '.[] | "\(.databaseId)\t\(.name)"' 2>/dev/null)
      if [ -n "$FAILED_RUNS" ]; then
        echo "$FAILED_RUNS" | while IFS=$'\t' read -r RUN_ID RUN_NAME; do
          echo "--- Failed: $RUN_NAME (run $RUN_ID) ---"
          gh run view "$RUN_ID" --repo "$REPO" --log-failed 2>/dev/null | tail -80
          echo ""
        done
      fi
      break
    elif [ -n "$NEW_THREAD_MSG" ]; then
      echo "NEW COMMENTS: ${NEW_THREAD_MSG# | } (all $TOTAL checks passed)"
      break
    else
      echo "ALL CLEAR: $TOTAL/$TOTAL checks passed, no new comments."
      break
    fi
  fi
done

After Notification

When the monitor reports back:

  • CHECKS FAILED: Fetch the failed check logs, diagnose the failures, and report them to the user. If fixable, fix, commit, push, then re-invoke /monitor-ci.
  • NEW COMMENTS: Fetch and display unresolved threads, fix issues, commit, push, then re-invoke /monitor-ci
  • ALL CLEAR: CI is green with no new review findings. Inform the user.

Fetching Unresolved Threads

gh api graphql -f query='
{
  repository(owner: "OWNER", name: "REPO") {
    pullRequest(number: PR) {
      reviewThreads(first: 30) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes { body }
          }
        }
      }
    }
  }
}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)]
  | .[] | "\(.id)\n\(.comments.nodes[0].body | split("<!-- DESCRIPTION START -->") | .[1] // "" | split("<!-- DESCRIPTION END -->") | .[0] // .comments.nodes[0].body[0:200])\n==="'

Resolving Threads

gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "THREAD_ID"}) { thread { isResolved } } }'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment