@oss-autopilot/core 1.17.0 → 1.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.bundle.cjs +47 -47
- package/dist/commands/startup.js +33 -18
- package/dist/core/pr-monitor.d.ts +9 -4
- package/dist/core/pr-monitor.js +113 -13
- package/dist/formatters/json.d.ts +7 -0
- package/dist/formatters/json.js +1 -0
- package/package.json +1 -1
package/dist/commands/startup.js
CHANGED
|
@@ -222,32 +222,46 @@ export async function runStartup() {
|
|
|
222
222
|
}
|
|
223
223
|
// 3. Run daily check
|
|
224
224
|
const daily = await executeDailyCheck(token);
|
|
225
|
-
// 4. Launch interactive SPA dashboard
|
|
226
|
-
//
|
|
225
|
+
// 4. Launch interactive SPA dashboard.
|
|
226
|
+
//
|
|
227
|
+
// Launched unconditionally once setup and auth pass. A prior heuristic skipped
|
|
228
|
+
// launch whenever `totalActivePRs === 0`, assuming that meant a genuine first
|
|
229
|
+
// run and deferring to the CLI's welcome flow. That gate also swallowed the
|
|
230
|
+
// dashboard in three legitimate cases: a misconfigured/stale `githubUsername`
|
|
231
|
+
// (Search API returns zero), transient GitHub API flakes (state still holds
|
|
232
|
+
// merged PRs but the live count is zero), and users genuinely between PRs.
|
|
233
|
+
// The dashboard's own empty-state UI renders "no PRs" cleanly, so always
|
|
234
|
+
// surfacing it is the right default.
|
|
227
235
|
let dashboardUrl;
|
|
228
236
|
let dashboardStatus;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
openInBrowser(spaResult.url);
|
|
240
|
-
dashboardStatus = 'opened';
|
|
241
|
-
}
|
|
237
|
+
let dashboardError;
|
|
238
|
+
try {
|
|
239
|
+
const spaResult = await launchDashboardServer();
|
|
240
|
+
if (spaResult) {
|
|
241
|
+
dashboardUrl = spaResult.url;
|
|
242
|
+
if (spaResult.alreadyRunning) {
|
|
243
|
+
const refreshed = await triggerDashboardRefresh(spaResult.port);
|
|
244
|
+
dashboardStatus = refreshed ? 'refreshed' : 'running';
|
|
242
245
|
}
|
|
243
246
|
else {
|
|
244
|
-
|
|
247
|
+
dashboardStatus = 'opened';
|
|
245
248
|
}
|
|
249
|
+
// `open`/`xdg-open`/`start` focus an existing tab matching the URL
|
|
250
|
+
// instead of duplicating it, so this is safe whether the server was
|
|
251
|
+
// just started or was already running. Closes #830 properly — a user
|
|
252
|
+
// can close the dashboard tab while the daemon keeps running, leaving
|
|
253
|
+
// subsequent /oss runs with no visible dashboard if we didn't re-open.
|
|
254
|
+
openInBrowser(spaResult.url);
|
|
246
255
|
}
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
else {
|
|
257
|
+
dashboardError = 'Dashboard SPA assets not found. Build with: cd packages/dashboard && pnpm run build';
|
|
258
|
+
console.error(`[STARTUP] ${dashboardError}`);
|
|
249
259
|
}
|
|
250
260
|
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
dashboardError = `SPA dashboard launch failed: ${errorMessage(error)}`;
|
|
263
|
+
console.error(`[STARTUP] ${dashboardError}`);
|
|
264
|
+
}
|
|
251
265
|
// Append dashboard status to brief summary
|
|
252
266
|
if (dashboardStatus === 'opened') {
|
|
253
267
|
daily.briefSummary += ' | Dashboard opened in browser';
|
|
@@ -266,6 +280,7 @@ export async function runStartup() {
|
|
|
266
280
|
autoDetected,
|
|
267
281
|
daily,
|
|
268
282
|
dashboardUrl,
|
|
283
|
+
dashboardError,
|
|
269
284
|
issueList,
|
|
270
285
|
};
|
|
271
286
|
}
|
|
@@ -18,6 +18,8 @@ export { computeDisplayLabel } from './display-utils.js';
|
|
|
18
18
|
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
19
19
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
20
20
|
export { determineStatus } from './status-determination.js';
|
|
21
|
+
declare function isPlaceholderUsername(username: string): boolean;
|
|
22
|
+
export { isPlaceholderUsername };
|
|
21
23
|
/**
|
|
22
24
|
* Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
|
|
23
25
|
* Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
|
|
@@ -34,10 +36,13 @@ export interface FetchPRsResult {
|
|
|
34
36
|
prs: FetchedPR[];
|
|
35
37
|
failures: PRCheckFailure[];
|
|
36
38
|
/**
|
|
37
|
-
* Non-fatal warnings accumulated while fetching.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* Non-fatal warnings accumulated while fetching. Populated by:
|
|
40
|
+
* - Placeholder auto-repair (stale/example `githubUsername` replaced with
|
|
41
|
+
* the authenticated viewer's login before the search runs).
|
|
42
|
+
* - Post-fetch viewer-mismatch guardrail (configured username differs
|
|
43
|
+
* from the authenticated viewer when the search returned zero PRs).
|
|
44
|
+
* - Search API 1000-result truncation (#1057 M25).
|
|
45
|
+
* Callers (daily, dashboard) surface these so users see the signal.
|
|
41
46
|
*/
|
|
42
47
|
warnings?: string[];
|
|
43
48
|
}
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -17,7 +17,7 @@ import { getStateManager } from './state.js';
|
|
|
17
17
|
import { daysBetween, parseGitHubUrl, extractOwnerRepo, isOwnRepo, DEFAULT_CONCURRENCY } from './utils.js';
|
|
18
18
|
import { determineStatus } from './status-determination.js';
|
|
19
19
|
import { runWorkerPool } from './concurrency.js';
|
|
20
|
-
import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
|
|
20
|
+
import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitOrAuthError, } from './errors.js';
|
|
21
21
|
import { paginateAll } from './pagination.js';
|
|
22
22
|
import { debug, warn, timed } from './logger.js';
|
|
23
23
|
import { getHttpCache, cachedRequest } from './http-cache.js';
|
|
@@ -32,6 +32,29 @@ export { computeDisplayLabel } from './display-utils.js';
|
|
|
32
32
|
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
33
33
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
34
34
|
export { determineStatus } from './status-determination.js';
|
|
35
|
+
/**
|
|
36
|
+
* Known placeholder values that can end up in `config.githubUsername` from
|
|
37
|
+
* doc snippets, example configs, or aborted setup flows. When the configured
|
|
38
|
+
* username matches one of these, the PR fetch silently returns zero results
|
|
39
|
+
* and the dashboard looks like a fresh install. Detecting these lets us
|
|
40
|
+
* auto-repair the config from the authenticated viewer before fetching.
|
|
41
|
+
*
|
|
42
|
+
* Entries must be lowercase — `Lowercase<string>` on the source tuple makes
|
|
43
|
+
* a non-lowercase entry a compile error, keeping the case-insensitive lookup
|
|
44
|
+
* contract type-checked instead of comment-documented.
|
|
45
|
+
*/
|
|
46
|
+
const PLACEHOLDER_USERNAMES = [
|
|
47
|
+
'example-user',
|
|
48
|
+
'your-username',
|
|
49
|
+
'your-github-username',
|
|
50
|
+
];
|
|
51
|
+
const KNOWN_PLACEHOLDER_USERNAMES = new Set(PLACEHOLDER_USERNAMES);
|
|
52
|
+
function isPlaceholderUsername(username) {
|
|
53
|
+
return KNOWN_PLACEHOLDER_USERNAMES.has(username.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
// Module-private on purpose: callers should only use the predicate so the
|
|
56
|
+
// `.toLowerCase()` contract can't be bypassed by reading the set directly.
|
|
57
|
+
export { isPlaceholderUsername };
|
|
35
58
|
/**
|
|
36
59
|
* Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
|
|
37
60
|
* Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
|
|
@@ -78,17 +101,71 @@ export class PRMonitor {
|
|
|
78
101
|
* ```
|
|
79
102
|
*/
|
|
80
103
|
async fetchUserOpenPRs() {
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
104
|
+
const initialConfig = this.stateManager.getState().config;
|
|
105
|
+
if (!initialConfig.githubUsername) {
|
|
83
106
|
throw new ConfigurationError('No GitHub username configured. Run setup first.');
|
|
84
107
|
}
|
|
85
|
-
|
|
108
|
+
// Non-fatal warnings threaded into the result (#1057 M25). When the
|
|
109
|
+
// Search API's hard 1000-result ceiling truncates the user's PR list we
|
|
110
|
+
// previously silently dropped the overflow; now the caller can surface
|
|
111
|
+
// it so the daily digest doesn't quietly report a partial view.
|
|
112
|
+
const warnings = [];
|
|
113
|
+
// Username used for the search — mutated below if the pre-fetch placeholder
|
|
114
|
+
// repair fires. Writing to config is separate from rebinding this local.
|
|
115
|
+
let searchUsername = initialConfig.githubUsername;
|
|
116
|
+
// Proactive placeholder repair: if the configured username is a known
|
|
117
|
+
// placeholder (e.g. "example-user" carried over from docs or an aborted
|
|
118
|
+
// setup), cross-check against the authenticated viewer and persist the
|
|
119
|
+
// corrected name before fetching. Without this, the search silently
|
|
120
|
+
// returns zero results and the dashboard looks like a fresh install.
|
|
121
|
+
// Errors here are non-fatal; rate-limit/auth failures still abort so we
|
|
122
|
+
// don't mask a revoked token by downgrading to a no-op.
|
|
123
|
+
let didRepair = false;
|
|
124
|
+
if (isPlaceholderUsername(searchUsername)) {
|
|
125
|
+
try {
|
|
126
|
+
const { data: viewer } = await this.octokit.users.getAuthenticated();
|
|
127
|
+
const newLogin = viewer.login?.trim();
|
|
128
|
+
// Guard against an empty/whitespace viewer login (enterprise proxies,
|
|
129
|
+
// stubbed Octokit clients) and against the pathological case where the
|
|
130
|
+
// authenticated viewer's login is itself one of our placeholder strings
|
|
131
|
+
// — persisting either would swap one broken config for another.
|
|
132
|
+
if (!newLogin || isPlaceholderUsername(newLogin)) {
|
|
133
|
+
const message = `Placeholder username "${searchUsername}" detected but authenticated viewer ` +
|
|
134
|
+
`returned an unusable login (${JSON.stringify(viewer.login)}); skipping auto-repair.`;
|
|
135
|
+
warnings.push(message);
|
|
136
|
+
warn(MODULE, message);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.stateManager.updateConfig({ githubUsername: newLogin });
|
|
140
|
+
searchUsername = newLogin;
|
|
141
|
+
didRepair = true;
|
|
142
|
+
const message = `Configured GitHub username "${initialConfig.githubUsername}" looks like a placeholder. ` +
|
|
143
|
+
`Auto-repaired to "${newLogin}" using the authenticated viewer.`;
|
|
144
|
+
warnings.push(message);
|
|
145
|
+
warn(MODULE, message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (isRateLimitOrAuthError(err))
|
|
150
|
+
throw err;
|
|
151
|
+
// Non-fatal viewer-lookup failures (5xx, network, unexpected shape):
|
|
152
|
+
// surface as a warning (not debug) so the daily digest shows that
|
|
153
|
+
// auto-repair was attempted and couldn't complete. Falls through to
|
|
154
|
+
// the normal fetch with the placeholder, which will then return zero
|
|
155
|
+
// results — the post-fetch guardrail skips its own getAuthenticated
|
|
156
|
+
// attempt since this one already failed the same way.
|
|
157
|
+
const message = `Could not auto-repair placeholder username "${searchUsername}": ${errorMessage(err)}`;
|
|
158
|
+
warnings.push(message);
|
|
159
|
+
warn(MODULE, message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
debug('pr-monitor', `Fetching open PRs for @${searchUsername}...`);
|
|
86
163
|
// Search for all open PRs authored by the user with pagination
|
|
87
164
|
const allItems = [];
|
|
88
165
|
let page = 1;
|
|
89
166
|
const perPage = 100;
|
|
90
167
|
const firstPage = await this.octokit.search.issuesAndPullRequests({
|
|
91
|
-
q: `is:pr is:open is:public author:${
|
|
168
|
+
q: `is:pr is:open is:public author:${searchUsername}`,
|
|
92
169
|
sort: 'updated',
|
|
93
170
|
order: 'desc',
|
|
94
171
|
per_page: perPage,
|
|
@@ -101,13 +178,36 @@ export class PRMonitor {
|
|
|
101
178
|
const SEARCH_API_RESULT_CAP = 1000;
|
|
102
179
|
const MAX_PAGES = Math.ceil(SEARCH_API_RESULT_CAP / perPage); // 10 pages at per_page=100
|
|
103
180
|
const totalPages = Math.min(Math.ceil(totalCount / perPage), MAX_PAGES);
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
181
|
+
// Guardrail: if the Search API returned zero PRs, cross-check the
|
|
182
|
+
// configured username against the authenticated viewer. This catches
|
|
183
|
+
// stale usernames (e.g. a renamed GitHub account) that are not in the
|
|
184
|
+
// known-placeholder set. Skipped when the pre-fetch repair already
|
|
185
|
+
// reconciled the two — no need to spend a second getAuthenticated call
|
|
186
|
+
// just to confirm a match we already established.
|
|
187
|
+
if (totalCount === 0 && !didRepair) {
|
|
188
|
+
try {
|
|
189
|
+
const { data: viewer } = await this.octokit.users.getAuthenticated();
|
|
190
|
+
if (viewer.login.toLowerCase() !== searchUsername.toLowerCase()) {
|
|
191
|
+
const message = `Configured GitHub username @${searchUsername} does not match ` +
|
|
192
|
+
`authenticated user @${viewer.login}. Did you mean to run ` +
|
|
193
|
+
`\`oss-autopilot config username ${viewer.login}\`? Zero PRs returned.`;
|
|
194
|
+
warnings.push(message);
|
|
195
|
+
warn(MODULE, message);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
// Rate-limit/401/403 errors must abort the run just like every sibling
|
|
200
|
+
// fetch in this pipeline — swallowing them here would mask the exact
|
|
201
|
+
// class of failure the guardrail is meant to surface (e.g. revoked
|
|
202
|
+
// token returning 401 while the unauthenticated Search above still
|
|
203
|
+
// succeeds with zero results).
|
|
204
|
+
if (isRateLimitOrAuthError(err))
|
|
205
|
+
throw err;
|
|
206
|
+
debug(MODULE, `Could not cross-check viewer login: ${errorMessage(err)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
109
209
|
if (totalCount > SEARCH_API_RESULT_CAP) {
|
|
110
|
-
warnings.push(`GitHub Search API returned ${totalCount} PRs for @${
|
|
210
|
+
warnings.push(`GitHub Search API returned ${totalCount} PRs for @${searchUsername}, ` +
|
|
111
211
|
`but results are capped at ${SEARCH_API_RESULT_CAP}. ` +
|
|
112
212
|
`Showing the ${SEARCH_API_RESULT_CAP} most recently updated PRs.`);
|
|
113
213
|
warn(MODULE, warnings[warnings.length - 1]);
|
|
@@ -115,7 +215,7 @@ export class PRMonitor {
|
|
|
115
215
|
while (page < totalPages) {
|
|
116
216
|
page++;
|
|
117
217
|
const nextPage = await this.octokit.search.issuesAndPullRequests({
|
|
118
|
-
q: `is:pr is:open is:public author:${
|
|
218
|
+
q: `is:pr is:open is:public author:${searchUsername}`,
|
|
119
219
|
sort: 'updated',
|
|
120
220
|
order: 'desc',
|
|
121
221
|
per_page: perPage,
|
|
@@ -135,7 +235,7 @@ export class PRMonitor {
|
|
|
135
235
|
warn('pr-monitor', `Skipping PR with unparseable URL: ${item.html_url}`);
|
|
136
236
|
return false;
|
|
137
237
|
}
|
|
138
|
-
if (isOwnRepo(parsed.owner,
|
|
238
|
+
if (isOwnRepo(parsed.owner, searchUsername))
|
|
139
239
|
return false;
|
|
140
240
|
return true;
|
|
141
241
|
});
|
|
@@ -274,6 +274,13 @@ export interface StartupOutput {
|
|
|
274
274
|
daily?: DailyOutput;
|
|
275
275
|
/** URL of the interactive SPA dashboard server, when running (e.g., "http://localhost:3000") */
|
|
276
276
|
dashboardUrl?: string;
|
|
277
|
+
/**
|
|
278
|
+
* Set when the dashboard launch or refresh failed (assets missing, port
|
|
279
|
+
* conflict, spawn error, etc.). The dashboard is always attempted, so JSON
|
|
280
|
+
* consumers — which previously saw only a missing `dashboardUrl` — now have
|
|
281
|
+
* a structured signal to surface or recover from the failure.
|
|
282
|
+
*/
|
|
283
|
+
dashboardError?: string;
|
|
277
284
|
issueList?: IssueListInfo;
|
|
278
285
|
}
|
|
279
286
|
/**
|
package/dist/formatters/json.js
CHANGED