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 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 (requires OPENAI_API_KEY)',
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 llmEnabled = options.llm || !!process.env['OPENAI_API_KEY'];
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 llmEnabled = options.llm || !!process.env['OPENAI_API_KEY'];
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
- // Shell script/runtime errors from bash/sh.
247
- if (/^.+:\s*line\s+\d+:\s*.+$/i.test(line) ||
248
- /\bcommand not found\b/i.test(line) ||
249
- /\bpermission denied\b/i.test(line) ||
250
- /\bno such file or directory\b/i.test(line)) {
251
- failures.push(createInfraFailure(lines, i, line));
252
- continue;
253
- }
254
- // HTTP errors.
255
- if (/\bHTTP\s*(?:403|404|429|5\d{2})\b/i.test(line) || /\b(?:403 Forbidden|404 Not Found)\b/i.test(line)) {
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
- // GitHub Pages deploy failures.
276
- if (/\bError:\s*Get Pages site failed\b/i.test(line) ||
277
- (/\bHttpError:\s*Not Found\b/i.test(line) && /\bpages?\b/i.test(context))) {
278
- failures.push(createInfraFailure(lines, i, line));
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.0",
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": "^22.13.10",
44
+ "@types/node": "^25.3.1",
45
45
  "typescript": "^5.9.2",
46
46
  "vitest": "^4.0.18"
47
47
  },