ci-triage 0.3.0 → 0.3.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/index.js +24 -3
- 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/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getDbStats, getFlakes, getRemediations, markResolved, markVerified, per
|
|
|
10
10
|
import { analyzeLlm } from './llm-analyzer.js';
|
|
11
11
|
import { fetchRepoContext } from './repo-context.js';
|
|
12
12
|
import { buildMultiJson, detectSharedPatterns, 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';
|
|
@@ -39,8 +40,10 @@ function usage() {
|
|
|
39
40
|
' --md <path> Write markdown report to file',
|
|
40
41
|
' --json Output JSON instead of human text',
|
|
41
42
|
' --provider <p> Force CI provider: github | gitlab | circleci',
|
|
42
|
-
' --llm Enable LLM root-cause analysis (
|
|
43
|
+
' --llm Enable LLM root-cause analysis (auto-on when OPENAI_API_KEY is set)',
|
|
44
|
+
' --no-llm Disable LLM even when OPENAI_API_KEY is set (alias: CI_TRIAGE_NO_LLM=1)',
|
|
43
45
|
' --llm-model <model> Override LLM model (default: gpt-4.1-mini)',
|
|
46
|
+
' --mcp Start MCP server (stdio transport) for agent tool use',
|
|
44
47
|
' --version Print version and exit',
|
|
45
48
|
].join('\n'));
|
|
46
49
|
process.exit(1);
|
|
@@ -176,6 +179,8 @@ export function parseArgs(argv) {
|
|
|
176
179
|
outputJson: args.includes('--json'),
|
|
177
180
|
provider,
|
|
178
181
|
llm: args.includes('--llm'),
|
|
182
|
+
noLlm: args.includes('--no-llm'),
|
|
183
|
+
mcp: args.includes('--mcp'),
|
|
179
184
|
llmModel,
|
|
180
185
|
};
|
|
181
186
|
}
|
|
@@ -299,7 +304,12 @@ export async function triageRepo(options) {
|
|
|
299
304
|
event: metadata.event,
|
|
300
305
|
},
|
|
301
306
|
});
|
|
302
|
-
const
|
|
307
|
+
const noLlm = options.noLlm || !!process.env['CI_TRIAGE_NO_LLM'];
|
|
308
|
+
const autoLlm = !noLlm && !!process.env['OPENAI_API_KEY'];
|
|
309
|
+
if (autoLlm && !options.llm && !noLlm) {
|
|
310
|
+
process.stderr.write('ℹ using LLM analysis (set CI_TRIAGE_NO_LLM=1 or pass --no-llm to disable)\n');
|
|
311
|
+
}
|
|
312
|
+
const llmEnabled = !noLlm && (options.llm || !!process.env['OPENAI_API_KEY']);
|
|
303
313
|
if (llmEnabled) {
|
|
304
314
|
const repoContext = await repoContextPromise;
|
|
305
315
|
const failureEntries = classified.map((f) => ({
|
|
@@ -341,6 +351,7 @@ async function runMultiCommand(options) {
|
|
|
341
351
|
repo,
|
|
342
352
|
provider: options.provider,
|
|
343
353
|
llm: options.llm,
|
|
354
|
+
noLlm: options.noLlm,
|
|
344
355
|
llmModel: options.llmModel,
|
|
345
356
|
});
|
|
346
357
|
reports.push(report);
|
|
@@ -359,6 +370,11 @@ async function runMultiCommand(options) {
|
|
|
359
370
|
}
|
|
360
371
|
async function main() {
|
|
361
372
|
const options = parseArgs(process.argv);
|
|
373
|
+
// --mcp: start MCP server in stdio mode
|
|
374
|
+
if (options.mcp) {
|
|
375
|
+
await startMcpServer();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
362
378
|
// Handle subcommands
|
|
363
379
|
if (options.subcommand === 'flakes') {
|
|
364
380
|
await runFlakesCommand(options.repo);
|
|
@@ -442,7 +458,12 @@ async function main() {
|
|
|
442
458
|
},
|
|
443
459
|
});
|
|
444
460
|
// LLM analysis (gated)
|
|
445
|
-
const
|
|
461
|
+
const noLlm = options.noLlm || !!process.env['CI_TRIAGE_NO_LLM'];
|
|
462
|
+
const autoLlm = !noLlm && !!process.env['OPENAI_API_KEY'];
|
|
463
|
+
if (autoLlm && !options.llm && !noLlm) {
|
|
464
|
+
process.stderr.write('ℹ using LLM analysis (set CI_TRIAGE_NO_LLM=1 or pass --no-llm to disable)\n');
|
|
465
|
+
}
|
|
466
|
+
const llmEnabled = !noLlm && (options.llm || !!process.env['OPENAI_API_KEY']);
|
|
446
467
|
if (llmEnabled) {
|
|
447
468
|
const repoContext = await repoContextPromise;
|
|
448
469
|
const failureEntries = classified.map((f) => ({
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ci-triage",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
},
|