prwatcher 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # prwatcher
2
2
 
3
- CLI tool that watches GitHub PRs and sends Slack notifications when CI passes, fails, or the PR is ready to merge.
3
+ CLI tool that watches GitHub PRs and sends Slack notifications when CI passes, fails, or the PR is ready to merge. Optionally auto-merges when all checks pass.
4
4
 
5
5
  **Zero setup for developers** — just run one command. No Slack app creation, no servers, no ngrok.
6
6
 
@@ -29,17 +29,39 @@ Or use directly with npx:
29
29
  npx prwatcher https://github.com/owner/repo/pull/123
30
30
  ```
31
31
 
32
+ ### GitHub Token Setup
33
+
34
+ On first run you'll be prompted for a GitHub personal access token. Here's what permissions it needs depending on how you use the tool:
35
+
36
+ #### Watching only (no auto-merge)
37
+
38
+ **Classic token** — check `repo` scope
39
+
40
+ **Fine-grained token** — enable:
41
+ - `Metadata` → Read-only (required)
42
+ - `Pull requests` → Read-only
43
+
44
+ #### Watching + Auto-merge (`--auto-merge`)
45
+
46
+ **Classic token** — check `repo` scope
47
+
48
+ **Fine-grained token** — enable:
49
+ - `Metadata` → Read-only (required)
50
+ - `Pull requests` → Read and write
51
+ - `Contents` → Read and write ← required for merging
52
+
53
+ > Your token is saved to `~/.watch-pr` on your machine with owner-only permissions (0600). It is never sent anywhere except the GitHub API.
54
+
32
55
  ### Usage
33
56
 
34
57
  ```bash
35
58
  prwatcher https://github.com/owner/repo/pull/123
36
59
  ```
37
60
 
38
- On first run, you'll be prompted for:
39
- 1. **GitHub token** — [create one here](https://github.com/settings/tokens) with `repo` scope
40
- 2. **Slack webhook URL** — your team's [Incoming Webhook](https://api.slack.com/messaging/webhooks)
61
+ On first run, you'll also be prompted for:
62
+ - **Slack webhook URL** — your team's [Incoming Webhook](https://api.slack.com/messaging/webhooks)
41
63
 
42
- These are saved to `~/.prwatcher` so you only enter them once.
64
+ Both are saved to `~/.watch-pr` so you only enter them once.
43
65
 
44
66
  ### Options
45
67
 
@@ -48,6 +70,7 @@ prwatcher <pr-url> [options]
48
70
 
49
71
  Options:
50
72
  --interval <minutes> Polling interval in minutes (default: 1)
73
+ --auto-merge Automatically merge PR once all required checks pass
51
74
  --reset-config Re-enter GitHub token and Slack webhook URL
52
75
  -V, --version Output version number
53
76
  -h, --help Display help
@@ -59,6 +82,9 @@ Options:
59
82
  # Watch a PR with default 1-minute polling
60
83
  prwatcher https://github.com/org/repo/pull/42
61
84
 
85
+ # Watch and auto-merge when ready
86
+ prwatcher https://github.com/org/repo/pull/42 --auto-merge
87
+
62
88
  # Poll every 30 seconds
63
89
  prwatcher https://github.com/org/repo/pull/42 --interval 0.5
64
90
 
@@ -78,8 +104,10 @@ You'll get Slack messages when:
78
104
  | Required CI checks fail | ⚠️ CI checks failed (lists which ones) |
79
105
  | PR needs rebase | 🔄 PR needs rebase |
80
106
  | PR is ready to merge | ✅ PR is ready to merge! |
107
+ | PR is auto-merged | 🎉 PR was auto-merged! |
81
108
  | PR is merged | 🎉 PR was merged! |
82
109
  | PR is closed | ❌ PR was closed without merging |
110
+ | Auto-merge failed | ⚠️ Auto-merge failed (with reason) |
83
111
 
84
112
  Notifications only fire on **state changes** — no spam.
85
113
 
@@ -113,6 +141,7 @@ Detect state change (CI failed, ready, merged, etc.)
113
141
 
114
142
  POST to Slack webhook
115
143
 
144
+ --auto-merge? → merge via GitHub API → exit
116
145
  PR merged/closed? → exit
117
146
  ```
118
147
 
package/bin/watch-pr.js CHANGED
@@ -6,9 +6,11 @@ const { run } = require('../src/cli');
6
6
  program
7
7
  .name('prwatcher')
8
8
  .description('Watch a GitHub PR and get Slack notifications on state changes')
9
- .version('2.0.0')
9
+ .version('2.2.0')
10
10
  .argument('<pr-url>', 'GitHub PR URL (e.g., https://github.com/owner/repo/pull/123)')
11
11
  .option('--interval <minutes>', 'polling interval in minutes', '1')
12
+ .option('--auto-merge', 'automatically merge PR once all required checks pass')
13
+ .option('--auto-rebase', 'automatically rebase PR branch when it falls behind (no conflicts only)')
12
14
  .option('--reset-config', 're-enter GitHub token and Slack webhook URL')
13
15
  .action((prUrl, options) => {
14
16
  run(prUrl, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prwatcher",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "CLI tool that watches GitHub PRs and sends Slack notifications when CI passes, fails, or PR is ready to merge",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const { parsePRUrl, buildPRKey } = require('./utils');
2
2
  const { ensureConfig } = require('./config');
3
- const { createClient, fetchRequiredChecks, fetchPRState } = require('./github');
3
+ const { createClient, fetchRequiredChecks, fetchPRState, mergePR, rebasePR } = require('./github');
4
4
  const { sendNotification, formatMerged, formatClosed, formatCIFailed, formatRebaseNeeded, formatReady } = require('./slack');
5
5
 
6
6
  async function run(prUrl, options = {}) {
@@ -70,6 +70,12 @@ async function run(prUrl, options = {}) {
70
70
  const intervalMs = intervalMinutes * 60 * 1000;
71
71
 
72
72
  console.log(` Polling every ${intervalMinutes >= 1 ? `${intervalMinutes} minute(s)` : `${intervalMinutes * 60} seconds`}`);
73
+ if (options.autoMerge) {
74
+ console.log(' ⚠ Auto-merge is ON — PR will be merged automatically when ready');
75
+ }
76
+ if (options.autoRebase) {
77
+ console.log(' ⚠ Auto-rebase is ON — PR branch will be updated automatically when behind (no conflicts only)');
78
+ }
73
79
  console.log(' Press Ctrl+C to stop\n');
74
80
 
75
81
  // 6. Poll function
@@ -111,12 +117,42 @@ async function run(prUrl, options = {}) {
111
117
 
112
118
  // Rebase needed
113
119
  if (state.mergeableState === 'behind' || state.mergeableState === 'dirty') {
114
- if (lastState !== 'rebase_needed') {
115
- console.log(` 🔄 ${timestamp}Needs rebase (${state.mergeableState})`);
116
- await sendNotification(config.slackWebhookUrl, formatRebaseNeeded(state.htmlUrl, state.title));
117
- lastState = 'rebase_needed';
118
- } else {
119
- console.log(` 🔄 ${timestamp} — Still needs rebase (already notified)`);
120
+ if (state.mergeableState === 'dirty') {
121
+ // Has conflicts can't auto-rebase, just notify
122
+ if (lastState !== 'rebase_needed') {
123
+ console.log(` 🔄 ${timestamp} — Needs rebase (has conflicts, manual fix required)`);
124
+ await sendNotification(config.slackWebhookUrl, formatRebaseNeeded(state.htmlUrl, state.title));
125
+ lastState = 'rebase_needed';
126
+ } else {
127
+ console.log(` 🔄 ${timestamp} — Still has conflicts (already notified)`);
128
+ }
129
+ } else if (state.mergeableState === 'behind') {
130
+ if (options.autoRebase) {
131
+ // No conflicts — safe to auto-rebase
132
+ try {
133
+ console.log(` 🔄 ${timestamp} — Auto-rebasing from ${state.baseBranch}...`);
134
+ await rebasePR(octokit, { owner, repo, prNumber });
135
+ console.log(` ✅ ${timestamp} — Auto-rebased! Waiting for CI to re-run...`);
136
+ await sendNotification(config.slackWebhookUrl, `🔄 *PR branch auto-rebased from ${state.baseBranch}*\n<${state.htmlUrl}|${state.title}>\nCI will re-run on the updated branch.`);
137
+ lastState = null; // reset so we notify again after CI re-runs
138
+ } catch (rebaseError) {
139
+ const rebaseMsg = rebaseError.response?.data?.message || rebaseError.message;
140
+ const hint = rebaseError.status === 403
141
+ ? ' (GitHub token needs "Contents: Read and write" permission)'
142
+ : '';
143
+ console.error(` ⚠ ${timestamp} — Auto-rebase failed: ${rebaseMsg}${hint}`);
144
+ await sendNotification(config.slackWebhookUrl, formatRebaseNeeded(state.htmlUrl, state.title));
145
+ lastState = 'rebase_needed';
146
+ }
147
+ } else {
148
+ if (lastState !== 'rebase_needed') {
149
+ console.log(` 🔄 ${timestamp} — Needs rebase (behind ${state.baseBranch})`);
150
+ await sendNotification(config.slackWebhookUrl, formatRebaseNeeded(state.htmlUrl, state.title));
151
+ lastState = 'rebase_needed';
152
+ } else {
153
+ console.log(` 🔄 ${timestamp} — Still behind (already notified)`);
154
+ }
155
+ }
120
156
  }
121
157
  return;
122
158
  }
@@ -127,12 +163,42 @@ async function run(prUrl, options = {}) {
127
163
  console.log(` ✅ ${timestamp} — Ready to merge!`);
128
164
  await sendNotification(config.slackWebhookUrl, formatReady(state.htmlUrl, state.title));
129
165
  lastState = 'ready';
166
+
167
+ if (options.autoMerge) {
168
+ try {
169
+ console.log(` 🔀 ${timestamp} — Auto-merging...`);
170
+ await mergePR(octokit, { owner, repo, prNumber });
171
+ console.log(` 🎉 ${timestamp} — Auto-merged!`);
172
+ await sendNotification(config.slackWebhookUrl, `🎉 *PR was auto-merged!*\n<${state.htmlUrl}|${state.title}>`);
173
+ cleanup();
174
+ process.exit(0);
175
+ } catch (mergeError) {
176
+ const mergeMsg = mergeError.response?.data?.message || mergeError.message;
177
+ const hint = mergeError.status === 403
178
+ ? ' (GitHub token needs "repo" scope or "Pull requests: write" permission)'
179
+ : '';
180
+ console.error(` ⚠ ${timestamp} — Auto-merge failed: ${mergeMsg}${hint}`);
181
+ await sendNotification(config.slackWebhookUrl, `⚠️ *Auto-merge failed*\n<${state.htmlUrl}|${state.title}>\nReason: ${mergeMsg}${hint}`);
182
+ }
183
+ }
130
184
  } else {
131
185
  console.log(` ✅ ${timestamp} — Still ready (already notified)`);
132
186
  }
133
187
  return;
134
188
  }
135
189
 
190
+ // Blocked — CI passed but review approval required
191
+ if (state.mergeableState === 'blocked') {
192
+ if (lastState !== 'needs_review') {
193
+ console.log(` 👀 ${timestamp} — Waiting for review approval`);
194
+ await sendNotification(config.slackWebhookUrl, `👀 *PR is waiting for review approval*\n<${state.htmlUrl}|${state.title}>\nAll CI checks passed — needs at least 1 approving review to merge.`);
195
+ lastState = 'needs_review';
196
+ } else {
197
+ console.log(` 👀 ${timestamp} — Still waiting for review (already notified)`);
198
+ }
199
+ return;
200
+ }
201
+
136
202
  // Pending / other state
137
203
  const pending = state.checks ? state.checks.pendingRequired.length : 0;
138
204
  const total = state.checks ? state.checks.requiredTotal : 0;
package/src/github.js CHANGED
@@ -115,8 +115,38 @@ async function fetchPRState(octokit, { owner, repo, prNumber, requiredCheckNames
115
115
  };
116
116
  }
117
117
 
118
+ /**
119
+ * Merge a PR via GitHub API.
120
+ * Returns { merged: true } on success, or throws on failure.
121
+ */
122
+ async function mergePR(octokit, { owner, repo, prNumber }) {
123
+ const { data } = await octokit.pulls.merge({
124
+ owner,
125
+ repo,
126
+ pull_number: prNumber,
127
+ merge_method: 'merge'
128
+ });
129
+ return data;
130
+ }
131
+
132
+ /**
133
+ * Update (rebase) a PR branch with the latest from its base branch.
134
+ * Only works when mergeable_state is 'behind' (no conflicts).
135
+ * Throws if there are conflicts or insufficient permissions.
136
+ */
137
+ async function rebasePR(octokit, { owner, repo, prNumber }) {
138
+ const { data } = await octokit.pulls.updateBranch({
139
+ owner,
140
+ repo,
141
+ pull_number: prNumber
142
+ });
143
+ return data;
144
+ }
145
+
118
146
  module.exports = {
119
147
  createClient,
120
148
  fetchRequiredChecks,
121
- fetchPRState
149
+ fetchPRState,
150
+ mergePR,
151
+ rebasePR
122
152
  };