@oss-autopilot/core 0.54.0 → 0.56.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 +63 -63
- package/dist/commands/comments.js +0 -1
- package/dist/commands/config.js +45 -5
- package/dist/commands/daily.js +190 -157
- package/dist/commands/dashboard-data.js +37 -30
- package/dist/commands/dashboard-server.js +0 -1
- package/dist/commands/dismiss.js +0 -6
- package/dist/commands/init.js +0 -1
- package/dist/commands/local-repos.js +1 -2
- package/dist/commands/move.js +12 -11
- package/dist/commands/setup.d.ts +2 -1
- package/dist/commands/setup.js +166 -130
- package/dist/commands/shelve.js +10 -10
- package/dist/commands/startup.js +30 -14
- package/dist/core/ci-analysis.d.ts +6 -0
- package/dist/core/ci-analysis.js +89 -12
- package/dist/core/daily-logic.js +24 -33
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/issue-discovery.d.ts +7 -44
- package/dist/core/issue-discovery.js +83 -188
- package/dist/core/issue-eligibility.d.ts +35 -0
- package/dist/core/issue-eligibility.js +126 -0
- package/dist/core/issue-vetting.d.ts +6 -21
- package/dist/core/issue-vetting.js +15 -279
- package/dist/core/pr-monitor.d.ts +7 -12
- package/dist/core/pr-monitor.js +14 -80
- package/dist/core/repo-health.d.ts +24 -0
- package/dist/core/repo-health.js +193 -0
- package/dist/core/search-phases.d.ts +55 -0
- package/dist/core/search-phases.js +155 -0
- package/dist/core/state.d.ts +11 -0
- package/dist/core/state.js +63 -4
- package/dist/core/types.d.ts +8 -1
- package/dist/core/types.js +7 -0
- package/dist/formatters/json.d.ts +1 -1
- package/package.json +1 -1
package/dist/commands/move.js
CHANGED
|
@@ -21,15 +21,17 @@ export async function runMove(options) {
|
|
|
21
21
|
// Use current time — the CLI doesn't have cached PR data. The override
|
|
22
22
|
// will auto-clear on the next daily run if the PR has new activity after this.
|
|
23
23
|
const lastActivityAt = new Date().toISOString();
|
|
24
|
-
stateManager.
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
stateManager.batch(() => {
|
|
25
|
+
stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
|
|
26
|
+
stateManager.unshelvePR(options.prUrl);
|
|
27
|
+
});
|
|
27
28
|
return { url: options.prUrl, target, description: `Moved to ${label}` };
|
|
28
29
|
}
|
|
29
30
|
case 'shelved': {
|
|
30
|
-
stateManager.
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
stateManager.batch(() => {
|
|
32
|
+
stateManager.shelvePR(options.prUrl);
|
|
33
|
+
stateManager.clearStatusOverride(options.prUrl);
|
|
34
|
+
});
|
|
33
35
|
return {
|
|
34
36
|
url: options.prUrl,
|
|
35
37
|
target,
|
|
@@ -37,11 +39,10 @@ export async function runMove(options) {
|
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
case 'auto': {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
42
|
+
stateManager.batch(() => {
|
|
43
|
+
stateManager.clearStatusOverride(options.prUrl);
|
|
44
|
+
stateManager.unshelvePR(options.prUrl);
|
|
45
|
+
});
|
|
45
46
|
return {
|
|
46
47
|
url: options.prUrl,
|
|
47
48
|
target,
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Setup command
|
|
3
3
|
* Interactive setup / configuration
|
|
4
4
|
*/
|
|
5
|
-
import { type ProjectCategory } from '../core/types.js';
|
|
5
|
+
import { type ProjectCategory, type IssueScope } from '../core/types.js';
|
|
6
6
|
interface SetupOptions {
|
|
7
7
|
reset?: boolean;
|
|
8
8
|
set?: string[];
|
|
@@ -23,6 +23,7 @@ export interface SetupCompleteOutput {
|
|
|
23
23
|
labels: string[];
|
|
24
24
|
projectCategories: ProjectCategory[];
|
|
25
25
|
preferredOrgs: string[];
|
|
26
|
+
scope: IssueScope[];
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
export interface SetupPrompt {
|
package/dist/commands/setup.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
|
|
6
6
|
import { ValidationError } from '../core/errors.js';
|
|
7
7
|
import { validateGitHubUsername } from './validation.js';
|
|
8
|
-
import { PROJECT_CATEGORIES } from '../core/types.js';
|
|
8
|
+
import { PROJECT_CATEGORIES, ISSUE_SCOPES } from '../core/types.js';
|
|
9
9
|
/** Parse and validate a positive integer setting value. */
|
|
10
10
|
function parsePositiveInt(value, settingName) {
|
|
11
11
|
const parsed = Number(value);
|
|
@@ -21,153 +21,181 @@ export async function runSetup(options) {
|
|
|
21
21
|
if (options.set && options.set.length > 0) {
|
|
22
22
|
const results = {};
|
|
23
23
|
const warnings = [];
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
case 'dormantDays': {
|
|
40
|
-
const dormant = parsePositiveInt(value, 'dormantDays');
|
|
41
|
-
stateManager.updateConfig({ dormantThresholdDays: dormant });
|
|
42
|
-
results[key] = String(dormant);
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
case 'approachingDays': {
|
|
46
|
-
const approaching = parsePositiveInt(value, 'approachingDays');
|
|
47
|
-
stateManager.updateConfig({ approachingDormantDays: approaching });
|
|
48
|
-
results[key] = String(approaching);
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case 'languages':
|
|
52
|
-
stateManager.updateConfig({ languages: value.split(',').map((l) => l.trim()) });
|
|
53
|
-
results[key] = value;
|
|
54
|
-
break;
|
|
55
|
-
case 'labels':
|
|
56
|
-
stateManager.updateConfig({ labels: value.split(',').map((l) => l.trim()) });
|
|
57
|
-
results[key] = value;
|
|
58
|
-
break;
|
|
59
|
-
case 'showHealthCheck':
|
|
60
|
-
stateManager.updateConfig({ showHealthCheck: value !== 'false' });
|
|
61
|
-
results[key] = value !== 'false' ? 'true' : 'false';
|
|
62
|
-
break;
|
|
63
|
-
case 'squashByDefault':
|
|
64
|
-
if (value === 'ask') {
|
|
65
|
-
stateManager.updateConfig({ squashByDefault: 'ask' });
|
|
66
|
-
results[key] = 'ask';
|
|
24
|
+
stateManager.batch(() => {
|
|
25
|
+
for (const setting of options.set) {
|
|
26
|
+
const [key, ...valueParts] = setting.split('=');
|
|
27
|
+
const value = valueParts.join('=');
|
|
28
|
+
switch (key) {
|
|
29
|
+
case 'username':
|
|
30
|
+
validateGitHubUsername(value);
|
|
31
|
+
stateManager.updateConfig({ githubUsername: value });
|
|
32
|
+
results[key] = value;
|
|
33
|
+
break;
|
|
34
|
+
case 'maxActivePRs': {
|
|
35
|
+
const maxPRs = parsePositiveInt(value, 'maxActivePRs');
|
|
36
|
+
stateManager.updateConfig({ maxActivePRs: maxPRs });
|
|
37
|
+
results[key] = String(maxPRs);
|
|
38
|
+
break;
|
|
67
39
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
40
|
+
case 'dormantDays': {
|
|
41
|
+
const dormant = parsePositiveInt(value, 'dormantDays');
|
|
42
|
+
stateManager.updateConfig({ dormantThresholdDays: dormant });
|
|
43
|
+
results[key] = String(dormant);
|
|
44
|
+
break;
|
|
71
45
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
46
|
+
case 'approachingDays': {
|
|
47
|
+
const approaching = parsePositiveInt(value, 'approachingDays');
|
|
48
|
+
stateManager.updateConfig({ approachingDormantDays: approaching });
|
|
49
|
+
results[key] = String(approaching);
|
|
50
|
+
break;
|
|
77
51
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const normalized = entry.replace(/\s+/g, '');
|
|
95
|
-
if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(normalized)) {
|
|
96
|
-
valid.push(normalized);
|
|
52
|
+
case 'languages':
|
|
53
|
+
stateManager.updateConfig({ languages: value.split(',').map((l) => l.trim()) });
|
|
54
|
+
results[key] = value;
|
|
55
|
+
break;
|
|
56
|
+
case 'labels':
|
|
57
|
+
stateManager.updateConfig({ labels: value.split(',').map((l) => l.trim()) });
|
|
58
|
+
results[key] = value;
|
|
59
|
+
break;
|
|
60
|
+
case 'showHealthCheck':
|
|
61
|
+
stateManager.updateConfig({ showHealthCheck: value !== 'false' });
|
|
62
|
+
results[key] = value !== 'false' ? 'true' : 'false';
|
|
63
|
+
break;
|
|
64
|
+
case 'squashByDefault':
|
|
65
|
+
if (value === 'ask') {
|
|
66
|
+
stateManager.updateConfig({ squashByDefault: 'ask' });
|
|
67
|
+
results[key] = 'ask';
|
|
97
68
|
}
|
|
98
69
|
else {
|
|
99
|
-
|
|
70
|
+
stateManager.updateConfig({ squashByDefault: value !== 'false' });
|
|
71
|
+
results[key] = value !== 'false' ? 'true' : 'false';
|
|
100
72
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
results[key] =
|
|
73
|
+
break;
|
|
74
|
+
case 'minStars': {
|
|
75
|
+
const stars = Number(value);
|
|
76
|
+
if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
|
|
77
|
+
throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
|
|
78
|
+
}
|
|
79
|
+
stateManager.updateConfig({ minStars: stars });
|
|
80
|
+
results[key] = String(stars);
|
|
109
81
|
break;
|
|
110
82
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
83
|
+
case 'includeDocIssues':
|
|
84
|
+
stateManager.updateConfig({ includeDocIssues: value === 'true' });
|
|
85
|
+
results[key] = value === 'true' ? 'true' : 'false';
|
|
86
|
+
break;
|
|
87
|
+
case 'aiPolicyBlocklist': {
|
|
88
|
+
const entries = value
|
|
89
|
+
.split(',')
|
|
90
|
+
.map((r) => r.trim())
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
const valid = [];
|
|
93
|
+
const invalid = [];
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const normalized = entry.replace(/\s+/g, '');
|
|
96
|
+
if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(normalized)) {
|
|
97
|
+
valid.push(normalized);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
invalid.push(entry);
|
|
101
|
+
}
|
|
125
102
|
}
|
|
126
|
-
|
|
127
|
-
|
|
103
|
+
if (invalid.length > 0) {
|
|
104
|
+
warnings.push(`Warning: Skipping invalid entries (expected "owner/repo" format): ${invalid.join(', ')}`);
|
|
105
|
+
results['aiPolicyBlocklist_invalidEntries'] = invalid.join(', ');
|
|
106
|
+
}
|
|
107
|
+
if (valid.length === 0 && entries.length > 0) {
|
|
108
|
+
warnings.push('Warning: All entries were invalid. Blocklist not updated.');
|
|
109
|
+
results[key] = '(all entries invalid)';
|
|
110
|
+
break;
|
|
128
111
|
}
|
|
112
|
+
stateManager.updateConfig({ aiPolicyBlocklist: valid });
|
|
113
|
+
results[key] = valid.length > 0 ? valid.join(', ') : '(empty)';
|
|
114
|
+
break;
|
|
129
115
|
}
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
case 'projectCategories': {
|
|
117
|
+
const categories = value
|
|
118
|
+
.split(',')
|
|
119
|
+
.map((c) => c.trim())
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
const validCategories = [];
|
|
122
|
+
const invalidCategories = [];
|
|
123
|
+
for (const cat of categories) {
|
|
124
|
+
if (PROJECT_CATEGORIES.includes(cat)) {
|
|
125
|
+
validCategories.push(cat);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
invalidCategories.push(cat);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (invalidCategories.length > 0) {
|
|
132
|
+
warnings.push(`Unknown project categories: ${invalidCategories.join(', ')}. Valid: ${PROJECT_CATEGORIES.join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
const dedupedCategories = [...new Set(validCategories)];
|
|
135
|
+
stateManager.updateConfig({ projectCategories: dedupedCategories });
|
|
136
|
+
results[key] = dedupedCategories.length > 0 ? dedupedCategories.join(', ') : '(empty)';
|
|
137
|
+
break;
|
|
132
138
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
case 'preferredOrgs': {
|
|
140
|
+
const orgs = value
|
|
141
|
+
.split(',')
|
|
142
|
+
.map((o) => o.trim())
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
const validOrgs = [];
|
|
145
|
+
for (const org of orgs) {
|
|
146
|
+
if (org.includes('/')) {
|
|
147
|
+
warnings.push(`"${org}" looks like a repo path. Use org name only (e.g., "vercel" not "vercel/next.js").`);
|
|
148
|
+
}
|
|
149
|
+
else if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(org)) {
|
|
150
|
+
warnings.push(`"${org}" is not a valid GitHub organization name. Skipping.`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
validOrgs.push(org.toLowerCase());
|
|
154
|
+
}
|
|
147
155
|
}
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
const dedupedOrgs = [...new Set(validOrgs)];
|
|
157
|
+
stateManager.updateConfig({ preferredOrgs: dedupedOrgs });
|
|
158
|
+
results[key] = dedupedOrgs.length > 0 ? dedupedOrgs.join(', ') : '(empty)';
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'scope': {
|
|
162
|
+
const scopeValues = value
|
|
163
|
+
.split(',')
|
|
164
|
+
.map((s) => s.trim())
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
const validScopes = [];
|
|
167
|
+
const invalidScopes = [];
|
|
168
|
+
for (const s of scopeValues) {
|
|
169
|
+
if (ISSUE_SCOPES.includes(s)) {
|
|
170
|
+
validScopes.push(s);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
invalidScopes.push(s);
|
|
174
|
+
}
|
|
150
175
|
}
|
|
151
|
-
|
|
152
|
-
|
|
176
|
+
if (invalidScopes.length > 0) {
|
|
177
|
+
warnings.push(`Unknown issue scopes: ${invalidScopes.join(', ')}. Valid: ${ISSUE_SCOPES.join(', ')}`);
|
|
153
178
|
}
|
|
179
|
+
const dedupedScopes = [...new Set(validScopes)];
|
|
180
|
+
stateManager.updateConfig({ scope: dedupedScopes.length > 0 ? dedupedScopes : undefined });
|
|
181
|
+
results[key] = dedupedScopes.length > 0 ? dedupedScopes.join(', ') : '(empty — using labels only)';
|
|
182
|
+
break;
|
|
154
183
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
case 'issueListPath':
|
|
185
|
+
stateManager.updateConfig({ issueListPath: value || undefined });
|
|
186
|
+
results[key] = value || '(cleared)';
|
|
187
|
+
break;
|
|
188
|
+
case 'complete':
|
|
189
|
+
if (value === 'true') {
|
|
190
|
+
stateManager.markSetupComplete();
|
|
191
|
+
results[key] = 'true';
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
warnings.push(`Unknown setting: ${key}`);
|
|
159
196
|
}
|
|
160
|
-
case 'complete':
|
|
161
|
-
if (value === 'true') {
|
|
162
|
-
stateManager.markSetupComplete();
|
|
163
|
-
results[key] = 'true';
|
|
164
|
-
}
|
|
165
|
-
break;
|
|
166
|
-
default:
|
|
167
|
-
warnings.push(`Unknown setting: ${key}`);
|
|
168
197
|
}
|
|
169
|
-
}
|
|
170
|
-
stateManager.save();
|
|
198
|
+
});
|
|
171
199
|
return { success: true, settings: results, warnings: warnings.length > 0 ? warnings : undefined };
|
|
172
200
|
}
|
|
173
201
|
// Show setup status
|
|
@@ -183,6 +211,7 @@ export async function runSetup(options) {
|
|
|
183
211
|
labels: config.labels,
|
|
184
212
|
projectCategories: config.projectCategories ?? [],
|
|
185
213
|
preferredOrgs: config.preferredOrgs ?? [],
|
|
214
|
+
scope: config.scope ?? [],
|
|
186
215
|
},
|
|
187
216
|
};
|
|
188
217
|
}
|
|
@@ -232,6 +261,13 @@ export async function runSetup(options) {
|
|
|
232
261
|
default: ['good first issue', 'help wanted'],
|
|
233
262
|
type: 'list',
|
|
234
263
|
},
|
|
264
|
+
{
|
|
265
|
+
setting: 'scope',
|
|
266
|
+
prompt: 'What scope of issues do you want to discover? (beginner, intermediate, advanced — leave empty for default labels only)',
|
|
267
|
+
current: config.scope ?? [],
|
|
268
|
+
default: [],
|
|
269
|
+
type: 'list',
|
|
270
|
+
},
|
|
235
271
|
{
|
|
236
272
|
setting: 'aiPolicyBlocklist',
|
|
237
273
|
prompt: 'Repos with anti-AI contribution policies to block (owner/repo, comma-separated)?',
|
package/dist/commands/shelve.js
CHANGED
|
@@ -15,21 +15,21 @@ export async function runShelve(options) {
|
|
|
15
15
|
validateUrl(options.prUrl);
|
|
16
16
|
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
17
17
|
const stateManager = getStateManager();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
stateManager.
|
|
22
|
-
}
|
|
18
|
+
let added = false;
|
|
19
|
+
stateManager.batch(() => {
|
|
20
|
+
added = stateManager.shelvePR(options.prUrl);
|
|
21
|
+
stateManager.clearStatusOverride(options.prUrl);
|
|
22
|
+
});
|
|
23
23
|
return { shelved: added, url: options.prUrl };
|
|
24
24
|
}
|
|
25
25
|
export async function runUnshelve(options) {
|
|
26
26
|
validateUrl(options.prUrl);
|
|
27
27
|
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
28
28
|
const stateManager = getStateManager();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
stateManager.
|
|
33
|
-
}
|
|
29
|
+
let removed = false;
|
|
30
|
+
stateManager.batch(() => {
|
|
31
|
+
removed = stateManager.unshelvePR(options.prUrl);
|
|
32
|
+
stateManager.clearStatusOverride(options.prUrl);
|
|
33
|
+
});
|
|
34
34
|
return { unshelved: removed, url: options.prUrl };
|
|
35
35
|
}
|
package/dist/commands/startup.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as fs from 'fs';
|
|
|
10
10
|
import { execFile } from 'child_process';
|
|
11
11
|
import { getStateManager, getGitHubToken, getCLIVersion, detectGitHubUsername } 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 { launchDashboardServer } from './dashboard-lifecycle.js';
|
|
15
16
|
/**
|
|
@@ -53,22 +54,37 @@ export function countIssueListItems(content) {
|
|
|
53
54
|
export function detectIssueList() {
|
|
54
55
|
let issueListPath = '';
|
|
55
56
|
let source = 'auto-detected';
|
|
56
|
-
// 1. Check config
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
issueListPath = configuredPath;
|
|
64
|
-
source = 'configured';
|
|
65
|
-
}
|
|
57
|
+
// 1. Check state.json config (primary)
|
|
58
|
+
try {
|
|
59
|
+
const stateManager = getStateManager();
|
|
60
|
+
const configuredPath = stateManager.getState().config.issueListPath;
|
|
61
|
+
if (configuredPath && fs.existsSync(configuredPath)) {
|
|
62
|
+
issueListPath = configuredPath;
|
|
63
|
+
source = 'configured';
|
|
66
64
|
}
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// State manager may not be initialized yet — fall through to legacy config.md
|
|
68
|
+
warn('startup', `Could not read issueListPath from state: ${errorMessage(error)}`);
|
|
69
|
+
}
|
|
70
|
+
// 2. Fallback: config.md (legacy — will be removed in future)
|
|
71
|
+
if (!issueListPath) {
|
|
72
|
+
const configPath = '.claude/oss-autopilot/config.md';
|
|
73
|
+
if (fs.existsSync(configPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
76
|
+
const configuredPath = parseIssueListPathFromConfig(configContent);
|
|
77
|
+
if (configuredPath && fs.existsSync(configuredPath)) {
|
|
78
|
+
issueListPath = configuredPath;
|
|
79
|
+
source = 'configured';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('[STARTUP] Failed to read config:', errorMessage(error));
|
|
84
|
+
}
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
|
-
//
|
|
87
|
+
// 3. Probe known paths
|
|
72
88
|
if (!issueListPath) {
|
|
73
89
|
const probes = ['open-source/potential-issue-list.md', 'oss/issue-list.md', 'issues.md'];
|
|
74
90
|
for (const probe of probes) {
|
|
@@ -81,7 +97,7 @@ export function detectIssueList() {
|
|
|
81
97
|
}
|
|
82
98
|
if (!issueListPath)
|
|
83
99
|
return undefined;
|
|
84
|
-
//
|
|
100
|
+
// 4. Count available/completed items
|
|
85
101
|
try {
|
|
86
102
|
const content = fs.readFileSync(issueListPath, 'utf-8');
|
|
87
103
|
const { availableCount, completedCount } = countIssueListItems(content);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CI Analysis - Classification and analysis of CI check runs and combined statuses.
|
|
3
3
|
* Extracted from PRMonitor to isolate CI-related logic (#263).
|
|
4
4
|
*/
|
|
5
|
+
import type { Octokit } from '@octokit/rest';
|
|
5
6
|
import { CIFailureCategory, ClassifiedCheck, CIStatusResult } from './types.js';
|
|
6
7
|
/**
|
|
7
8
|
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145).
|
|
@@ -61,4 +62,9 @@ export declare function analyzeCombinedStatus(combinedStatus: {
|
|
|
61
62
|
* Priority: failing > pending > passing > unknown.
|
|
62
63
|
*/
|
|
63
64
|
export declare function mergeStatuses(checkRunAnalysis: CheckRunAnalysis, combinedAnalysis: CombinedStatusAnalysis, checkRunCount: number): CIStatusResult;
|
|
65
|
+
/**
|
|
66
|
+
* Get CI status for a commit SHA by querying both the combined status API and check runs API.
|
|
67
|
+
* Returns the merged status and names of any failing checks for diagnostics.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getCIStatus(octokit: Octokit, owner: string, repo: string, sha: string): Promise<CIStatusResult>;
|
|
64
70
|
export {};
|
package/dist/core/ci-analysis.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* CI Analysis - Classification and analysis of CI check runs and combined statuses.
|
|
3
3
|
* Extracted from PRMonitor to isolate CI-related logic (#263).
|
|
4
4
|
*/
|
|
5
|
+
import { getHttpStatusCode, errorMessage } from './errors.js';
|
|
6
|
+
import { debug, warn } from './logger.js';
|
|
7
|
+
/** Return a fresh unknown CI status (avoids shared mutable state between callers). */
|
|
8
|
+
function unknownCIStatus() {
|
|
9
|
+
return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
10
|
+
}
|
|
5
11
|
/**
|
|
6
12
|
* Known CI check name patterns that indicate fork limitations rather than real failures (#81).
|
|
7
13
|
* These are deployment/preview services that require repo-level secrets unavailable in forks.
|
|
@@ -119,15 +125,23 @@ export function analyzeCombinedStatus(combinedStatus) {
|
|
|
119
125
|
const hasRealFailure = realStatuses.some((s) => s.state === 'failure' || s.state === 'error');
|
|
120
126
|
const hasRealPending = realStatuses.some((s) => s.state === 'pending');
|
|
121
127
|
const hasRealSuccess = realStatuses.some((s) => s.state === 'success');
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
let effectiveCombinedState;
|
|
129
|
+
if (hasRealFailure) {
|
|
130
|
+
effectiveCombinedState = 'failure';
|
|
131
|
+
}
|
|
132
|
+
else if (hasRealPending) {
|
|
133
|
+
effectiveCombinedState = 'pending';
|
|
134
|
+
}
|
|
135
|
+
else if (hasRealSuccess) {
|
|
136
|
+
effectiveCombinedState = 'success';
|
|
137
|
+
}
|
|
138
|
+
else if (realStatuses.length === 0) {
|
|
139
|
+
// All statuses were auth gates; don't inherit original failure
|
|
140
|
+
effectiveCombinedState = 'success';
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
effectiveCombinedState = combinedStatus.state;
|
|
144
|
+
}
|
|
131
145
|
const hasStatuses = combinedStatus.statuses.length > 0;
|
|
132
146
|
// Collect failing status names from combined status API
|
|
133
147
|
const failingStatusNames = [];
|
|
@@ -157,9 +171,72 @@ export function mergeStatuses(checkRunAnalysis, combinedAnalysis, checkRunCount)
|
|
|
157
171
|
if (hasSuccessfulChecks || effectiveCombinedState === 'success') {
|
|
158
172
|
return { status: 'passing', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
159
173
|
}
|
|
160
|
-
// No checks found at all
|
|
174
|
+
// No checks found at all — common for repos without CI
|
|
161
175
|
if (!hasStatuses && checkRunCount === 0) {
|
|
162
|
-
return
|
|
176
|
+
return unknownCIStatus();
|
|
177
|
+
}
|
|
178
|
+
return unknownCIStatus();
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get CI status for a commit SHA by querying both the combined status API and check runs API.
|
|
182
|
+
* Returns the merged status and names of any failing checks for diagnostics.
|
|
183
|
+
*/
|
|
184
|
+
export async function getCIStatus(octokit, owner, repo, sha) {
|
|
185
|
+
if (!sha)
|
|
186
|
+
return unknownCIStatus();
|
|
187
|
+
try {
|
|
188
|
+
// Fetch both combined status and check runs in parallel
|
|
189
|
+
const [statusResponse, checksResponse] = await Promise.all([
|
|
190
|
+
octokit.repos.getCombinedStatusForRef({ owner, repo, ref: sha }),
|
|
191
|
+
// 404 is expected for repos without check runs configured; log other errors for debugging
|
|
192
|
+
octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
|
|
193
|
+
const status = getHttpStatusCode(err);
|
|
194
|
+
// Rate limit errors must propagate — matches listReviewComments pattern (#481)
|
|
195
|
+
if (status === 429)
|
|
196
|
+
throw err;
|
|
197
|
+
if (status === 403) {
|
|
198
|
+
const msg = errorMessage(err).toLowerCase();
|
|
199
|
+
if (msg.includes('rate limit') || msg.includes('abuse detection'))
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
if (status === 404) {
|
|
203
|
+
debug('pr-monitor', `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
warn('pr-monitor', `Non-404 error fetching check runs for ${owner}/${repo}@${sha.slice(0, 7)}: ${status ?? err}`);
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}),
|
|
210
|
+
]);
|
|
211
|
+
const combinedStatus = statusResponse.data;
|
|
212
|
+
const allCheckRuns = checksResponse?.data?.check_runs || [];
|
|
213
|
+
// Deduplicate check runs by name, keeping only the most recent run per unique name.
|
|
214
|
+
// GitHub returns all historical runs (including re-runs), so without deduplication
|
|
215
|
+
// a superseded failure will incorrectly flag the PR as failing even after a re-run passes.
|
|
216
|
+
const latestCheckRunsByName = new Map();
|
|
217
|
+
for (const check of allCheckRuns) {
|
|
218
|
+
const existing = latestCheckRunsByName.get(check.name);
|
|
219
|
+
if (!existing || new Date(check.started_at ?? 0) > new Date(existing.started_at ?? 0)) {
|
|
220
|
+
latestCheckRunsByName.set(check.name, check);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const checkRuns = [...latestCheckRunsByName.values()];
|
|
224
|
+
const checkRunAnalysis = analyzeCheckRuns(checkRuns);
|
|
225
|
+
const combinedAnalysis = analyzeCombinedStatus(combinedStatus);
|
|
226
|
+
return mergeStatuses(checkRunAnalysis, combinedAnalysis, checkRuns.length);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
const statusCode = getHttpStatusCode(error);
|
|
230
|
+
if (statusCode === 401 || statusCode === 403 || statusCode === 429) {
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
else if (statusCode === 404) {
|
|
234
|
+
// Repo might not have CI configured, this is normal
|
|
235
|
+
debug('pr-monitor', `CI check 404 for ${owner}/${repo} (no CI configured)`);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage(error)}`);
|
|
239
|
+
}
|
|
240
|
+
return unknownCIStatus();
|
|
163
241
|
}
|
|
164
|
-
return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
|
|
165
242
|
}
|