@hardlydifficult/pr-analyzer 1.0.31 → 1.0.33
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 +120 -182
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/pr-analyzer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A GitHub PR analyzer that classifies pull requests by status and determines available actions (merge, mark ready, auto-merge).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,146 +10,122 @@ npm install @hardlydifficult/pr-analyzer
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
-
Analyze a single PR and determine its status:
|
|
14
|
-
|
|
15
13
|
```typescript
|
|
14
|
+
import { GitHubClient } from "@hardlydifficult/github";
|
|
16
15
|
import { scanSinglePR } from "@hardlydifficult/pr-analyzer";
|
|
17
|
-
import { createGitHubClient } from "@hardlydifficult/github";
|
|
18
16
|
|
|
19
|
-
const client =
|
|
20
|
-
|
|
17
|
+
const client = new GitHubClient({ token: process.env.GITHUB_TOKEN! });
|
|
18
|
+
|
|
19
|
+
const pr = await scanSinglePR(
|
|
21
20
|
client,
|
|
22
21
|
"@cursor",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
22
|
+
"HardlyDifficult",
|
|
23
|
+
"typescript",
|
|
24
|
+
123
|
|
26
25
|
);
|
|
27
26
|
|
|
28
|
-
console.log(
|
|
29
|
-
console.log(
|
|
27
|
+
console.log(pr.status); // e.g., "ready_to_merge"
|
|
28
|
+
console.log(pr.ciSummary); // e.g., "CI passed: 2 checks"
|
|
29
|
+
|
|
30
|
+
const actions = getAvailableActions(pr);
|
|
31
|
+
console.log(actions.map(a => a.label)); // e.g., ["Merge"]
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
##
|
|
34
|
+
## Analysis
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
Analyzes a PR's status by fetching CI checks, comments, reviews, and merge conflicts, then determines its readiness.
|
|
35
37
|
|
|
36
|
-
### `analyzePR
|
|
38
|
+
### `analyzePR`
|
|
37
39
|
|
|
38
|
-
Analyzes a single PR and returns detailed status
|
|
40
|
+
Analyzes a single PR and returns detailed status.
|
|
39
41
|
|
|
40
42
|
```typescript
|
|
43
|
+
import { GitHubClient } from "@hardlydifficult/github";
|
|
41
44
|
import { analyzePR } from "@hardlydifficult/pr-analyzer";
|
|
42
45
|
|
|
43
|
-
const
|
|
44
|
-
client,
|
|
45
|
-
"owner",
|
|
46
|
-
"repo",
|
|
47
|
-
prObject,
|
|
48
|
-
"@cursor"
|
|
49
|
-
);
|
|
46
|
+
const client = new GitHubClient({ token: process.env.GITHUB_TOKEN! });
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// - waitingOnBot: boolean (true if bot was mentioned but hasn't replied)
|
|
56
|
-
// - daysSinceUpdate: number
|
|
57
|
-
// - pr: Original PullRequest object
|
|
58
|
-
// - repo: Repository object
|
|
59
|
-
```
|
|
48
|
+
const pr = await client
|
|
49
|
+
.repo("owner", "repo")
|
|
50
|
+
.pr(42)
|
|
51
|
+
.get();
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- `ready_to_merge` — All checks passed, no issues
|
|
69
|
-
- `approved` — PR is approved but CI not yet passed
|
|
70
|
-
- `needs_review` — Default status, awaiting review
|
|
53
|
+
const result = await analyzePR(client, "owner", "repo", pr, "@bot");
|
|
54
|
+
console.log(result.status); // "ready_to_merge", "ci_failed", etc.
|
|
55
|
+
console.log(result.ciSummary); // Human-readable CI summary
|
|
56
|
+
console.log(result.hasConflicts); // true if merge conflicts exist
|
|
57
|
+
console.log(result.waitingOnBot); // true if PR is waiting on bot response
|
|
58
|
+
console.log(result.daysSinceUpdate); // Days since last update
|
|
59
|
+
```
|
|
71
60
|
|
|
72
|
-
### `analyzeAll
|
|
61
|
+
### `analyzeAll`
|
|
73
62
|
|
|
74
|
-
Analyzes multiple discovered PRs
|
|
63
|
+
Analyzes multiple discovered PRs, skipping those that fail.
|
|
75
64
|
|
|
76
65
|
```typescript
|
|
77
66
|
import { analyzeAll } from "@hardlydifficult/pr-analyzer";
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
68
|
+
interface DiscoveredPR {
|
|
69
|
+
pr: PullRequest;
|
|
70
|
+
repoOwner: string;
|
|
71
|
+
repoName: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const prs: DiscoveredPR[] = [
|
|
75
|
+
{ pr: pr1, repoOwner: "owner", repoName: "repo" },
|
|
76
|
+
{ pr: pr2, repoOwner: "owner", repoName: "other" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const results = await analyzeAll(prs, client, "@bot", logger);
|
|
85
80
|
```
|
|
86
81
|
|
|
87
|
-
### `scanSinglePR
|
|
82
|
+
### `scanSinglePR`
|
|
88
83
|
|
|
89
|
-
|
|
84
|
+
Real-time PR scan for event handling (e.g., webhook, cron).
|
|
90
85
|
|
|
91
86
|
```typescript
|
|
92
87
|
import { scanSinglePR } from "@hardlydifficult/pr-analyzer";
|
|
93
88
|
|
|
94
|
-
const
|
|
89
|
+
const pr = await scanSinglePR(
|
|
95
90
|
client,
|
|
96
91
|
"@cursor",
|
|
97
92
|
"owner",
|
|
98
93
|
"repo",
|
|
99
|
-
|
|
94
|
+
123
|
|
100
95
|
);
|
|
101
96
|
```
|
|
102
97
|
|
|
103
|
-
|
|
98
|
+
### Hooks
|
|
104
99
|
|
|
105
|
-
|
|
100
|
+
Customize status resolution with hooks:
|
|
106
101
|
|
|
107
102
|
```typescript
|
|
108
|
-
import { analyzePR } from "@hardlydifficult/pr-analyzer";
|
|
109
|
-
import type { AnalyzerHooks } from "@hardlydifficult/pr-analyzer";
|
|
103
|
+
import { analyzePR, type AnalyzerHooks } from "@hardlydifficult/pr-analyzer";
|
|
110
104
|
|
|
111
105
|
const hooks: AnalyzerHooks = {
|
|
112
|
-
resolveStatus
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return "lint_failed";
|
|
106
|
+
resolveStatus(coreStatus, details) {
|
|
107
|
+
if (coreStatus === "ci_failed") {
|
|
108
|
+
return "ai_processing";
|
|
116
109
|
}
|
|
117
110
|
return undefined;
|
|
118
|
-
}
|
|
111
|
+
},
|
|
119
112
|
};
|
|
120
113
|
|
|
121
|
-
const
|
|
122
|
-
client,
|
|
123
|
-
"owner",
|
|
124
|
-
"repo",
|
|
125
|
-
prObject,
|
|
126
|
-
"@cursor",
|
|
127
|
-
hooks
|
|
128
|
-
);
|
|
114
|
+
const result = await analyzePR(client, "owner", "repo", pr, "@bot", hooks);
|
|
129
115
|
```
|
|
130
116
|
|
|
131
|
-
|
|
132
|
-
- `coreStatus` — The determined core status
|
|
133
|
-
- `details` — `AnalysisDetails` with `comments`, `checks`, `reviews`, `ciStatus`, `hasConflicts`, `waitingOnBot`
|
|
134
|
-
|
|
135
|
-
## PR Classification
|
|
136
|
-
|
|
137
|
-
Classify analyzed PRs into action buckets for workflow management.
|
|
117
|
+
## Classification
|
|
138
118
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Groups PRs into four buckets based on status:
|
|
119
|
+
Classifies PRs into buckets based on status: `readyForHuman`, `needsBotBump`, `inProgress`, `blocked`.
|
|
142
120
|
|
|
143
121
|
```typescript
|
|
144
122
|
import { classifyPRs } from "@hardlydifficult/pr-analyzer";
|
|
145
123
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// result.blocked — draft, ci_failed, has_conflicts
|
|
152
|
-
// result.all — all PRs
|
|
124
|
+
const buckets = classifyPRs(results);
|
|
125
|
+
console.log(buckets.readyForHuman.length); // PRs needing human action
|
|
126
|
+
console.log(buckets.needsBotBump.length); // PRs waiting on bot
|
|
127
|
+
console.log(buckets.inProgress.length); // PRs with CI running
|
|
128
|
+
console.log(buckets.blocked.length); // PRs stuck (draft, failed, conflicts)
|
|
153
129
|
```
|
|
154
130
|
|
|
155
131
|
### Extending Classification
|
|
@@ -157,128 +133,90 @@ const result = classifyPRs(scannedPRs);
|
|
|
157
133
|
Add custom statuses to buckets via `ClassificationConfig`:
|
|
158
134
|
|
|
159
135
|
```typescript
|
|
160
|
-
const
|
|
136
|
+
const config: ClassificationConfig = {
|
|
161
137
|
readyForHuman: ["custom_ready"],
|
|
162
138
|
inProgress: ["ai_processing"],
|
|
163
139
|
blocked: ["custom_blocked"],
|
|
164
|
-
needsBotBump: ["custom_waiting"]
|
|
165
|
-
}
|
|
140
|
+
needsBotBump: ["custom_waiting"],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const buckets = classifyPRs(results, config);
|
|
166
144
|
```
|
|
167
145
|
|
|
168
|
-
##
|
|
146
|
+
## Actions
|
|
169
147
|
|
|
170
|
-
|
|
148
|
+
Determines which actions are available for a PR based on its status.
|
|
171
149
|
|
|
172
|
-
###
|
|
150
|
+
### Core Actions
|
|
173
151
|
|
|
174
|
-
|
|
152
|
+
| Type | Available When | Description |
|
|
153
|
+
|------|----------------|-------------|
|
|
154
|
+
| `"merge"` | `ready_to_merge`, `approved` | Squash and merge |
|
|
155
|
+
| `"mark_ready"` | `draft` with CI passed & no conflicts | Mark draft as ready |
|
|
156
|
+
| `"enable_auto_merge"` | `ci_running`, `needs_review` | Enable auto-merge |
|
|
175
157
|
|
|
176
158
|
```typescript
|
|
177
159
|
import { getAvailableActions } from "@hardlydifficult/pr-analyzer";
|
|
178
160
|
|
|
179
|
-
const actions = getAvailableActions(
|
|
180
|
-
|
|
181
|
-
// Returns PRActionDescriptor[] with type, label, description
|
|
182
|
-
// Example: [{ type: "merge", label: "Merge", description: "Squash and merge this PR" }]
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
**Core actions** (status-dependent):
|
|
186
|
-
- `merge` — Available for `ready_to_merge` or `approved` status
|
|
187
|
-
- `mark_ready` — Available for `draft` status when CI passed and no conflicts
|
|
188
|
-
- `enable_auto_merge` — Available for `ci_running` or `needs_review` status (non-draft, non-conflicting, unmerged)
|
|
189
|
-
|
|
190
|
-
### `PR_ACTIONS`
|
|
191
|
-
|
|
192
|
-
Reference object for core action metadata:
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
import { PR_ACTIONS } from "@hardlydifficult/pr-analyzer";
|
|
196
|
-
|
|
197
|
-
console.log(PR_ACTIONS.merge);
|
|
198
|
-
// { label: "Merge", description: "Squash and merge this PR" }
|
|
161
|
+
const actions = getAvailableActions(pr);
|
|
162
|
+
// e.g., [{ type: "merge", label: "Merge", description: "Squash and merge this PR" }]
|
|
199
163
|
```
|
|
200
164
|
|
|
201
165
|
### Custom Actions
|
|
202
166
|
|
|
203
|
-
|
|
167
|
+
Define extra actions that trigger on conditions:
|
|
204
168
|
|
|
205
169
|
```typescript
|
|
206
|
-
import type
|
|
207
|
-
|
|
208
|
-
const customActions: ActionDefinition[] = [
|
|
209
|
-
{
|
|
210
|
-
type: "fix_ci",
|
|
211
|
-
label: "Fix CI",
|
|
212
|
-
description: "Post @cursor fix CI comment",
|
|
213
|
-
when: (pr, ctx) => pr.status === "ci_failed" && ctx["isWorkPR"] === true
|
|
214
|
-
}
|
|
215
|
-
];
|
|
216
|
-
|
|
217
|
-
const actions = getAvailableActions(scannedPR, customActions, {
|
|
218
|
-
isWorkPR: true
|
|
219
|
-
});
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
The `when` function receives:
|
|
223
|
-
- `pr` — The `ScannedPR` object
|
|
224
|
-
- `context` — Key-value boolean flags for conditional logic
|
|
225
|
-
|
|
226
|
-
## Setup
|
|
170
|
+
import { getAvailableActions, type ActionDefinition } from "@hardlydifficult/pr-analyzer";
|
|
227
171
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
172
|
+
const fixCiAction: ActionDefinition = {
|
|
173
|
+
type: "fix_ci",
|
|
174
|
+
label: "Fix CI",
|
|
175
|
+
description: "Post @cursor fix CI comment",
|
|
176
|
+
when: (pr, ctx) => pr.status === "ci_failed" && ctx["isWorkPR"] === true,
|
|
177
|
+
};
|
|
234
178
|
|
|
235
|
-
const
|
|
179
|
+
const actions = getAvailableActions(pr, [fixCiAction], { isWorkPR: true });
|
|
236
180
|
```
|
|
237
181
|
|
|
238
|
-
|
|
182
|
+
## Types
|
|
239
183
|
|
|
240
|
-
|
|
184
|
+
### Core Statuses
|
|
241
185
|
|
|
242
|
-
|
|
186
|
+
| Status | Meaning |
|
|
187
|
+
|--------|---------|
|
|
188
|
+
| `"draft"` | Draft PR |
|
|
189
|
+
| `"ci_running"` | CI checks in progress |
|
|
190
|
+
| `"ci_failed"` | CI checks failed |
|
|
191
|
+
| `"needs_review"` | CI passed, no reviews yet |
|
|
192
|
+
| `"changes_requested"` | Review requests changes |
|
|
193
|
+
| `"approved"` | Review approved |
|
|
194
|
+
| `"has_conflicts"` | Merge conflicts |
|
|
195
|
+
| `"ready_to_merge"` | CI passed, approved, no conflicts |
|
|
196
|
+
| `"waiting_on_bot"` | Bot mentioned but not replied |
|
|
243
197
|
|
|
244
|
-
|
|
198
|
+
### ScannedPR
|
|
245
199
|
|
|
246
200
|
```typescript
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
201
|
+
interface ScannedPR {
|
|
202
|
+
pr: PullRequest;
|
|
203
|
+
repo: Repository;
|
|
204
|
+
status: string; // core or custom status
|
|
205
|
+
ciStatus: CIStatus;
|
|
206
|
+
ciSummary: string;
|
|
207
|
+
hasConflicts: boolean;
|
|
208
|
+
waitingOnBot: boolean;
|
|
209
|
+
daysSinceUpdate: number;
|
|
210
|
+
}
|
|
255
211
|
```
|
|
256
212
|
|
|
257
|
-
|
|
213
|
+
### CIStatus
|
|
258
214
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
| All checks with conclusion `success`, `skipped`, or `neutral` | `allPassed: true` |
|
|
268
|
-
| No checks | `allPassed: true`, `summary: "No CI checks"` |
|
|
269
|
-
|
|
270
|
-
### Review Status Determination
|
|
271
|
-
|
|
272
|
-
Latest review per reviewer is considered. If multiple reviews from the same user exist, only the most recent is used.
|
|
273
|
-
|
|
274
|
-
### Bot Detection
|
|
275
|
-
|
|
276
|
-
The analyzer recognizes these bot usernames (case-insensitive):
|
|
277
|
-
- `cursor`, `cursor-bot`
|
|
278
|
-
- `github-actions`, `github-actions[bot]`
|
|
279
|
-
- `dependabot`, `dependabot[bot]`
|
|
280
|
-
- `renovate`, `renovate[bot]`
|
|
281
|
-
- `codecov`, `codecov[bot]`
|
|
282
|
-
- `vercel`, `vercel[bot]`
|
|
283
|
-
- `claude`
|
|
284
|
-
- Any username ending with `[bot]` or containing `bot`
|
|
215
|
+
```typescript
|
|
216
|
+
interface CIStatus {
|
|
217
|
+
isRunning: boolean;
|
|
218
|
+
hasFailed: boolean;
|
|
219
|
+
allPassed: boolean;
|
|
220
|
+
summary: string;
|
|
221
|
+
}
|
|
222
|
+
```
|