@hardlydifficult/pr-analyzer 1.0.1 → 1.0.2
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 +284 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# @hardlydifficult/pr-analyzer
|
|
2
|
+
|
|
3
|
+
Analyzes and classifies GitHub pull requests by status, determining available actions and enabling custom logic via hooks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hardlydifficult/pr-analyzer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Analyze a single PR and determine its status:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { scanSinglePR } from "@hardlydifficult/pr-analyzer";
|
|
17
|
+
import { createGitHubClient } from "@hardlydifficult/github";
|
|
18
|
+
|
|
19
|
+
const client = createGitHubClient(process.env.GITHUB_TOKEN);
|
|
20
|
+
const scanned = await scanSinglePR(
|
|
21
|
+
client,
|
|
22
|
+
"@cursor",
|
|
23
|
+
"owner",
|
|
24
|
+
"repo",
|
|
25
|
+
42
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
console.log(scanned.status); // "ready_to_merge", "ci_running", "draft", etc.
|
|
29
|
+
console.log(scanned.ciStatus); // { isRunning, hasFailed, allPassed, summary }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## PR Analysis
|
|
33
|
+
|
|
34
|
+
Analyze a PR to fetch CI checks, reviews, comments, and determine its status.
|
|
35
|
+
|
|
36
|
+
### `analyzePR()`
|
|
37
|
+
|
|
38
|
+
Analyzes a single PR and returns detailed status information:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { analyzePR } from "@hardlydifficult/pr-analyzer";
|
|
42
|
+
|
|
43
|
+
const scanned = await analyzePR(
|
|
44
|
+
client,
|
|
45
|
+
"owner",
|
|
46
|
+
"repo",
|
|
47
|
+
prObject,
|
|
48
|
+
"@cursor"
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Returns ScannedPR with:
|
|
52
|
+
// - status: Core status like "ready_to_merge", "ci_running", "draft", etc.
|
|
53
|
+
// - ciStatus: { isRunning, hasFailed, allPassed, summary }
|
|
54
|
+
// - hasConflicts: boolean
|
|
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
|
+
```
|
|
60
|
+
|
|
61
|
+
**Core statuses** (in priority order):
|
|
62
|
+
- `draft` — PR is in draft mode
|
|
63
|
+
- `ci_running` — CI checks are in progress
|
|
64
|
+
- `ci_failed` — One or more CI checks failed
|
|
65
|
+
- `has_conflicts` — PR has merge conflicts
|
|
66
|
+
- `waiting_on_bot` — Bot was mentioned but hasn't replied
|
|
67
|
+
- `changes_requested` — Reviewer requested changes
|
|
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
|
|
71
|
+
|
|
72
|
+
### `analyzeAll()`
|
|
73
|
+
|
|
74
|
+
Analyzes multiple discovered PRs in sequence, logging failures:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { analyzeAll } from "@hardlydifficult/pr-analyzer";
|
|
78
|
+
|
|
79
|
+
const scanned = await analyzeAll(
|
|
80
|
+
discoveredPRs,
|
|
81
|
+
client,
|
|
82
|
+
"@cursor",
|
|
83
|
+
logger // optional Logger
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `scanSinglePR()`
|
|
88
|
+
|
|
89
|
+
Convenience function for real-time event handling — fetches and analyzes a PR by number:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { scanSinglePR } from "@hardlydifficult/pr-analyzer";
|
|
93
|
+
|
|
94
|
+
const scanned = await scanSinglePR(
|
|
95
|
+
client,
|
|
96
|
+
"@cursor",
|
|
97
|
+
"owner",
|
|
98
|
+
"repo",
|
|
99
|
+
42
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Custom Status Logic
|
|
104
|
+
|
|
105
|
+
Override status determination with custom logic via `AnalyzerHooks`:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { analyzePR } from "@hardlydifficult/pr-analyzer";
|
|
109
|
+
import type { AnalyzerHooks } from "@hardlydifficult/pr-analyzer";
|
|
110
|
+
|
|
111
|
+
const hooks: AnalyzerHooks = {
|
|
112
|
+
resolveStatus: (coreStatus, details) => {
|
|
113
|
+
// Return custom status string to override, or undefined to keep core status
|
|
114
|
+
if (coreStatus === "ci_failed" && details.checks.some(c => c.name === "lint")) {
|
|
115
|
+
return "lint_failed";
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const scanned = await analyzePR(
|
|
122
|
+
client,
|
|
123
|
+
"owner",
|
|
124
|
+
"repo",
|
|
125
|
+
prObject,
|
|
126
|
+
"@cursor",
|
|
127
|
+
hooks
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The `resolveStatus` hook receives:
|
|
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.
|
|
138
|
+
|
|
139
|
+
### `classifyPRs()`
|
|
140
|
+
|
|
141
|
+
Groups PRs into four buckets based on status:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { classifyPRs } from "@hardlydifficult/pr-analyzer";
|
|
145
|
+
|
|
146
|
+
const result = classifyPRs(scannedPRs);
|
|
147
|
+
|
|
148
|
+
// result.readyForHuman — needs_review, changes_requested, approved, ready_to_merge
|
|
149
|
+
// result.needsBotBump — waiting_on_bot
|
|
150
|
+
// result.inProgress — ci_running
|
|
151
|
+
// result.blocked — draft, ci_failed, has_conflicts
|
|
152
|
+
// result.all — all PRs
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Extending Classification
|
|
156
|
+
|
|
157
|
+
Add custom statuses to buckets via `ClassificationConfig`:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const result = classifyPRs(scannedPRs, {
|
|
161
|
+
readyForHuman: ["custom_ready"],
|
|
162
|
+
inProgress: ["ai_processing"],
|
|
163
|
+
blocked: ["custom_blocked"],
|
|
164
|
+
needsBotBump: ["custom_waiting"]
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## PR Actions
|
|
169
|
+
|
|
170
|
+
Determine which actions are available for a PR based on its status.
|
|
171
|
+
|
|
172
|
+
### `getAvailableActions()`
|
|
173
|
+
|
|
174
|
+
Returns available actions for a PR:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { getAvailableActions } from "@hardlydifficult/pr-analyzer";
|
|
178
|
+
|
|
179
|
+
const actions = getAvailableActions(scannedPR);
|
|
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" }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Custom Actions
|
|
202
|
+
|
|
203
|
+
Extend available actions with custom logic:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import type { ActionDefinition } from "@hardlydifficult/pr-analyzer";
|
|
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
|
|
227
|
+
|
|
228
|
+
### GitHub Authentication
|
|
229
|
+
|
|
230
|
+
Provide a GitHub token via environment variable or directly:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { createGitHubClient } from "@hardlydifficult/github";
|
|
234
|
+
|
|
235
|
+
const client = createGitHubClient(process.env.GITHUB_TOKEN);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Bot Mention
|
|
239
|
+
|
|
240
|
+
Pass the bot mention string (e.g., `"@cursor"`) to analysis functions. The analyzer detects when this mention appears in PR comments and tracks whether the bot has replied.
|
|
241
|
+
|
|
242
|
+
### Logger (Optional)
|
|
243
|
+
|
|
244
|
+
Implement the `Logger` interface for error handling in `analyzeAll()`:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import type { Logger } from "@hardlydifficult/pr-analyzer";
|
|
248
|
+
|
|
249
|
+
const logger: Logger = {
|
|
250
|
+
info: (message, context) => console.log(message, context),
|
|
251
|
+
error: (message, context) => console.error(message, context)
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
await analyzeAll(prs, client, "@cursor", logger);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Appendix
|
|
258
|
+
|
|
259
|
+
### CI Status Determination
|
|
260
|
+
|
|
261
|
+
CI status is determined from GitHub check runs:
|
|
262
|
+
|
|
263
|
+
| Condition | Result |
|
|
264
|
+
|-----------|--------|
|
|
265
|
+
| Any check in `in_progress` or `queued` | `isRunning: true` |
|
|
266
|
+
| Any check with conclusion `failure`, `timed_out`, `cancelled`, or `action_required` | `hasFailed: true` |
|
|
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`
|