@vitest-evals/github-reporter 0.9.0-beta.5 → 0.9.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.mjs CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli.ts
4
- import { appendFile, readFile } from "fs/promises";
5
-
6
3
  // src/cli-options.ts
7
4
  function parseCliArgs(args, env = process.env) {
8
5
  const options = {
@@ -10,14 +7,16 @@ function parseCliArgs(args, env = process.env) {
10
7
  summaryEnabled: true,
11
8
  annotations: env.GITHUB_ACTIONS === "true",
12
9
  checkRun: false,
10
+ failOnFailures: false,
13
11
  failOnCheckError: false,
12
+ resultPatterns: [],
14
13
  help: false
15
14
  };
16
15
  for (let index = 0; index < args.length; index += 1) {
17
16
  const arg = args[index];
18
17
  switch (arg) {
19
18
  case "--json":
20
- options.jsonPath = readValue(args, ++index, arg);
19
+ options.resultPatterns.push(readValue(args, ++index, arg));
21
20
  break;
22
21
  case "--summary":
23
22
  options.summaryPath = readValue(args, ++index, arg);
@@ -35,6 +34,9 @@ function parseCliArgs(args, env = process.env) {
35
34
  case "--check-run":
36
35
  options.checkRun = true;
37
36
  break;
37
+ case "--fail-on-failures":
38
+ options.failOnFailures = true;
39
+ break;
38
40
  case "--fail-on-check-error":
39
41
  options.failOnCheckError = true;
40
42
  break;
@@ -68,8 +70,8 @@ function parseCliArgs(args, env = process.env) {
68
70
  options.help = true;
69
71
  return withDefaultJsonPath(options, env);
70
72
  default:
71
- if (!arg.startsWith("-") && !options.jsonPath) {
72
- options.jsonPath = arg;
73
+ if (!arg.startsWith("-")) {
74
+ options.resultPatterns.push(arg);
73
75
  break;
74
76
  }
75
77
  throw new Error(`Unknown argument: ${arg}`);
@@ -80,7 +82,7 @@ function parseCliArgs(args, env = process.env) {
80
82
  function withDefaultJsonPath(options, env) {
81
83
  return {
82
84
  ...options,
83
- jsonPath: options.jsonPath || env.VITEST_EVALS_JSON_REPORT || "vitest-results.json"
85
+ resultPatterns: options.resultPatterns.length > 0 ? options.resultPatterns : [env.VITEST_EVALS_JSON_REPORT || "vitest-results.json"]
84
86
  };
85
87
  }
86
88
  function readValue(args, index, flag) {
@@ -91,13 +93,16 @@ function readValue(args, index, flag) {
91
93
  return value;
92
94
  }
93
95
  function readInteger(args, index, flag) {
94
- const value = Number.parseInt(readValue(args, index, flag), 10);
95
- if (!Number.isFinite(value) || value < 0) {
96
+ const rawValue = readValue(args, index, flag);
97
+ if (!/^\d+$/.test(rawValue)) {
96
98
  throw new Error(`Invalid integer for ${flag}`);
97
99
  }
98
- return value;
100
+ return Number(rawValue);
99
101
  }
100
102
 
103
+ // src/report.ts
104
+ import { appendFile, readFile } from "fs/promises";
105
+
101
106
  // src/utils.ts
102
107
  import { posix } from "path";
103
108
  function isRecord(value) {
@@ -189,6 +194,98 @@ function escapeCommandProperty(value) {
189
194
  return escapeCommandData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
190
195
  }
191
196
 
197
+ // src/annotations.ts
198
+ var DEFAULT_MAX_WORKFLOW_ANNOTATIONS = 10;
199
+ var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
200
+ var MAX_CHECK_FIELD_LENGTH = 64e3;
201
+ function renderWorkflowCommands(report, options = {}) {
202
+ const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
203
+ return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
204
+ (testCase) => formatWorkflowCommand({
205
+ command: "error",
206
+ properties: {
207
+ file: testCase.displayFile,
208
+ line: String(testCase.location.line),
209
+ col: String(testCase.location.column),
210
+ title: "vitest-evals"
211
+ },
212
+ message: formatWorkflowMessage(testCase)
213
+ })
214
+ );
215
+ }
216
+ function buildCheckAnnotations(report, options = {}) {
217
+ const maxAnnotations = Math.min(
218
+ options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS,
219
+ DEFAULT_MAX_CHECK_ANNOTATIONS
220
+ );
221
+ return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
222
+ path: testCase.displayFile,
223
+ start_line: testCase.location.line,
224
+ end_line: testCase.location.line,
225
+ annotation_level: "failure",
226
+ title: truncate(
227
+ `${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
228
+ 255
229
+ ),
230
+ message: truncate(
231
+ formatWorkflowMessage(testCase),
232
+ MAX_CHECK_FIELD_LENGTH
233
+ ),
234
+ raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
235
+ }));
236
+ }
237
+ function hasAnnotationLocation(testCase) {
238
+ return Boolean(testCase.location);
239
+ }
240
+ function formatWorkflowMessage(testCase) {
241
+ const failure = testCase.primaryFailure;
242
+ const parts = [
243
+ testCase.displayName,
244
+ `score ${formatScore(failure?.score ?? testCase.eval?.avgScore)}`
245
+ ];
246
+ if (failure?.judgeName) {
247
+ parts.push(failure.judgeName);
248
+ }
249
+ const reason = compactLine(failure?.reason ?? "", 320);
250
+ if (reason) {
251
+ parts.push(reason);
252
+ }
253
+ return parts.join(" - ");
254
+ }
255
+ function formatRawDetails(testCase) {
256
+ const lines = [
257
+ `Test: ${testCase.displayName}`,
258
+ `Location: ${testCase.displayFile}:${testCase.location.line}`,
259
+ `Harness: ${testCase.harness?.name ?? "n/a"}`,
260
+ `Score: ${formatScore(testCase.primaryFailure?.score ?? testCase.eval?.avgScore)}`,
261
+ `Judge: ${testCase.primaryFailure?.judgeName ?? "n/a"}`,
262
+ "",
263
+ "Reason:",
264
+ testCase.primaryFailure?.reason ?? "n/a"
265
+ ];
266
+ const finalOutput = testCase.eval?.output ?? testCase.harness?.output;
267
+ if (finalOutput !== void 0) {
268
+ lines.push("", "Final:", stringifyValue(finalOutput, 8e3));
269
+ }
270
+ if (testCase.harness?.toolCalls.length) {
271
+ lines.push("", "Tools:");
272
+ for (const toolCall of testCase.harness.toolCalls) {
273
+ lines.push(
274
+ `- ${toolCall.name}: ${toolCall.error ? `error: ${toolCall.error}` : "ok"}`
275
+ );
276
+ }
277
+ }
278
+ return lines.join("\n");
279
+ }
280
+ function formatWorkflowCommand({
281
+ command,
282
+ properties,
283
+ message
284
+ }) {
285
+ const renderedProperties = Object.entries(properties).map(([key, value]) => `${key}=${escapeCommandProperty(value)}`).join(",");
286
+ return `::${command} ${renderedProperties}::${escapeCommandData(message)}`;
287
+ }
288
+
192
289
  // src/collect.ts
193
290
  function collectEvalReport(input, options = {}) {
194
291
  const cases = input.testResults.flatMap(
@@ -309,7 +406,6 @@ function getUsage(value) {
309
406
  outputTokens: numberField(value.outputTokens),
310
407
  reasoningTokens: numberField(value.reasoningTokens),
311
408
  totalTokens: numberField(value.totalTokens),
312
- estimatedCost: numberField(value.estimatedCost),
313
409
  toolCalls: numberField(value.toolCalls)
314
410
  };
315
411
  }
@@ -379,7 +475,6 @@ function sumUsage(cases) {
379
475
  outputTokens: 0,
380
476
  reasoningTokens: 0,
381
477
  totalTokens: 0,
382
- estimatedCost: 0,
383
478
  toolCalls: 0
384
479
  };
385
480
  for (const testCase of cases) {
@@ -388,7 +483,6 @@ function sumUsage(cases) {
388
483
  usage2.outputTokens += caseUsage?.outputTokens ?? 0;
389
484
  usage2.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
390
485
  usage2.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
391
- usage2.estimatedCost += caseUsage?.estimatedCost ?? 0;
392
486
  usage2.toolCalls += caseUsage?.toolCalls ?? testCase.harness?.toolCalls.length ?? 0;
393
487
  }
394
488
  return usage2;
@@ -402,95 +496,6 @@ function resolveRunDuration(input) {
402
496
  return Math.max(...endTimes) - Math.min(...startTimes);
403
497
  }
404
498
 
405
- // src/annotations.ts
406
- var DEFAULT_MAX_WORKFLOW_ANNOTATIONS = 10;
407
- var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
408
- var MAX_CHECK_FIELD_LENGTH = 64e3;
409
- function renderWorkflowCommands(report, options = {}) {
410
- const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
411
- return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
412
- (testCase) => formatWorkflowCommand({
413
- command: "error",
414
- properties: {
415
- file: testCase.displayFile,
416
- line: String(testCase.location.line),
417
- col: String(testCase.location.column),
418
- title: "vitest-evals"
419
- },
420
- message: formatWorkflowMessage(testCase)
421
- })
422
- );
423
- }
424
- function buildCheckAnnotations(report, options = {}) {
425
- const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS;
426
- return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
427
- path: testCase.displayFile,
428
- start_line: testCase.location.line,
429
- end_line: testCase.location.line,
430
- annotation_level: "failure",
431
- title: truncate(
432
- `${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
433
- 255
434
- ),
435
- message: truncate(
436
- formatWorkflowMessage(testCase),
437
- MAX_CHECK_FIELD_LENGTH
438
- ),
439
- raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
440
- }));
441
- }
442
- function hasAnnotationLocation(testCase) {
443
- return Boolean(testCase.location);
444
- }
445
- function formatWorkflowMessage(testCase) {
446
- const failure = testCase.primaryFailure;
447
- const parts = [
448
- testCase.displayName,
449
- `score ${formatScore(failure?.score ?? testCase.eval?.avgScore)}`
450
- ];
451
- if (failure?.judgeName) {
452
- parts.push(failure.judgeName);
453
- }
454
- const reason = compactLine(failure?.reason ?? "", 320);
455
- if (reason) {
456
- parts.push(reason);
457
- }
458
- return parts.join(" - ");
459
- }
460
- function formatRawDetails(testCase) {
461
- const lines = [
462
- `Test: ${testCase.displayName}`,
463
- `Location: ${testCase.displayFile}:${testCase.location.line}`,
464
- `Harness: ${testCase.harness?.name ?? "n/a"}`,
465
- `Score: ${formatScore(testCase.primaryFailure?.score ?? testCase.eval?.avgScore)}`,
466
- `Judge: ${testCase.primaryFailure?.judgeName ?? "n/a"}`,
467
- "",
468
- "Reason:",
469
- testCase.primaryFailure?.reason ?? "n/a"
470
- ];
471
- const finalOutput = testCase.eval?.output ?? testCase.harness?.output;
472
- if (finalOutput !== void 0) {
473
- lines.push("", "Final:", stringifyValue(finalOutput, 8e3));
474
- }
475
- if (testCase.harness?.toolCalls.length) {
476
- lines.push("", "Tools:");
477
- for (const toolCall of testCase.harness.toolCalls) {
478
- lines.push(
479
- `- ${toolCall.name}: ${toolCall.error ? `error: ${toolCall.error}` : "ok"}`
480
- );
481
- }
482
- }
483
- return lines.join("\n");
484
- }
485
- function formatWorkflowCommand({
486
- command,
487
- properties,
488
- message
489
- }) {
490
- const renderedProperties = Object.entries(properties).map(([key, value]) => `${key}=${escapeCommandProperty(value)}`).join(",");
491
- return `::${command} ${renderedProperties}::${escapeCommandData(message)}`;
492
- }
493
-
494
499
  // src/summary.ts
495
500
  var DEFAULT_MAX_FAILURES = 20;
496
501
  var DEFAULT_MAX_REASON_CHARS = 8e3;
@@ -543,14 +548,6 @@ function formatCountLine(passed, failed, total) {
543
548
  function renderSummaryTable(report, nonEvalFailures) {
544
549
  const rows = [
545
550
  ["Status", report.status],
546
- [
547
- "Tests",
548
- formatCountLine(
549
- report.totals.passed,
550
- report.totals.failed,
551
- report.totals.total
552
- )
553
- ],
554
551
  [
555
552
  "Evals",
556
553
  formatCountLine(
@@ -563,10 +560,6 @@ function renderSummaryTable(report, nonEvalFailures) {
563
560
  if (report.score) {
564
561
  rows.push(["Score", formatScoreSummary(report.score)]);
565
562
  }
566
- const usage2 = formatUsage(report.usage);
567
- if (usage2) {
568
- rows.push(["Usage", usage2]);
569
- }
570
563
  if (nonEvalFailures > 0) {
571
564
  rows.push([
572
565
  "Other Failures",
@@ -605,7 +598,7 @@ function renderScoreDistribution(report) {
605
598
  }
606
599
  const maxCount = Math.max(...counts);
607
600
  return [
608
- "Score distribution",
601
+ "## Scores",
609
602
  "",
610
603
  "```text",
611
604
  ...SCORE_DISTRIBUTION_BUCKETS.map(
@@ -760,21 +753,6 @@ function renderAsciiTable(headers, rows) {
760
753
  ...rows.map(renderRow)
761
754
  ];
762
755
  }
763
- function formatUsage(usage2) {
764
- const parts = [];
765
- if (usage2.totalTokens > 0) {
766
- parts.push(`${formatNumber(usage2.totalTokens)} tokens`);
767
- }
768
- if (usage2.toolCalls > 0) {
769
- parts.push(
770
- `${formatNumber(usage2.toolCalls)} tool${usage2.toolCalls === 1 ? "" : "s"}`
771
- );
772
- }
773
- if (usage2.estimatedCost > 0) {
774
- parts.push(`$${usage2.estimatedCost.toFixed(4)}`);
775
- }
776
- return parts.join(", ");
777
- }
778
756
  function formatCaseUsage(testCase) {
779
757
  const usage2 = testCase.harness?.usage;
780
758
  const parts = [];
@@ -879,27 +857,209 @@ function truncateCheckSummary(summary) {
879
857
  return `${summary.slice(0, MAX_CHECK_SUMMARY_LENGTH - CHECK_SUMMARY_TRUNCATION_SUFFIX.length).trimEnd()}${CHECK_SUMMARY_TRUNCATION_SUFFIX}`;
880
858
  }
881
859
 
882
- // src/cli.ts
883
- main().catch((error) => {
884
- console.error(error instanceof Error ? error.message : String(error));
885
- process.exitCode = 1;
886
- });
887
- async function main() {
888
- const options = parseCliArgs(process.argv.slice(2));
889
- if (options.help) {
890
- console.log(usage());
891
- return;
860
+ // src/merge.ts
861
+ function mergeEvalReports(reports) {
862
+ const cases = reports.flatMap((report) => report.cases);
863
+ const failures = reports.flatMap((report) => report.failures);
864
+ const scoredCases = cases.map((testCase) => testCase.eval?.avgScore).filter(
865
+ (score) => typeof score === "number" && Number.isFinite(score)
866
+ );
867
+ const startedAtValues = reports.map((report) => report.startedAt).filter(
868
+ (startedAt2) => typeof startedAt2 === "number" && Number.isFinite(startedAt2)
869
+ );
870
+ const startedAt = startedAtValues.length > 0 ? Math.min(...startedAtValues) : void 0;
871
+ return {
872
+ status: reports.some((report) => report.status === "failed") ? "failed" : "passed",
873
+ startedAt,
874
+ durationMs: mergeDuration(reports),
875
+ totals: {
876
+ total: sum(reports, (report) => report.totals.total),
877
+ passed: sum(reports, (report) => report.totals.passed),
878
+ failed: sum(reports, (report) => report.totals.failed),
879
+ skipped: sum(reports, (report) => report.totals.skipped),
880
+ evalTotal: sum(reports, (report) => report.totals.evalTotal),
881
+ evalPassed: sum(reports, (report) => report.totals.evalPassed),
882
+ evalFailed: sum(reports, (report) => report.totals.evalFailed)
883
+ },
884
+ score: scoredCases.length > 0 ? {
885
+ average: scoredCases.reduce((total, score) => total + score, 0) / scoredCases.length,
886
+ minimum: Math.min(...scoredCases)
887
+ } : void 0,
888
+ usage: mergeUsage(reports.map((report) => report.usage)),
889
+ cases,
890
+ failures
891
+ };
892
+ }
893
+ function mergeUsage(usages) {
894
+ return {
895
+ inputTokens: sum(usages, (usage2) => usage2.inputTokens),
896
+ outputTokens: sum(usages, (usage2) => usage2.outputTokens),
897
+ reasoningTokens: sum(usages, (usage2) => usage2.reasoningTokens),
898
+ totalTokens: sum(usages, (usage2) => usage2.totalTokens),
899
+ toolCalls: sum(usages, (usage2) => usage2.toolCalls)
900
+ };
901
+ }
902
+ function mergeDuration(reports) {
903
+ const intervals = reports.map((report) => {
904
+ if (typeof report.startedAt !== "number" || !Number.isFinite(report.startedAt) || typeof report.durationMs !== "number" || !Number.isFinite(report.durationMs)) {
905
+ return void 0;
906
+ }
907
+ return {
908
+ start: report.startedAt,
909
+ end: report.startedAt + report.durationMs
910
+ };
911
+ }).filter(
912
+ (interval) => Boolean(interval)
913
+ );
914
+ if (intervals.length > 0) {
915
+ return Math.max(...intervals.map((interval) => interval.end)) - Math.min(...intervals.map((interval) => interval.start));
892
916
  }
893
- const json = JSON.parse(
894
- await readFile(options.jsonPath, "utf8")
917
+ const durations = reports.map((report) => report.durationMs).filter(
918
+ (durationMs) => typeof durationMs === "number" && Number.isFinite(durationMs)
895
919
  );
896
- const report = collectEvalReport(json, {
897
- workspace: options.workspace ?? process.env.GITHUB_WORKSPACE ?? process.cwd()
920
+ return durations.length > 0 ? durations.reduce((total, durationMs) => total + durationMs, 0) : void 0;
921
+ }
922
+ function sum(items, select) {
923
+ return items.reduce((total, item) => total + select(item), 0);
924
+ }
925
+
926
+ // src/results.ts
927
+ import { readdir } from "fs/promises";
928
+ import { isAbsolute, relative, resolve, sep } from "path";
929
+ var GLOB_META_PATTERN = /[*?]/;
930
+ async function resolveResultFiles(patterns, options = {}) {
931
+ const cwd = options.cwd ?? process.cwd();
932
+ const files = [];
933
+ for (const pattern of patterns.map((entry) => entry.trim()).filter(Boolean)) {
934
+ if (hasGlob(pattern)) {
935
+ files.push(...await expandGlob(pattern, cwd));
936
+ } else {
937
+ files.push(isAbsolute(pattern) ? pattern : resolve(cwd, pattern));
938
+ }
939
+ }
940
+ return [...new Set(files)].sort();
941
+ }
942
+ function hasGlob(pattern) {
943
+ return GLOB_META_PATTERN.test(pattern);
944
+ }
945
+ async function expandGlob(pattern, cwd) {
946
+ const normalizedPattern = normalizeGlobPattern(pattern);
947
+ const absolutePattern = isAbsolute(pattern);
948
+ const base = globBase(normalizedPattern);
949
+ const basePath = absolutePattern ? base || sep : resolve(cwd, base || ".");
950
+ const regex = globToRegExp(normalizedPattern);
951
+ const matches = [];
952
+ for (const file of await listFiles(basePath)) {
953
+ const normalizedFile = normalizePath(file);
954
+ const candidate = absolutePattern ? normalizedFile : normalizePath(relative(resolve(cwd), file));
955
+ if (regex.test(candidate)) {
956
+ matches.push(file);
957
+ }
958
+ }
959
+ return matches;
960
+ }
961
+ function globBase(pattern) {
962
+ const segments = pattern.split("/");
963
+ const baseSegments = [];
964
+ for (const segment of segments) {
965
+ if (hasGlob(segment)) {
966
+ break;
967
+ }
968
+ baseSegments.push(segment);
969
+ }
970
+ return baseSegments.join("/");
971
+ }
972
+ async function listFiles(directory) {
973
+ const entries = await readDirectory(directory);
974
+ if (!entries) {
975
+ return [];
976
+ }
977
+ const files = [];
978
+ for (const entry of entries) {
979
+ const child = resolve(directory, entry.name);
980
+ if (entry.isDirectory()) {
981
+ files.push(...await listFiles(child));
982
+ } else if (entry.isFile()) {
983
+ files.push(child);
984
+ }
985
+ }
986
+ return files;
987
+ }
988
+ async function readDirectory(directory) {
989
+ try {
990
+ return await readdir(directory, { withFileTypes: true });
991
+ } catch {
992
+ return void 0;
993
+ }
994
+ }
995
+ function globToRegExp(pattern) {
996
+ let source = "";
997
+ for (let index = 0; index < pattern.length; index += 1) {
998
+ const char = pattern[index];
999
+ const next = pattern[index + 1];
1000
+ if (char === "*" && next === "*") {
1001
+ const following = pattern[index + 2];
1002
+ if (following === "/") {
1003
+ source += "(?:.*/)?";
1004
+ index += 2;
1005
+ } else {
1006
+ source += ".*";
1007
+ index += 1;
1008
+ }
1009
+ continue;
1010
+ }
1011
+ if (char === "*") {
1012
+ source += "[^/]*";
1013
+ continue;
1014
+ }
1015
+ if (char === "?") {
1016
+ source += "[^/]";
1017
+ continue;
1018
+ }
1019
+ source += escapeRegExp(char ?? "");
1020
+ }
1021
+ return new RegExp(`^${source}$`);
1022
+ }
1023
+ function escapeRegExp(value) {
1024
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
1025
+ }
1026
+ function normalizePath(path) {
1027
+ return path.replace(/\\/g, "/");
1028
+ }
1029
+ function normalizeGlobPattern(pattern) {
1030
+ const normalizedPattern = normalizePath(pattern);
1031
+ if (isAbsolute(pattern)) {
1032
+ return normalizedPattern;
1033
+ }
1034
+ return normalizedPattern.replace(/^(\.\/)+/, "");
1035
+ }
1036
+
1037
+ // src/report.ts
1038
+ async function publishEvalReport(options) {
1039
+ const resultFiles = await resolveResultFiles(options.resultPatterns, {
1040
+ cwd: options.cwd
898
1041
  });
1042
+ if (resultFiles.length === 0) {
1043
+ throw new Error(
1044
+ `No eval result files matched: ${options.resultPatterns.join(", ")}`
1045
+ );
1046
+ }
1047
+ const reports = await Promise.all(
1048
+ resultFiles.map(async (resultFile) => {
1049
+ const json = await readVitestJsonReport(resultFile);
1050
+ return collectEvalReport(json, {
1051
+ workspace: options.workspace
1052
+ });
1053
+ })
1054
+ );
1055
+ const report = mergeEvalReports(reports);
899
1056
  const summary = renderJobSummary(report, {
900
- maxFailures: options.maxFailures
1057
+ maxFailures: options.maxFailures,
1058
+ maxOutputChars: options.maxOutputChars,
1059
+ maxReasonChars: options.maxReasonChars,
1060
+ maxToolCalls: options.maxToolCalls
901
1061
  });
902
- if (options.summaryEnabled) {
1062
+ if (options.summaryEnabled !== false) {
903
1063
  if (options.summaryPath) {
904
1064
  await appendFile(options.summaryPath, `${summary}
905
1065
  `);
@@ -914,28 +1074,81 @@ async function main() {
914
1074
  console.log(command);
915
1075
  }
916
1076
  }
1077
+ let checkRun;
917
1078
  if (options.checkRun) {
918
1079
  try {
919
- const result = await publishCheckRun(report, {
1080
+ checkRun = await publishCheckRun(report, {
920
1081
  checkRunId: options.checkRunId,
921
1082
  maxAnnotations: options.maxAnnotations,
922
1083
  maxFailures: options.maxFailures,
1084
+ maxOutputChars: options.maxOutputChars,
1085
+ maxReasonChars: options.maxReasonChars,
1086
+ maxToolCalls: options.maxToolCalls,
923
1087
  name: options.checkName,
924
1088
  repository: options.repository,
925
1089
  sha: options.sha,
926
1090
  token: options.token
927
1091
  });
928
- if (result.status === "skipped") {
929
- warn(`GitHub Check Run skipped: ${result.reason}`);
1092
+ if (checkRun.status === "skipped") {
1093
+ options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
930
1094
  }
931
1095
  } catch (error) {
932
1096
  const message = error instanceof Error ? error.message : String(error);
933
1097
  if (options.failOnCheckError) {
934
1098
  throw error;
935
1099
  }
936
- warn(message);
1100
+ options.warn?.(message);
937
1101
  }
938
1102
  }
1103
+ return {
1104
+ report,
1105
+ resultFiles,
1106
+ checkRun
1107
+ };
1108
+ }
1109
+ async function readVitestJsonReport(resultFile) {
1110
+ try {
1111
+ return JSON.parse(await readFile(resultFile, "utf8"));
1112
+ } catch (error) {
1113
+ const message = error instanceof Error ? error.message : String(error);
1114
+ throw new Error(
1115
+ `Failed to read eval result file ${resultFile}: ${message}`
1116
+ );
1117
+ }
1118
+ }
1119
+
1120
+ // src/cli.ts
1121
+ main().catch((error) => {
1122
+ console.error(error instanceof Error ? error.message : String(error));
1123
+ process.exitCode = 1;
1124
+ });
1125
+ async function main() {
1126
+ const options = parseCliArgs(process.argv.slice(2));
1127
+ if (options.help) {
1128
+ console.log(usage());
1129
+ return;
1130
+ }
1131
+ const result = await publishEvalReport({
1132
+ resultPatterns: options.resultPatterns,
1133
+ cwd: options.workspace ?? process.env.GITHUB_WORKSPACE ?? process.cwd(),
1134
+ workspace: options.workspace ?? process.env.GITHUB_WORKSPACE ?? process.cwd(),
1135
+ summaryEnabled: options.summaryEnabled,
1136
+ summaryPath: options.summaryPath,
1137
+ annotations: options.annotations,
1138
+ checkRun: options.checkRun,
1139
+ checkRunId: options.checkRunId,
1140
+ checkName: options.checkName,
1141
+ failOnCheckError: options.failOnCheckError,
1142
+ maxAnnotations: options.maxAnnotations,
1143
+ maxFailures: options.maxFailures,
1144
+ repository: options.repository,
1145
+ sha: options.sha,
1146
+ token: options.token,
1147
+ warn
1148
+ });
1149
+ if (options.failOnFailures && result.report.status === "failed") {
1150
+ process.exitCode = 1;
1151
+ }
939
1152
  }
940
1153
  function warn(message) {
941
1154
  if (process.env.GITHUB_ACTIONS === "true") {
@@ -946,15 +1159,16 @@ function warn(message) {
946
1159
  }
947
1160
  function usage() {
948
1161
  return [
949
- "Usage: vitest-evals-github-report [vitest-results.json] [--json <path>]",
1162
+ "Usage: vitest-evals-github-report [vitest-results.json ...] [--json <path>]",
950
1163
  "",
951
1164
  "Options:",
952
- " --json <path> Read Vitest JSON report from this path",
1165
+ " --json <path> Read a Vitest JSON report from this path or glob",
953
1166
  " --summary <path> Write job summary markdown to this path",
954
1167
  " --no-summary Disable summary output",
955
1168
  " --annotations Emit GitHub workflow-command annotations",
956
1169
  " --no-annotations Disable workflow-command annotations",
957
1170
  " --check-run Publish a GitHub Check Run when configured",
1171
+ " --fail-on-failures Exit non-zero when the combined report failed",
958
1172
  " --fail-on-check-error Fail when Check Run publishing fails",
959
1173
  " --check-run-id <id> Update an existing Check Run",
960
1174
  " --check-name <name> Check Run name (default: vitest-evals)",