@oss-autopilot/core 0.44.1 → 0.44.3
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-registry.d.ts +2 -1
- package/dist/cli.bundle.cjs +93 -94
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/daily.js +3 -60
- package/dist/commands/dashboard-data.d.ts +13 -0
- package/dist/commands/dashboard-data.js +56 -56
- package/dist/commands/dashboard-server.js +60 -29
- package/dist/commands/dashboard-templates.js +1 -1
- package/dist/core/checklist-analysis.js +3 -1
- package/dist/core/ci-analysis.d.ts +3 -17
- package/dist/core/ci-analysis.js +3 -3
- package/dist/core/errors.d.ts +8 -0
- package/dist/core/errors.js +26 -0
- package/dist/core/github-stats.d.ts +0 -20
- package/dist/core/github-stats.js +1 -1
- package/dist/core/github.js +2 -2
- package/dist/core/http-cache.d.ts +0 -2
- package/dist/core/http-cache.js +0 -4
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/core/issue-conversation.js +2 -2
- package/dist/core/issue-discovery.d.ts +0 -5
- package/dist/core/issue-discovery.js +4 -11
- package/dist/core/issue-vetting.d.ts +0 -2
- package/dist/core/issue-vetting.js +30 -44
- package/dist/core/pr-monitor.js +3 -4
- package/dist/core/test-utils.d.ts +1 -3
- package/dist/core/test-utils.js +0 -41
- package/dist/core/utils.d.ts +2 -0
- package/dist/core/utils.js +5 -1
- package/package.json +1 -1
package/dist/commands/daily.js
CHANGED
|
@@ -7,22 +7,12 @@
|
|
|
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,
|
|
10
|
+
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
11
11
|
import { warn } from '../core/logger.js';
|
|
12
12
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
13
|
+
import { updateMonthlyAnalytics } from './dashboard-data.js';
|
|
13
14
|
import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
|
|
14
15
|
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
|
-
}
|
|
26
16
|
// Re-export domain functions so existing consumers (tests, dashboard, startup)
|
|
27
17
|
// can continue importing from './daily.js' without changes.
|
|
28
18
|
export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
@@ -218,53 +208,6 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
218
208
|
warn(MODULE, `[ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
|
|
219
209
|
}
|
|
220
210
|
}
|
|
221
|
-
/**
|
|
222
|
-
* Phase 3: Persist monthly chart analytics to state.
|
|
223
|
-
* Stores merged, closed, and combined opened counts per month.
|
|
224
|
-
* Each metric is isolated so partial failures don't produce inconsistent state.
|
|
225
|
-
*/
|
|
226
|
-
function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
|
|
227
|
-
const stateManager = getStateManager();
|
|
228
|
-
// Store monthly chart data (non-critical — each metric isolated so partial failures don't leave inconsistent state).
|
|
229
|
-
// Guard: skip overwriting when the data is empty to avoid wiping existing chart data on transient API failures.
|
|
230
|
-
// An empty object means the fetch failed and fell back to emptyPRCountsResult(), so we preserve previous state.
|
|
231
|
-
try {
|
|
232
|
-
if (Object.keys(monthlyCounts).length > 0) {
|
|
233
|
-
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
warn(MODULE, `Failed to store monthly merged counts: ${errorMessage(error)}`);
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
241
|
-
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
warn(MODULE, `Failed to store monthly closed counts: ${errorMessage(error)}`);
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
// Build combined monthly opened counts from merged + closed + currently-open PRs
|
|
249
|
-
const combinedOpenedCounts = { ...openedFromMerged };
|
|
250
|
-
for (const [month, count] of Object.entries(openedFromClosed)) {
|
|
251
|
-
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
|
|
252
|
-
}
|
|
253
|
-
// Add currently-open PR creation dates
|
|
254
|
-
for (const pr of prs) {
|
|
255
|
-
if (pr.createdAt) {
|
|
256
|
-
const month = pr.createdAt.slice(0, 7);
|
|
257
|
-
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
261
|
-
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
warn(MODULE, `Failed to compute/store monthly opened counts: ${errorMessage(error)}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
211
|
/**
|
|
269
212
|
* Phase 4: Expire snoozes and partition PRs into active vs shelved buckets.
|
|
270
213
|
* Auto-unshelves PRs where maintainers have engaged, generates the digest,
|
|
@@ -459,7 +402,7 @@ async function executeDailyCheckInternal(token) {
|
|
|
459
402
|
// Phase 2: Update repo scores (signals, star counts, trust sync)
|
|
460
403
|
await updateRepoScores(prMonitor, prs, mergedCounts, closedCounts);
|
|
461
404
|
// Phase 3: Persist monthly analytics
|
|
462
|
-
|
|
405
|
+
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
463
406
|
// Phase 4: Expire snoozes, partition PRs, generate and save digest
|
|
464
407
|
const { activePRs, shelvedPRs, digest } = partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
465
408
|
// Phase 5: Build structured output (capacity, dismiss filter, action menu)
|
|
@@ -12,10 +12,23 @@ export interface DashboardStats {
|
|
|
12
12
|
mergeRate: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
|
|
15
|
+
/**
|
|
16
|
+
* Persist monthly chart analytics (merged, closed, opened) to state.
|
|
17
|
+
* Each metric is isolated so partial failures don't produce inconsistent state.
|
|
18
|
+
* Skips overwriting when data is empty to avoid wiping chart data on transient API failures.
|
|
19
|
+
*/
|
|
20
|
+
export declare function updateMonthlyAnalytics(prs: Array<{
|
|
21
|
+
createdAt?: string;
|
|
22
|
+
}>, monthlyCounts: Record<string, number>, monthlyClosedCounts: Record<string, number>, openedFromMerged: Record<string, number>, openedFromClosed: Record<string, number>): void;
|
|
15
23
|
export interface DashboardFetchResult {
|
|
16
24
|
digest: DailyDigest;
|
|
17
25
|
commentedIssues: CommentedIssue[];
|
|
18
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Fetch fresh dashboard data from GitHub.
|
|
29
|
+
* Returns the digest and commented issues, updating state as a side effect.
|
|
30
|
+
* Throws if the fetch fails entirely (caller should fall back to cached data).
|
|
31
|
+
*/
|
|
19
32
|
export declare function fetchDashboardData(token: string): Promise<DashboardFetchResult>;
|
|
20
33
|
/**
|
|
21
34
|
* Compute PRs grouped by repository from a digest and state.
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
|
|
7
|
-
import { errorMessage,
|
|
7
|
+
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
8
|
+
import { warn } from '../core/logger.js';
|
|
8
9
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
9
10
|
import { toShelvedPRRef } from './daily.js';
|
|
11
|
+
const MODULE = 'dashboard-data';
|
|
10
12
|
export function buildDashboardStats(digest, state) {
|
|
11
13
|
const summary = digest.summary || {
|
|
12
14
|
totalActivePRs: 0,
|
|
@@ -22,21 +24,53 @@ export function buildDashboardStats(digest, state) {
|
|
|
22
24
|
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
23
25
|
};
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Persist monthly chart analytics (merged, closed, opened) to state.
|
|
29
|
+
* Each metric is isolated so partial failures don't produce inconsistent state.
|
|
30
|
+
* Skips overwriting when data is empty to avoid wiping chart data on transient API failures.
|
|
31
|
+
*/
|
|
32
|
+
export function updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
|
|
33
|
+
const stateManager = getStateManager();
|
|
34
|
+
try {
|
|
35
|
+
if (Object.keys(monthlyCounts).length > 0) {
|
|
36
|
+
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
warn(MODULE, `Failed to store monthly merged counts: ${errorMessage(error)}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
44
|
+
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
warn(MODULE, `Failed to store monthly closed counts: ${errorMessage(error)}`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const combinedOpenedCounts = { ...openedFromMerged };
|
|
52
|
+
for (const [month, count] of Object.entries(openedFromClosed)) {
|
|
53
|
+
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
|
|
54
|
+
}
|
|
55
|
+
for (const pr of prs) {
|
|
56
|
+
if (pr.createdAt) {
|
|
57
|
+
const month = pr.createdAt.slice(0, 7);
|
|
58
|
+
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
62
|
+
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
warn(MODULE, `Failed to store monthly opened counts: ${errorMessage(error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
25
69
|
/**
|
|
26
70
|
* Fetch fresh dashboard data from GitHub.
|
|
27
71
|
* Returns the digest and commented issues, updating state as a side effect.
|
|
28
72
|
* Throws if the fetch fails entirely (caller should fall back to cached data).
|
|
29
73
|
*/
|
|
30
|
-
function isRateLimitOrAuthError(err) {
|
|
31
|
-
const status = getHttpStatusCode(err);
|
|
32
|
-
if (status === 401 || status === 429)
|
|
33
|
-
return true;
|
|
34
|
-
if (status === 403) {
|
|
35
|
-
const msg = errorMessage(err).toLowerCase();
|
|
36
|
-
return msg.includes('rate limit') || msg.includes('abuse detection');
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
74
|
export async function fetchDashboardData(token) {
|
|
41
75
|
const stateManager = getStateManager();
|
|
42
76
|
const prMonitor = new PRMonitor(token);
|
|
@@ -46,34 +80,34 @@ export async function fetchDashboardData(token) {
|
|
|
46
80
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
47
81
|
if (isRateLimitOrAuthError(err))
|
|
48
82
|
throw err;
|
|
49
|
-
|
|
83
|
+
warn(MODULE, `Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
50
84
|
return [];
|
|
51
85
|
}),
|
|
52
86
|
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
53
87
|
if (isRateLimitOrAuthError(err))
|
|
54
88
|
throw err;
|
|
55
|
-
|
|
89
|
+
warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
56
90
|
return [];
|
|
57
91
|
}),
|
|
58
92
|
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
59
93
|
if (isRateLimitOrAuthError(err))
|
|
60
94
|
throw err;
|
|
61
|
-
|
|
95
|
+
warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
62
96
|
return emptyPRCountsResult();
|
|
63
97
|
}),
|
|
64
98
|
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
65
99
|
if (isRateLimitOrAuthError(err))
|
|
66
100
|
throw err;
|
|
67
|
-
|
|
101
|
+
warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
68
102
|
return emptyPRCountsResult();
|
|
69
103
|
}),
|
|
70
104
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
71
105
|
const msg = errorMessage(error);
|
|
72
106
|
if (msg.includes('No GitHub username configured')) {
|
|
73
|
-
|
|
107
|
+
warn(MODULE, `Issue conversation tracking requires setup: ${msg}`);
|
|
74
108
|
}
|
|
75
109
|
else {
|
|
76
|
-
|
|
110
|
+
warn(MODULE, `Issue conversation fetch failed: ${msg}`);
|
|
77
111
|
}
|
|
78
112
|
return {
|
|
79
113
|
issues: [],
|
|
@@ -83,49 +117,15 @@ export async function fetchDashboardData(token) {
|
|
|
83
117
|
]);
|
|
84
118
|
const commentedIssues = fetchedIssues.issues;
|
|
85
119
|
if (fetchedIssues.failures.length > 0) {
|
|
86
|
-
|
|
120
|
+
warn(MODULE, `${fetchedIssues.failures.length} issue conversation check(s) failed`);
|
|
87
121
|
}
|
|
88
122
|
if (failures.length > 0) {
|
|
89
|
-
|
|
123
|
+
warn(MODULE, `${failures.length} PR fetch(es) failed`);
|
|
90
124
|
}
|
|
91
125
|
// Store monthly chart data (opened/merged/closed) so charts have data
|
|
92
126
|
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
93
127
|
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
if (Object.keys(monthlyCounts).length > 0) {
|
|
97
|
-
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
console.error('[DASHBOARD] Failed to store monthly merged counts:', errorMessage(error));
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
105
|
-
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
console.error('[DASHBOARD] Failed to store monthly closed counts:', errorMessage(error));
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
const combinedOpenedCounts = { ...openedFromMerged };
|
|
113
|
-
for (const [month, count] of Object.entries(openedFromClosed)) {
|
|
114
|
-
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + count;
|
|
115
|
-
}
|
|
116
|
-
for (const pr of prs) {
|
|
117
|
-
if (pr.createdAt) {
|
|
118
|
-
const month = pr.createdAt.slice(0, 7);
|
|
119
|
-
combinedOpenedCounts[month] = (combinedOpenedCounts[month] || 0) + 1;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
123
|
-
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
console.error('[DASHBOARD] Failed to store monthly opened counts:', errorMessage(error));
|
|
128
|
-
}
|
|
128
|
+
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
129
129
|
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
130
130
|
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
131
131
|
// Dormant PRs are treated as shelved for display purposes
|
|
@@ -139,9 +139,9 @@ export async function fetchDashboardData(token) {
|
|
|
139
139
|
stateManager.save();
|
|
140
140
|
}
|
|
141
141
|
catch (error) {
|
|
142
|
-
|
|
142
|
+
warn(MODULE, `Failed to save dashboard digest to state: ${errorMessage(error)}`);
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
145
145
|
return { digest, commentedIssues };
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
@@ -10,6 +10,7 @@ import * as fs from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { getStateManager, getGitHubToken, getDataDir } from '../core/index.js';
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
|
+
import { warn } from '../core/logger.js';
|
|
13
14
|
import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN, } from './validation.js';
|
|
14
15
|
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
|
|
15
16
|
import { openInBrowser } from './startup.js';
|
|
@@ -22,7 +23,9 @@ const VALID_ACTIONS = new Set([
|
|
|
22
23
|
'dismiss',
|
|
23
24
|
'undismiss',
|
|
24
25
|
]);
|
|
26
|
+
const MODULE = 'dashboard-server';
|
|
25
27
|
const MAX_BODY_BYTES = 10_240;
|
|
28
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
26
29
|
const MIME_TYPES = {
|
|
27
30
|
'.html': 'text/html',
|
|
28
31
|
'.js': 'application/javascript',
|
|
@@ -48,7 +51,7 @@ export function readDashboardServerInfo() {
|
|
|
48
51
|
typeof parsed.pid !== 'number' ||
|
|
49
52
|
typeof parsed.port !== 'number' ||
|
|
50
53
|
typeof parsed.startedAt !== 'string') {
|
|
51
|
-
|
|
54
|
+
warn(MODULE, 'PID file has invalid structure, ignoring');
|
|
52
55
|
return null;
|
|
53
56
|
}
|
|
54
57
|
return parsed;
|
|
@@ -56,7 +59,7 @@ export function readDashboardServerInfo() {
|
|
|
56
59
|
catch (err) {
|
|
57
60
|
const code = err.code;
|
|
58
61
|
if (code !== 'ENOENT') {
|
|
59
|
-
|
|
62
|
+
warn(MODULE, `Failed to read PID file: ${err.message}`);
|
|
60
63
|
}
|
|
61
64
|
return null;
|
|
62
65
|
}
|
|
@@ -68,7 +71,7 @@ export function removeDashboardServerInfo() {
|
|
|
68
71
|
catch (err) {
|
|
69
72
|
const code = err.code;
|
|
70
73
|
if (code !== 'ENOENT') {
|
|
71
|
-
|
|
74
|
+
warn(MODULE, `Failed to remove PID file: ${err.message}`);
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
}
|
|
@@ -98,7 +101,7 @@ export async function findRunningDashboardServer() {
|
|
|
98
101
|
catch (err) {
|
|
99
102
|
const code = err.code;
|
|
100
103
|
if (code !== 'ESRCH' && code !== 'EPERM') {
|
|
101
|
-
|
|
104
|
+
warn(MODULE, `Unexpected error checking PID ${info.pid}: ${err.message}`);
|
|
102
105
|
}
|
|
103
106
|
// ESRCH = no process at that PID; EPERM = PID recycled to another user's process
|
|
104
107
|
// Either way, our dashboard server is no longer running — clean up stale PID file
|
|
@@ -170,10 +173,32 @@ function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
|
170
173
|
});
|
|
171
174
|
});
|
|
172
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Set security headers on every response.
|
|
178
|
+
*/
|
|
179
|
+
function setSecurityHeaders(res) {
|
|
180
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
181
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
182
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'");
|
|
183
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Validate that POST requests originate from the local dashboard.
|
|
187
|
+
* Returns true if the Origin is acceptable, false otherwise.
|
|
188
|
+
*/
|
|
189
|
+
function isValidOrigin(req, port) {
|
|
190
|
+
const origin = req.headers['origin'];
|
|
191
|
+
if (!origin)
|
|
192
|
+
return true; // No Origin header = same-origin request (non-browser or same-page)
|
|
193
|
+
const allowed = [`http://localhost:${port}`, `http://127.0.0.1:${port}`];
|
|
194
|
+
return allowed.includes(origin);
|
|
195
|
+
}
|
|
173
196
|
/**
|
|
174
197
|
* Send a JSON response.
|
|
175
198
|
*/
|
|
176
199
|
function sendJson(res, statusCode, data) {
|
|
200
|
+
setSecurityHeaders(res);
|
|
201
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
177
202
|
const body = JSON.stringify(data);
|
|
178
203
|
res.writeHead(statusCode, {
|
|
179
204
|
'Content-Type': 'application/json',
|
|
@@ -199,9 +224,7 @@ export async function startDashboardServer(options) {
|
|
|
199
224
|
let cachedDigest = stateManager.getState().lastDigest;
|
|
200
225
|
let cachedCommentedIssues = [];
|
|
201
226
|
if (!cachedDigest) {
|
|
202
|
-
|
|
203
|
-
console.error(' GITHUB_TOKEN=$(gh auth token) npm start -- daily');
|
|
204
|
-
process.exit(1);
|
|
227
|
+
throw new Error('No dashboard data available. Run the daily check first: GITHUB_TOKEN=$(gh auth token) npm start -- daily');
|
|
205
228
|
}
|
|
206
229
|
// ── Build cached JSON response ───────────────────────────────────────────
|
|
207
230
|
let cachedJsonData;
|
|
@@ -209,9 +232,7 @@ export async function startDashboardServer(options) {
|
|
|
209
232
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
210
233
|
}
|
|
211
234
|
catch (error) {
|
|
212
|
-
|
|
213
|
-
console.error('Your state data may be corrupted. Try running: daily --json');
|
|
214
|
-
process.exit(1);
|
|
235
|
+
throw new Error(`Failed to build dashboard data: ${errorMessage(error)}. State data may be corrupted — try running: daily --json`, { cause: error });
|
|
215
236
|
}
|
|
216
237
|
// ── Request handler ──────────────────────────────────────────────────────
|
|
217
238
|
const server = http.createServer(async (req, res) => {
|
|
@@ -224,10 +245,18 @@ export async function startDashboardServer(options) {
|
|
|
224
245
|
return;
|
|
225
246
|
}
|
|
226
247
|
if (url === '/api/action' && method === 'POST') {
|
|
248
|
+
if (!isValidOrigin(req, actualPort)) {
|
|
249
|
+
sendError(res, 403, 'Invalid origin');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
227
252
|
await handleAction(req, res);
|
|
228
253
|
return;
|
|
229
254
|
}
|
|
230
255
|
if (url === '/api/refresh' && method === 'POST') {
|
|
256
|
+
if (!isValidOrigin(req, actualPort)) {
|
|
257
|
+
sendError(res, 403, 'Invalid origin');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
231
260
|
await handleRefresh(req, res);
|
|
232
261
|
return;
|
|
233
262
|
}
|
|
@@ -239,12 +268,13 @@ export async function startDashboardServer(options) {
|
|
|
239
268
|
sendError(res, 405, 'Method not allowed');
|
|
240
269
|
}
|
|
241
270
|
catch (error) {
|
|
242
|
-
|
|
271
|
+
warn(MODULE, `Unhandled request error: ${method} ${url} ${errorMessage(error)}`);
|
|
243
272
|
if (!res.headersSent) {
|
|
244
273
|
sendError(res, 500, 'Internal server error');
|
|
245
274
|
}
|
|
246
275
|
}
|
|
247
276
|
});
|
|
277
|
+
server.requestTimeout = REQUEST_TIMEOUT_MS;
|
|
248
278
|
// ── POST /api/action handler ─────────────────────────────────────────────
|
|
249
279
|
async function handleAction(req, res) {
|
|
250
280
|
let body;
|
|
@@ -279,7 +309,7 @@ export async function startDashboardServer(options) {
|
|
|
279
309
|
sendError(res, 400, err.message);
|
|
280
310
|
}
|
|
281
311
|
else {
|
|
282
|
-
|
|
312
|
+
warn(MODULE, `Unexpected error during URL validation: ${errorMessage(err)}`);
|
|
283
313
|
sendError(res, 400, 'Invalid URL');
|
|
284
314
|
}
|
|
285
315
|
return;
|
|
@@ -300,7 +330,7 @@ export async function startDashboardServer(options) {
|
|
|
300
330
|
sendError(res, 400, err.message);
|
|
301
331
|
}
|
|
302
332
|
else {
|
|
303
|
-
|
|
333
|
+
warn(MODULE, `Unexpected error during message validation: ${errorMessage(err)}`);
|
|
304
334
|
sendError(res, 400, 'Invalid reason');
|
|
305
335
|
}
|
|
306
336
|
return;
|
|
@@ -333,8 +363,8 @@ export async function startDashboardServer(options) {
|
|
|
333
363
|
stateManager.save();
|
|
334
364
|
}
|
|
335
365
|
catch (error) {
|
|
336
|
-
|
|
337
|
-
sendError(res, 500,
|
|
366
|
+
warn(MODULE, `Action failed: ${body.action} ${body.url} ${errorMessage(error)}`);
|
|
367
|
+
sendError(res, 500, 'Action failed');
|
|
338
368
|
return;
|
|
339
369
|
}
|
|
340
370
|
// Rebuild dashboard data from cached digest + updated state
|
|
@@ -349,7 +379,7 @@ export async function startDashboardServer(options) {
|
|
|
349
379
|
return;
|
|
350
380
|
}
|
|
351
381
|
try {
|
|
352
|
-
|
|
382
|
+
warn(MODULE, 'Refreshing dashboard data from GitHub...');
|
|
353
383
|
const result = await fetchDashboardData(currentToken);
|
|
354
384
|
cachedDigest = result.digest;
|
|
355
385
|
cachedCommentedIssues = result.commentedIssues;
|
|
@@ -357,8 +387,8 @@ export async function startDashboardServer(options) {
|
|
|
357
387
|
sendJson(res, 200, cachedJsonData);
|
|
358
388
|
}
|
|
359
389
|
catch (error) {
|
|
360
|
-
|
|
361
|
-
sendError(res, 500,
|
|
390
|
+
warn(MODULE, `Dashboard refresh failed: ${errorMessage(error)}`);
|
|
391
|
+
sendError(res, 500, 'Refresh failed');
|
|
362
392
|
}
|
|
363
393
|
}
|
|
364
394
|
// ── Static file serving ──────────────────────────────────────────────────
|
|
@@ -368,8 +398,8 @@ export async function startDashboardServer(options) {
|
|
|
368
398
|
try {
|
|
369
399
|
urlPath = decodeURIComponent(requestUrl.split('?')[0]);
|
|
370
400
|
}
|
|
371
|
-
catch (
|
|
372
|
-
|
|
401
|
+
catch (_err) {
|
|
402
|
+
warn(MODULE, `Malformed URL received: ${requestUrl}`);
|
|
373
403
|
sendError(res, 400, 'Malformed URL');
|
|
374
404
|
return;
|
|
375
405
|
}
|
|
@@ -399,7 +429,7 @@ export async function startDashboardServer(options) {
|
|
|
399
429
|
filePath = path.join(resolvedAssetsDir, 'index.html');
|
|
400
430
|
}
|
|
401
431
|
else {
|
|
402
|
-
|
|
432
|
+
warn(MODULE, `Failed to stat file: ${filePath}`);
|
|
403
433
|
sendError(res, 500, 'Internal server error');
|
|
404
434
|
return;
|
|
405
435
|
}
|
|
@@ -408,9 +438,11 @@ export async function startDashboardServer(options) {
|
|
|
408
438
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
409
439
|
try {
|
|
410
440
|
const content = fs.readFileSync(filePath);
|
|
441
|
+
setSecurityHeaders(res);
|
|
411
442
|
res.writeHead(200, {
|
|
412
443
|
'Content-Type': contentType,
|
|
413
444
|
'Content-Length': content.length,
|
|
445
|
+
'Cache-Control': 'public, max-age=3600',
|
|
414
446
|
});
|
|
415
447
|
res.end(content);
|
|
416
448
|
}
|
|
@@ -420,7 +452,7 @@ export async function startDashboardServer(options) {
|
|
|
420
452
|
sendError(res, 404, 'Not found');
|
|
421
453
|
}
|
|
422
454
|
else {
|
|
423
|
-
|
|
455
|
+
warn(MODULE, `Failed to serve static file: ${filePath}`);
|
|
424
456
|
sendError(res, 500, 'Failed to read file');
|
|
425
457
|
}
|
|
426
458
|
}
|
|
@@ -440,18 +472,17 @@ export async function startDashboardServer(options) {
|
|
|
440
472
|
catch (err) {
|
|
441
473
|
const nodeErr = err;
|
|
442
474
|
if (nodeErr.code === 'EADDRINUSE' && attempt < MAX_PORT_ATTEMPTS - 1) {
|
|
443
|
-
|
|
475
|
+
warn(MODULE, `Port ${actualPort} is in use, trying ${actualPort + 1}...`);
|
|
444
476
|
actualPort++;
|
|
445
477
|
continue;
|
|
446
478
|
}
|
|
447
|
-
|
|
448
|
-
process.exit(1);
|
|
479
|
+
throw new Error(`Failed to start server: ${nodeErr.message}`, { cause: err });
|
|
449
480
|
}
|
|
450
481
|
}
|
|
451
482
|
// Write PID file so other processes can detect this running server
|
|
452
483
|
writeDashboardServerInfo({ pid: process.pid, port: actualPort, startedAt: new Date().toISOString() });
|
|
453
484
|
const serverUrl = `http://localhost:${actualPort}`;
|
|
454
|
-
|
|
485
|
+
warn(MODULE, `Dashboard server running at ${serverUrl}`);
|
|
455
486
|
// ── Background refresh ─────────────────────────────────────────────────
|
|
456
487
|
// Port is bound and PID file written — now fetch fresh data from GitHub
|
|
457
488
|
// so subsequent /api/data requests get live data instead of cached state.
|
|
@@ -461,10 +492,10 @@ export async function startDashboardServer(options) {
|
|
|
461
492
|
cachedDigest = result.digest;
|
|
462
493
|
cachedCommentedIssues = result.commentedIssues;
|
|
463
494
|
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
464
|
-
|
|
495
|
+
warn(MODULE, 'Background data refresh complete');
|
|
465
496
|
})
|
|
466
497
|
.catch((error) => {
|
|
467
|
-
|
|
498
|
+
warn(MODULE, `Background data refresh failed (serving cached data): ${errorMessage(error)}`);
|
|
468
499
|
});
|
|
469
500
|
}
|
|
470
501
|
// ── Open browser ─────────────────────────────────────────────────────────
|
|
@@ -473,7 +504,7 @@ export async function startDashboardServer(options) {
|
|
|
473
504
|
}
|
|
474
505
|
// ── Clean shutdown ───────────────────────────────────────────────────────
|
|
475
506
|
const shutdown = () => {
|
|
476
|
-
|
|
507
|
+
warn(MODULE, 'Shutting down dashboard server...');
|
|
477
508
|
removeDashboardServerInfo();
|
|
478
509
|
server.close(() => {
|
|
479
510
|
process.exit(0);
|
|
@@ -42,7 +42,7 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
|
|
|
42
42
|
<meta charset="UTF-8">
|
|
43
43
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
44
44
|
<title>OSS Autopilot - Mission Control</title>
|
|
45
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
45
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
|
|
46
46
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
47
47
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
48
48
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
@@ -32,8 +32,10 @@ export function analyzeChecklist(body) {
|
|
|
32
32
|
return { hasIncompleteChecklist: false };
|
|
33
33
|
// Filter out conditional checklist items that are intentionally unchecked
|
|
34
34
|
const nonConditionalUnchecked = uncheckedLines.filter((line) => !isConditionalChecklistItem(line));
|
|
35
|
+
// Use consistent total that excludes conditional items (matches hasIncompleteChecklist logic)
|
|
36
|
+
const effectiveTotal = checked + nonConditionalUnchecked.length;
|
|
35
37
|
return {
|
|
36
38
|
hasIncompleteChecklist: nonConditionalUnchecked.length > 0,
|
|
37
|
-
checklistStats: { checked, total },
|
|
39
|
+
checklistStats: { checked, total: effectiveTotal },
|
|
38
40
|
};
|
|
39
41
|
}
|
|
@@ -3,21 +3,6 @@
|
|
|
3
3
|
* Extracted from PRMonitor to isolate CI-related logic (#263).
|
|
4
4
|
*/
|
|
5
5
|
import { CIFailureCategory, ClassifiedCheck, CIStatusResult } from './types.js';
|
|
6
|
-
/**
|
|
7
|
-
* Known CI check name patterns that indicate fork limitations rather than real failures (#81).
|
|
8
|
-
* These are deployment/preview services that require repo-level secrets unavailable in forks.
|
|
9
|
-
*/
|
|
10
|
-
export declare const FORK_LIMITATION_PATTERNS: RegExp[];
|
|
11
|
-
/**
|
|
12
|
-
* Known CI check name patterns that indicate authorization gates (#81).
|
|
13
|
-
* These require maintainer approval and are not real failures.
|
|
14
|
-
*/
|
|
15
|
-
export declare const AUTH_GATE_PATTERNS: RegExp[];
|
|
16
|
-
/**
|
|
17
|
-
* Known CI check name patterns that indicate infrastructure/transient failures (#145).
|
|
18
|
-
* These are runner issues, dependency install problems, or service outages — not code failures.
|
|
19
|
-
*/
|
|
20
|
-
export declare const INFRASTRUCTURE_PATTERNS: RegExp[];
|
|
21
6
|
/**
|
|
22
7
|
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145).
|
|
23
8
|
* Default is 'actionable' — only known patterns get reclassified.
|
|
@@ -45,7 +30,7 @@ export declare function analyzeCheckRuns(checkRuns: Array<{
|
|
|
45
30
|
failingCheckConclusions: Map<string, string>;
|
|
46
31
|
};
|
|
47
32
|
/** Result shape from analyzeCheckRuns, used by mergeStatuses. */
|
|
48
|
-
|
|
33
|
+
interface CheckRunAnalysis {
|
|
49
34
|
hasFailingChecks: boolean;
|
|
50
35
|
hasPendingChecks: boolean;
|
|
51
36
|
hasSuccessfulChecks: boolean;
|
|
@@ -53,7 +38,7 @@ export interface CheckRunAnalysis {
|
|
|
53
38
|
failingCheckConclusions: Map<string, string>;
|
|
54
39
|
}
|
|
55
40
|
/** Result shape from analyzeCombinedStatus, used by mergeStatuses. */
|
|
56
|
-
|
|
41
|
+
interface CombinedStatusAnalysis {
|
|
57
42
|
effectiveCombinedState: string;
|
|
58
43
|
hasStatuses: boolean;
|
|
59
44
|
failingStatusNames: string[];
|
|
@@ -76,3 +61,4 @@ export declare function analyzeCombinedStatus(combinedStatus: {
|
|
|
76
61
|
* Priority: failing > pending > passing > unknown.
|
|
77
62
|
*/
|
|
78
63
|
export declare function mergeStatuses(checkRunAnalysis: CheckRunAnalysis, combinedAnalysis: CombinedStatusAnalysis, checkRunCount: number): CIStatusResult;
|
|
64
|
+
export {};
|
package/dist/core/ci-analysis.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Known CI check name patterns that indicate fork limitations rather than real failures (#81).
|
|
7
7
|
* These are deployment/preview services that require repo-level secrets unavailable in forks.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
const FORK_LIMITATION_PATTERNS = [
|
|
10
10
|
/vercel/i,
|
|
11
11
|
/netlify/i,
|
|
12
12
|
/\bpreview\s*deploy/i,
|
|
@@ -20,12 +20,12 @@ export const FORK_LIMITATION_PATTERNS = [
|
|
|
20
20
|
* Known CI check name patterns that indicate authorization gates (#81).
|
|
21
21
|
* These require maintainer approval and are not real failures.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
const AUTH_GATE_PATTERNS = [/authoriz/i, /approval/i, /\bcla\b/i, /license\/cla/i];
|
|
24
24
|
/**
|
|
25
25
|
* Known CI check name patterns that indicate infrastructure/transient failures (#145).
|
|
26
26
|
* These are runner issues, dependency install problems, or service outages — not code failures.
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
const INFRASTRUCTURE_PATTERNS = [
|
|
29
29
|
/\binstall\s*(os\s*)?dep(endenc|s\b)/i,
|
|
30
30
|
/\bsetup\s+fail(ed|ure)?\b/i,
|
|
31
31
|
/\bservice\s*unavailable/i,
|