@oss-autopilot/core 0.59.0 → 0.60.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 +150 -111
- package/dist/core/ci-analysis.d.ts +5 -4
- package/dist/core/ci-analysis.js +13 -8
- package/dist/core/state-persistence.d.ts +1 -0
- package/dist/core/state-persistence.js +46 -84
- package/dist/core/state-schema.d.ts +539 -0
- package/dist/core/state-schema.js +214 -0
- package/dist/core/types.d.ts +4 -312
- package/dist/core/types.js +7 -41
- package/package.json +3 -2
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
import type { Octokit } from '@octokit/rest';
|
|
6
6
|
import { CIFailureCategory, ClassifiedCheck, CIStatusResult } from './types.js';
|
|
7
7
|
/**
|
|
8
|
-
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145).
|
|
8
|
+
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145, #743).
|
|
9
9
|
* Default is 'actionable' — only known patterns get reclassified.
|
|
10
|
-
*
|
|
10
|
+
* Conclusion-based classification (cancelled, timed_out, action_required) takes precedence
|
|
11
|
+
* over name-based pattern matching.
|
|
11
12
|
*/
|
|
12
13
|
export declare function classifyCICheck(name: string, description?: string, conclusion?: string): CIFailureCategory;
|
|
13
14
|
/**
|
|
14
|
-
* Classify all failing checks and return
|
|
15
|
-
* Accepts optional conclusion data to detect infrastructure failures.
|
|
15
|
+
* Classify all failing checks and return a ClassifiedCheck array (#81, #145, #743).
|
|
16
|
+
* Accepts optional conclusion data to detect infrastructure failures and auth gates.
|
|
16
17
|
*/
|
|
17
18
|
export declare function classifyFailingChecks(failingCheckNames: string[], conclusions?: Map<string, string>): ClassifiedCheck[];
|
|
18
19
|
/**
|
package/dist/core/ci-analysis.js
CHANGED
|
@@ -38,16 +38,21 @@ const INFRASTRUCTURE_PATTERNS = [
|
|
|
38
38
|
/\bservice\s*unavailable/i,
|
|
39
39
|
/\binfrastructure/i,
|
|
40
40
|
/\bblacksmith\b/i,
|
|
41
|
+
/\breadthedocs\b/i,
|
|
41
42
|
];
|
|
42
43
|
/**
|
|
43
|
-
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145).
|
|
44
|
+
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145, #743).
|
|
44
45
|
* Default is 'actionable' — only known patterns get reclassified.
|
|
45
|
-
*
|
|
46
|
+
* Conclusion-based classification (cancelled, timed_out, action_required) takes precedence
|
|
47
|
+
* over name-based pattern matching.
|
|
46
48
|
*/
|
|
47
49
|
export function classifyCICheck(name, description, conclusion) {
|
|
48
50
|
// Infrastructure: cancelled or timed_out jobs are transient failures (#145)
|
|
49
51
|
if (conclusion === 'cancelled' || conclusion === 'timed_out')
|
|
50
52
|
return 'infrastructure';
|
|
53
|
+
// Auth gate: action_required means the workflow needs external approval (e.g., fork PR or first-time contributor)
|
|
54
|
+
if (conclusion === 'action_required')
|
|
55
|
+
return 'auth_gate';
|
|
51
56
|
const nameLower = name.toLowerCase();
|
|
52
57
|
// Check name first (more reliable than description)
|
|
53
58
|
if (AUTH_GATE_PATTERNS.some((p) => p.test(nameLower)))
|
|
@@ -69,8 +74,8 @@ export function classifyCICheck(name, description, conclusion) {
|
|
|
69
74
|
return 'actionable';
|
|
70
75
|
}
|
|
71
76
|
/**
|
|
72
|
-
* Classify all failing checks and return
|
|
73
|
-
* Accepts optional conclusion data to detect infrastructure failures.
|
|
77
|
+
* Classify all failing checks and return a ClassifiedCheck array (#81, #145, #743).
|
|
78
|
+
* Accepts optional conclusion data to detect infrastructure failures and auth gates.
|
|
74
79
|
*/
|
|
75
80
|
export function classifyFailingChecks(failingCheckNames, conclusions) {
|
|
76
81
|
return failingCheckNames.map((name) => {
|
|
@@ -93,14 +98,14 @@ export function analyzeCheckRuns(checkRuns) {
|
|
|
93
98
|
const failingCheckNames = [];
|
|
94
99
|
const failingCheckConclusions = new Map();
|
|
95
100
|
for (const check of checkRuns) {
|
|
96
|
-
if (check.conclusion === 'failure' ||
|
|
101
|
+
if (check.conclusion === 'failure' ||
|
|
102
|
+
check.conclusion === 'cancelled' ||
|
|
103
|
+
check.conclusion === 'timed_out' ||
|
|
104
|
+
check.conclusion === 'action_required') {
|
|
97
105
|
hasFailingChecks = true;
|
|
98
106
|
failingCheckNames.push(check.name);
|
|
99
107
|
failingCheckConclusions.set(check.name, check.conclusion);
|
|
100
108
|
}
|
|
101
|
-
else if (check.conclusion === 'action_required') {
|
|
102
|
-
hasPendingChecks = true; // Maintainer approval gate, not a real failure
|
|
103
|
-
}
|
|
104
109
|
else if (check.status === 'in_progress' || check.status === 'queued') {
|
|
105
110
|
hasPendingChecks = true;
|
|
106
111
|
}
|
|
@@ -24,6 +24,7 @@ export declare function releaseLock(lockPath: string): void;
|
|
|
24
24
|
export declare function atomicWriteFileSync(filePath: string, data: string, mode?: number): void;
|
|
25
25
|
/**
|
|
26
26
|
* Create a fresh state (v2: fresh GitHub fetching).
|
|
27
|
+
* Leverages Zod schema defaults to produce a complete state.
|
|
27
28
|
*/
|
|
28
29
|
export declare function createFreshState(): AgentState;
|
|
29
30
|
/**
|
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
-
import {
|
|
8
|
+
import { AgentStateSchema } from './state-schema.js';
|
|
9
9
|
import { getStatePath, getBackupDir, getDataDir } from './utils.js';
|
|
10
10
|
import { errorMessage } from './errors.js';
|
|
11
11
|
import { debug, warn } from './logger.js';
|
|
12
12
|
const MODULE = 'state';
|
|
13
|
-
// Current state version
|
|
14
|
-
const CURRENT_STATE_VERSION = 2;
|
|
15
13
|
// Lock file timeout: if a lock is older than this, it is considered stale
|
|
16
14
|
const LOCK_TIMEOUT_MS = 30_000; // 30 seconds
|
|
17
15
|
// Legacy path for migration
|
|
@@ -139,66 +137,12 @@ function migrateV1ToV2(rawState) {
|
|
|
139
137
|
debug(MODULE, `Migration complete. Preserved ${Object.keys(repoScores).length} repo scores.`);
|
|
140
138
|
return migratedState;
|
|
141
139
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Validate that a loaded state has the required structure.
|
|
144
|
-
* Handles both v1 (with PR arrays) and v2 (without).
|
|
145
|
-
*/
|
|
146
|
-
function isValidState(state) {
|
|
147
|
-
if (!state || typeof state !== 'object')
|
|
148
|
-
return false;
|
|
149
|
-
const s = state;
|
|
150
|
-
// Migrate older states that don't have repoScores
|
|
151
|
-
if (s.repoScores === undefined) {
|
|
152
|
-
s.repoScores = {};
|
|
153
|
-
}
|
|
154
|
-
// Migrate older states that don't have events
|
|
155
|
-
if (s.events === undefined) {
|
|
156
|
-
s.events = [];
|
|
157
|
-
}
|
|
158
|
-
// Migrate older states that don't have mergedPRs
|
|
159
|
-
if (s.mergedPRs === undefined) {
|
|
160
|
-
s.mergedPRs = [];
|
|
161
|
-
}
|
|
162
|
-
// Base requirements for all versions
|
|
163
|
-
const hasBaseFields = typeof s.version === 'number' &&
|
|
164
|
-
typeof s.repoScores === 'object' &&
|
|
165
|
-
s.repoScores !== null &&
|
|
166
|
-
Array.isArray(s.events) &&
|
|
167
|
-
typeof s.config === 'object' &&
|
|
168
|
-
s.config !== null;
|
|
169
|
-
if (!hasBaseFields)
|
|
170
|
-
return false;
|
|
171
|
-
// v1 requires base PR arrays to be present (they will be dropped during migration)
|
|
172
|
-
if (s.version === 1) {
|
|
173
|
-
return (Array.isArray(s.activePRs) &&
|
|
174
|
-
Array.isArray(s.dormantPRs) &&
|
|
175
|
-
Array.isArray(s.mergedPRs) &&
|
|
176
|
-
Array.isArray(s.closedPRs));
|
|
177
|
-
}
|
|
178
|
-
// v2+ doesn't require PR arrays
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
140
|
/**
|
|
182
141
|
* Create a fresh state (v2: fresh GitHub fetching).
|
|
142
|
+
* Leverages Zod schema defaults to produce a complete state.
|
|
183
143
|
*/
|
|
184
144
|
export function createFreshState() {
|
|
185
|
-
return {
|
|
186
|
-
version: CURRENT_STATE_VERSION,
|
|
187
|
-
activeIssues: [],
|
|
188
|
-
repoScores: {},
|
|
189
|
-
config: {
|
|
190
|
-
...INITIAL_STATE.config,
|
|
191
|
-
setupComplete: false,
|
|
192
|
-
languages: [...INITIAL_STATE.config.languages],
|
|
193
|
-
labels: [...INITIAL_STATE.config.labels],
|
|
194
|
-
excludeRepos: [],
|
|
195
|
-
trustedProjects: [],
|
|
196
|
-
shelvedPRUrls: [],
|
|
197
|
-
dismissedIssues: {},
|
|
198
|
-
},
|
|
199
|
-
events: [],
|
|
200
|
-
lastRunAt: new Date().toISOString(),
|
|
201
|
-
};
|
|
145
|
+
return AgentStateSchema.parse({ version: 2 });
|
|
202
146
|
}
|
|
203
147
|
/**
|
|
204
148
|
* Migrate state from legacy ./data/ location to ~/.oss-autopilot/.
|
|
@@ -298,13 +242,15 @@ function tryRestoreFromBackup() {
|
|
|
298
242
|
const backupPath = path.join(backupDir, backupFile);
|
|
299
243
|
try {
|
|
300
244
|
const data = fs.readFileSync(backupPath, 'utf-8');
|
|
301
|
-
let
|
|
302
|
-
if (
|
|
245
|
+
let raw = JSON.parse(data);
|
|
246
|
+
// Migrate from v1 to v2 if needed (before schema validation)
|
|
247
|
+
if (typeof raw === 'object' && raw !== null && raw.version === 1) {
|
|
248
|
+
raw = migrateV1ToV2(raw);
|
|
249
|
+
}
|
|
250
|
+
const parsed = AgentStateSchema.safeParse(raw);
|
|
251
|
+
if (parsed.success) {
|
|
252
|
+
const state = parsed.data;
|
|
303
253
|
debug(MODULE, `Successfully restored state from backup: ${backupFile}`);
|
|
304
|
-
// Migrate from v1 to v2 if needed
|
|
305
|
-
if (state.version === 1) {
|
|
306
|
-
state = migrateV1ToV2(state);
|
|
307
|
-
}
|
|
308
254
|
const repoCount = Object.keys(state.repoScores).length;
|
|
309
255
|
debug(MODULE, `Restored state v${state.version}: ${repoCount} repo scores`);
|
|
310
256
|
// Overwrite the corrupted main state file with the restored backup (atomic write)
|
|
@@ -313,6 +259,10 @@ function tryRestoreFromBackup() {
|
|
|
313
259
|
debug(MODULE, 'Restored backup written to main state file');
|
|
314
260
|
return state;
|
|
315
261
|
}
|
|
262
|
+
// safeParse failed — log and try next backup
|
|
263
|
+
const summary = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
264
|
+
warn(MODULE, `Backup ${backupFile} failed schema validation: ${summary}`);
|
|
265
|
+
debug(MODULE, `Backup ${backupFile} full validation errors:`, parsed.error.issues);
|
|
316
266
|
}
|
|
317
267
|
catch (backupErr) {
|
|
318
268
|
// This backup is also corrupted, try the next one
|
|
@@ -335,10 +285,29 @@ export function loadState() {
|
|
|
335
285
|
try {
|
|
336
286
|
if (fs.existsSync(statePath)) {
|
|
337
287
|
const data = fs.readFileSync(statePath, 'utf-8');
|
|
338
|
-
let
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
288
|
+
let raw = JSON.parse(data);
|
|
289
|
+
// Migrate from v1 to v2 if needed (before schema validation)
|
|
290
|
+
let wasMigrated = false;
|
|
291
|
+
if (typeof raw === 'object' && raw !== null && raw.version === 1) {
|
|
292
|
+
raw = migrateV1ToV2(raw);
|
|
293
|
+
wasMigrated = true;
|
|
294
|
+
}
|
|
295
|
+
// Validate through Zod schema (strips unknown keys in memory; stale keys persist on disk until next save)
|
|
296
|
+
const parsed = AgentStateSchema.safeParse(raw);
|
|
297
|
+
if (!parsed.success) {
|
|
298
|
+
const summary = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
299
|
+
warn(MODULE, `Invalid state file structure: ${summary}`);
|
|
300
|
+
warn(MODULE, 'Attempting to restore from backup...');
|
|
301
|
+
debug(MODULE, 'Full validation errors:', parsed.error.issues);
|
|
302
|
+
// Preserve the rejected state file so the user can recover
|
|
303
|
+
try {
|
|
304
|
+
const rejectedPath = statePath + '.rejected-' + Date.now();
|
|
305
|
+
fs.copyFileSync(statePath, rejectedPath);
|
|
306
|
+
warn(MODULE, `Previous state preserved at: ${rejectedPath}`);
|
|
307
|
+
}
|
|
308
|
+
catch (preserveErr) {
|
|
309
|
+
warn(MODULE, `Could not preserve rejected state file: ${errorMessage(preserveErr)}`);
|
|
310
|
+
}
|
|
342
311
|
const restoredState = tryRestoreFromBackup();
|
|
343
312
|
if (restoredState) {
|
|
344
313
|
const mtimeMs = safeGetMtimeMs(statePath);
|
|
@@ -347,23 +316,16 @@ export function loadState() {
|
|
|
347
316
|
warn(MODULE, 'No valid backup found, starting fresh');
|
|
348
317
|
return { state: createFreshState(), mtimeMs: 0 };
|
|
349
318
|
}
|
|
350
|
-
//
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
atomicWriteFileSync(statePath, JSON.stringify(state, null, 2), 0o600);
|
|
355
|
-
debug(MODULE, 'Migrated state saved');
|
|
319
|
+
// Save migrated state only after validation succeeds
|
|
320
|
+
if (wasMigrated) {
|
|
321
|
+
atomicWriteFileSync(statePath, JSON.stringify(parsed.data, null, 2), 0o600);
|
|
322
|
+
debug(MODULE, 'Migrated and validated state saved');
|
|
356
323
|
}
|
|
357
|
-
|
|
358
|
-
//
|
|
324
|
+
const state = parsed.data;
|
|
325
|
+
// Strip PR URLs from dismissedIssues (PR dismiss removed).
|
|
326
|
+
// This filters values inside a known field — Zod .strip() only removes unknown keys.
|
|
359
327
|
try {
|
|
360
328
|
let needsCleanupSave = false;
|
|
361
|
-
const rawConfig = state.config;
|
|
362
|
-
if (rawConfig.snoozedPRs) {
|
|
363
|
-
delete rawConfig.snoozedPRs;
|
|
364
|
-
needsCleanupSave = true;
|
|
365
|
-
}
|
|
366
|
-
// Strip PR URLs from dismissedIssues (PR dismiss removed)
|
|
367
329
|
if (state.config.dismissedIssues) {
|
|
368
330
|
const PR_URL_RE = /\/pull\/\d+$/;
|
|
369
331
|
for (const url of Object.keys(state.config.dismissedIssues)) {
|
|
@@ -375,7 +337,7 @@ export function loadState() {
|
|
|
375
337
|
}
|
|
376
338
|
if (needsCleanupSave) {
|
|
377
339
|
atomicWriteFileSync(statePath, JSON.stringify(state, null, 2), 0o600);
|
|
378
|
-
warn(MODULE, 'Cleaned up
|
|
340
|
+
warn(MODULE, 'Cleaned up dismissed PR URLs from persisted state');
|
|
379
341
|
}
|
|
380
342
|
}
|
|
381
343
|
catch (cleanupError) {
|