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