Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

github-review-poller

Polls GitHub on a schedule for pull requests waiting on your review and DMs you in Slack. Runs locally. No server, no Python dependencies, no Slack app.

scheduler (default every 50 min)
  └─ poller.py
     ├─ gh auth token  ──▶  GitHub: review-requested + team-review-requested
     ├─ age window, diff against seen.json
     └─ if anything is new:  claude -p ──▶ Slack MCP ──▶ DM

Requires Python 3.8+, gh logged in, and the claude CLI with Slack's MCP connected. Works on macOS (launchd), Linux (systemd or cron), and Windows (Task Scheduler).

Install

gh auth refresh -h github.com -s read:org    # read:org is required, see below
python3 install.py
python3 poller.py --check                    # confirm; non-zero if anything is wrong

The installer asks for your teams, the age window, and your Slack member ID, then writes the config, sends a test DM, silently adopts your current backlog so day one isn't a flood, and schedules the job.

To script it instead, see Driving this from an agent.

read:org is not optional

Two searches run per cycle, because review-requested:@me does not match PRs where a team you belong to was asked:

is:open is:pr archived:false review-requested:@me
is:open is:pr archived:false team-review-requested:<org>/<team>

Without read:org the second silently returns nothing and you miss an entire class of request. Relatedly, if GitHub can't resolve a team slug it drops the qualifier rather than erroring, matching every open PR on GitHub — so any result over 200 is refused and --check names the query.

Configuration

config.json in the config directory (poller.py --check prints the path). Plain JSON; missing keys use defaults. Changes apply on the next poll, except interval_seconds.

Key Default Meaning
teams [] "org/team" slugs to watch. Omitted teams go unnoticed.
ignore_drafts false Skip draft PRs.
interval_seconds 3000 50 min. Floor 30. Needs install.py --reschedule to take effect.
max_age_days 30 Only notify about PRs active in N days. 0 = no limit.
gh_path "" Absolute path to gh. "" auto-detects.
claude_path "" Absolute path to claude. "" auto-detects.
slack_user_id "" Your Slack member ID (U…), the DM target.

Two things that fail silently if you get them wrong:

  • interval_seconds is baked into the scheduled job at install time. Editing the file alone changes nothing; run install.py --reschedule.
  • Set the absolute paths. Schedulers run with a near-empty PATH that excludes Homebrew and ~/.local/bin, so auto-detection works by hand and fails once scheduled. The installer writes them for you.

GitHub token resolves as $GRP_GITHUB_TOKENgh auth tokensecrets.json. Read fresh each run and never cached, so gh rotation needs no reinstall. secrets.json is the fallback if gh's credential store is unreadable from a background job.

Environment overrides: GRP_GITHUB_TOKEN, GRP_CONFIG_DIR, GRP_STATE_DIR, GRP_LOG_DIR. launchd and systemd bake any GRP_* set at install time into the job; cron and Task Scheduler do not.

The age window

max_age_days skips requests that have gone quiet, measured on last activity, not open date:

max_age_days: 30
Opened 4 months ago, review re-requested today notifies
Opened 10 days ago, untouched for 40 days skipped

Stale PRs are marked rather than dropped, so retuning the window never replays old PRs as new. (Why: reviewpoller/github.py.)

Removing it

python3 install.py --purge     # job + every file created; lists them, then asks
python3 install.py --uninstall # job only, keeps config

By hand — run --check first to print the real paths, which move if $XDG_CONFIG_HOME, %APPDATA%, or GRP_*_DIR are set:

# macOS
launchctl bootout "gui/$(id -u)/com.github-review-poller"
rm -f  ~/Library/LaunchAgents/com.github-review-poller.plist
rm -rf ~/Library/Application\ Support/github-review-poller ~/Library/Logs/github-review-poller

# Linux (systemd; without it, remove the crontab line tagged # github-review-poller)
systemctl --user disable --now github-review-poller.timer
rm -f ~/.config/systemd/user/github-review-poller.{service,timer}
systemctl --user daemon-reload
rm -rf ~/.config/github-review-poller ~/.local/state/github-review-poller

# Windows
schtasks /delete /tn GitHubReviewPoller /f
rmdir /s /q "%APPDATA%\github-review-poller" "%LOCALAPPDATA%\github-review-poller"

Your gh login is never touched — this stores no GitHub token.

Driving this from an agent

gh auth login and connecting the Slack MCP are browser OAuth flows a human does once. Everything else is scriptable:

python3 install.py --non-interactive \
    --slack-user-id U0123ABCDEF \
    --teams org/a,org/b \
    --max-age-days 30 --interval 3000

python3 poller.py --check --json
python3 install.py --reschedule
python3 install.py --purge --yes

Exactly one of --teams, --all-teams, --no-teams is required — not defaulted, because a missing team silently means never hearing about its requests. --non-interactive never prompts or guesses; anything it can't determine is an error naming the flag that fixes it.

Exit Meaning
0 Success
2 Config incomplete or invalid — nothing written
3 Installed, but the test DM could not be confirmed

--check --json returns the same information as the human report with the same exit codes (0 clean, 1 problems). Keys: ok, failures, warnings, paths, config, binaries, github, slack, state, scheduler. Check ok, read failures. github.token_source says where the credential came from, never its value.

Setting GRP_CONFIG_DIR/GRP_STATE_DIR also scopes the job name, so a second instance coexists with a live one and a scoped --purge can't reach through and remove it.

Why delivery goes through claude -p

Bot tokens and incoming webhooks need an installed Slack app; workflow webhooks need Workflow Builder. Where an administrator allows none of those, there's no credential to hold — but an already-connected Slack MCP is itself an authorised path, and its OAuth lives on disk, so a scheduled job can reach it via claude -p.

Python composes the exact message text and hands Claude one tool, so nothing varies run to run. Costs: no ok: true to check (success requires an exact sentinel, biased toward a duplicate DM over a dropped one), 10–30s per cycle that has something to send, and model tokens — quiet cycles invoke nothing.

If you can get a bot token, prefer it — faster, free, real delivery receipt. Notifier.send(text) is the only interface to replace.

Commands

python3 poller.py                       # one poll (what the scheduler runs)
python3 poller.py --check [--json]      # diagnose; --post-test sends a real DM
python3 poller.py --dry-run             # what would be sent; touches nothing
python3 poller.py --seed [--announce]   # adopt the backlog, optionally summarise it
python3 poller.py --watch               # poll in the foreground
python3 install.py [--non-interactive | --reschedule | --uninstall | --purge]
python3 -m unittest discover -s tests -v

Behaviour

  • First run seeds silently — your backlog is adopted, not replayed.
  • A delivery failure does not mark the PR seen, so the next cycle retries.
  • Resolved requests are pruned, which is what lets a re-request notify again.
  • State is per-machine; two machines each DM you once.
  • Transient failures exit 0 — a closed laptop lid isn't a scheduler failure.

Troubleshooting

Run python3 poller.py --check first.

Symptom Cause
Works by hand, silent once scheduled gh_path/claude_path unset — the stripped-PATH trap
Schedule doesn't match config.json Needs install.py --reschedule
Auth error only when scheduled Background job can't read gh's credential store; put a PAT in secrets.json
No team PRs found Missing read:org
A query reports a refused count Unresolvable team slug in config.json
Private repos return nothing Token lacks repo, or needs SSO authorization
claude did not confirm delivery Run --check --post-test to see the actual reply

poller.log (self-truncating at 1 MB) holds the structured log on every platform. Anything escaping the logger goes to launchd-stderr.log on macOS, the journal (journalctl --user -u github-review-poller) on systemd, local mail under cron, and nowhere on Windows.

About

Get a Slack DM when a GitHub pull request is waiting on your review. Cross-platform, stdlib-only Python.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages