Last active
July 8, 2025 14:48
-
-
Save the21st/aeab60a5b4fb4e3d763f4a6762009d0f to your computer and use it in GitHub Desktop.
prwatch – a script that watches a PR's checks and send a OS notification once all pass or some are failing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Script to watch GitHub PR checks and send a notification. | |
| # Usage: ./prwatch.sh [--all] <FULL_PR_URL> | |
| # --all: Wait for all checks to complete before reporting (don't fail eagerly) | |
| # Prerequisites: gh (GitHub CLI) and jq must be installed and configured. | |
| # --- Configuration: Customize your notification command --- | |
| # Uncomment the line for your OS and comment out/delete the others. | |
| # You can also customize the message further if you like. | |
| # macOS: | |
| send_notification() { | |
| # Title, Message | |
| osascript -e "display notification \"$2\" with title \"$1\"" | |
| } | |
| # Linux (requires libnotify-bin or similar package for notify-send): | |
| # send_notification() { | |
| # # Title, Message | |
| # notify-send "$1" "$2" | |
| # } | |
| # Windows (using PowerShell - make sure BurntToast module is installed, or use msg.exe): | |
| # This is a bit more involved to call directly from bash, often easier to have a separate .ps1 | |
| # or use WSL's interop if you're in that environment. | |
| # For a simpler (but less pretty) built-in Windows notification via WSL or Git Bash: | |
| # send_notification() { | |
| # # Title (ignored by msg), Message | |
| # msg * "$1: $2" | |
| # } | |
| # --- End Configuration --- | |
| if [ -z "$1" ]; then | |
| echo "Usage: $0 [--all] <FULL_PR_URL>" | |
| echo " --all: Wait for all checks to complete before reporting final result" | |
| echo " (instead of failing eagerly when first check fails)" | |
| exit 1 | |
| fi | |
| # Parse parameters | |
| WAIT_FOR_ALL=false | |
| PR_URL="" | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --all) | |
| WAIT_FOR_ALL=true | |
| shift | |
| ;; | |
| -*) | |
| echo "Unknown option $1" | |
| echo "Usage: $0 [--all] <FULL_PR_URL>" | |
| exit 1 | |
| ;; | |
| *) | |
| if [ -z "$PR_URL" ]; then | |
| PR_URL="$1" | |
| else | |
| echo "Error: Multiple PR URLs provided. Only one is allowed." | |
| echo "Usage: $0 [--all] <FULL_PR_URL>" | |
| exit 1 | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| if [ -z "$PR_URL" ]; then | |
| echo "Error: No PR URL provided." | |
| echo "Usage: $0 [--all] <FULL_PR_URL>" | |
| exit 1 | |
| fi | |
| # Attempt to get a nice identifier for the PR (e.g., owner/repo#123) | |
| # If this fails, it might be an invalid URL or gh issue. | |
| PR_IDENTIFIER=$(gh pr view "$PR_URL" --json "number,repository" --template '{{.repository.owner.login}}/{{.repository.name}}#{{.number}}' 2>/dev/null) | |
| if [ -z "$PR_IDENTIFIER" ]; then | |
| echo "Error: Could not retrieve PR information. Is the URL valid and gh configured?" | |
| # Fallback to using the raw URL if identifier fetch fails but URL might still work for status | |
| PR_IDENTIFIER_MSG="PR at $PR_URL" | |
| else | |
| PR_IDENTIFIER_MSG="$PR_IDENTIFIER" | |
| fi | |
| echo "Watching PR: $PR_IDENTIFIER_MSG" | |
| if $WAIT_FOR_ALL; then | |
| echo "Mode: Wait for all checks to complete before reporting final result." | |
| else | |
| echo "Mode: Report immediately when any check fails (eager fail)." | |
| fi | |
| echo "Will notify when checks complete (all pass or one fails)." | |
| echo "Prerequisites: gh and jq installed. Notification command configured in script." | |
| echo "---" | |
| while true; do | |
| # Fetch all check states for the PR using 'gh pr checks'. | |
| # This provides a more direct and reliable list of states than statusCheckRollup. | |
| ALL_STATES_RAW=$(gh pr checks "$PR_URL" --json state --jq '.[] | .state' 2>/dev/null) | |
| # Check if the command failed or returned no output (e.g., network issue, invalid PR) | |
| if [ $? -ne 0 ] || [ -z "$ALL_STATES_RAW" ]; then | |
| CURRENT_TIMESTAMP=$(date +"%T") | |
| # Check if it's the initial PR_IDENTIFIER fetch that's failing, or the checks fetch | |
| # The PR_IDENTIFIER_MSG is already set based on an earlier gh call. | |
| # If PR_IDENTIFIER is empty, it means the initial gh pr view for metadata failed. | |
| if [ -z "$PR_IDENTIFIER" ] && [[ "$PR_IDENTIFIER_MSG" == "PR at $PR_URL" ]]; then | |
| echo "[$CURRENT_TIMESTAMP] Error: Could not retrieve initial PR information. Is the URL valid and gh configured? Retrying in 30s..." | |
| else | |
| echo "[$CURRENT_TIMESTAMP] Error: Failed to fetch PR check statuses from GitHub. Retrying in 30s..." | |
| fi | |
| sleep 30 | |
| continue | |
| fi | |
| ALL_STATES=$(echo "$ALL_STATES_RAW" | sort | uniq) | |
| # Determine current overall state based on ALL_STATES | |
| # The logic differs based on WAIT_FOR_ALL mode: | |
| # - Eager mode (default): Report failure/error immediately, success when no pending/failing | |
| # - Wait-all mode: Only report final result when no checks are pending/in-progress | |
| if $WAIT_FOR_ALL; then | |
| # Wait-all mode: Only proceed to final result when no checks are pending | |
| if echo "$ALL_STATES" | grep -q "IN_PROGRESS"; then | |
| CURRENT_STATE="PENDING" | |
| elif echo "$ALL_STATES" | grep -q "PENDING"; then | |
| CURRENT_STATE="PENDING" | |
| elif echo "$ALL_STATES" | grep -q "QUEUED"; then | |
| CURRENT_STATE="PENDING" | |
| # All checks have completed, now determine final result | |
| elif echo "$ALL_STATES" | grep -q "FAILURE"; then | |
| CURRENT_STATE="FAILURE" | |
| elif echo "$ALL_STATES" | grep -q "ERROR"; then | |
| CURRENT_STATE="ERROR" | |
| elif echo "$ALL_STATES" | grep -q "SUCCESS"; then | |
| CURRENT_STATE="SUCCESS" | |
| elif echo "$ALL_STATES" | grep -q "SKIPPED"; then | |
| CURRENT_STATE="SUCCESS" # Treat "all skipped" as success | |
| elif [ -z "$ALL_STATES" ]; then | |
| if echo "$ALL_STATES_RAW" | grep -q '[a-zA-Z]'; then | |
| CURRENT_STATE="UNKNOWN" | |
| else | |
| CURRENT_STATE="NO_CHECKS" | |
| fi | |
| else | |
| CURRENT_STATE="UNKNOWN" | |
| fi | |
| else | |
| # Eager mode (original behavior): Report failure/error immediately | |
| # Order of precedence: FAILURE > ERROR > PENDING > SUCCESS > NO_CHECKS / UNKNOWN | |
| if echo "$ALL_STATES" | grep -q "FAILURE"; then | |
| CURRENT_STATE="FAILURE" | |
| elif echo "$ALL_STATES" | grep -q "ERROR"; then # Though 'ERROR' is not a typical state from 'gh pr checks', keep for robustness | |
| CURRENT_STATE="ERROR" | |
| elif echo "$ALL_STATES" | grep -q "IN_PROGRESS"; then # 'gh pr checks' uses IN_PROGRESS | |
| CURRENT_STATE="PENDING" | |
| elif echo "$ALL_STATES" | grep -q "PENDING"; then # Generic PENDING state | |
| CURRENT_STATE="PENDING" | |
| elif echo "$ALL_STATES" | grep -q "QUEUED"; then # Generic QUEUED state | |
| CURRENT_STATE="PENDING" | |
| elif echo "$ALL_STATES" | grep -q "SUCCESS"; then | |
| # If SUCCESS is present, and no failure/error/pending states were found, | |
| # it means all non-skipped checks are successful, or only success/skipped exist. | |
| CURRENT_STATE="SUCCESS" | |
| elif echo "$ALL_STATES" | grep -q "SKIPPED"; then | |
| # If only SKIPPED states are left (or SKIPPED and unknown states not caught above), | |
| # consider it a success as nothing is actively failing or pending. | |
| CURRENT_STATE="SUCCESS" # Treat "all skipped" or "skipped among non-critical" as success | |
| elif [ -z "$ALL_STATES" ]; then | |
| # This means ALL_STATES_RAW was empty after sort|uniq, which implies no checks reported. | |
| # This could also happen if ALL_STATES_RAW contained only newlines or whitespace. | |
| # Let's refine to check if ALL_STATES_RAW effectively yielded no actual states. | |
| if echo "$ALL_STATES_RAW" | grep -q '[a-zA-Z]'; then # Check if there was any actual state text | |
| CURRENT_STATE="UNKNOWN" # Had some text, but didn't match known states after uniq | |
| else | |
| CURRENT_STATE="NO_CHECKS" # Truly no states reported | |
| fi | |
| else | |
| # Contains states not explicitly handled (e.g., new GitHub states) | |
| CURRENT_STATE="UNKNOWN" | |
| fi | |
| fi | |
| CURRENT_TIMESTAMP=$(date +"%T") | |
| case "$CURRENT_STATE" in | |
| "SUCCESS") | |
| echo "[$CURRENT_TIMESTAMP] ✅ All checks passed for $PR_IDENTIFIER_MSG!" | |
| send_notification "✅ PR Checks Passed!" "All checks for $PR_IDENTIFIER_MSG have passed." | |
| break | |
| ;; | |
| "FAILURE") | |
| echo "[$CURRENT_TIMESTAMP] ❌ Checks failed for $PR_IDENTIFIER_MSG." | |
| send_notification "❌ PR Checks Failed!" "One or more checks for $PR_IDENTIFIER_MSG failed." | |
| break | |
| ;; | |
| "ERROR") | |
| echo "[$CURRENT_TIMESTAMP] ⚠️ Checks reported an error for $PR_IDENTIFIER_MSG." | |
| send_notification "⚠️ PR Checks Errored!" "Checks for $PR_IDENTIFIER_MSG reported an error." | |
| break | |
| ;; | |
| "NO_CHECKS") | |
| echo "[$CURRENT_TIMESTAMP] ℹ️ No checks defined for $PR_IDENTIFIER_MSG." | |
| send_notification "ℹ️ No Checks" "No checks were run for $PR_IDENTIFIER_MSG." | |
| break | |
| ;; | |
| "PENDING") | |
| echo "[$CURRENT_TIMESTAMP] ⏳ Checks pending for $PR_IDENTIFIER_MSG..." | |
| # No notification, just wait and loop again. | |
| ;; | |
| "UNKNOWN") | |
| echo "[$CURRENT_TIMESTAMP] 🤔 Unknown state: '$CURRENT_STATE' for $PR_IDENTIFIER_MSG. Will re-check." | |
| # This case should ideally not be hit if GitHub API is consistent. | |
| ;; | |
| *) | |
| echo "[$CURRENT_TIMESTAMP] 🤔 Unknown state: '$CURRENT_STATE' for $PR_IDENTIFIER_MSG. Will re-check." | |
| # This case should ideally not be hit if GitHub API is consistent. | |
| ;; | |
| esac | |
| sleep 5 # Wait 5 seconds before checking again | |
| done | |
| echo "---" | |
| echo "Monitoring finished for $PR_IDENTIFIER_MSG." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment