ci-triage 0.3.0 → 0.3.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/index.js +110 -14
- package/dist/multi.js +56 -17
- package/dist/parser.js +20 -19
- package/dist/parsers/deploy-pages-failure.js +33 -0
- package/dist/parsers/http-error.js +13 -0
- package/dist/parsers/index.js +9 -0
- package/dist/parsers/shell-failure.js +14 -0
- package/dist/parsers/types.js +1 -0
- package/dist/providers/circleci.js +1 -0
- package/dist/providers/github.js +2 -1
- package/dist/providers/gitlab.js +2 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import { getProvider, detectProvider } from './providers/index.js';
|
|
|
9
9
|
import { getDbStats, getFlakes, getRemediations, markResolved, markVerified, persistRun, pruneRuns } from './flake-store.js';
|
|
10
10
|
import { analyzeLlm } from './llm-analyzer.js';
|
|
11
11
|
import { fetchRepoContext } from './repo-context.js';
|
|
12
|
-
import { buildMultiJson, detectSharedPatterns, formatMultiSummary } from './multi.js';
|
|
12
|
+
import { buildMultiJson, detectSharedPatterns, filterReportsSince, formatMultiSummary } from './multi.js';
|
|
13
|
+
import { startMcpServer } from './mcp-server.js';
|
|
13
14
|
// --version support
|
|
14
15
|
const _require = createRequire(import.meta.url);
|
|
15
16
|
let pkgVersion = 'unknown';
|
|
@@ -26,10 +27,12 @@ function usage() {
|
|
|
26
27
|
'',
|
|
27
28
|
'Usage:',
|
|
28
29
|
' ci-triage owner/repo [limit] [--run <id>] [--md report.md] [--json]',
|
|
29
|
-
' ci-triage multi owner/repo1
|
|
30
|
+
' ci-triage multi owner/repo1,owner/repo2 [--json] [--since 24h]',
|
|
31
|
+
' ci-triage --multi owner/repo1,owner/repo2 [--json] [--since 24h]',
|
|
30
32
|
' ci-triage flakes owner/repo',
|
|
31
33
|
' ci-triage db prune [--days 90]',
|
|
32
34
|
' ci-triage db stats [owner/repo] [--json]',
|
|
35
|
+
' ci-triage mcp',
|
|
33
36
|
' ci-triage resolve <owner/repo> <run-id> "<description>" [--commit <sha>]',
|
|
34
37
|
' ci-triage verify <owner/repo> <run-id> --clean-run <clean-run-id>',
|
|
35
38
|
' ci-triage history <owner/repo> [--json]',
|
|
@@ -38,9 +41,13 @@ function usage() {
|
|
|
38
41
|
' --run <id> Triage a specific run ID',
|
|
39
42
|
' --md <path> Write markdown report to file',
|
|
40
43
|
' --json Output JSON instead of human text',
|
|
44
|
+
' --multi Run cross-repo triage mode',
|
|
45
|
+
' --since <duration> Multi mode time filter, e.g. 24h, 7d, 30m',
|
|
41
46
|
' --provider <p> Force CI provider: github | gitlab | circleci',
|
|
42
|
-
' --llm Enable LLM root-cause analysis (
|
|
47
|
+
' --llm Enable LLM root-cause analysis (auto-on when OPENAI_API_KEY is set)',
|
|
48
|
+
' --no-llm Disable LLM even when OPENAI_API_KEY is set (alias: CI_TRIAGE_NO_LLM=1)',
|
|
43
49
|
' --llm-model <model> Override LLM model (default: gpt-4.1-mini)',
|
|
50
|
+
' --mcp Start MCP server (stdio transport) for agent tool use',
|
|
44
51
|
' --version Print version and exit',
|
|
45
52
|
].join('\n'));
|
|
46
53
|
process.exit(1);
|
|
@@ -56,6 +63,52 @@ export function emitEmptyOutputWarning(totalFailures, llmRan) {
|
|
|
56
63
|
process.stderr.write(EMPTY_OUTPUT_WARNING);
|
|
57
64
|
}
|
|
58
65
|
}
|
|
66
|
+
function positionalArgsExcludingOptionValues(args) {
|
|
67
|
+
const optionsWithValues = new Set([
|
|
68
|
+
'--run',
|
|
69
|
+
'--md',
|
|
70
|
+
'--provider',
|
|
71
|
+
'--llm-model',
|
|
72
|
+
'--days',
|
|
73
|
+
'--commit',
|
|
74
|
+
'--clean-run',
|
|
75
|
+
'--since',
|
|
76
|
+
]);
|
|
77
|
+
const positional = [];
|
|
78
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
79
|
+
const token = args[i];
|
|
80
|
+
if (optionsWithValues.has(token)) {
|
|
81
|
+
i += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (token.startsWith('--'))
|
|
85
|
+
continue;
|
|
86
|
+
positional.push(token);
|
|
87
|
+
}
|
|
88
|
+
return positional;
|
|
89
|
+
}
|
|
90
|
+
function parseSinceHours(raw) {
|
|
91
|
+
if (!raw)
|
|
92
|
+
return undefined;
|
|
93
|
+
const match = raw.match(/^(\d+)([mhd])$/i);
|
|
94
|
+
if (!match)
|
|
95
|
+
return undefined;
|
|
96
|
+
const value = Number(match[1]);
|
|
97
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
const unit = match[2].toLowerCase();
|
|
100
|
+
if (unit === 'm')
|
|
101
|
+
return value / 60;
|
|
102
|
+
if (unit === 'h')
|
|
103
|
+
return value;
|
|
104
|
+
return value * 24;
|
|
105
|
+
}
|
|
106
|
+
function parseMultiRepos(args) {
|
|
107
|
+
return args
|
|
108
|
+
.flatMap((arg) => arg.split(','))
|
|
109
|
+
.map((repo) => repo.trim())
|
|
110
|
+
.filter((repo) => repo.length > 0);
|
|
111
|
+
}
|
|
59
112
|
function buildRepoContextHint(repoContext, failures) {
|
|
60
113
|
if (!repoContext)
|
|
61
114
|
return null;
|
|
@@ -72,6 +125,10 @@ export function parseArgs(argv) {
|
|
|
72
125
|
process.stdout.write(`ci-triage v${pkgVersion}\n`);
|
|
73
126
|
process.exit(0);
|
|
74
127
|
}
|
|
128
|
+
// subcommand/flag: ci-triage mcp OR ci-triage --mcp
|
|
129
|
+
if (args[0] === 'mcp' || args.includes('--mcp')) {
|
|
130
|
+
return { repo: '', limit: 0, outputJson: false, mcp: true, subcommand: 'mcp' };
|
|
131
|
+
}
|
|
75
132
|
// subcommand: ci-triage flakes owner/repo
|
|
76
133
|
if (args[0] === 'flakes') {
|
|
77
134
|
const repo = args[1];
|
|
@@ -81,15 +138,32 @@ export function parseArgs(argv) {
|
|
|
81
138
|
}
|
|
82
139
|
return { repo, limit: 0, outputJson: false, subcommand: 'flakes' };
|
|
83
140
|
}
|
|
84
|
-
// subcommand: ci-triage multi owner/repo1
|
|
85
|
-
|
|
86
|
-
|
|
141
|
+
// subcommand/flag: ci-triage multi owner/repo1,owner/repo2 [--json] [--since 24h]
|
|
142
|
+
// or: ci-triage --multi owner/repo1,owner/repo2 [--json] [--since 24h]
|
|
143
|
+
if (args[0] === 'multi' || args.includes('--multi')) {
|
|
144
|
+
const sinceIdx = args.indexOf('--since');
|
|
145
|
+
const sinceRaw = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
|
|
146
|
+
const sinceHours = parseSinceHours(sinceRaw);
|
|
147
|
+
if (sinceIdx >= 0 && sinceHours === undefined) {
|
|
148
|
+
console.error('Usage: --since <duration>, e.g. 24h, 7d, 30m');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const multiArgs = args[0] === 'multi' ? args.slice(1) : args;
|
|
152
|
+
const repos = parseMultiRepos(positionalArgsExcludingOptionValues(multiArgs));
|
|
87
153
|
if (repos.length === 0 ||
|
|
88
154
|
repos.some((repo) => !/^[A-Za-z0-9_./%-]+\/[A-Za-z0-9_./%-]+$/.test(repo))) {
|
|
89
|
-
console.error('Usage: ci-triage multi owner/repo1
|
|
155
|
+
console.error('Usage: ci-triage multi owner/repo1,owner/repo2 [--json] [--since 24h]');
|
|
90
156
|
process.exit(1);
|
|
91
157
|
}
|
|
92
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
repo: repos[0],
|
|
160
|
+
repos,
|
|
161
|
+
multi: true,
|
|
162
|
+
sinceHours,
|
|
163
|
+
limit: 0,
|
|
164
|
+
outputJson: args.includes('--json'),
|
|
165
|
+
subcommand: 'multi',
|
|
166
|
+
};
|
|
93
167
|
}
|
|
94
168
|
// subcommand: ci-triage db prune [--days 90]
|
|
95
169
|
if (args[0] === 'db' && args[1] === 'prune') {
|
|
@@ -156,7 +230,7 @@ export function parseArgs(argv) {
|
|
|
156
230
|
if (!repo || !/^[A-Za-z0-9_./%-]+\/[A-Za-z0-9_./%-]+$/.test(repo)) {
|
|
157
231
|
usage();
|
|
158
232
|
}
|
|
159
|
-
const positionalArgs = args.slice(1)
|
|
233
|
+
const positionalArgs = positionalArgsExcludingOptionValues(args.slice(1));
|
|
160
234
|
const parsedLimit = positionalArgs[0] ? Number(positionalArgs[0]) : 10;
|
|
161
235
|
const limit = Math.min(Math.max(Number.isFinite(parsedLimit) ? parsedLimit : 10, 1), 100);
|
|
162
236
|
const runIdx = args.indexOf('--run');
|
|
@@ -176,6 +250,8 @@ export function parseArgs(argv) {
|
|
|
176
250
|
outputJson: args.includes('--json'),
|
|
177
251
|
provider,
|
|
178
252
|
llm: args.includes('--llm'),
|
|
253
|
+
noLlm: args.includes('--no-llm'),
|
|
254
|
+
mcp: args.includes('--mcp'),
|
|
179
255
|
llmModel,
|
|
180
256
|
};
|
|
181
257
|
}
|
|
@@ -299,7 +375,15 @@ export async function triageRepo(options) {
|
|
|
299
375
|
event: metadata.event,
|
|
300
376
|
},
|
|
301
377
|
});
|
|
302
|
-
|
|
378
|
+
if (run.createdAt && Number.isFinite(Date.parse(run.createdAt))) {
|
|
379
|
+
report.timestamp = run.createdAt;
|
|
380
|
+
}
|
|
381
|
+
const noLlm = options.noLlm || !!process.env['CI_TRIAGE_NO_LLM'];
|
|
382
|
+
const autoLlm = !noLlm && !!process.env['OPENAI_API_KEY'];
|
|
383
|
+
if (autoLlm && !options.llm && !noLlm) {
|
|
384
|
+
process.stderr.write('ℹ using LLM analysis (set CI_TRIAGE_NO_LLM=1 or pass --no-llm to disable)\n');
|
|
385
|
+
}
|
|
386
|
+
const llmEnabled = !noLlm && (options.llm || !!process.env['OPENAI_API_KEY']);
|
|
303
387
|
if (llmEnabled) {
|
|
304
388
|
const repoContext = await repoContextPromise;
|
|
305
389
|
const failureEntries = classified.map((f) => ({
|
|
@@ -341,6 +425,7 @@ async function runMultiCommand(options) {
|
|
|
341
425
|
repo,
|
|
342
426
|
provider: options.provider,
|
|
343
427
|
llm: options.llm,
|
|
428
|
+
noLlm: options.noLlm,
|
|
344
429
|
llmModel: options.llmModel,
|
|
345
430
|
});
|
|
346
431
|
reports.push(report);
|
|
@@ -350,15 +435,21 @@ async function runMultiCommand(options) {
|
|
|
350
435
|
console.error(`Failed to triage ${repo}: ${message}`);
|
|
351
436
|
}
|
|
352
437
|
}
|
|
353
|
-
const
|
|
438
|
+
const filteredReports = filterReportsSince(reports, options.sinceHours);
|
|
439
|
+
const patterns = detectSharedPatterns(filteredReports);
|
|
354
440
|
if (options.outputJson) {
|
|
355
|
-
process.stdout.write(buildMultiJson(
|
|
441
|
+
process.stdout.write(buildMultiJson(filteredReports, patterns));
|
|
356
442
|
return;
|
|
357
443
|
}
|
|
358
|
-
process.stdout.write(formatMultiSummary(
|
|
444
|
+
process.stdout.write(formatMultiSummary(filteredReports, patterns));
|
|
359
445
|
}
|
|
360
446
|
async function main() {
|
|
361
447
|
const options = parseArgs(process.argv);
|
|
448
|
+
// --mcp: start MCP server in stdio mode
|
|
449
|
+
if (options.mcp) {
|
|
450
|
+
await startMcpServer();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
362
453
|
// Handle subcommands
|
|
363
454
|
if (options.subcommand === 'flakes') {
|
|
364
455
|
await runFlakesCommand(options.repo);
|
|
@@ -442,7 +533,12 @@ async function main() {
|
|
|
442
533
|
},
|
|
443
534
|
});
|
|
444
535
|
// LLM analysis (gated)
|
|
445
|
-
const
|
|
536
|
+
const noLlm = options.noLlm || !!process.env['CI_TRIAGE_NO_LLM'];
|
|
537
|
+
const autoLlm = !noLlm && !!process.env['OPENAI_API_KEY'];
|
|
538
|
+
if (autoLlm && !options.llm && !noLlm) {
|
|
539
|
+
process.stderr.write('ℹ using LLM analysis (set CI_TRIAGE_NO_LLM=1 or pass --no-llm to disable)\n');
|
|
540
|
+
}
|
|
541
|
+
const llmEnabled = !noLlm && (options.llm || !!process.env['OPENAI_API_KEY']);
|
|
446
542
|
if (llmEnabled) {
|
|
447
543
|
const repoContext = await repoContextPromise;
|
|
448
544
|
const failureEntries = classified.map((f) => ({
|
package/dist/multi.js
CHANGED
|
@@ -10,6 +10,47 @@ function reportFailures(report) {
|
|
|
10
10
|
function normalizeError(error) {
|
|
11
11
|
return error.replace(/\s+/g, ' ').trim();
|
|
12
12
|
}
|
|
13
|
+
function toEpochMs(value) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return 0;
|
|
16
|
+
const ts = Date.parse(value);
|
|
17
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
18
|
+
}
|
|
19
|
+
export function filterReportsSince(reports, sinceHours, nowMs = Date.now()) {
|
|
20
|
+
if (sinceHours === undefined)
|
|
21
|
+
return reports;
|
|
22
|
+
const cutoffMs = nowMs - sinceHours * 60 * 60 * 1000;
|
|
23
|
+
return reports.filter((report) => toEpochMs(report.timestamp) >= cutoffMs);
|
|
24
|
+
}
|
|
25
|
+
export function rankReportsBySeverity(reports) {
|
|
26
|
+
const weights = {
|
|
27
|
+
critical: 4,
|
|
28
|
+
high: 3,
|
|
29
|
+
medium: 2,
|
|
30
|
+
low: 1,
|
|
31
|
+
};
|
|
32
|
+
return reports
|
|
33
|
+
.map((report) => {
|
|
34
|
+
const failures = reportFailures(report);
|
|
35
|
+
const severity_counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
36
|
+
let severity_score = 0;
|
|
37
|
+
for (const failure of failures) {
|
|
38
|
+
const sev = failure.severity;
|
|
39
|
+
if (sev in weights) {
|
|
40
|
+
severity_counts[sev] += 1;
|
|
41
|
+
severity_score += weights[sev];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
repo: report.repo,
|
|
46
|
+
severity_score,
|
|
47
|
+
total_failures: failures.length,
|
|
48
|
+
severity_counts,
|
|
49
|
+
root_cause: report.summary.root_cause,
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
.sort((a, b) => b.severity_score - a.severity_score || b.total_failures - a.total_failures || a.repo.localeCompare(b.repo));
|
|
53
|
+
}
|
|
13
54
|
export function detectSharedPatterns(reports) {
|
|
14
55
|
const byFixAction = new Map();
|
|
15
56
|
const byCategory = new Map();
|
|
@@ -74,12 +115,18 @@ export function detectSharedPatterns(reports) {
|
|
|
74
115
|
return patterns;
|
|
75
116
|
}
|
|
76
117
|
export function formatMultiSummary(reports, patterns) {
|
|
118
|
+
const ranked = rankReportsBySeverity(reports);
|
|
77
119
|
const lines = [`Multi-repo triage: ${reports.length} repos`, ''];
|
|
120
|
+
lines.push('Ranked by failure severity:');
|
|
121
|
+
for (const rank of ranked) {
|
|
122
|
+
lines.push(` ${rank.repo} - score ${rank.severity_score} (${rank.total_failures} failures, root cause: ${rank.root_cause})`);
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
78
125
|
if (patterns.length === 0) {
|
|
79
|
-
lines.push('Shared
|
|
126
|
+
lines.push('Shared root causes detected: none', '');
|
|
80
127
|
}
|
|
81
128
|
else {
|
|
82
|
-
lines.push('Shared
|
|
129
|
+
lines.push('Shared root causes detected:');
|
|
83
130
|
for (const pattern of patterns) {
|
|
84
131
|
const repos = pattern.repos.map((repo) => repo.split('/').at(-1) ?? repo).join(', ');
|
|
85
132
|
if (pattern.fix_action) {
|
|
@@ -95,22 +142,14 @@ export function formatMultiSummary(reports, patterns) {
|
|
|
95
142
|
}
|
|
96
143
|
lines.push('');
|
|
97
144
|
}
|
|
98
|
-
lines.push('Per-repo:');
|
|
99
|
-
for (const report of reports) {
|
|
100
|
-
const failures = reportFailures(report);
|
|
101
|
-
const actionCounts = new Map();
|
|
102
|
-
for (const failure of failures) {
|
|
103
|
-
const action = failure.fix_action ?? 'unknown';
|
|
104
|
-
actionCounts.set(action, (actionCounts.get(action) ?? 0) + 1);
|
|
105
|
-
}
|
|
106
|
-
const actionSummary = [...actionCounts.entries()]
|
|
107
|
-
.sort((a, b) => b[1] - a[1])
|
|
108
|
-
.map(([action, count]) => `${action} x${count}`)
|
|
109
|
-
.join(', ');
|
|
110
|
-
lines.push(` ${report.repo} - ${failures.length} failures${actionSummary ? ` (${actionSummary})` : ''}`);
|
|
111
|
-
}
|
|
112
145
|
return `${lines.join('\n')}\n`;
|
|
113
146
|
}
|
|
114
147
|
export function buildMultiJson(reports, patterns) {
|
|
115
|
-
|
|
148
|
+
const ranked = rankReportsBySeverity(reports);
|
|
149
|
+
return `${JSON.stringify({
|
|
150
|
+
repos: reports,
|
|
151
|
+
ranked_repos: ranked,
|
|
152
|
+
shared_root_causes: patterns,
|
|
153
|
+
patterns,
|
|
154
|
+
}, null, 2)}\n`;
|
|
116
155
|
}
|
package/dist/parser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { infraParsers } from './parsers/index.js';
|
|
1
2
|
const ANSI_REGEX = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
2
3
|
const TIMESTAMP_PREFIX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s*/;
|
|
3
4
|
const MAX_LINES = 25_000;
|
|
@@ -10,7 +11,12 @@ const ERROR_LINE_REGEX = [
|
|
|
10
11
|
/^FAILED\b.+$/,
|
|
11
12
|
/^Caused by:\s+.+$/i,
|
|
12
13
|
/\b(?:heap out of memory|permission denied|cannot find module|failed to start container|no such image)\b/i,
|
|
14
|
+
/\b(?:command not found|script returned exit code|the command exited with)\b/i,
|
|
13
15
|
/\b(?:eacces|enoent|econnrefused|etimedout|timed out|rate limit(?:ed)?|too many requests)\b/i,
|
|
16
|
+
/\bcurl:\s*\(\d+\).*(?:4\d{2}|5\d{2})\b/i,
|
|
17
|
+
/\b(?:http(?:\/\d(?:\.\d)?)?\s+|status(?: code)?[:=]?\s*)(?:4\d{2}|5\d{2})\b/i,
|
|
18
|
+
/\b(?:pages?\s+(?:deploy|deployment).*(?:failed|error)|failed to create deployment)\b/i,
|
|
19
|
+
/\b(?:codeql|code[- ]scanning).*(?:workflow error|analysis failed|database .* failed|required permissions|init failed|analyze failed)\b/i,
|
|
14
20
|
/\b(?:environment variable .* not set|missing env(?:ironment)? variable|undefined env)\b/i,
|
|
15
21
|
/\b(?:npm audit|vulnerabilities|eslint|linting failed|error ts\d{4})\b/i,
|
|
16
22
|
/\berror\s+TS\d{4}\b/i,
|
|
@@ -243,17 +249,16 @@ export function parseInfraFailures(rawLog) {
|
|
|
243
249
|
continue;
|
|
244
250
|
}
|
|
245
251
|
const context = `${lines[i - 1] ?? ''}\n${line}\n${lines[i + 1] ?? ''}`;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
failures.push(createInfraFailure(lines, i, line));
|
|
252
|
+
const matchedByHandler = infraParsers
|
|
253
|
+
.map((handler) => handler({
|
|
254
|
+
line,
|
|
255
|
+
prevLine: lines[i - 1] ?? '',
|
|
256
|
+
nextLine: lines[i + 1] ?? '',
|
|
257
|
+
context,
|
|
258
|
+
}))
|
|
259
|
+
.find((value) => Boolean(value));
|
|
260
|
+
if (matchedByHandler) {
|
|
261
|
+
failures.push(createInfraFailure(lines, i, matchedByHandler));
|
|
257
262
|
continue;
|
|
258
263
|
}
|
|
259
264
|
// GitHub Actions explicit step failure line.
|
|
@@ -272,14 +277,10 @@ export function parseInfraFailures(rawLog) {
|
|
|
272
277
|
failures.push(createInfraFailure(lines, i, line));
|
|
273
278
|
continue;
|
|
274
279
|
}
|
|
275
|
-
//
|
|
276
|
-
if (/\
|
|
277
|
-
(/\
|
|
278
|
-
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
// CodeQL/configuration errors.
|
|
282
|
-
if (/\bconfiguration error\b/i.test(line) && /\b(?:codeql|code[- ]scanning)\b/i.test(context)) {
|
|
280
|
+
// CodeQL/configuration/workflow errors.
|
|
281
|
+
if ((/\bconfiguration error\b/i.test(line) && /\b(?:codeql|code[- ]scanning)\b/i.test(context)) ||
|
|
282
|
+
(/\b(?:workflow error|analysis failed|database .* failed|required permissions|init failed|analyze failed|autobuild failed)\b/i.test(line) &&
|
|
283
|
+
/\b(?:codeql|code[- ]scanning|github\/codeql-action)\b/i.test(context))) {
|
|
283
284
|
failures.push(createInfraFailure(lines, i, line));
|
|
284
285
|
continue;
|
|
285
286
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const GITHUB_PAGES_PATTERNS = [
|
|
2
|
+
/\bError:\s*Get Pages site failed\b/i,
|
|
3
|
+
/\bfailed to create deployment\b/i,
|
|
4
|
+
/\bpages?\s+(?:deploy|deployment).*(?:failed|error)\b/i,
|
|
5
|
+
];
|
|
6
|
+
const VERCEL_PATTERNS = [
|
|
7
|
+
/\bvercel\b.*\b(?:error|failed|failure)\b/i,
|
|
8
|
+
/\b(?:vercel|vc)\s+deploy\b.*\b(?:error|failed)\b/i,
|
|
9
|
+
/\b(?:project|team)\s+not found\b.*\bvercel\b/i,
|
|
10
|
+
/\bNo Output Directory named\b/i,
|
|
11
|
+
/\bCommand\s+\".+\"\s+exited with\s+[1-9]\d*\b/i,
|
|
12
|
+
];
|
|
13
|
+
const CLOUDFLARE_PATTERNS = [
|
|
14
|
+
/\bcloudflare\b.*\b(?:error|failed|failure)\b/i,
|
|
15
|
+
/\bcloudflare pages\b.*\b(?:error|failed|failure)\b/i,
|
|
16
|
+
/\bwrangler\b.*\b(?:error|failed|failure)\b/i,
|
|
17
|
+
/\bA request to the Cloudflare API\b.*\bfailed\b/i,
|
|
18
|
+
];
|
|
19
|
+
export const parseDeployPagesFailure = ({ line, context }) => {
|
|
20
|
+
if (GITHUB_PAGES_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
21
|
+
return line.trim();
|
|
22
|
+
}
|
|
23
|
+
if (/\bHttpError:\s*Not Found\b/i.test(line) && /\b(?:pages?|deploy)\b/i.test(context)) {
|
|
24
|
+
return line.trim();
|
|
25
|
+
}
|
|
26
|
+
if (VERCEL_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
27
|
+
return line.trim();
|
|
28
|
+
}
|
|
29
|
+
if (CLOUDFLARE_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
30
|
+
return line.trim();
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const HTTP_PATTERNS = [
|
|
2
|
+
/\bHTTP(?:\/\d(?:\.\d)?)?\s+(?:4\d{2}|5\d{2})\b/i,
|
|
3
|
+
/\b(?:status|status code)[:=]?\s*(?:4\d{2}|5\d{2})\b/i,
|
|
4
|
+
/\bcurl:\s*\(\d+\).*(?:4\d{2}|5\d{2})\b/i,
|
|
5
|
+
/\bfetch\b.*\b(?:HTTP|status|status code)[:=]?\s*(?:4\d{2}|5\d{2})\b/i,
|
|
6
|
+
/\b(?:4\d{2}|5\d{2})\s+(?:Bad Request|Unauthorized|Forbidden|Not Found|Too Many Requests|Internal Server Error|Bad Gateway|Service Unavailable|Gateway Timeout)\b/i,
|
|
7
|
+
];
|
|
8
|
+
export const parseHttpError = ({ line }) => {
|
|
9
|
+
if (HTTP_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
10
|
+
return line.trim();
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { parseDeployPagesFailure } from './deploy-pages-failure.js';
|
|
2
|
+
import { parseHttpError } from './http-error.js';
|
|
3
|
+
import { parseShellScriptFailure } from './shell-failure.js';
|
|
4
|
+
export const infraParsers = [
|
|
5
|
+
parseShellScriptFailure,
|
|
6
|
+
parseHttpError,
|
|
7
|
+
parseDeployPagesFailure,
|
|
8
|
+
];
|
|
9
|
+
export { parseDeployPagesFailure, parseHttpError, parseShellScriptFailure };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const SHELL_PATTERNS = [
|
|
2
|
+
/^.+:\s*line\s+\d+:\s*.+$/i,
|
|
3
|
+
/\bcommand not found\b/i,
|
|
4
|
+
/\bpermission denied\b/i,
|
|
5
|
+
/\bno such file or directory\b/i,
|
|
6
|
+
/\b(?:script returned exit code|the command exited with)\s*([1-9]\d*)\b/i,
|
|
7
|
+
/\bexit code\s+([1-9]\d*)\b/i,
|
|
8
|
+
];
|
|
9
|
+
export const parseShellScriptFailure = ({ line }) => {
|
|
10
|
+
if (SHELL_PATTERNS.some((pattern) => pattern.test(line))) {
|
|
11
|
+
return line.trim();
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/providers/github.js
CHANGED
|
@@ -49,7 +49,7 @@ export class GitHubProvider {
|
|
|
49
49
|
async listRuns(repo, limit) {
|
|
50
50
|
const runs = runJson([
|
|
51
51
|
'run', 'list', '--repo', repo, '--limit', String(limit),
|
|
52
|
-
'--json', 'databaseId,displayTitle,workflowName,conclusion,url',
|
|
52
|
+
'--json', 'databaseId,displayTitle,workflowName,conclusion,url,createdAt',
|
|
53
53
|
]);
|
|
54
54
|
return runs.map((r) => ({
|
|
55
55
|
id: r.databaseId,
|
|
@@ -57,6 +57,7 @@ export class GitHubProvider {
|
|
|
57
57
|
workflowName: r.workflowName,
|
|
58
58
|
conclusion: r.conclusion,
|
|
59
59
|
url: r.url,
|
|
60
|
+
createdAt: r.createdAt,
|
|
60
61
|
}));
|
|
61
62
|
}
|
|
62
63
|
async resolveRun(repo, runId) {
|
package/dist/providers/gitlab.js
CHANGED
|
@@ -39,6 +39,7 @@ export class GitLabProvider {
|
|
|
39
39
|
workflowName: 'GitLab CI',
|
|
40
40
|
conclusion: p.status === 'success' ? 'success' : p.status === 'failed' ? 'failure' : p.status,
|
|
41
41
|
url: p.web_url,
|
|
42
|
+
createdAt: p.created_at,
|
|
42
43
|
}));
|
|
43
44
|
}
|
|
44
45
|
async resolveRun(repo, runId) {
|
|
@@ -50,6 +51,7 @@ export class GitLabProvider {
|
|
|
50
51
|
workflowName: 'GitLab CI',
|
|
51
52
|
conclusion: pipeline.status === 'success' ? 'success' : pipeline.status === 'failed' ? 'failure' : pipeline.status,
|
|
52
53
|
url: pipeline.web_url,
|
|
54
|
+
createdAt: pipeline.created_at,
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
const runs = await this.listRuns(repo, 20);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ci-triage",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Open-source CI failure triage for humans and agents — smart log parsing, flake detection, structured JSON, MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"homepage": "https://github.com/clankamode/ci-triage",
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/better-sqlite3": "^7.6.13",
|
|
44
|
-
"@types/node": "^
|
|
44
|
+
"@types/node": "^25.3.1",
|
|
45
45
|
"typescript": "^5.9.2",
|
|
46
46
|
"vitest": "^4.0.18"
|
|
47
47
|
},
|