@hardlydifficult/pr-analyzer 1.0.31 → 1.0.32

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.
Files changed (2) hide show
  1. package/README.md +120 -182
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/pr-analyzer
2
2
 
3
- Analyzes and classifies GitHub pull requests by status, determining available actions and enabling custom logic via hooks.
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 = createGitHubClient(process.env.GITHUB_TOKEN);
20
- const scanned = await scanSinglePR(
17
+ const client = new GitHubClient({ token: process.env.GITHUB_TOKEN! });
18
+
19
+ const pr = await scanSinglePR(
21
20
  client,
22
21
  "@cursor",
23
- "owner",
24
- "repo",
25
- 42
22
+ "HardlyDifficult",
23
+ "typescript",
24
+ 123
26
25
  );
27
26
 
28
- console.log(scanned.status); // "ready_to_merge", "ci_running", "draft", etc.
29
- console.log(scanned.ciStatus); // { isRunning, hasFailed, allPassed, summary }
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
- ## PR Analysis
34
+ ## Analysis
33
35
 
34
- Analyze a PR to fetch CI checks, reviews, comments, and determine its status.
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 information:
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 scanned = await analyzePR(
44
- client,
45
- "owner",
46
- "repo",
47
- prObject,
48
- "@cursor"
49
- );
46
+ const client = new GitHubClient({ token: process.env.GITHUB_TOKEN! });
50
47
 
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
- ```
48
+ const pr = await client
49
+ .repo("owner", "repo")
50
+ .pr(42)
51
+ .get();
60
52
 
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
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 in sequence, logging failures:
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
- const scanned = await analyzeAll(
80
- discoveredPRs,
81
- client,
82
- "@cursor",
83
- logger // optional Logger
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
- Convenience function for real-time event handling fetches and analyzes a PR by number:
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 scanned = await scanSinglePR(
89
+ const pr = await scanSinglePR(
95
90
  client,
96
91
  "@cursor",
97
92
  "owner",
98
93
  "repo",
99
- 42
94
+ 123
100
95
  );
101
96
  ```
102
97
 
103
- ## Custom Status Logic
98
+ ### Hooks
104
99
 
105
- Override status determination with custom logic via `AnalyzerHooks`:
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: (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";
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 scanned = await analyzePR(
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
- 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.
117
+ ## Classification
138
118
 
139
- ### `classifyPRs()`
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 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
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 result = classifyPRs(scannedPRs, {
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
- ## PR Actions
146
+ ## Actions
169
147
 
170
- Determine which actions are available for a PR based on its status.
148
+ Determines which actions are available for a PR based on its status.
171
149
 
172
- ### `getAvailableActions()`
150
+ ### Core Actions
173
151
 
174
- Returns available actions for a PR:
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(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" }
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
- Extend available actions with custom logic:
167
+ Define extra actions that trigger on conditions:
204
168
 
205
169
  ```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
170
+ import { getAvailableActions, type ActionDefinition } from "@hardlydifficult/pr-analyzer";
227
171
 
228
- ### GitHub Authentication
229
-
230
- Provide a GitHub token via environment variable or directly:
231
-
232
- ```typescript
233
- import { createGitHubClient } from "@hardlydifficult/github";
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 client = createGitHubClient(process.env.GITHUB_TOKEN);
179
+ const actions = getAvailableActions(pr, [fixCiAction], { isWorkPR: true });
236
180
  ```
237
181
 
238
- ### Bot Mention
182
+ ## Types
239
183
 
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.
184
+ ### Core Statuses
241
185
 
242
- ### Logger (Optional)
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
- Implement the `Logger` interface for error handling in `analyzeAll()`:
198
+ ### ScannedPR
245
199
 
246
200
  ```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);
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
- ## Appendix
213
+ ### CIStatus
258
214
 
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`
215
+ ```typescript
216
+ interface CIStatus {
217
+ isRunning: boolean;
218
+ hasFailed: boolean;
219
+ allPassed: boolean;
220
+ summary: string;
221
+ }
222
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/pr-analyzer",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [