@oss-autopilot/core 1.13.1 → 1.15.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/dist/cli.bundle.cjs +66 -66
- package/dist/commands/daily.js +16 -25
- package/dist/commands/dashboard-data.d.ts +38 -1
- package/dist/commands/dashboard-data.js +33 -62
- package/dist/commands/dashboard-server.js +24 -1
- package/dist/commands/scout-bridge.js +7 -0
- package/dist/core/errors.d.ts +24 -0
- package/dist/core/errors.js +28 -0
- package/dist/core/github-stats.js +38 -71
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/package.json +2 -2
package/dist/commands/daily.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
9
|
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
|
-
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
10
|
+
import { errorMessage, isRateLimitOrAuthError, nonFatalCatch } from '../core/errors.js';
|
|
11
11
|
import { warn } from '../core/logger.js';
|
|
12
12
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
13
13
|
import { createAutopilotScout } from './scout-bridge.js';
|
|
@@ -60,30 +60,21 @@ async function fetchPRData(prMonitor, token) {
|
|
|
60
60
|
// All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
|
|
61
61
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
62
62
|
const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
|
|
63
|
-
prMonitor.fetchUserMergedPRCounts(starFilter).catch((
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
warn(MODULE, `Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
79
|
-
return [];
|
|
80
|
-
}),
|
|
81
|
-
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
82
|
-
if (isRateLimitOrAuthError(err))
|
|
83
|
-
throw err;
|
|
84
|
-
warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
85
|
-
return [];
|
|
86
|
-
}),
|
|
63
|
+
prMonitor.fetchUserMergedPRCounts(starFilter).catch(nonFatalCatch({
|
|
64
|
+
module: MODULE,
|
|
65
|
+
label: 'fetch merged PR counts',
|
|
66
|
+
fallback: emptyPRCountsResult(),
|
|
67
|
+
})),
|
|
68
|
+
prMonitor
|
|
69
|
+
.fetchUserClosedPRCounts(starFilter)
|
|
70
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch closed PR counts', fallback: emptyPRCountsResult() })),
|
|
71
|
+
prMonitor
|
|
72
|
+
.fetchRecentlyClosedPRs()
|
|
73
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] })),
|
|
74
|
+
prMonitor
|
|
75
|
+
.fetchRecentlyMergedPRs()
|
|
76
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently merged PRs', fallback: [] })),
|
|
77
|
+
// Issue conversation fetch has custom messaging based on the error content, so it keeps its bespoke catch.
|
|
87
78
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
88
79
|
if (isRateLimitOrAuthError(error))
|
|
89
80
|
throw error;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
|
-
import { type DailyDigest, type AgentState, type ClosedPR, type MergedPR, type StoredMergedPR, type StoredClosedPR, type CommentedIssue } from '../core/types.js';
|
|
6
|
+
import { type DailyDigest, type AgentState, type ClosedPR, type MergedPR, type StoredMergedPR, type StoredClosedPR, type CommentedIssue, type CommentedIssueWithResponse, type FetchedPR, type ShelvedPRRef, type RepoMetadataEntry } from '../core/types.js';
|
|
7
|
+
import type { ParseIssueListOutput } from '../formatters/json.js';
|
|
7
8
|
export interface DashboardStats {
|
|
8
9
|
activePRs: number;
|
|
9
10
|
shelvedPRs: number;
|
|
@@ -12,6 +13,42 @@ export interface DashboardStats {
|
|
|
12
13
|
mergeRate: string;
|
|
13
14
|
availableIssues?: number;
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Shape of the JSON payload served by `GET /api/data` from the dashboard
|
|
18
|
+
* HTTP server. Single source of truth (#965) — imported by
|
|
19
|
+
* dashboard-server.ts, which previously redeclared this interface inline
|
|
20
|
+
* and so silently drifted whenever this module changed shape.
|
|
21
|
+
*/
|
|
22
|
+
export interface DashboardJsonData {
|
|
23
|
+
stats: DashboardStats;
|
|
24
|
+
prsByRepo: Record<string, {
|
|
25
|
+
active: number;
|
|
26
|
+
merged: number;
|
|
27
|
+
closed: number;
|
|
28
|
+
}>;
|
|
29
|
+
topRepos: Array<{
|
|
30
|
+
repo: string;
|
|
31
|
+
active: number;
|
|
32
|
+
merged: number;
|
|
33
|
+
closed: number;
|
|
34
|
+
}>;
|
|
35
|
+
monthlyMerged: Record<string, number>;
|
|
36
|
+
monthlyOpened: Record<string, number>;
|
|
37
|
+
monthlyClosed: Record<string, number>;
|
|
38
|
+
activePRs: FetchedPR[];
|
|
39
|
+
shelvedPRUrls: string[];
|
|
40
|
+
recentlyMergedPRs: MergedPR[];
|
|
41
|
+
recentlyClosedPRs: ClosedPR[];
|
|
42
|
+
autoUnshelvedPRs: ShelvedPRRef[];
|
|
43
|
+
commentedIssues: CommentedIssue[];
|
|
44
|
+
issueResponses: CommentedIssueWithResponse[];
|
|
45
|
+
allMergedPRs: MergedPR[];
|
|
46
|
+
allClosedPRs: ClosedPR[];
|
|
47
|
+
repoMetadata: Record<string, RepoMetadataEntry>;
|
|
48
|
+
vettedIssues?: ParseIssueListOutput | null;
|
|
49
|
+
offline?: boolean;
|
|
50
|
+
lastUpdated?: string;
|
|
51
|
+
}
|
|
15
52
|
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number, storedClosedCount?: number): DashboardStats;
|
|
16
53
|
/**
|
|
17
54
|
* Merge fresh API counts into existing stored counts.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor, getOctokit } from '../core/index.js';
|
|
7
|
-
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
7
|
+
import { errorMessage, isRateLimitOrAuthError, nonFatalCatch } from '../core/errors.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
9
9
|
import { emptyPRCountsResult, fetchMergedPRsSince, fetchClosedPRsSince } from '../core/github-stats.js';
|
|
10
10
|
import { parseGitHubUrl } from '../core/utils.js';
|
|
@@ -110,30 +110,21 @@ export async function fetchDashboardData(token) {
|
|
|
110
110
|
const closedWatermark = stateManager.getClosedPRWatermark();
|
|
111
111
|
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues, newMergedPRs, newClosedPRs,] = await Promise.all([
|
|
112
112
|
prMonitor.fetchUserOpenPRs(),
|
|
113
|
-
prMonitor
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
prMonitor.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
129
|
-
return emptyPRCountsResult();
|
|
130
|
-
}),
|
|
131
|
-
prMonitor.fetchUserClosedPRCounts(starFilter).catch((err) => {
|
|
132
|
-
if (isRateLimitOrAuthError(err))
|
|
133
|
-
throw err;
|
|
134
|
-
warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
135
|
-
return emptyPRCountsResult();
|
|
136
|
-
}),
|
|
113
|
+
prMonitor
|
|
114
|
+
.fetchRecentlyClosedPRs()
|
|
115
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] })),
|
|
116
|
+
prMonitor
|
|
117
|
+
.fetchRecentlyMergedPRs()
|
|
118
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently merged PRs', fallback: [] })),
|
|
119
|
+
prMonitor.fetchUserMergedPRCounts(starFilter).catch(nonFatalCatch({
|
|
120
|
+
module: MODULE,
|
|
121
|
+
label: 'fetch merged PR counts',
|
|
122
|
+
fallback: emptyPRCountsResult(),
|
|
123
|
+
})),
|
|
124
|
+
prMonitor
|
|
125
|
+
.fetchUserClosedPRCounts(starFilter)
|
|
126
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch closed PR counts', fallback: emptyPRCountsResult() })),
|
|
127
|
+
// Issue conversation fetch has custom messaging based on the error content, so it keeps its bespoke catch.
|
|
137
128
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
138
129
|
if (isRateLimitOrAuthError(error))
|
|
139
130
|
throw error;
|
|
@@ -149,18 +140,8 @@ export async function fetchDashboardData(token) {
|
|
|
149
140
|
failures: [{ issueUrl: 'N/A', error: `Issue conversation fetch failed: ${msg}` }],
|
|
150
141
|
};
|
|
151
142
|
}),
|
|
152
|
-
fetchMergedPRsSince(octokit, config, watermark).catch((
|
|
153
|
-
|
|
154
|
-
throw err;
|
|
155
|
-
warn(MODULE, `Failed to fetch merged PRs for storage: ${errorMessage(err)}`);
|
|
156
|
-
return [];
|
|
157
|
-
}),
|
|
158
|
-
fetchClosedPRsSince(octokit, config, closedWatermark).catch((err) => {
|
|
159
|
-
if (isRateLimitOrAuthError(err))
|
|
160
|
-
throw err;
|
|
161
|
-
warn(MODULE, `Failed to fetch closed PRs for storage: ${errorMessage(err)}`);
|
|
162
|
-
return [];
|
|
163
|
-
}),
|
|
143
|
+
fetchMergedPRsSince(octokit, config, watermark).catch(nonFatalCatch({ module: MODULE, label: 'fetch merged PRs for storage', fallback: [] })),
|
|
144
|
+
fetchClosedPRsSince(octokit, config, closedWatermark).catch(nonFatalCatch({ module: MODULE, label: 'fetch closed PRs for storage', fallback: [] })),
|
|
164
145
|
]);
|
|
165
146
|
const commentedIssues = fetchedIssues.issues;
|
|
166
147
|
if (fetchedIssues.failures.length > 0) {
|
|
@@ -216,10 +197,12 @@ export async function fetchDashboardData(token) {
|
|
|
216
197
|
return { digest, commentedIssues, allMergedPRs, allClosedPRs };
|
|
217
198
|
}
|
|
218
199
|
/**
|
|
219
|
-
* Convert StoredMergedPR[]
|
|
220
|
-
*
|
|
200
|
+
* Convert a stored-shape PR array (StoredMergedPR[] or StoredClosedPR[]) to
|
|
201
|
+
* its display-shape equivalent (MergedPR[] / ClosedPR[]) by deriving `repo`
|
|
202
|
+
* and `number` from the URL. Skips entries whose URL cannot be parsed.
|
|
203
|
+
* Shared by storedToMergedPRs and storedToClosedPRs (#959).
|
|
221
204
|
*/
|
|
222
|
-
|
|
205
|
+
function storedToPRs(stored, dateField, label) {
|
|
223
206
|
const results = [];
|
|
224
207
|
let skipped = 0;
|
|
225
208
|
for (const pr of stored) {
|
|
@@ -233,39 +216,27 @@ export function storedToMergedPRs(stored) {
|
|
|
233
216
|
repo: `${parsed.owner}/${parsed.repo}`,
|
|
234
217
|
number: parsed.number,
|
|
235
218
|
title: pr.title,
|
|
236
|
-
|
|
219
|
+
[dateField]: pr[dateField],
|
|
237
220
|
});
|
|
238
221
|
}
|
|
239
222
|
if (skipped > 0) {
|
|
240
|
-
warn(MODULE, `Skipped ${skipped} stored
|
|
223
|
+
warn(MODULE, `Skipped ${skipped} stored ${label} PR(s) with unparseable URLs`);
|
|
241
224
|
}
|
|
242
225
|
return results;
|
|
243
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Convert StoredMergedPR[] to MergedPR[] by deriving repo and number from URL.
|
|
229
|
+
* Skips entries with unparseable URLs.
|
|
230
|
+
*/
|
|
231
|
+
export function storedToMergedPRs(stored) {
|
|
232
|
+
return storedToPRs(stored, 'mergedAt', 'merged');
|
|
233
|
+
}
|
|
244
234
|
/**
|
|
245
235
|
* Convert StoredClosedPR[] to ClosedPR[] by deriving repo and number from URL.
|
|
246
236
|
* Skips entries with unparseable URLs.
|
|
247
237
|
*/
|
|
248
238
|
export function storedToClosedPRs(stored) {
|
|
249
|
-
|
|
250
|
-
let skipped = 0;
|
|
251
|
-
for (const pr of stored) {
|
|
252
|
-
const parsed = parseGitHubUrl(pr.url);
|
|
253
|
-
if (!parsed) {
|
|
254
|
-
skipped++;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
results.push({
|
|
258
|
-
url: pr.url,
|
|
259
|
-
repo: `${parsed.owner}/${parsed.repo}`,
|
|
260
|
-
number: parsed.number,
|
|
261
|
-
title: pr.title,
|
|
262
|
-
closedAt: pr.closedAt,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
if (skipped > 0) {
|
|
266
|
-
warn(MODULE, `Skipped ${skipped} stored closed PR(s) with unparseable URLs`);
|
|
267
|
-
}
|
|
268
|
-
return results;
|
|
239
|
+
return storedToPRs(stored, 'closedAt', 'closed');
|
|
269
240
|
}
|
|
270
241
|
/**
|
|
271
242
|
* Compute PRs grouped by repository from a digest and state.
|
|
@@ -51,6 +51,21 @@ function readVettedIssues() {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the mtime of the vetted issue list file in ms, or null if unknown.
|
|
56
|
+
* Used to detect external edits and invalidate the cached dashboard payload.
|
|
57
|
+
*/
|
|
58
|
+
function getIssueListMtimeMs() {
|
|
59
|
+
try {
|
|
60
|
+
const info = detectIssueList();
|
|
61
|
+
if (!info)
|
|
62
|
+
return null;
|
|
63
|
+
return fs.statSync(info.path).mtimeMs;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
54
69
|
/**
|
|
55
70
|
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
56
71
|
*/
|
|
@@ -186,6 +201,7 @@ export async function startDashboardServer(options) {
|
|
|
186
201
|
}
|
|
187
202
|
// ── Build cached JSON response ───────────────────────────────────────────
|
|
188
203
|
let cachedJsonData;
|
|
204
|
+
let cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
189
205
|
try {
|
|
190
206
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
191
207
|
}
|
|
@@ -217,9 +233,13 @@ export async function startDashboardServer(options) {
|
|
|
217
233
|
else {
|
|
218
234
|
stateChanged = stateManager.reloadIfChanged();
|
|
219
235
|
}
|
|
220
|
-
|
|
236
|
+
// Also rebuild when the vetted issue list file was edited outside this server (#924)
|
|
237
|
+
const currentIssueListMtimeMs = getIssueListMtimeMs();
|
|
238
|
+
const issueListChanged = currentIssueListMtimeMs !== cachedIssueListMtimeMs;
|
|
239
|
+
if (stateChanged || issueListChanged) {
|
|
221
240
|
try {
|
|
222
241
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
242
|
+
cachedIssueListMtimeMs = currentIssueListMtimeMs;
|
|
223
243
|
}
|
|
224
244
|
catch (error) {
|
|
225
245
|
warn(MODULE, `Failed to rebuild dashboard data after state reload: ${errorMessage(error)}`);
|
|
@@ -336,6 +356,7 @@ export async function startDashboardServer(options) {
|
|
|
336
356
|
}
|
|
337
357
|
// Rebuild dashboard data from cached digest + updated state
|
|
338
358
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
359
|
+
cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
339
360
|
sendJson(res, 200, cachedJsonData);
|
|
340
361
|
}
|
|
341
362
|
// ── POST /api/refresh handler ────────────────────────────────────────────
|
|
@@ -357,6 +378,7 @@ export async function startDashboardServer(options) {
|
|
|
357
378
|
cachedDigest = result.digest;
|
|
358
379
|
cachedCommentedIssues = result.commentedIssues;
|
|
359
380
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
|
|
381
|
+
cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
360
382
|
sendJson(res, 200, cachedJsonData);
|
|
361
383
|
}
|
|
362
384
|
catch (error) {
|
|
@@ -476,6 +498,7 @@ export async function startDashboardServer(options) {
|
|
|
476
498
|
cachedDigest = result.digest;
|
|
477
499
|
cachedCommentedIssues = result.commentedIssues;
|
|
478
500
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
|
|
501
|
+
cachedIssueListMtimeMs = getIssueListMtimeMs();
|
|
479
502
|
warn(MODULE, 'Background data refresh complete');
|
|
480
503
|
})
|
|
481
504
|
.catch((error) => {
|
|
@@ -44,6 +44,13 @@ export function buildScoutState() {
|
|
|
44
44
|
title: pr.title,
|
|
45
45
|
closedAt: pr.closedAt,
|
|
46
46
|
})),
|
|
47
|
+
// Map ephemeral openPRs (regenerated each daily run) from the last digest
|
|
48
|
+
// so oss-scout's Phase 0 also searches repos with active-but-unmerged PRs.
|
|
49
|
+
openPRs: (state.lastDigest?.openPRs ?? []).map((pr) => ({
|
|
50
|
+
url: pr.url,
|
|
51
|
+
title: pr.title,
|
|
52
|
+
openedAt: pr.createdAt,
|
|
53
|
+
})),
|
|
47
54
|
savedResults: [],
|
|
48
55
|
skippedIssues: [],
|
|
49
56
|
lastRunAt: state.lastRunAt,
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -45,6 +45,30 @@ export declare function getHttpStatusCode(error: unknown): number | undefined;
|
|
|
45
45
|
export declare function isRateLimitError(error: unknown): boolean;
|
|
46
46
|
/** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
|
|
47
47
|
export declare function isRateLimitOrAuthError(err: unknown): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Build a `.catch()` handler for the "non-fatal parallel fetch" pattern used
|
|
50
|
+
* by daily.ts and dashboard-data.ts (#960). When a sibling fetch fails during
|
|
51
|
+
* a bulk-parallel orchestration, we want:
|
|
52
|
+
*
|
|
53
|
+
* - rate-limit / auth errors to propagate (those abort the whole run — the
|
|
54
|
+
* user needs to see them), and
|
|
55
|
+
* - every other error to log a warning and fall back to a safe default so
|
|
56
|
+
* the other siblings can still succeed.
|
|
57
|
+
*
|
|
58
|
+
* Inline at each call site, this is ~4 lines of boilerplate repeated 10+
|
|
59
|
+
* times. Consolidated here so the rate-limit-rethrow rule lives in exactly
|
|
60
|
+
* one place.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* prMonitor.fetchRecentlyClosedPRs().catch(
|
|
64
|
+
* nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] as ClosedPR[] })
|
|
65
|
+
* );
|
|
66
|
+
*/
|
|
67
|
+
export declare function nonFatalCatch<T>(params: {
|
|
68
|
+
module: string;
|
|
69
|
+
label: string;
|
|
70
|
+
fallback: T;
|
|
71
|
+
}): (err: unknown) => T;
|
|
48
72
|
/**
|
|
49
73
|
* Map an unknown error to a structured ErrorCode for JSON output.
|
|
50
74
|
* Checks custom error classes, HTTP status codes (Octokit errors),
|
package/dist/core/errors.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* propagate to the caller via isRateLimitError/isRateLimitOrAuthError.
|
|
8
8
|
* Other errors degrade gracefully — modules return partial results and log warnings.
|
|
9
9
|
*/
|
|
10
|
+
import { warn } from './logger.js';
|
|
10
11
|
/**
|
|
11
12
|
* Base error for all oss-autopilot errors.
|
|
12
13
|
*/
|
|
@@ -87,6 +88,33 @@ export function isRateLimitOrAuthError(err) {
|
|
|
87
88
|
}
|
|
88
89
|
return false;
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a `.catch()` handler for the "non-fatal parallel fetch" pattern used
|
|
93
|
+
* by daily.ts and dashboard-data.ts (#960). When a sibling fetch fails during
|
|
94
|
+
* a bulk-parallel orchestration, we want:
|
|
95
|
+
*
|
|
96
|
+
* - rate-limit / auth errors to propagate (those abort the whole run — the
|
|
97
|
+
* user needs to see them), and
|
|
98
|
+
* - every other error to log a warning and fall back to a safe default so
|
|
99
|
+
* the other siblings can still succeed.
|
|
100
|
+
*
|
|
101
|
+
* Inline at each call site, this is ~4 lines of boilerplate repeated 10+
|
|
102
|
+
* times. Consolidated here so the rate-limit-rethrow rule lives in exactly
|
|
103
|
+
* one place.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* prMonitor.fetchRecentlyClosedPRs().catch(
|
|
107
|
+
* nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] as ClosedPR[] })
|
|
108
|
+
* );
|
|
109
|
+
*/
|
|
110
|
+
export function nonFatalCatch(params) {
|
|
111
|
+
return (err) => {
|
|
112
|
+
if (isRateLimitOrAuthError(err))
|
|
113
|
+
throw err;
|
|
114
|
+
warn(params.module, `Failed to ${params.label}: ${errorMessage(err)}`);
|
|
115
|
+
return params.fallback;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
90
118
|
/**
|
|
91
119
|
* Map an unknown error to a structured ErrorCode for JSON output.
|
|
92
120
|
* Checks custom error classes, HTTP status codes (Octokit errors),
|
|
@@ -235,22 +235,18 @@ export async function fetchRecentlyMergedPRs(octokit, config, days = 7) {
|
|
|
235
235
|
};
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
* Fetch merged PRs since a watermark date for incremental storage.
|
|
240
|
-
* If no watermark is provided (first-ever fetch), fetches all merged PRs (up to pagination cap).
|
|
241
|
-
* Returns StoredMergedPR[] (minimal: url, title, mergedAt) for state persistence.
|
|
242
|
-
*/
|
|
243
|
-
export async function fetchMergedPRsSince(octokit, config, since) {
|
|
238
|
+
async function fetchPRsSince(octokit, config, adapter, since) {
|
|
244
239
|
if (!config.githubUsername) {
|
|
245
|
-
warn(MODULE,
|
|
240
|
+
warn(MODULE, `Skipping ${adapter.kind} PRs fetch: no githubUsername configured.`);
|
|
246
241
|
return [];
|
|
247
242
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
debug(MODULE, `Fetching merged PRs${since ? ` since ${since}` : ' (all time)'}...`);
|
|
243
|
+
const q = adapter.buildQuery(config.githubUsername, since);
|
|
244
|
+
debug(MODULE, `Fetching ${adapter.kind} PRs${since ? ` since ${since}` : ' (all time)'}...`);
|
|
251
245
|
const results = [];
|
|
252
246
|
let page = 1;
|
|
253
247
|
let fetched = 0;
|
|
248
|
+
// Populated from the first Search API response; always set before we exit the loop.
|
|
249
|
+
let totalCount;
|
|
254
250
|
while (true) {
|
|
255
251
|
const { data } = await octokit.search.issuesAndPullRequests({
|
|
256
252
|
q,
|
|
@@ -259,34 +255,48 @@ export async function fetchMergedPRsSince(octokit, config, since) {
|
|
|
259
255
|
per_page: 100,
|
|
260
256
|
page,
|
|
261
257
|
});
|
|
258
|
+
totalCount = data.total_count;
|
|
262
259
|
for (const item of data.items) {
|
|
263
260
|
const parsed = parseGitHubUrl(item.html_url);
|
|
264
261
|
if (!parsed) {
|
|
265
|
-
warn(MODULE, `Skipping
|
|
262
|
+
warn(MODULE, `Skipping ${adapter.kind} PR with unparseable URL: ${item.html_url}`);
|
|
266
263
|
continue;
|
|
267
264
|
}
|
|
268
265
|
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
269
266
|
continue;
|
|
270
|
-
const
|
|
271
|
-
if (!
|
|
272
|
-
warn(MODULE, `Skipping
|
|
267
|
+
const date = adapter.extractDate(item);
|
|
268
|
+
if (!date) {
|
|
269
|
+
warn(MODULE, `Skipping ${adapter.kind} PR with no ${adapter.dateNoun} date: ${item.html_url}`);
|
|
273
270
|
continue;
|
|
274
271
|
}
|
|
275
|
-
results.push(
|
|
276
|
-
url: item.html_url,
|
|
277
|
-
title: item.title,
|
|
278
|
-
mergedAt,
|
|
279
|
-
});
|
|
272
|
+
results.push(adapter.buildRecord(item, date));
|
|
280
273
|
}
|
|
281
274
|
fetched += data.items.length;
|
|
282
|
-
if (fetched >=
|
|
275
|
+
if (fetched >= totalCount || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
283
276
|
break;
|
|
284
277
|
}
|
|
285
278
|
page++;
|
|
286
279
|
}
|
|
287
|
-
|
|
280
|
+
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
281
|
+
warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} ${adapter.kind} PRs. Oldest PRs may be missing.`);
|
|
282
|
+
}
|
|
283
|
+
debug(MODULE, `Fetched ${results.length} ${adapter.kind} PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
288
284
|
return results;
|
|
289
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Fetch merged PRs since a watermark date for incremental storage.
|
|
288
|
+
* If no watermark is provided (first-ever fetch), fetches all merged PRs (up to pagination cap).
|
|
289
|
+
* Returns StoredMergedPR[] (minimal: url, title, mergedAt) for state persistence.
|
|
290
|
+
*/
|
|
291
|
+
export async function fetchMergedPRsSince(octokit, config, since) {
|
|
292
|
+
return fetchPRsSince(octokit, config, {
|
|
293
|
+
kind: 'merged',
|
|
294
|
+
dateNoun: 'merge',
|
|
295
|
+
buildQuery: (u, s) => `is:pr is:merged author:${u} -user:${u}${s ? ` merged:>${s}` : ''}`,
|
|
296
|
+
extractDate: (item) => item.pull_request?.merged_at || item.closed_at || '',
|
|
297
|
+
buildRecord: (item, date) => ({ url: item.html_url, title: item.title, mergedAt: date }),
|
|
298
|
+
}, since);
|
|
299
|
+
}
|
|
290
300
|
/**
|
|
291
301
|
* Fetch closed-without-merge PRs since a watermark date for incremental storage.
|
|
292
302
|
* If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
|
|
@@ -294,54 +304,11 @@ export async function fetchMergedPRsSince(octokit, config, since) {
|
|
|
294
304
|
* Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
|
|
295
305
|
*/
|
|
296
306
|
export async function fetchClosedPRsSince(octokit, config, since) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const results = [];
|
|
305
|
-
let page = 1;
|
|
306
|
-
let fetched = 0;
|
|
307
|
-
let totalCount;
|
|
308
|
-
while (true) {
|
|
309
|
-
const { data } = await octokit.search.issuesAndPullRequests({
|
|
310
|
-
q,
|
|
311
|
-
sort: 'updated',
|
|
312
|
-
order: 'desc',
|
|
313
|
-
per_page: 100,
|
|
314
|
-
page,
|
|
315
|
-
});
|
|
316
|
-
totalCount = data.total_count;
|
|
317
|
-
for (const item of data.items) {
|
|
318
|
-
const parsed = parseGitHubUrl(item.html_url);
|
|
319
|
-
if (!parsed) {
|
|
320
|
-
warn(MODULE, `Skipping closed PR with unparseable URL: ${item.html_url}`);
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
324
|
-
continue;
|
|
325
|
-
const closedAt = item.closed_at || '';
|
|
326
|
-
if (!closedAt) {
|
|
327
|
-
warn(MODULE, `Skipping closed PR with no close date: ${item.html_url}`);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
results.push({
|
|
331
|
-
url: item.html_url,
|
|
332
|
-
title: item.title,
|
|
333
|
-
closedAt,
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
fetched += data.items.length;
|
|
337
|
-
if (fetched >= totalCount || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
page++;
|
|
341
|
-
}
|
|
342
|
-
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
343
|
-
warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} closed PRs. Oldest PRs may be missing.`);
|
|
344
|
-
}
|
|
345
|
-
debug(MODULE, `Fetched ${results.length} closed PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
346
|
-
return results;
|
|
307
|
+
return fetchPRsSince(octokit, config, {
|
|
308
|
+
kind: 'closed',
|
|
309
|
+
dateNoun: 'close',
|
|
310
|
+
buildQuery: (u, s) => `is:pr is:closed is:unmerged author:${u} -user:${u}${s ? ` closed:>${s}` : ''}`,
|
|
311
|
+
extractDate: (item) => item.closed_at || '',
|
|
312
|
+
buildRecord: (item, date) => ({ url: item.html_url, title: item.title, closedAt: date }),
|
|
313
|
+
}, since);
|
|
347
314
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
9
9
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
10
10
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
11
11
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
12
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
12
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
15
15
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
package/dist/core/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { IssueConversationMonitor } from './issue-conversation.js';
|
|
|
10
10
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
11
11
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
12
12
|
export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, stateFileExists, DEFAULT_CONCURRENCY, } from './utils.js';
|
|
13
|
-
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, resolveErrorCode, } from './errors.js';
|
|
13
|
+
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
|
|
14
14
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
15
15
|
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
16
16
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-autopilot/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "CLI and core library for managing open source contributions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@octokit/plugin-throttling": "^11.0.3",
|
|
52
52
|
"@octokit/rest": "^22.0.1",
|
|
53
|
-
"@oss-scout/core": "^0.
|
|
53
|
+
"@oss-scout/core": "^0.5.0",
|
|
54
54
|
"commander": "^14.0.3",
|
|
55
55
|
"zod": "^4.3.6"
|
|
56
56
|
},
|