@vitest-evals/github-reporter 0.16.0 → 0.17.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
@@ -40,6 +40,12 @@ function parseCliArgs(args, env = process.env) {
40
40
  case "--fail-on-check-error":
41
41
  options.failOnCheckError = true;
42
42
  break;
43
+ case "--soft-fail":
44
+ options.softFail = true;
45
+ break;
46
+ case "--no-soft-fail":
47
+ options.softFail = false;
48
+ break;
43
49
  case "--min-pass-rate":
44
50
  options.minPassRate = readRatio(args, ++index, arg);
45
51
  break;
@@ -321,7 +327,8 @@ function collectEvalReport(input, options = {}) {
321
327
  const cases = workspace.cases.map(collectEvalCase);
322
328
  const failures = cases.filter((testCase) => testCase.status === "failed");
323
329
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
324
- const usage2 = sumUsage(cases);
330
+ const usage2 = sumAppUsage(cases);
331
+ const judgeUsage = sumJudgeUsage(cases);
325
332
  const durationMs = workspace.runs[0]?.durationMs;
326
333
  return {
327
334
  status: input.success && failures.length === 0 ? "passed" : "failed",
@@ -341,6 +348,7 @@ function collectEvalReport(input, options = {}) {
341
348
  minimum: Math.min(...evalScores)
342
349
  } : void 0,
343
350
  usage: usage2,
351
+ judgeUsage,
344
352
  cases,
345
353
  failures
346
354
  };
@@ -465,24 +473,67 @@ function stringifyReason(value) {
465
473
  }
466
474
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
467
475
  }
468
- function sumUsage(cases) {
469
- const usage2 = {
476
+ function emptyUsage() {
477
+ return {
470
478
  inputTokens: 0,
471
479
  outputTokens: 0,
472
480
  reasoningTokens: 0,
473
481
  totalTokens: 0,
474
482
  toolCalls: 0
475
483
  };
484
+ }
485
+ function addRunUsage(total, usage2) {
486
+ total.inputTokens += usage2?.inputTokens ?? 0;
487
+ total.outputTokens += usage2?.outputTokens ?? 0;
488
+ total.reasoningTokens += usage2?.reasoningTokens ?? 0;
489
+ total.totalTokens += usage2?.totalTokens ?? (usage2?.inputTokens ?? 0) + (usage2?.outputTokens ?? 0) + (usage2?.reasoningTokens ?? 0);
490
+ if (usage2?.costUsd !== void 0) {
491
+ total.costUsd = (total.costUsd ?? 0) + usage2.costUsd;
492
+ }
493
+ total.toolCalls += usage2?.toolCalls ?? 0;
494
+ }
495
+ function sumAppUsage(cases) {
496
+ const usage2 = emptyUsage();
497
+ const runUsages = cases.map(appUsageForCase).filter((item) => item !== void 0);
498
+ for (const runUsage of runUsages) {
499
+ addRunUsage(usage2, runUsage);
500
+ }
501
+ omitPartialCost(usage2, runUsages);
502
+ return usage2;
503
+ }
504
+ function appUsageForCase(testCase) {
505
+ const usage2 = testCase.harness?.usage;
506
+ const effectiveToolCalls = toolCallCount(testCase);
507
+ if (!usage2 && effectiveToolCalls === 0) {
508
+ return void 0;
509
+ }
510
+ return {
511
+ ...usage2,
512
+ ...effectiveToolCalls > 0 ? { toolCalls: effectiveToolCalls } : {}
513
+ };
514
+ }
515
+ function sumJudgeUsage(cases) {
516
+ const usage2 = emptyUsage();
517
+ const runUsages = [];
476
518
  for (const testCase of cases) {
477
- const caseUsage = testCase.harness?.usage;
478
- usage2.inputTokens += caseUsage?.inputTokens ?? 0;
479
- usage2.outputTokens += caseUsage?.outputTokens ?? 0;
480
- usage2.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
481
- usage2.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
482
- usage2.toolCalls += toolCallCount(testCase);
519
+ for (const score of testCase.eval?.scores ?? []) {
520
+ for (const run of score.judgeRuns ?? []) {
521
+ runUsages.push(run.usage);
522
+ addRunUsage(usage2, run.usage);
523
+ }
524
+ }
483
525
  }
526
+ omitPartialCost(usage2, runUsages);
484
527
  return usage2;
485
528
  }
529
+ function omitPartialCost(total, usages) {
530
+ if (usages.some(hasUsageWithoutCost)) {
531
+ total.costUsd = void 0;
532
+ }
533
+ }
534
+ function hasUsageWithoutCost(usage2) {
535
+ return usage2?.costUsd === void 0 && ((usage2?.totalTokens ?? (usage2?.inputTokens ?? 0) + (usage2?.outputTokens ?? 0) + (usage2?.reasoningTokens ?? 0)) > 0 || (usage2?.toolCalls ?? 0) > 0);
536
+ }
486
537
  function toolCallCount(testCase) {
487
538
  const usageToolCalls = testCase.harness?.usage?.toolCalls;
488
539
  if (usageToolCalls !== void 0) {
@@ -519,8 +570,8 @@ function evaluateEvalGate(report, policy = {}) {
519
570
  status: "failed",
520
571
  enforced: true,
521
572
  passRate,
522
- title: "Eval report hard failure",
523
- message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
573
+ title: "Eval run failed",
574
+ message: `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases; ${counts}`
524
575
  };
525
576
  }
526
577
  if (report.totals.evalTotal === 0) {
@@ -529,7 +580,7 @@ function evaluateEvalGate(report, policy = {}) {
529
580
  status: "failed",
530
581
  enforced: true,
531
582
  passRate: null,
532
- title: "Eval report hard failure",
583
+ title: "Eval run failed",
533
584
  message: "no eval cases were reported"
534
585
  };
535
586
  }
@@ -539,8 +590,8 @@ function evaluateEvalGate(report, policy = {}) {
539
590
  status: "failed",
540
591
  enforced: true,
541
592
  passRate,
542
- title: "Eval report hard failure",
543
- message: `vitest run failed without counted test failures; ${counts}`
593
+ title: "Eval run failed",
594
+ message: `Vitest failed without reporting a failed test; ${counts}`
544
595
  };
545
596
  }
546
597
  if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
@@ -550,7 +601,7 @@ function evaluateEvalGate(report, policy = {}) {
550
601
  enforced: true,
551
602
  passRate,
552
603
  title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
553
- message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
604
+ message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
554
605
  };
555
606
  }
556
607
  if (minScoreAverage !== void 0) {
@@ -561,8 +612,8 @@ function evaluateEvalGate(report, policy = {}) {
561
612
  status: "failed",
562
613
  enforced: true,
563
614
  passRate,
564
- title: "Eval score gate failed",
565
- message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
615
+ title: "Average score unavailable",
616
+ message: `no average score was reported; minimum ${formatScore(minScoreAverage)}`
566
617
  };
567
618
  }
568
619
  if (average + Number.EPSILON < minScoreAverage) {
@@ -571,8 +622,8 @@ function evaluateEvalGate(report, policy = {}) {
571
622
  status: "failed",
572
623
  enforced: true,
573
624
  passRate,
574
- title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
575
- message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
625
+ title: `Average score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
626
+ message: `average score is below the minimum: ${counts}; minimum ${formatScore(minScoreAverage)}`
576
627
  };
577
628
  }
578
629
  }
@@ -582,7 +633,7 @@ function evaluateEvalGate(report, policy = {}) {
582
633
  enforced: true,
583
634
  passRate,
584
635
  title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
585
- message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
636
+ message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
586
637
  };
587
638
  }
588
639
  function computePassRate(report) {
@@ -615,7 +666,7 @@ function resolveMinPassRate(policy) {
615
666
  }
616
667
  function defaultCheckTitle(report) {
617
668
  if (report.failures.length === 0 && report.status === "passed") {
618
- return "No eval failures";
669
+ return "No eval cases failed";
619
670
  }
620
671
  if (report.failures.length === 0) {
621
672
  return "Vitest run failed";
@@ -624,10 +675,10 @@ function defaultCheckTitle(report) {
624
675
  }
625
676
  function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
626
677
  if (minPassRate !== void 0) {
627
- return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
678
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
628
679
  }
629
680
  if (minScoreAverage !== void 0) {
630
- return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
681
+ return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
631
682
  }
632
683
  return defaultCheckTitle(report);
633
684
  }
@@ -636,19 +687,22 @@ function formatEvalCounts(report, passRate) {
636
687
  const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
637
688
  return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
638
689
  report.totals.evalTotal
639
- )} passed (${passRateText}), avg score ${scoreText}`;
690
+ )} passed (${passRateText}), average score ${scoreText}`;
640
691
  }
641
- function formatFloorSuffix(minPassRate, minScoreAverage) {
692
+ function formatMinimumSuffix(minPassRate, minScoreAverage) {
642
693
  const parts = [];
643
694
  if (minPassRate !== void 0) {
644
- parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
695
+ parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
645
696
  }
646
697
  if (minScoreAverage !== void 0) {
647
- parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
698
+ parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
648
699
  }
649
700
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
650
701
  }
651
702
 
703
+ // src/github.ts
704
+ import { readFileSync } from "fs";
705
+
652
706
  // src/summary.ts
653
707
  var DEFAULT_MAX_FAILURES = 20;
654
708
  var DEFAULT_MAX_REASON_CHARS = 8e3;
@@ -676,23 +730,23 @@ function renderJobSummary(report, options = {}) {
676
730
  ""
677
731
  ];
678
732
  if (report.failures.length > 0) {
679
- const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
733
+ const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
680
734
  lines.push(failureHeading, "");
681
735
  failures.forEach((testCase, index) => {
682
736
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
683
737
  });
684
738
  if (report.failures.length > failures.length) {
685
- const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
739
+ const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
686
740
  lines.push(
687
741
  `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
688
742
  ""
689
743
  );
690
744
  }
691
745
  } else if (report.totals.evalTotal > 0) {
692
- lines.push("### Failures", "", "No eval failures.", "");
746
+ lines.push("### Failures", "", "No eval cases failed.", "");
693
747
  }
694
748
  if (report.totals.evalTotal === 0) {
695
- lines.push("No eval metadata was found in the Vitest JSON report.", "");
749
+ lines.push("No eval results were found in the Vitest JSON report.", "");
696
750
  }
697
751
  return `${lines.join("\n")}
698
752
  `;
@@ -702,9 +756,9 @@ function formatCountLine(passed, failed, total) {
702
756
  }
703
757
  function renderSummaryTable(report, nonEvalFailures, gate) {
704
758
  const rows = [
705
- ["Status", gate?.status ?? report.status],
759
+ ["Status", capitalize(gate?.status ?? report.status)],
706
760
  [
707
- "Evals",
761
+ "Eval cases",
708
762
  formatCountLine(
709
763
  report.totals.evalPassed,
710
764
  report.totals.evalFailed,
@@ -713,23 +767,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
713
767
  ]
714
768
  ];
715
769
  if (gate?.passRate !== void 0 && gate.passRate !== null) {
716
- rows.push(["Pass Rate", formatPercent(gate.passRate)]);
770
+ rows.push(["Pass rate", formatPercent(gate.passRate)]);
717
771
  } else if (report.totals.evalTotal > 0) {
718
772
  rows.push([
719
- "Pass Rate",
773
+ "Pass rate",
720
774
  formatPercent(report.totals.evalPassed / report.totals.evalTotal)
721
775
  ]);
722
776
  }
723
777
  if (report.score) {
724
778
  rows.push(["Score", formatScoreSummary(report.score)]);
725
779
  }
780
+ if (hasUsage(report.usage) || hasUsage(report.judgeUsage)) {
781
+ rows.push(["App usage", formatUsage(report.usage)]);
782
+ rows.push(["Judge usage", formatUsage(report.judgeUsage)]);
783
+ rows.push([
784
+ "Total usage",
785
+ formatUsage(sumUsage(report.usage, report.judgeUsage))
786
+ ]);
787
+ }
726
788
  if (gate?.enforced) {
727
- rows.push(["Gate", gate.message]);
789
+ rows.push(["Requirements", gate.message]);
728
790
  }
729
791
  if (nonEvalFailures > 0) {
730
792
  rows.push([
731
- "Other Failures",
732
- `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}`
793
+ "Other test failures",
794
+ `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
733
795
  ]);
734
796
  }
735
797
  rows.push(["Duration", formatDuration(report.durationMs)]);
@@ -742,7 +804,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
742
804
  ];
743
805
  }
744
806
  function formatScoreSummary(score) {
745
- return `avg ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, min ${formatScore(score.minimum)}`}`;
807
+ return `average ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, lowest ${formatScore(score.minimum)}`}`;
808
+ }
809
+ function capitalize(value) {
810
+ return value.charAt(0).toUpperCase() + value.slice(1);
811
+ }
812
+ function hasUsage(usage2) {
813
+ return usage2.totalTokens > 0 || usage2.costUsd !== void 0 || usage2.toolCalls > 0;
814
+ }
815
+ function sumUsage(app, judge) {
816
+ const appCostKnown = !hasUsageWithoutCost2(app);
817
+ const judgeCostKnown = !hasUsageWithoutCost2(judge);
818
+ return {
819
+ inputTokens: app.inputTokens + judge.inputTokens,
820
+ outputTokens: app.outputTokens + judge.outputTokens,
821
+ reasoningTokens: app.reasoningTokens + judge.reasoningTokens,
822
+ totalTokens: app.totalTokens + judge.totalTokens,
823
+ ...appCostKnown && judgeCostKnown ? { costUsd: (app.costUsd ?? 0) + (judge.costUsd ?? 0) } : {},
824
+ toolCalls: app.toolCalls + judge.toolCalls
825
+ };
826
+ }
827
+ function hasUsageWithoutCost2(usage2) {
828
+ return usage2.costUsd === void 0 && (usage2.totalTokens > 0 || usage2.toolCalls > 0);
829
+ }
830
+ function formatUsage(usage2) {
831
+ const parts = [];
832
+ if (usage2.totalTokens > 0) {
833
+ parts.push(`${formatNumber(usage2.totalTokens)} tokens`);
834
+ }
835
+ if (usage2.costUsd !== void 0) {
836
+ parts.push(
837
+ usage2.costUsd.toLocaleString("en-US", {
838
+ style: "currency",
839
+ currency: "USD",
840
+ minimumFractionDigits: 2,
841
+ maximumFractionDigits: 6
842
+ })
843
+ );
844
+ }
845
+ if (hasUsageWithoutCost2(usage2)) {
846
+ parts.push("cost unavailable");
847
+ }
848
+ if (usage2.toolCalls > 0) {
849
+ parts.push(
850
+ `${formatNumber(usage2.toolCalls)} tool call${usage2.toolCalls === 1 ? "" : "s"}`
851
+ );
852
+ }
853
+ return parts.join(", ") || "none";
746
854
  }
747
855
  function escapeTableCell(value) {
748
856
  return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -822,7 +930,7 @@ function renderFailureBlock(testCase, {
822
930
  ["Case", `${number}. ${testCase.displayName}`],
823
931
  ["Status", testCase.status],
824
932
  ["Location", formatLocation(testCase.displayFile, testCase.location)],
825
- ["Harness", testCase.harness?.name ?? "n/a"],
933
+ ["App runner", testCase.harness?.name ?? "n/a"],
826
934
  ["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
827
935
  ["Judge", failure?.judgeName ?? "n/a"]
828
936
  ];
@@ -860,7 +968,7 @@ function renderFailureBlock(testCase, {
860
968
  if (finalOutput !== void 0) {
861
969
  lines.push(
862
970
  ...renderAsciiSection(
863
- "Final Output",
971
+ "Output",
864
972
  stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
865
973
  ),
866
974
  ""
@@ -888,7 +996,7 @@ function renderFailureBlock(testCase, {
888
996
  if (testCase.harness?.errors.length) {
889
997
  lines.push(
890
998
  ...renderAsciiSection(
891
- "Harness Errors",
999
+ "App errors",
892
1000
  stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
893
1001
  ),
894
1002
  ""
@@ -937,7 +1045,9 @@ function formatCaseUsage(testCase) {
937
1045
  parts.push(`${formatNumber(totalTokens)} tokens`);
938
1046
  }
939
1047
  if (toolCalls > 0) {
940
- parts.push(`${formatNumber(toolCalls)} tool${toolCalls === 1 ? "" : "s"}`);
1048
+ parts.push(
1049
+ `${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
1050
+ );
941
1051
  }
942
1052
  if (testCase.harness?.timingMs !== void 0) {
943
1053
  parts.push(formatDuration(testCase.harness.timingMs));
@@ -949,10 +1059,44 @@ function formatCaseUsage(testCase) {
949
1059
  var DEFAULT_CHECK_NAME = "vitest-evals";
950
1060
  var MAX_CHECK_SUMMARY_LENGTH = 64e3;
951
1061
  var CHECK_SUMMARY_TRUNCATION_SUFFIX = "\n\n[truncated for GitHub Check Run]\n";
1062
+ function resolveCheckSha(env = process.env, options = {}) {
1063
+ const explicit = options.sha?.trim() || env.GITHUB_PR_HEAD_SHA?.trim();
1064
+ if (explicit) {
1065
+ return explicit;
1066
+ }
1067
+ const eventPath = options.eventPath?.trim() || env.GITHUB_EVENT_PATH?.trim();
1068
+ if (eventPath) {
1069
+ try {
1070
+ const event = JSON.parse(readFileSync(eventPath, "utf8"));
1071
+ const headSha = event.pull_request?.head?.sha;
1072
+ if (typeof headSha === "string" && headSha.trim()) {
1073
+ return headSha.trim();
1074
+ }
1075
+ } catch {
1076
+ }
1077
+ }
1078
+ return env.GITHUB_SHA?.trim() || void 0;
1079
+ }
1080
+ function resolveCheckDetailsUrl(env = process.env, options = {}) {
1081
+ const explicit = options.detailsUrl?.trim();
1082
+ if (explicit) {
1083
+ return explicit;
1084
+ }
1085
+ const server = env.GITHUB_SERVER_URL?.replace(/\/$/, "");
1086
+ const repository = env.GITHUB_REPOSITORY?.trim();
1087
+ const runId = env.GITHUB_RUN_ID?.trim();
1088
+ if (!server || !repository || !runId) {
1089
+ return void 0;
1090
+ }
1091
+ return `${server}/${repository}/actions/runs/${runId}`;
1092
+ }
952
1093
  async function publishCheckRun(report, options = {}) {
953
1094
  const token = options.token ?? process.env.GITHUB_TOKEN;
954
1095
  const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
955
- const sha = options.sha ?? process.env.GITHUB_SHA;
1096
+ const sha = resolveCheckSha(process.env, { sha: options.sha });
1097
+ const detailsUrl = resolveCheckDetailsUrl(process.env, {
1098
+ detailsUrl: options.detailsUrl
1099
+ });
956
1100
  if (!token) {
957
1101
  return { status: "skipped", reason: "missing GITHUB_TOKEN" };
958
1102
  }
@@ -960,7 +1104,10 @@ async function publishCheckRun(report, options = {}) {
960
1104
  return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
961
1105
  }
962
1106
  if (!sha && options.checkRunId === void 0) {
963
- return { status: "skipped", reason: "missing GITHUB_SHA" };
1107
+ return {
1108
+ status: "skipped",
1109
+ reason: "missing commit SHA (set --sha / options.sha, GITHUB_PR_HEAD_SHA, pull_request.head.sha, or GITHUB_SHA)"
1110
+ };
964
1111
  }
965
1112
  const [owner, repo] = repository.split("/");
966
1113
  if (!owner || !repo) {
@@ -969,7 +1116,7 @@ async function publishCheckRun(report, options = {}) {
969
1116
  reason: `invalid GitHub repository: ${repository}`
970
1117
  };
971
1118
  }
972
- const payload = buildCheckRunPayload(report, options);
1119
+ const payload = buildCheckRunPayload(report, options, detailsUrl);
973
1120
  const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
974
1121
  const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
975
1122
  const response = await fetch(requestUrl, {
@@ -984,6 +1131,7 @@ async function publishCheckRun(report, options = {}) {
984
1131
  options.checkRunId === void 0 ? {
985
1132
  name: options.name ?? DEFAULT_CHECK_NAME,
986
1133
  head_sha: sha,
1134
+ ...options.externalId ? { external_id: options.externalId } : {},
987
1135
  ...payload
988
1136
  } : payload
989
1137
  )
@@ -998,10 +1146,11 @@ async function publishCheckRun(report, options = {}) {
998
1146
  return {
999
1147
  status: options.checkRunId === void 0 ? "created" : "updated",
1000
1148
  id: data.id,
1001
- htmlUrl: data.html_url
1149
+ htmlUrl: data.html_url,
1150
+ sha
1002
1151
  };
1003
1152
  }
1004
- function buildCheckRunPayload(report, options) {
1153
+ function buildCheckRunPayload(report, options, detailsUrl) {
1005
1154
  const gate = options.gate ?? evaluateEvalGate(report);
1006
1155
  const annotations = buildCheckAnnotations(report, {
1007
1156
  maxAnnotations: options.maxAnnotations,
@@ -1011,6 +1160,7 @@ function buildCheckRunPayload(report, options) {
1011
1160
  status: "completed",
1012
1161
  conclusion: gate.ok ? "success" : "failure",
1013
1162
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
1163
+ ...detailsUrl ? { details_url: detailsUrl } : {},
1014
1164
  output: {
1015
1165
  title: gate.title,
1016
1166
  summary: truncateCheckSummary(
@@ -1063,16 +1213,22 @@ function mergeEvalReports(reports) {
1063
1213
  minimum: Math.min(...scoredCases)
1064
1214
  } : void 0,
1065
1215
  usage: mergeUsage(reports.map((report) => report.usage)),
1216
+ judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
1066
1217
  cases,
1067
1218
  failures
1068
1219
  };
1069
1220
  }
1070
1221
  function mergeUsage(usages) {
1222
+ const costs = usages.map((usage2) => usage2.costUsd).filter((cost) => cost !== void 0);
1223
+ const costComplete = !usages.some(
1224
+ (usage2) => usage2.costUsd === void 0 && (usage2.totalTokens > 0 || usage2.toolCalls > 0)
1225
+ );
1071
1226
  return {
1072
1227
  inputTokens: sum(usages, (usage2) => usage2.inputTokens),
1073
1228
  outputTokens: sum(usages, (usage2) => usage2.outputTokens),
1074
1229
  reasoningTokens: sum(usages, (usage2) => usage2.reasoningTokens),
1075
1230
  totalTokens: sum(usages, (usage2) => usage2.totalTokens),
1231
+ ...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
1076
1232
  toolCalls: sum(usages, (usage2) => usage2.toolCalls)
1077
1233
  };
1078
1234
  }
@@ -1164,11 +1320,17 @@ async function publishEvalReport(options) {
1164
1320
  name: options.checkName,
1165
1321
  repository: options.repository,
1166
1322
  sha: options.sha,
1323
+ detailsUrl: options.detailsUrl,
1324
+ externalId: options.externalId,
1167
1325
  token: options.token,
1168
1326
  gate
1169
1327
  });
1170
1328
  if (checkRun.status === "skipped") {
1171
1329
  options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
1330
+ } else if (checkRun.htmlUrl) {
1331
+ console.log(`published check run: ${checkRun.htmlUrl}`);
1332
+ } else if (checkRun.id !== void 0) {
1333
+ console.log(`published check run id: ${checkRun.id}`);
1172
1334
  }
1173
1335
  } catch (error) {
1174
1336
  const message = error instanceof Error ? error.message : String(error);
@@ -1178,14 +1340,21 @@ async function publishEvalReport(options) {
1178
1340
  options.warn?.(message);
1179
1341
  }
1180
1342
  }
1343
+ const gateFailed = gate.enforced && !gate.ok;
1344
+ const wantsSoftFail = options.softFail ?? options.checkRun === true;
1345
+ const softFail = wantsSoftFail && checkRunPublished(checkRun);
1346
+ const shouldFail = gateFailed && !softFail;
1181
1347
  return {
1182
1348
  report,
1183
1349
  resultFiles,
1184
1350
  gate,
1185
- shouldFail: gate.enforced && !gate.ok,
1351
+ shouldFail,
1186
1352
  checkRun
1187
1353
  };
1188
1354
  }
1355
+ function checkRunPublished(checkRun) {
1356
+ return checkRun?.status === "created" || checkRun?.status === "updated";
1357
+ }
1189
1358
 
1190
1359
  // src/cli.ts
1191
1360
  main().catch((error) => {
@@ -1210,6 +1379,7 @@ async function main() {
1210
1379
  checkName: options.checkName,
1211
1380
  failOnCheckError: options.failOnCheckError,
1212
1381
  failOnFailures: options.failOnFailures,
1382
+ softFail: options.softFail,
1213
1383
  minPassRate: options.minPassRate,
1214
1384
  minScoreAverage: options.minScoreAverage,
1215
1385
  maxAnnotations: options.maxAnnotations,
@@ -1245,12 +1415,14 @@ function usage() {
1245
1415
  " --fail-on-failures Exit non-zero when any eval case failed",
1246
1416
  " --min-pass-rate <0-1> Exit non-zero when eval pass rate is below this floor",
1247
1417
  " --min-score-average <0-1> Exit non-zero when average score is below this floor",
1418
+ " --soft-fail Keep exit 0 when a published Check Run owns gate status",
1419
+ " --no-soft-fail Always exit non-zero when an enforced gate fails",
1248
1420
  " --fail-on-check-error Fail when Check Run publishing fails",
1249
1421
  " --check-run-id <id> Update an existing Check Run",
1250
1422
  " --check-name <name> Check Run name (default: vitest-evals)",
1251
1423
  " --token <token> GitHub token (default: GITHUB_TOKEN)",
1252
1424
  " --repo <owner/repo> GitHub repository (default: GITHUB_REPOSITORY)",
1253
- " --sha <sha> Git commit SHA (default: GITHUB_SHA)",
1425
+ " --sha <sha> Git commit SHA (default: PR head, then GITHUB_SHA)",
1254
1426
  " --workspace <path> Workspace path for relative annotation files",
1255
1427
  " --max-annotations <n> Maximum annotations to emit",
1256
1428
  " --max-failures <n> Maximum failures to include in details"