@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/README.md +16 -3
- package/dist/cli.js +222 -50
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +222 -50
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +218 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +215 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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 =
|
|
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
|
|
469
|
-
|
|
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
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
523
|
-
message: `${formatNumber(nonEvalFailures)}
|
|
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
|
|
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
|
|
543
|
-
message: `
|
|
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: `
|
|
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: "
|
|
565
|
-
message: `no score
|
|
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: `
|
|
575
|
-
message: `
|
|
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: `
|
|
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
|
|
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
|
|
678
|
+
return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
|
|
628
679
|
}
|
|
629
680
|
if (minScoreAverage !== void 0) {
|
|
630
|
-
return `
|
|
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}),
|
|
690
|
+
)} passed (${passRateText}), average score ${scoreText}`;
|
|
640
691
|
}
|
|
641
|
-
function
|
|
692
|
+
function formatMinimumSuffix(minPassRate, minScoreAverage) {
|
|
642
693
|
const parts = [];
|
|
643
694
|
if (minPassRate !== void 0) {
|
|
644
|
-
parts.push(`pass rate
|
|
695
|
+
parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
|
|
645
696
|
}
|
|
646
697
|
if (minScoreAverage !== void 0) {
|
|
647
|
-
parts.push(`
|
|
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 ? "###
|
|
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 ? "
|
|
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
|
|
746
|
+
lines.push("### Failures", "", "No eval cases failed.", "");
|
|
693
747
|
}
|
|
694
748
|
if (report.totals.evalTotal === 0) {
|
|
695
|
-
lines.push("No eval
|
|
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
|
-
"
|
|
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
|
|
770
|
+
rows.push(["Pass rate", formatPercent(gate.passRate)]);
|
|
717
771
|
} else if (report.totals.evalTotal > 0) {
|
|
718
772
|
rows.push([
|
|
719
|
-
"Pass
|
|
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(["
|
|
789
|
+
rows.push(["Requirements", gate.message]);
|
|
728
790
|
}
|
|
729
791
|
if (nonEvalFailures > 0) {
|
|
730
792
|
rows.push([
|
|
731
|
-
"Other
|
|
732
|
-
`${formatNumber(nonEvalFailures)}
|
|
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 `
|
|
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
|
-
["
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
|
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"
|