Skip to content

About

Keeps your open PRs up to date and notifies you when one conflicts. A GitHub CLI extension.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

22 Commits

Folders and files

Repository files navigation

gh-pr-watcher

gh-pr-watcher

Keeps your open PRs up to date and tells you when one runs into a conflict.
A GitHub CLI extension that runs on its own a few times a day.

CI Release License: MIT gh extension Stars

English · Español


It does what GitHub's "Update branch" button does, but for you and on all your PRs at once. Nothing changes in the repo: no Actions, no bots, no new permissions. It never resolves conflicts, rebases or forces anything: when a PR conflicts, it only notifies you. It also tells you, once, when a review requests changes and, if you turn it on, when CI fails on a branch it updated. If you ask for it, it also approves the PRs of the people you choose once their CI passes.

Built for repos with the "Require branches to be up to date before merging" rule: every merge to the base leaves all open PRs behind, and each one has to be updated before it can merge.

Conflict notification

What a run looks like (the same lines go to gh pr-watcher log):

$ gh pr-watcher run
2026-10-06 10:00:04 acme/api#412: 3 behind → branch updated from main (CI will run)
2026-10-06 10:00:06 acme/api#418: up to date
2026-10-06 10:00:09 acme/web#97: CONFLICT (5 behind)

Install

gh extension install ezeed/gh-pr-watcher
gh pr-watcher install          # asks for the config and schedules the run
gh pr-watcher run --dry-run    # test: reports what it would do, without updating any branch

To pin a version: gh extension install ezeed/gh-pr-watcher --pin v0.3.0 (see releases).

With an agent

Ask your coding agent (Claude Code, Codex, Cursor…):

Install gh-pr-watcher following https://github.com/ezeed/gh-pr-watcher/blob/main/AGENTS.md

It reads your accounts, orgs and repos with detect and installs with flags, no console prompts:

gh pr-watcher detect
gh pr-watcher install --owners "AcmeCorp" --every 1h

Requirements

  • macOS for scheduling (launchd). On Linux, run works the same and install gives you the crontab line.
  • gh authenticated (gh auth login), with write access to each PR's branch. Your PRs in the org's repo already have it. From a fork, the PR needs "Allow edits from maintainers".
  • jq (brew install jq).
  • Optional: terminal-notifier (brew install terminal-notifier), so clicking the notification opens the PR. Without it, notifications go through osascript and aren't clickable.

Configuration

It lives in ~/.config/gh-pr-watcher/config: KEY=VALUE lines that are read, never executed. Changes apply from the next run; only EVERY_MINUTES needs gh pr-watcher install --yes to reschedule.

Key Default What it controls
GH_USER (empty) gh account to use, even if another one is active. Empty = the active gh account on each run.
OWNERS (empty) Orgs or users whose repos are watched, space-separated. Empty = all your open PRs, in any repo. The installer suggests your orgs.
REPOS (empty) Specific owner/repo repos, space-separated. If set, only those are watched and OWNERS is ignored (GitHub search combines org and repo with AND, not OR).
EVERY_MINUTES 60 How often it runs, in minutes: 15, 30, 60, 120 or 180, counting from 0:00 (180 = 0:00, 3:00, 6:00…). If the machine is asleep at that time, launchd runs it on wake. Each update triggers CI, so a shorter interval can mean more CI runs when several merges land close together. A config with the old EVERY_HOURS keeps working until the next install.
INCLUDE_DRAFTS false true = drafts get updated too.
SKIP_LABEL (empty) PRs with this label are left alone. The installer offers no-autoupdate; in the file you can use any label.
NOTIFY_IMAGE (the logo) Local image (PNG or JPG) shown on the right of the notification. Only with terminal-notifier. The icon on the left can't be changed: macOS takes it from the notifying app.
NOTIFY_CHANGES_REQUESTED true true = one notification when a review moves a PR to changes requested. It isn't repeated while the PR stays that way; if it's approved and changes get requested again, you're notified again.
NOTIFY_CI_FAILED false true = one notification when CI fails on a branch the watcher updated. Only on its own merge commit: if you push in between or on top, it stops following it. It stops after 24 h without a result too.
AUTO_APPROVE_AUTHORS (empty) GitHub logins, space-separated, whose open PRs in the watched orgs or repos get approved as your account. Empty = nothing is approved.
AUTO_APPROVE_UNTIL (empty) Last day it approves, YYYY-MM-DD, inclusive. Empty = no end date.
AUTO_APPROVE_REQUIRE_CI true true = approve only when the PR has checks and every one passed (success, neutral or skipped). Pending, failing or none: it tries again next run.

A misspelled key (OWNER=) or a line that isn't KEY=VALUE isn't silently ignored: you get a warning, with a suggestion if it looks like a real key. install also checks that every owner and repo exists and that your account can see it, and that every login in AUTO_APPROVE_AUTHORS exists.

Auto-approval approves code on your behalf. Each run searches the open PRs of those logins in the same orgs or repos as your own (with neither set, in any repo) and approves the ones without a live approval of yours: not drafts, not PRs with SKIP_LABEL, never your own. A push that dismisses your approval, or a review where you requested changes, makes it approve again. That's why it waits for CI by default and why an end date is a good idea: --auto-approve without one gives it a week. Once it's past, it approves nothing and logs it once.

The log and state live in ~/.local/state/gh-pr-watcher/. While a PR stays conflicting, you get reminded at most once an hour (right away if a new one shows up or the list changes): a single notification for all of them, which opens the PR if there is one or a local page listing them if there are several. The state only remembers since when each conflict has been open: deleting it is harmless. Reviews that request changes follow the same scheme (one notification, or a page if several arrive in the same run), but only once per PR; the same goes for CI failing after an update. Each log keeps its last 2000 lines (from days to a month, depending on the frequency and how many PRs are open); older ones are dropped on each run.

Commands

Command What it does
install [options] Saves the config and schedules the run (launchd on macOS; on Linux it prints the crontab line). With a terminal and no options, it asks.
run [--dry-run] Runs once now. With --dry-run it reports what it would do, without updating branches, notifying or saving state.
list [--json] Your open PRs as a run sees them (same filters), the ones that need you first: conflict, changes requested, CI failing, behind, up to date. Shows state, CI, reviews, what the last run did, age and title. Changes nothing. --json gives one object per PR.
status [--json] Config, whether the agent is loaded, the last run, the last error and the tail of the log.
doctor [--json] Checks everything a run needs and says how to fix each problem. Changes nothing.
log [N] The last N log lines (30 by default).
config The config file path and contents.
test-notify Sends a test notification.
detect [--user LOGIN] JSON with accounts, orgs, repos with your PRs, dependencies, config and current install. Meant for agents.
uninstall Removes the schedule. Config and log stay.
version · help Version and help.

A misspelled command suggests the right one (rub → run). Errors go to stderr prefixed with error: and a non-zero exit code.

install options

With any of these options (except --dry-run), install asks nothing: the options override your existing config and anything missing comes from the defaults.

Option What it does
--user LOGIN gh account to use (GH_USER).
--owners "A B" Orgs or users to watch (OWNERS). "" = all your PRs.
--repos "o/r o/r" Only these repos (REPOS); replaces --owners.
--every 15m|30m|1h|2h|3h How often it runs (EVERY_MINUTES). --every-hours 1|2|3 still works.
--drafts · --no-drafts Update drafts or not (INCLUDE_DRAFTS).
--skip-label L · --no-skip-label Exclude PRs with label L or not (SKIP_LABEL).
--notify-image PATH Notification image (NOTIFY_IMAGE). "" = the logo.
--notify-changes-requested · --no-notify-changes-requested Notify when a review requests changes or not (NOTIFY_CHANGES_REQUESTED).
--notify-ci-failed · --no-notify-ci-failed Notify when CI fails on a branch it updated or not (NOTIFY_CI_FAILED).
--auto-approve "a b" · --no-auto-approve Approve the PRs of these logins, or stop (AUTO_APPROVE_AUTHORS).
--auto-approve-for 1w|<N>d|forever Until when it approves (AUTO_APPROVE_UNTIL): 1w = today + 7 days; forever = no end date. Without it and with no date in the config, 1w. --auto-approve-until YYYY-MM-DD sets the date.
--auto-approve-require-ci · --no-auto-approve-require-ci Approve only once CI passed, or regardless (AUTO_APPROVE_REQUIRE_CI).
--yes Don't ask: use the existing config and the defaults. Useful to reschedule after editing the file.
--dry-run Shows the config and the LaunchAgent it would write, without touching anything. On its own, it still asks the questions.

Troubleshooting

gh pr-watcher doctor

It flags each problem with the command that fixes it and exits non-zero if something fails. It changes nothing and sends no notifications. It checks:

  • gh and jq installed, and terminal-notifier (as a warning only).
  • The config: misspelled keys and invalid values.
  • The gh session, the repo scope, and that each owner and repo exists and your account sees it.
  • Auto-approval, when it's on: whose PRs and until when, with a warning once the end date passed.
  • The schedule: that the LaunchAgent is loaded, that it points to a gh that still exists (brew sometimes moves it) and that its schedule matches EVERY_MINUTES.
  • That it actually ran. No notification covers this: if launchd stops firing, there's no error. doctor compares the last run with the last scheduled time; if the machine was off at that time it doesn't flag it, and if it was asleep it waits for launchd to run it on wake.
  • Another watcher running at the same time, the last run's error and a corrupted state.

doctor --json returns the same for an agent: {ok, checks: [{id, status, message, fix}]}.

Upgrade and uninstall

gh extension upgrade pr-watcher     # the schedule keeps working, no need to reinstall
gh pr-watcher uninstall && gh extension remove pr-watcher

uninstall keeps the config and the log; to start from scratch, also delete ~/.config/gh-pr-watcher and ~/.local/state/gh-pr-watcher.

Development

tests/run_tests.sh            # all the tests
tests/run_tests.sh conflict   # only those whose name contains "conflict"

The tests run the script against fakes of gh, terminal-notifier, osascript, launchctl and sleep (in tests/bin/, placed first in PATH) that answer from JSON fixtures: no network, no gh session, and nothing touched outside a temp folder. Only jq is needed. CI runs them on Linux and macOS, plus shellcheck.

License

MIT.

About

Keeps your open PRs up to date and notifies you when one conflicts. A GitHub CLI extension.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages