@oss-autopilot/core 0.43.0 → 0.43.1
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 +263 -17002
- package/dist/commands/config.js +2 -1
- package/dist/commands/daily.js +57 -36
- package/dist/commands/dashboard-server.js +3 -1
- package/dist/commands/startup.js +30 -3
- package/dist/commands/vet.js +2 -1
- package/dist/core/concurrency.d.ts +2 -1
- package/dist/core/concurrency.js +12 -2
- package/dist/core/pr-monitor.js +12 -12
- package/dist/core/utils.d.ts +1 -2
- package/dist/core/utils.js +4 -2
- package/package.json +2 -2
package/dist/commands/config.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shows or updates configuration
|
|
4
4
|
*/
|
|
5
5
|
import { getStateManager } from '../core/index.js';
|
|
6
|
+
import { validateGitHubUsername } from './validation.js';
|
|
6
7
|
export async function runConfig(options) {
|
|
7
8
|
const stateManager = getStateManager();
|
|
8
9
|
const currentConfig = stateManager.getState().config;
|
|
@@ -17,7 +18,7 @@ export async function runConfig(options) {
|
|
|
17
18
|
// Handle specific config keys
|
|
18
19
|
switch (options.key) {
|
|
19
20
|
case 'username':
|
|
20
|
-
stateManager.updateConfig({ githubUsername: value });
|
|
21
|
+
stateManager.updateConfig({ githubUsername: validateGitHubUsername(value) });
|
|
21
22
|
break;
|
|
22
23
|
case 'add-language':
|
|
23
24
|
if (!currentConfig.languages.includes(value)) {
|
package/dist/commands/daily.js
CHANGED
|
@@ -7,9 +7,22 @@
|
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
9
|
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
|
-
import { errorMessage } from '../core/errors.js';
|
|
10
|
+
import { errorMessage, getHttpStatusCode } from '../core/errors.js';
|
|
11
|
+
import { warn } from '../core/logger.js';
|
|
11
12
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
12
13
|
import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
|
|
14
|
+
const MODULE = 'daily';
|
|
15
|
+
/** Return true for errors that should propagate (not degrade gracefully). */
|
|
16
|
+
function isRateLimitOrAuthError(err) {
|
|
17
|
+
const status = getHttpStatusCode(err);
|
|
18
|
+
if (status === 401 || status === 429)
|
|
19
|
+
return true;
|
|
20
|
+
if (status === 403) {
|
|
21
|
+
const msg = errorMessage(err).toLowerCase();
|
|
22
|
+
return msg.includes('rate limit') || msg.includes('abuse detection');
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
13
26
|
// Re-export domain functions so existing consumers (tests, dashboard, startup)
|
|
14
27
|
// can continue importing from './daily.js' without changes.
|
|
15
28
|
export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
@@ -26,35 +39,45 @@ async function fetchPRData(prMonitor, token) {
|
|
|
26
39
|
const { prs, failures } = await prMonitor.fetchUserOpenPRs();
|
|
27
40
|
// Log any failures (but continue with successful checks)
|
|
28
41
|
if (failures.length > 0) {
|
|
29
|
-
|
|
42
|
+
warn(MODULE, `${failures.length} PR fetch(es) failed`);
|
|
30
43
|
}
|
|
31
44
|
// Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
|
|
32
45
|
// All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
|
|
33
46
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
34
47
|
const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
|
|
35
48
|
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
36
|
-
|
|
49
|
+
if (isRateLimitOrAuthError(err))
|
|
50
|
+
throw err;
|
|
51
|
+
warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
37
52
|
return emptyPRCountsResult();
|
|
38
53
|
}),
|
|
39
54
|
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
40
|
-
|
|
55
|
+
if (isRateLimitOrAuthError(err))
|
|
56
|
+
throw err;
|
|
57
|
+
warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
41
58
|
return emptyPRCountsResult();
|
|
42
59
|
}),
|
|
43
60
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
44
|
-
|
|
61
|
+
if (isRateLimitOrAuthError(err))
|
|
62
|
+
throw err;
|
|
63
|
+
warn(MODULE, `Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
45
64
|
return [];
|
|
46
65
|
}),
|
|
47
66
|
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
48
|
-
|
|
67
|
+
if (isRateLimitOrAuthError(err))
|
|
68
|
+
throw err;
|
|
69
|
+
warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
49
70
|
return [];
|
|
50
71
|
}),
|
|
51
72
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
73
|
+
if (isRateLimitOrAuthError(error))
|
|
74
|
+
throw error;
|
|
52
75
|
const msg = errorMessage(error);
|
|
53
76
|
if (msg.includes('No GitHub username configured')) {
|
|
54
|
-
|
|
77
|
+
warn(MODULE, `Issue conversation tracking requires setup: ${msg}`);
|
|
55
78
|
}
|
|
56
79
|
else {
|
|
57
|
-
|
|
80
|
+
warn(MODULE, `Issue conversation fetch failed: ${msg}`);
|
|
58
81
|
}
|
|
59
82
|
return {
|
|
60
83
|
issues: [],
|
|
@@ -64,7 +87,7 @@ async function fetchPRData(prMonitor, token) {
|
|
|
64
87
|
]);
|
|
65
88
|
const commentedIssues = issueConversationResult.issues;
|
|
66
89
|
if (issueConversationResult.failures.length > 0) {
|
|
67
|
-
|
|
90
|
+
warn(MODULE, `${issueConversationResult.failures.length} issue conversation check(s) failed`);
|
|
68
91
|
}
|
|
69
92
|
const { repos: mergedCounts, monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
70
93
|
const { repos: closedCounts, monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed, } = closedResult;
|
|
@@ -94,7 +117,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
94
117
|
// skip the reset to avoid wiping scores due to transient API failures.
|
|
95
118
|
const existingReposWithMerges = Object.values(stateManager.getState().repoScores).filter((s) => s.mergedPRCount > 0);
|
|
96
119
|
if (mergedCounts.size === 0 && existingReposWithMerges.length > 0) {
|
|
97
|
-
|
|
120
|
+
warn(MODULE, `Skipping stale repo reset: API returned 0 merged PR results but state has ${existingReposWithMerges.length} repo(s) with merges. Possible API issue.`);
|
|
98
121
|
}
|
|
99
122
|
else {
|
|
100
123
|
for (const score of Object.values(stateManager.getState().repoScores)) {
|
|
@@ -111,18 +134,18 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
111
134
|
}
|
|
112
135
|
catch (error) {
|
|
113
136
|
mergedCountFailures++;
|
|
114
|
-
|
|
137
|
+
warn(MODULE, `Failed to update merged count for ${repo}: ${errorMessage(error)}`);
|
|
115
138
|
}
|
|
116
139
|
}
|
|
117
140
|
if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
118
|
-
|
|
141
|
+
warn(MODULE, `[ALL_MERGED_COUNT_UPDATES_FAILED] All ${mergedCounts.size} merged count update(s) failed.`);
|
|
119
142
|
}
|
|
120
143
|
// Populate closedWithoutMergeCount in repo scores.
|
|
121
144
|
// Diagnostic: warn if API returned empty but we have known closed PRs (possible transient API failure).
|
|
122
145
|
// Unlike merged counts above, there is no stale-reset loop for closed counts, so no skip is needed.
|
|
123
146
|
const existingReposWithClosed = Object.values(stateManager.getState().repoScores).filter((s) => (s.closedWithoutMergeCount || 0) > 0);
|
|
124
147
|
if (closedCounts.size === 0 && existingReposWithClosed.length > 0) {
|
|
125
|
-
|
|
148
|
+
warn(MODULE, `API returned 0 closed PR results but state has ${existingReposWithClosed.length} repo(s) with closed PRs. Possible transient API issue.`);
|
|
126
149
|
}
|
|
127
150
|
let closedCountFailures = 0;
|
|
128
151
|
for (const [repo, count] of closedCounts) {
|
|
@@ -131,11 +154,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
131
154
|
}
|
|
132
155
|
catch (error) {
|
|
133
156
|
closedCountFailures++;
|
|
134
|
-
|
|
157
|
+
warn(MODULE, `Failed to update closed count for ${repo}: ${errorMessage(error)}`);
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
|
|
138
|
-
|
|
161
|
+
warn(MODULE, `[ALL_CLOSED_COUNT_UPDATES_FAILED] All ${closedCounts.size} closed count update(s) failed.`);
|
|
139
162
|
}
|
|
140
163
|
// Update repo signals from observed open PR data (responsiveness, active maintainers).
|
|
141
164
|
// Only repos with current open PRs get signal updates — repos with no open PRs
|
|
@@ -150,11 +173,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
150
173
|
}
|
|
151
174
|
catch (error) {
|
|
152
175
|
signalUpdateFailures++;
|
|
153
|
-
|
|
176
|
+
warn(MODULE, `Failed to update signals for ${repo}: ${errorMessage(error)}`);
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
|
|
157
|
-
|
|
180
|
+
warn(MODULE, `[ALL_SIGNAL_UPDATES_FAILED] All ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
|
|
158
181
|
}
|
|
159
182
|
// Fetch star counts for all scored repos (used by dashboard minStars filter, #216)
|
|
160
183
|
const allRepos = Object.keys(stateManager.getState().repoScores);
|
|
@@ -163,8 +186,8 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
163
186
|
starCounts = await prMonitor.fetchRepoStarCounts(allRepos);
|
|
164
187
|
}
|
|
165
188
|
catch (error) {
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
warn(MODULE, `Failed to fetch repo star counts: ${errorMessage(error)}`);
|
|
190
|
+
warn(MODULE, 'Dashboard minStars filter will use cached star counts (or be skipped for repos without cached data).');
|
|
168
191
|
starCounts = new Map();
|
|
169
192
|
}
|
|
170
193
|
let starUpdateFailures = 0;
|
|
@@ -174,11 +197,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
174
197
|
}
|
|
175
198
|
catch (error) {
|
|
176
199
|
starUpdateFailures++;
|
|
177
|
-
|
|
200
|
+
warn(MODULE, `Failed to update star count for ${repo}: ${errorMessage(error)}`);
|
|
178
201
|
}
|
|
179
202
|
}
|
|
180
203
|
if (starUpdateFailures === starCounts.size && starCounts.size > 0) {
|
|
181
|
-
|
|
204
|
+
warn(MODULE, `[ALL_STAR_COUNT_UPDATES_FAILED] All ${starCounts.size} star count update(s) failed.`);
|
|
182
205
|
}
|
|
183
206
|
// Auto-sync trustedProjects from repos with merged PRs
|
|
184
207
|
let trustSyncFailures = 0;
|
|
@@ -188,11 +211,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
188
211
|
}
|
|
189
212
|
catch (error) {
|
|
190
213
|
trustSyncFailures++;
|
|
191
|
-
|
|
214
|
+
warn(MODULE, `Failed to sync trusted project ${repo}: ${errorMessage(error)}`);
|
|
192
215
|
}
|
|
193
216
|
}
|
|
194
217
|
if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
195
|
-
|
|
218
|
+
warn(MODULE, `[ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
221
|
/**
|
|
@@ -211,7 +234,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
211
234
|
}
|
|
212
235
|
}
|
|
213
236
|
catch (error) {
|
|
214
|
-
|
|
237
|
+
warn(MODULE, `Failed to store monthly merged counts: ${errorMessage(error)}`);
|
|
215
238
|
}
|
|
216
239
|
try {
|
|
217
240
|
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
@@ -219,7 +242,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
219
242
|
}
|
|
220
243
|
}
|
|
221
244
|
catch (error) {
|
|
222
|
-
|
|
245
|
+
warn(MODULE, `Failed to store monthly closed counts: ${errorMessage(error)}`);
|
|
223
246
|
}
|
|
224
247
|
try {
|
|
225
248
|
// Build combined monthly opened counts from merged + closed + currently-open PRs
|
|
@@ -239,7 +262,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
239
262
|
}
|
|
240
263
|
}
|
|
241
264
|
catch (error) {
|
|
242
|
-
|
|
265
|
+
warn(MODULE, `Failed to compute/store monthly opened counts: ${errorMessage(error)}`);
|
|
243
266
|
}
|
|
244
267
|
}
|
|
245
268
|
/**
|
|
@@ -254,15 +277,13 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
254
277
|
try {
|
|
255
278
|
const expiredSnoozes = stateManager.expireSnoozes();
|
|
256
279
|
if (expiredSnoozes.length > 0) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
console.error(` - ${url}`);
|
|
260
|
-
}
|
|
280
|
+
const urls = expiredSnoozes.map((url) => ` - ${url}`).join('\n');
|
|
281
|
+
warn(MODULE, `${expiredSnoozes.length} snoozed PR(s) expired and will resurface:\n${urls}`);
|
|
261
282
|
stateManager.save();
|
|
262
283
|
}
|
|
263
284
|
}
|
|
264
285
|
catch (error) {
|
|
265
|
-
|
|
286
|
+
warn(MODULE, `Failed to expire/persist snoozes: ${errorMessage(error)}`);
|
|
266
287
|
}
|
|
267
288
|
// Partition PRs into active vs shelved, auto-unshelving when maintainers engage
|
|
268
289
|
const shelvedPRs = [];
|
|
@@ -322,12 +343,12 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
322
343
|
if (isNaN(responseTime) || isNaN(dismissTime)) {
|
|
323
344
|
// Invalid timestamp — fail open (include issue to be safe) without
|
|
324
345
|
// permanently removing dismiss record (may be a transient data issue)
|
|
325
|
-
|
|
346
|
+
warn(MODULE, `Invalid timestamp in dismiss check for ${issue.url}, including issue`);
|
|
326
347
|
return true;
|
|
327
348
|
}
|
|
328
349
|
if (responseTime > dismissTime) {
|
|
329
350
|
// New activity after dismiss — auto-undismiss and resurface
|
|
330
|
-
|
|
351
|
+
warn(MODULE, `Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`);
|
|
331
352
|
stateManager.undismissIssue(issue.url);
|
|
332
353
|
hasAutoUndismissed = true;
|
|
333
354
|
return true;
|
|
@@ -349,12 +370,12 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
349
370
|
if (isNaN(activityTime) || isNaN(dismissTime)) {
|
|
350
371
|
// Invalid timestamp — fail open (include PR to be safe) without
|
|
351
372
|
// permanently removing dismiss record (may be a transient data issue)
|
|
352
|
-
|
|
373
|
+
warn(MODULE, `Invalid timestamp in PR dismiss check for ${pr.url}, including PR`);
|
|
353
374
|
return true;
|
|
354
375
|
}
|
|
355
376
|
if (activityTime > dismissTime) {
|
|
356
377
|
// New activity after dismiss — auto-undismiss and resurface
|
|
357
|
-
|
|
378
|
+
warn(MODULE, `Auto-undismissing PR ${pr.url}: new activity at ${pr.updatedAt} after dismiss at ${dismissedAt}`);
|
|
358
379
|
stateManager.undismissIssue(pr.url);
|
|
359
380
|
hasAutoUndismissed = true;
|
|
360
381
|
return true;
|
|
@@ -368,7 +389,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
368
389
|
stateManager.save();
|
|
369
390
|
}
|
|
370
391
|
catch (error) {
|
|
371
|
-
|
|
392
|
+
warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
|
|
372
393
|
}
|
|
373
394
|
}
|
|
374
395
|
const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
|
|
@@ -114,7 +114,7 @@ export async function findRunningDashboardServer() {
|
|
|
114
114
|
function buildDashboardJson(digest, state, commentedIssues) {
|
|
115
115
|
const prsByRepo = computePRsByRepo(digest, state);
|
|
116
116
|
const topRepos = computeTopRepos(prsByRepo);
|
|
117
|
-
const { monthlyMerged } = getMonthlyData(state);
|
|
117
|
+
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
118
118
|
const stats = buildDashboardStats(digest, state);
|
|
119
119
|
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
120
120
|
return {
|
|
@@ -122,6 +122,8 @@ function buildDashboardJson(digest, state, commentedIssues) {
|
|
|
122
122
|
prsByRepo,
|
|
123
123
|
topRepos: topRepos.map(([repo, data]) => ({ repo, ...data })),
|
|
124
124
|
monthlyMerged,
|
|
125
|
+
monthlyOpened,
|
|
126
|
+
monthlyClosed,
|
|
125
127
|
activePRs: digest.openPRs || [],
|
|
126
128
|
shelvedPRUrls: state.config.shelvedPRUrls || [],
|
|
127
129
|
commentedIssues,
|
package/dist/commands/startup.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { execFile } from 'child_process';
|
|
11
|
-
import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
|
|
11
|
+
import { getStateManager, getGitHubToken, getCLIVersion, getStatePath, getDashboardPath } from '../core/index.js';
|
|
12
12
|
import { errorMessage } from '../core/errors.js';
|
|
13
|
+
import { warn } from '../core/logger.js';
|
|
13
14
|
import { executeDailyCheck } from './daily.js';
|
|
14
15
|
import { writeDashboardFromState } from './dashboard.js';
|
|
15
16
|
import { launchDashboardServer } from './dashboard-lifecycle.js';
|
|
@@ -116,6 +117,25 @@ function openInBrowser(filePath) {
|
|
|
116
117
|
}
|
|
117
118
|
});
|
|
118
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Check whether the dashboard HTML file is at least as recent as state.json.
|
|
122
|
+
* Returns true when the dashboard exists and its mtime >= state mtime,
|
|
123
|
+
* meaning there is no need to regenerate it.
|
|
124
|
+
*/
|
|
125
|
+
function isDashboardFresh() {
|
|
126
|
+
try {
|
|
127
|
+
const dashPath = getDashboardPath();
|
|
128
|
+
if (!fs.existsSync(dashPath))
|
|
129
|
+
return false;
|
|
130
|
+
const dashMtime = fs.statSync(dashPath).mtimeMs;
|
|
131
|
+
const stateMtime = fs.statSync(getStatePath()).mtimeMs;
|
|
132
|
+
return dashMtime >= stateMtime;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
warn('startup', `Failed to check dashboard freshness, will regenerate: ${errorMessage(error)}`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
119
139
|
/**
|
|
120
140
|
* Run startup checks and return structured output.
|
|
121
141
|
* Returns StartupOutput with one of three shapes:
|
|
@@ -143,10 +163,17 @@ export async function runStartup() {
|
|
|
143
163
|
}
|
|
144
164
|
// 3. Run daily check
|
|
145
165
|
const daily = await executeDailyCheck(token);
|
|
146
|
-
// 4. Generate static HTML dashboard (
|
|
166
|
+
// 4. Generate static HTML dashboard (serves as fallback + snapshot).
|
|
167
|
+
// Skip regeneration if the dashboard HTML is already newer than state.json.
|
|
147
168
|
let dashboardPath;
|
|
148
169
|
try {
|
|
149
|
-
|
|
170
|
+
if (isDashboardFresh()) {
|
|
171
|
+
dashboardPath = getDashboardPath();
|
|
172
|
+
console.error('[STARTUP] Dashboard HTML is fresh, skipping regeneration');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
dashboardPath = writeDashboardFromState();
|
|
176
|
+
}
|
|
150
177
|
}
|
|
151
178
|
catch (error) {
|
|
152
179
|
console.error('[STARTUP] Dashboard generation failed:', errorMessage(error));
|
package/dist/commands/vet.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Vets a specific issue before working on it
|
|
4
4
|
*/
|
|
5
5
|
import { IssueDiscovery, requireGitHubToken } from '../core/index.js';
|
|
6
|
-
import { validateUrl } from './validation.js';
|
|
6
|
+
import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
7
7
|
export async function runVet(options) {
|
|
8
8
|
validateUrl(options.issueUrl);
|
|
9
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
|
|
9
10
|
const token = requireGitHubToken();
|
|
10
11
|
const discovery = new IssueDiscovery(token);
|
|
11
12
|
const candidate = await discovery.vetIssue(options.issueUrl);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runs a worker pool that processes items with bounded concurrency.
|
|
3
|
-
* N workers consume from a shared index
|
|
3
|
+
* N workers consume from a shared index. On any worker error, remaining
|
|
4
|
+
* workers are aborted via a shared flag and the error is propagated.
|
|
4
5
|
*/
|
|
5
6
|
export declare function runWorkerPool<T>(items: T[], worker: (item: T) => Promise<void>, concurrency: number): Promise<void>;
|
package/dist/core/concurrency.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runs a worker pool that processes items with bounded concurrency.
|
|
3
|
-
* N workers consume from a shared index
|
|
3
|
+
* N workers consume from a shared index. On any worker error, remaining
|
|
4
|
+
* workers are aborted via a shared flag and the error is propagated.
|
|
4
5
|
*/
|
|
5
6
|
export async function runWorkerPool(items, worker, concurrency) {
|
|
6
7
|
let index = 0;
|
|
8
|
+
let aborted = false;
|
|
7
9
|
const poolWorker = async () => {
|
|
8
10
|
while (index < items.length) {
|
|
11
|
+
if (aborted)
|
|
12
|
+
break;
|
|
9
13
|
const item = items[index++];
|
|
10
|
-
|
|
14
|
+
try {
|
|
15
|
+
await worker(item);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
aborted = true;
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
11
21
|
}
|
|
12
22
|
};
|
|
13
23
|
const workerCount = Math.min(concurrency, items.length);
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -350,11 +350,7 @@ export class PRMonitor {
|
|
|
350
350
|
* Check if PR has merge conflict
|
|
351
351
|
*/
|
|
352
352
|
hasMergeConflict(mergeable, mergeableState) {
|
|
353
|
-
|
|
354
|
-
return true;
|
|
355
|
-
if (mergeableState === 'dirty')
|
|
356
|
-
return true;
|
|
357
|
-
return false;
|
|
353
|
+
return mergeable === false || mergeableState === 'dirty';
|
|
358
354
|
}
|
|
359
355
|
/**
|
|
360
356
|
* Get CI status from combined status API and check runs.
|
|
@@ -371,6 +367,14 @@ export class PRMonitor {
|
|
|
371
367
|
// 404 is expected for repos without check runs configured; log other errors for debugging
|
|
372
368
|
this.octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
|
|
373
369
|
const status = getHttpStatusCode(err);
|
|
370
|
+
// Rate limit errors must propagate — matches listReviewComments pattern (#481)
|
|
371
|
+
if (status === 429)
|
|
372
|
+
throw err;
|
|
373
|
+
if (status === 403) {
|
|
374
|
+
const msg = errorMessage(err).toLowerCase();
|
|
375
|
+
if (msg.includes('rate limit') || msg.includes('abuse detection'))
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
374
378
|
if (status === 404) {
|
|
375
379
|
debug('pr-monitor', `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
|
|
376
380
|
}
|
|
@@ -400,12 +404,8 @@ export class PRMonitor {
|
|
|
400
404
|
}
|
|
401
405
|
catch (error) {
|
|
402
406
|
const statusCode = getHttpStatusCode(error);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
warn('pr-monitor', `CI check failed for ${owner}/${repo}: Invalid token`);
|
|
406
|
-
}
|
|
407
|
-
else if (statusCode === 403) {
|
|
408
|
-
warn('pr-monitor', `CI check failed for ${owner}/${repo}: Rate limit exceeded`);
|
|
407
|
+
if (statusCode === 401 || statusCode === 403 || statusCode === 429) {
|
|
408
|
+
throw error;
|
|
409
409
|
}
|
|
410
410
|
else if (statusCode === 404) {
|
|
411
411
|
// Repo might not have CI configured, this is normal
|
|
@@ -413,7 +413,7 @@ export class PRMonitor {
|
|
|
413
413
|
return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
414
414
|
}
|
|
415
415
|
else {
|
|
416
|
-
warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${
|
|
416
|
+
warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage(error)}`);
|
|
417
417
|
}
|
|
418
418
|
return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
419
419
|
}
|
package/dist/core/utils.d.ts
CHANGED
|
@@ -144,10 +144,9 @@ export declare function daysBetween(from: Date, to?: Date): number;
|
|
|
144
144
|
/**
|
|
145
145
|
* Splits an `"owner/repo"` string into its owner and repo components.
|
|
146
146
|
*
|
|
147
|
-
* Does not validate the input format; if no `/` is present, `repo` will be `undefined`.
|
|
148
|
-
*
|
|
149
147
|
* @param repoFullName - Full repository name in `"owner/repo"` format
|
|
150
148
|
* @returns Object with `owner` and `repo` string properties
|
|
149
|
+
* @throws {Error} If the input does not contain both an owner and repo separated by `/`
|
|
151
150
|
*
|
|
152
151
|
* @example
|
|
153
152
|
* splitRepo('facebook/react')
|
package/dist/core/utils.js
CHANGED
|
@@ -215,10 +215,9 @@ export function daysBetween(from, to = new Date()) {
|
|
|
215
215
|
/**
|
|
216
216
|
* Splits an `"owner/repo"` string into its owner and repo components.
|
|
217
217
|
*
|
|
218
|
-
* Does not validate the input format; if no `/` is present, `repo` will be `undefined`.
|
|
219
|
-
*
|
|
220
218
|
* @param repoFullName - Full repository name in `"owner/repo"` format
|
|
221
219
|
* @returns Object with `owner` and `repo` string properties
|
|
220
|
+
* @throws {Error} If the input does not contain both an owner and repo separated by `/`
|
|
222
221
|
*
|
|
223
222
|
* @example
|
|
224
223
|
* splitRepo('facebook/react')
|
|
@@ -226,6 +225,9 @@ export function daysBetween(from, to = new Date()) {
|
|
|
226
225
|
*/
|
|
227
226
|
export function splitRepo(repoFullName) {
|
|
228
227
|
const [owner, repo] = repoFullName.split('/');
|
|
228
|
+
if (!owner || !repo) {
|
|
229
|
+
throw new Error(`Invalid repo format: expected "owner/repo", got "${repoFullName}"`);
|
|
230
|
+
}
|
|
229
231
|
return { owner, repo };
|
|
230
232
|
}
|
|
231
233
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-autopilot/core",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.1",
|
|
4
4
|
"description": "CLI and core library for managing open source contributions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc",
|
|
63
|
-
"bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/cli.bundle.cjs",
|
|
63
|
+
"bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --minify --outfile=dist/cli.bundle.cjs",
|
|
64
64
|
"start": "tsx src/cli.ts",
|
|
65
65
|
"dev": "tsx watch src/cli.ts",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|