Skip to content

Instantly share code, notes, and snippets.

@icaromh
Last active November 7, 2025 17:08
Show Gist options
  • Save icaromh/1244c258b68408796e7375eaf1662ed9 to your computer and use it in GitHub Desktop.
Save icaromh/1244c258b68408796e7375eaf1662ed9 to your computer and use it in GitHub Desktop.

PR Check - Pipeline Status Monitor

Monitors your GitHub Pull Request pipeline and notifies you when all checks pass or any check fails.

Note

Currently works only on macOS.

Installation

You need two things:

  1. npx which you can get with Node.js
  2. gh (GitHub CLI) and you must be logged in

Usage

Run:

npx zx https://raw.githubusercontent.com/icaromh/pr-check/main/pr-check.mjs org repo pr

To automatically mark a draft PR as ready when all checks pass, add the --ready flag:

npx zx https://raw.githubusercontent.com/icaromh/pr-check/main/pr-check.mjs org repo pr --ready

Ok, that's long, so maybe create an alias:

alias prcheck='npx zx https://raw.githubusercontent.com/icaromh/pr-check/main/pr-check.mjs'
prcheck org repo pr
prcheck org repo pr --ready  # Mark as ready when checks pass

Make it permanent by adding the alias to your shell profile (e.g. ~/.zshrc or ~/.bashrc).

org/repo/pr is your GitHub organization, repository, and pull request number. Example:

prcheck acme-inc better-db 123
prcheck acme-inc better-db 123 --ready  # Mark as ready when checks pass

Auto-marking Draft PRs as Ready

When you use the --ready flag, the script will:

  • Monitor your PR's check runs as usual
  • If all checks pass AND the PR is currently a draft β†’ automatically mark it as "ready for review"
  • If the PR is already open (not a draft) β†’ just notify that checks passed
  • If any check fails β†’ won't mark as ready

Example workflow:

  1. Create a draft PR with your changes
  2. Run the monitor with --ready flag:
    prcheck acme-inc better-db 456 --ready
  3. The script monitors all checks
  4. When all checks pass, it automatically converts your draft to "ready for review"
  5. You get a notification: "PR #456 marked as ready and all checks passed"

If you work often with an organization or a repository you can create more aliases:

alias prcheck='npx zx https://raw.githubusercontent.com/icaromh/pr-check/main/pr-check.mjs'
alias prcheck-acme='prcheck acme-inc'
alias prcheck-bdb='prcheck-acme better-db'

prcheck-acme foo 2 # Will check PR 2 for acme-inc/foo
prcheck-bdb 1 # Will check PR 1 for acme-inc/better-db

Local Usage

If you have the script file locally:

npx zx pr-check.mjs org repo pr

Or with an alias pointing to the local file:

alias prcheck='npx zx /path/to/pr-check.mjs'
prcheck org repo pr

What it does

The script will:

  1. πŸ” Fetch the specified pull request

  2. πŸ•’ Monitor all check runs every 20 seconds

  3. πŸ“Š Display real-time status of each check with emojis:

    • βœ… Success
    • ❌ Failure
    • πŸ•’ In Progress
    • ⏩ Skipped
    • 🚫 Cancelled
    • ⏰ Timed Out
    • ❗ Action Required
  4. ⏰ Track and display elapsed time

  5. οΏ½ Optionally mark draft PR as ready (with --ready flag)

  6. οΏ½πŸ”” Show a macOS notification when complete

  7. πŸ”Š Play a sound notification

  8. Exit with status code:

    • 0 if all checks pass
    • 1 if any check fails

Let the script run. A notification window and sound will alert you when all checks are complete or when a check fails.

Example Output

# Fix bug in authentication flow
πŸ™ https://github.com/acme-inc/better-db/pull/123

Checks:
----------------------------------
βœ…  Build
βœ…  Lint
βœ…  Test
πŸ•’  Deploy Preview

⏳ Not all checks are completed yet. Retrying in 20 seconds...

When complete:

# Fix bug in authentication flow
πŸ™ https://github.com/acme-inc/better-db/pull/123

Checks:
----------------------------------
βœ…  Build
βœ…  Lint
βœ…  Test
βœ…  Deploy Preview

⏰ Took: 3m 42s

----------------------------------
🏁 All checks are completed! πŸŽ‰

βœ… PR #123 - All checks passed successfully

If using --ready flag with a draft PR:

----------------------------------
🏁 All checks are completed! πŸŽ‰

βœ… PR #123 - All checks passed successfully

πŸ”„ Marking PR #123 as ready for review...
βœ… PR #123 is now ready for review

Or if there's a failure:

# Fix bug in authentication flow
πŸ™ https://github.com/acme-inc/better-db/pull/123

Checks:
----------------------------------
βœ…  Build
βœ…  Lint
❌  Test
βœ…  Deploy Preview

----------------------------------
❌ Failures in the pipeline

⏰ Took: 2m 15s

❌ PR #123 has check failures

Requirements

  • macOS (for sound and notification support)
  • Node.js (for npx)
  • GitHub CLI (gh) authenticated with appropriate repository access

Troubleshooting

If you get an error about gh not being installed:

brew install gh
gh auth login

If you get permission errors, make sure your GitHub token has access to the repository:

gh auth status
#!/usr/bin/env zx
/*
* PR Check
* --------
* Author: Icaro Heimig
* Description:
* Watches a GitHub pull request's check runs and notifies
* you when all checks pass or any check fails.
* Optionally marks draft PRs as ready when all checks pass.
*
* Requirements:
* - Node.js with zx (`npx zx <script>`)
* - GitHub CLI (`gh`) authenticated and available in PATH
* - macOS (uses `afplay` for sound notifications)
*
* Usage:
* npx zx pr-check.mjs <org> <repo> <prNumber> [--ready]
*
* Options:
* --ready Mark PR as ready for review when all checks pass
*
* License: MIT
*/
if ((await which('gh', { nothrow: true })) === null) {
console.error('This script requires the GitHub CLI (gh) to be installed.');
process.exit(1);
}
if (argv._.length !== 3) {
console.error(
'Invalid input format. Expected format: org repo prNumber [--ready]'
);
process.exit(1);
}
const [org, repo, prNumber] = argv._;
const markAsReady = argv.ready || false;
const POLLING_TIME = 20_000; // 20 seconds in milliseconds
function clearTerminalScreen() {
console.clear();
process.stdout.write('\x1Bc');
}
function parseCheckConclusion(conclusion) {
const statusMap = {
skipped: '⏩',
success: 'βœ…',
failure: '❌',
cancelled: '🚫',
neutral: 'βšͺ',
timed_out: '⏰',
action_required: '❗',
};
return statusMap[conclusion] || '❓';
}
function parseCheckStatus(status) {
return status === 'in_progress' ? 'πŸ•’' : '⏸️';
}
async function getPullRequest(org, repo, prNumber) {
const result = JSON.parse(
await $`gh api repos/${org}/${repo}/pulls/${prNumber}`
);
return result;
}
async function getCheckRuns(org, repo, sha) {
const result = JSON.parse(
await $`gh api repos/${org}/${repo}/commits/${sha}/check-runs --jq '.check_runs'`
);
return result;
}
async function checkPR(org, repo, prNumber) {
const pullRequest = await getPullRequest(org, repo, prNumber);
const sha = pullRequest.head.sha;
const initialTime = new Date().getTime();
let allChecksCompleted = false;
let hasErrors = false;
let checkRuns = [];
while (!allChecksCompleted && !hasErrors) {
clearTerminalScreen();
checkRuns = await getCheckRuns(org, repo, sha);
// Remove duplicates based on title or name, using completed_at to disambiguate
const checkMap = new Map();
checkRuns.forEach((check) => {
const key = check.output?.title || check.name;
if (!key) return; // Skip entries without title or name
const existing = checkMap.get(key);
if (!existing) {
checkMap.set(key, check);
} else {
// Disambiguate using completed_at - keep the most recent one
const existingCompletedAt = existing.completed_at
? new Date(existing.completed_at).getTime()
: 0;
const currentCompletedAt = check.completed_at
? new Date(check.completed_at).getTime()
: 0;
if (currentCompletedAt > existingCompletedAt) {
checkMap.set(key, check);
}
}
});
checkRuns = Array.from(checkMap.values());
// Check if any check has failed
hasErrors = checkRuns.some((check) =>
['failure', 'cancelled'].includes(check.conclusion)
);
// Check if all checks are completed
allChecksCompleted = checkRuns.every(
(check) => check.status === 'completed' || check.conclusion === 'skipped'
);
// Display PR info
console.log(`# ${pullRequest.title}`);
console.log(`πŸ™ ${pullRequest.html_url}\n`);
console.log(`Checks:\n----------------------------------`);
// Display check statuses
checkRuns.forEach((check) => {
const title = check.output?.title || check.name;
if (check.status === 'in_progress') {
console.log(`${parseCheckStatus('in_progress')} ${title}`);
} else if (check.conclusion) {
console.log(`${parseCheckConclusion(check.conclusion)} ${title}`);
}
});
if (hasErrors) {
console.log('\n----------------------------------');
console.log('❌ Failures in the pipeline\n');
break;
}
if (!allChecksCompleted) {
console.log(
`\n⏳ Not all checks are completed yet. Retrying in ${
POLLING_TIME / 1000
} seconds...`
);
await sleep(POLLING_TIME);
}
}
// Calculate elapsed time
const elapsedSeconds = (new Date().getTime() - initialTime) / 1000;
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
const remainingSeconds = Math.round(elapsedSeconds % 60);
console.log(`⏰ Took: ${elapsedMinutes}m ${remainingSeconds}s\n`);
return { hasErrors, pullRequest };
}
const result = await spinner(
`Checking ${org}/${repo}#${prNumber}`,
async () => await checkPR(org, repo, prNumber)
);
// Play sound
$`afplay /System/Library/Sounds/Glass.aiff`;
if (result.hasErrors) {
console.log(`\n❌ PR #${prNumber} has check failures`);
await $`osascript -e 'display notification "PR #${prNumber} has check failures" with title "Pipeline Failed" sound name "Glass"'`;
process.exit(1);
} else {
console.log('\n----------------------------------');
console.log('🏁 All checks are completed! πŸŽ‰\n');
console.log(`βœ… PR #${prNumber} - All checks passed successfully`);
// Mark PR as ready if it's a draft and --ready flag is set
if (markAsReady && result.pullRequest.draft) {
console.log(`\nπŸ”„ Marking PR #${prNumber} as ready for review...`);
try {
await $`gh pr ready ${prNumber} --repo ${org}/${repo}`;
console.log(`βœ… PR #${prNumber} is now ready for review`);
await $`osascript -e 'display notification "PR #${prNumber} marked as ready and all checks passed" with title "Pipeline Success" sound name "Glass"'`;
} catch (error) {
console.error(`⚠️ Failed to mark PR as ready: ${error.message}`);
await $`osascript -e 'display notification "All checks passed for PR #${prNumber}" with title "Pipeline Success" sound name "Glass"'`;
}
} else if (markAsReady && !result.pullRequest.draft) {
console.log(`ℹ️ PR #${prNumber} is already ready for review`);
await $`osascript -e 'display notification "All checks passed for PR #${prNumber}" with title "Pipeline Success" sound name "Glass"'`;
} else {
await $`osascript -e 'display notification "All checks passed for PR #${prNumber}" with title "Pipeline Success" sound name "Glass"'`;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment