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 +34 -5
- package/bin/watch-pr.js +3 -1
- package/package.json +1 -1
- package/src/cli.js +73 -7
- package/src/github.js +31 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
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 (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
};
|