@vitest-evals/github-reporter 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -3
- package/dist/cli.js +303 -23
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +303 -23
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +125 -1
- package/dist/index.d.ts +125 -1
- package/dist/index.js +427 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +422 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,10 +31,39 @@ steps:
|
|
|
31
31
|
with:
|
|
32
32
|
results: vitest-results.json
|
|
33
33
|
publish-check: true
|
|
34
|
+
min-pass-rate: 0.8
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
On `pull_request`, the Check Run attaches to the PR head SHA (not the temporary
|
|
38
|
+
merge `GITHUB_SHA`) so it shows on the PR checks list. Override with the `sha`
|
|
39
|
+
input when needed.
|
|
40
|
+
|
|
41
|
+
When a Check Run publishes successfully with a gate, the action step soft-fails
|
|
42
|
+
by default so the Check Run owns green/red. If publishing is skipped, the step
|
|
43
|
+
still fails on a rejected gate. Set `soft-fail: false` to also fail the job.
|
|
44
|
+
|
|
36
45
|
If configuration or permission is missing, the action keeps the job summary and
|
|
37
|
-
workflow annotations and warns instead of failing.
|
|
46
|
+
workflow annotations and warns instead of failing solely for the missing check.
|
|
47
|
+
|
|
48
|
+
## Score and Pass-Rate Gates
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
- id: report
|
|
52
|
+
uses: getsentry/vitest-evals@v0
|
|
53
|
+
with:
|
|
54
|
+
results: eval-results/*.json
|
|
55
|
+
publish-check: true
|
|
56
|
+
min-pass-rate: 0.8
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `fail-on-failures: true` requires every eval case to pass
|
|
60
|
+
- `min-pass-rate` and `min-score-average` set aggregate floors in the `0`-`1` range
|
|
61
|
+
- `status` and Check Run conclusion/title follow the gate
|
|
62
|
+
- quality misses become warnings when the gate still passes
|
|
63
|
+
- non-eval / infrastructure failures still fail hard
|
|
64
|
+
- published Check Runs soft-fail the step by default; set `soft-fail: false` to
|
|
65
|
+
also fail the workflow job
|
|
66
|
+
- use `evals-failed` / `pass-rate` for raw tallies (`pass-rate` is a 0-1 ratio)
|
|
38
67
|
|
|
39
68
|
## Sharded Reports
|
|
40
69
|
|
|
@@ -52,6 +81,7 @@ final reducer job:
|
|
|
52
81
|
with:
|
|
53
82
|
results: eval-results/*.json
|
|
54
83
|
publish-check: true
|
|
84
|
+
min-pass-rate: 0.8
|
|
55
85
|
```
|
|
56
86
|
|
|
57
87
|
## Inputs
|
|
@@ -61,10 +91,14 @@ final reducer job:
|
|
|
61
91
|
| `results` | `vitest-results.json` | Vitest JSON result files. Supports paths, `*` and `**` globs, and newline-separated entries. |
|
|
62
92
|
| `publish-summary` | `true` | Write a GitHub Actions job summary. |
|
|
63
93
|
| `publish-annotations` | `true` | Emit GitHub workflow annotations for failed evals. |
|
|
64
|
-
| `publish-check` | `false` | Publish one GitHub Check Run for the combined report. |
|
|
94
|
+
| `publish-check` | `false` | Publish one GitHub Check Run for the combined report. Attaches to PR head on `pull_request`. |
|
|
65
95
|
| `check-name` | `vitest-evals` | Name of the GitHub Check Run. |
|
|
66
96
|
| `github-token` | `${{ github.token }}` | Token used for Check Run publishing. |
|
|
67
|
-
| `
|
|
97
|
+
| `sha` | PR head, else `GITHUB_SHA` | Commit SHA for the Check Run. |
|
|
98
|
+
| `fail-on-failures` | `false` | Fail the action when any eval case failed. Equivalent to `min-pass-rate: 1`. |
|
|
99
|
+
| `soft-fail` | auto | Keep the step green when a published Check Run owns a failed gate. |
|
|
100
|
+
| `min-pass-rate` | unset | Minimum fraction of eval cases that must pass (`0`-`1`). |
|
|
101
|
+
| `min-score-average` | unset | Minimum average eval score across scored cases (`0`-`1`). |
|
|
68
102
|
| `max-annotations` | unset | Maximum number of failure annotations to publish. Check Run annotations are capped at 50 by GitHub. |
|
|
69
103
|
| `max-failures` | unset | Maximum number of detailed failures to include in summaries and checks. |
|
|
70
104
|
|
package/dist/cli.js
CHANGED
|
@@ -41,6 +41,18 @@ function parseCliArgs(args, env = process.env) {
|
|
|
41
41
|
case "--fail-on-check-error":
|
|
42
42
|
options.failOnCheckError = true;
|
|
43
43
|
break;
|
|
44
|
+
case "--soft-fail":
|
|
45
|
+
options.softFail = true;
|
|
46
|
+
break;
|
|
47
|
+
case "--no-soft-fail":
|
|
48
|
+
options.softFail = false;
|
|
49
|
+
break;
|
|
50
|
+
case "--min-pass-rate":
|
|
51
|
+
options.minPassRate = readRatio(args, ++index, arg);
|
|
52
|
+
break;
|
|
53
|
+
case "--min-score-average":
|
|
54
|
+
options.minScoreAverage = readRatio(args, ++index, arg);
|
|
55
|
+
break;
|
|
44
56
|
case "--max-annotations":
|
|
45
57
|
options.maxAnnotations = readInteger(args, ++index, arg);
|
|
46
58
|
break;
|
|
@@ -100,6 +112,14 @@ function readInteger(args, index, flag) {
|
|
|
100
112
|
}
|
|
101
113
|
return Number(rawValue);
|
|
102
114
|
}
|
|
115
|
+
function readRatio(args, index, flag) {
|
|
116
|
+
const rawValue = readValue(args, index, flag);
|
|
117
|
+
const parsed = Number(rawValue);
|
|
118
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
119
|
+
throw new Error(`Invalid ratio for ${flag}`);
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
103
123
|
|
|
104
124
|
// src/report.ts
|
|
105
125
|
var import_promises = require("fs/promises");
|
|
@@ -188,9 +208,10 @@ var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
|
|
|
188
208
|
var MAX_CHECK_FIELD_LENGTH = 64e3;
|
|
189
209
|
function renderWorkflowCommands(report, options = {}) {
|
|
190
210
|
const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
|
|
211
|
+
const command = caseAnnotationCommand(options.gate);
|
|
191
212
|
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
|
|
192
213
|
(testCase) => formatWorkflowCommand({
|
|
193
|
-
command
|
|
214
|
+
command,
|
|
194
215
|
properties: {
|
|
195
216
|
file: testCase.displayFile,
|
|
196
217
|
line: String(testCase.location.line),
|
|
@@ -206,11 +227,12 @@ function buildCheckAnnotations(report, options = {}) {
|
|
|
206
227
|
options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS,
|
|
207
228
|
DEFAULT_MAX_CHECK_ANNOTATIONS
|
|
208
229
|
);
|
|
230
|
+
const annotationLevel = caseAnnotationLevel(options.gate);
|
|
209
231
|
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
|
|
210
232
|
path: testCase.displayFile,
|
|
211
233
|
start_line: testCase.location.line,
|
|
212
234
|
end_line: testCase.location.line,
|
|
213
|
-
annotation_level:
|
|
235
|
+
annotation_level: annotationLevel,
|
|
214
236
|
title: truncate(
|
|
215
237
|
`${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
|
|
216
238
|
255
|
|
@@ -222,6 +244,12 @@ function buildCheckAnnotations(report, options = {}) {
|
|
|
222
244
|
raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
|
|
223
245
|
}));
|
|
224
246
|
}
|
|
247
|
+
function caseAnnotationCommand(gate) {
|
|
248
|
+
return gate?.ok ? "warning" : "error";
|
|
249
|
+
}
|
|
250
|
+
function caseAnnotationLevel(gate) {
|
|
251
|
+
return gate?.ok ? "warning" : "failure";
|
|
252
|
+
}
|
|
225
253
|
function hasAnnotationLocation(testCase) {
|
|
226
254
|
return Boolean(testCase.location);
|
|
227
255
|
}
|
|
@@ -465,6 +493,167 @@ function toolCallCount(testCase) {
|
|
|
465
493
|
return testCase.toolCalls.length;
|
|
466
494
|
}
|
|
467
495
|
|
|
496
|
+
// src/gate.ts
|
|
497
|
+
function evaluateEvalGate(report, policy = {}) {
|
|
498
|
+
const minPassRate = resolveMinPassRate(policy);
|
|
499
|
+
const minScoreAverage = policy.minScoreAverage;
|
|
500
|
+
const enforced = minPassRate !== void 0 || minScoreAverage !== void 0 || policy.failOnFailures === true;
|
|
501
|
+
const passRate = computePassRate(report);
|
|
502
|
+
const counts = formatEvalCounts(report, passRate);
|
|
503
|
+
if (!enforced) {
|
|
504
|
+
const ok = report.status === "passed";
|
|
505
|
+
return {
|
|
506
|
+
ok,
|
|
507
|
+
status: ok ? "passed" : "failed",
|
|
508
|
+
enforced: false,
|
|
509
|
+
passRate,
|
|
510
|
+
title: defaultCheckTitle(report),
|
|
511
|
+
message: ok ? `eval report passed: ${counts}` : `eval report failed: ${counts}`
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const nonEvalFailures = Math.max(
|
|
515
|
+
0,
|
|
516
|
+
report.totals.failed - report.totals.evalFailed
|
|
517
|
+
);
|
|
518
|
+
if (nonEvalFailures > 0) {
|
|
519
|
+
return {
|
|
520
|
+
ok: false,
|
|
521
|
+
status: "failed",
|
|
522
|
+
enforced: true,
|
|
523
|
+
passRate,
|
|
524
|
+
title: "Eval report hard failure",
|
|
525
|
+
message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
if (report.totals.evalTotal === 0) {
|
|
529
|
+
return {
|
|
530
|
+
ok: false,
|
|
531
|
+
status: "failed",
|
|
532
|
+
enforced: true,
|
|
533
|
+
passRate: null,
|
|
534
|
+
title: "Eval report hard failure",
|
|
535
|
+
message: "no eval cases were reported"
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (report.status === "failed" && report.totals.failed === 0 && report.failures.length === 0) {
|
|
539
|
+
return {
|
|
540
|
+
ok: false,
|
|
541
|
+
status: "failed",
|
|
542
|
+
enforced: true,
|
|
543
|
+
passRate,
|
|
544
|
+
title: "Eval report hard failure",
|
|
545
|
+
message: `vitest run failed without counted test failures; ${counts}`
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
status: "failed",
|
|
552
|
+
enforced: true,
|
|
553
|
+
passRate,
|
|
554
|
+
title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
|
|
555
|
+
message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (minScoreAverage !== void 0) {
|
|
559
|
+
const average = report.score?.average;
|
|
560
|
+
if (average === void 0 || !Number.isFinite(average)) {
|
|
561
|
+
return {
|
|
562
|
+
ok: false,
|
|
563
|
+
status: "failed",
|
|
564
|
+
enforced: true,
|
|
565
|
+
passRate,
|
|
566
|
+
title: "Eval score gate failed",
|
|
567
|
+
message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (average + Number.EPSILON < minScoreAverage) {
|
|
571
|
+
return {
|
|
572
|
+
ok: false,
|
|
573
|
+
status: "failed",
|
|
574
|
+
enforced: true,
|
|
575
|
+
passRate,
|
|
576
|
+
title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
|
|
577
|
+
message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
status: "passed",
|
|
584
|
+
enforced: true,
|
|
585
|
+
passRate,
|
|
586
|
+
title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
|
|
587
|
+
message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function computePassRate(report) {
|
|
591
|
+
if (report.totals.evalTotal <= 0) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
return report.totals.evalPassed / report.totals.evalTotal;
|
|
595
|
+
}
|
|
596
|
+
function formatPercent(value) {
|
|
597
|
+
if (value == null || !Number.isFinite(value)) {
|
|
598
|
+
return "n/a";
|
|
599
|
+
}
|
|
600
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
601
|
+
}
|
|
602
|
+
function renderGateWorkflowCommand(gate) {
|
|
603
|
+
if (gate.ok || !gate.enforced) {
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
606
|
+
return `::error title=${escapeCommandProperty(gate.title)}::${escapeCommandData(gate.message)}`;
|
|
607
|
+
}
|
|
608
|
+
function resolveMinPassRate(policy) {
|
|
609
|
+
const configured = policy.minPassRate;
|
|
610
|
+
if (policy.failOnFailures) {
|
|
611
|
+
if (configured === void 0) {
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
return Math.max(configured, 1);
|
|
615
|
+
}
|
|
616
|
+
return configured;
|
|
617
|
+
}
|
|
618
|
+
function defaultCheckTitle(report) {
|
|
619
|
+
if (report.failures.length === 0 && report.status === "passed") {
|
|
620
|
+
return "No eval failures";
|
|
621
|
+
}
|
|
622
|
+
if (report.failures.length === 0) {
|
|
623
|
+
return "Vitest run failed";
|
|
624
|
+
}
|
|
625
|
+
return `${report.failures.length} eval failure${report.failures.length === 1 ? "" : "s"}`;
|
|
626
|
+
}
|
|
627
|
+
function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
|
|
628
|
+
if (minPassRate !== void 0) {
|
|
629
|
+
return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
|
|
630
|
+
}
|
|
631
|
+
if (minScoreAverage !== void 0) {
|
|
632
|
+
return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
|
|
633
|
+
}
|
|
634
|
+
return defaultCheckTitle(report);
|
|
635
|
+
}
|
|
636
|
+
function formatEvalCounts(report, passRate) {
|
|
637
|
+
const scoreText = report.score?.average === void 0 ? "n/a" : formatScore(report.score.average);
|
|
638
|
+
const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
|
|
639
|
+
return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
|
|
640
|
+
report.totals.evalTotal
|
|
641
|
+
)} passed (${passRateText}), avg score ${scoreText}`;
|
|
642
|
+
}
|
|
643
|
+
function formatFloorSuffix(minPassRate, minScoreAverage) {
|
|
644
|
+
const parts = [];
|
|
645
|
+
if (minPassRate !== void 0) {
|
|
646
|
+
parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
|
|
647
|
+
}
|
|
648
|
+
if (minScoreAverage !== void 0) {
|
|
649
|
+
parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
|
|
650
|
+
}
|
|
651
|
+
return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/github.ts
|
|
655
|
+
var import_node_fs = require("fs");
|
|
656
|
+
|
|
468
657
|
// src/summary.ts
|
|
469
658
|
var DEFAULT_MAX_FAILURES = 20;
|
|
470
659
|
var DEFAULT_MAX_REASON_CHARS = 8e3;
|
|
@@ -485,20 +674,22 @@ function renderJobSummary(report, options = {}) {
|
|
|
485
674
|
const lines = [
|
|
486
675
|
"# vitest-evals",
|
|
487
676
|
"",
|
|
488
|
-
...renderSummaryTable(report, nonEvalFailures),
|
|
677
|
+
...renderSummaryTable(report, nonEvalFailures, options.gate),
|
|
489
678
|
"",
|
|
490
679
|
...renderScoreDistribution(report),
|
|
491
680
|
"## Results",
|
|
492
681
|
""
|
|
493
682
|
];
|
|
494
683
|
if (report.failures.length > 0) {
|
|
495
|
-
|
|
684
|
+
const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
|
|
685
|
+
lines.push(failureHeading, "");
|
|
496
686
|
failures.forEach((testCase, index) => {
|
|
497
687
|
lines.push(...renderFailureDetails(testCase, index + 1, options), "");
|
|
498
688
|
});
|
|
499
689
|
if (report.failures.length > failures.length) {
|
|
690
|
+
const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
|
|
500
691
|
lines.push(
|
|
501
|
-
`${report.failures.length - failures.length} more
|
|
692
|
+
`${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
|
|
502
693
|
""
|
|
503
694
|
);
|
|
504
695
|
}
|
|
@@ -514,9 +705,9 @@ function renderJobSummary(report, options = {}) {
|
|
|
514
705
|
function formatCountLine(passed, failed, total) {
|
|
515
706
|
return `${formatNumber(passed)} passed, ${formatNumber(failed)} failed, ${formatNumber(total)} total`;
|
|
516
707
|
}
|
|
517
|
-
function renderSummaryTable(report, nonEvalFailures) {
|
|
708
|
+
function renderSummaryTable(report, nonEvalFailures, gate) {
|
|
518
709
|
const rows = [
|
|
519
|
-
["Status", report.status],
|
|
710
|
+
["Status", gate?.status ?? report.status],
|
|
520
711
|
[
|
|
521
712
|
"Evals",
|
|
522
713
|
formatCountLine(
|
|
@@ -526,9 +717,20 @@ function renderSummaryTable(report, nonEvalFailures) {
|
|
|
526
717
|
)
|
|
527
718
|
]
|
|
528
719
|
];
|
|
720
|
+
if (gate?.passRate !== void 0 && gate.passRate !== null) {
|
|
721
|
+
rows.push(["Pass Rate", formatPercent(gate.passRate)]);
|
|
722
|
+
} else if (report.totals.evalTotal > 0) {
|
|
723
|
+
rows.push([
|
|
724
|
+
"Pass Rate",
|
|
725
|
+
formatPercent(report.totals.evalPassed / report.totals.evalTotal)
|
|
726
|
+
]);
|
|
727
|
+
}
|
|
529
728
|
if (report.score) {
|
|
530
729
|
rows.push(["Score", formatScoreSummary(report.score)]);
|
|
531
730
|
}
|
|
731
|
+
if (gate?.enforced) {
|
|
732
|
+
rows.push(["Gate", gate.message]);
|
|
733
|
+
}
|
|
532
734
|
if (nonEvalFailures > 0) {
|
|
533
735
|
rows.push([
|
|
534
736
|
"Other Failures",
|
|
@@ -752,10 +954,44 @@ function formatCaseUsage(testCase) {
|
|
|
752
954
|
var DEFAULT_CHECK_NAME = "vitest-evals";
|
|
753
955
|
var MAX_CHECK_SUMMARY_LENGTH = 64e3;
|
|
754
956
|
var CHECK_SUMMARY_TRUNCATION_SUFFIX = "\n\n[truncated for GitHub Check Run]\n";
|
|
957
|
+
function resolveCheckSha(env = process.env, options = {}) {
|
|
958
|
+
const explicit = options.sha?.trim() || env.GITHUB_PR_HEAD_SHA?.trim();
|
|
959
|
+
if (explicit) {
|
|
960
|
+
return explicit;
|
|
961
|
+
}
|
|
962
|
+
const eventPath = options.eventPath?.trim() || env.GITHUB_EVENT_PATH?.trim();
|
|
963
|
+
if (eventPath) {
|
|
964
|
+
try {
|
|
965
|
+
const event = JSON.parse((0, import_node_fs.readFileSync)(eventPath, "utf8"));
|
|
966
|
+
const headSha = event.pull_request?.head?.sha;
|
|
967
|
+
if (typeof headSha === "string" && headSha.trim()) {
|
|
968
|
+
return headSha.trim();
|
|
969
|
+
}
|
|
970
|
+
} catch {
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return env.GITHUB_SHA?.trim() || void 0;
|
|
974
|
+
}
|
|
975
|
+
function resolveCheckDetailsUrl(env = process.env, options = {}) {
|
|
976
|
+
const explicit = options.detailsUrl?.trim();
|
|
977
|
+
if (explicit) {
|
|
978
|
+
return explicit;
|
|
979
|
+
}
|
|
980
|
+
const server = env.GITHUB_SERVER_URL?.replace(/\/$/, "");
|
|
981
|
+
const repository = env.GITHUB_REPOSITORY?.trim();
|
|
982
|
+
const runId = env.GITHUB_RUN_ID?.trim();
|
|
983
|
+
if (!server || !repository || !runId) {
|
|
984
|
+
return void 0;
|
|
985
|
+
}
|
|
986
|
+
return `${server}/${repository}/actions/runs/${runId}`;
|
|
987
|
+
}
|
|
755
988
|
async function publishCheckRun(report, options = {}) {
|
|
756
989
|
const token = options.token ?? process.env.GITHUB_TOKEN;
|
|
757
990
|
const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
|
|
758
|
-
const sha = options.sha
|
|
991
|
+
const sha = resolveCheckSha(process.env, { sha: options.sha });
|
|
992
|
+
const detailsUrl = resolveCheckDetailsUrl(process.env, {
|
|
993
|
+
detailsUrl: options.detailsUrl
|
|
994
|
+
});
|
|
759
995
|
if (!token) {
|
|
760
996
|
return { status: "skipped", reason: "missing GITHUB_TOKEN" };
|
|
761
997
|
}
|
|
@@ -763,7 +999,10 @@ async function publishCheckRun(report, options = {}) {
|
|
|
763
999
|
return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
|
|
764
1000
|
}
|
|
765
1001
|
if (!sha && options.checkRunId === void 0) {
|
|
766
|
-
return {
|
|
1002
|
+
return {
|
|
1003
|
+
status: "skipped",
|
|
1004
|
+
reason: "missing commit SHA (set --sha / options.sha, GITHUB_PR_HEAD_SHA, pull_request.head.sha, or GITHUB_SHA)"
|
|
1005
|
+
};
|
|
767
1006
|
}
|
|
768
1007
|
const [owner, repo] = repository.split("/");
|
|
769
1008
|
if (!owner || !repo) {
|
|
@@ -772,7 +1011,7 @@ async function publishCheckRun(report, options = {}) {
|
|
|
772
1011
|
reason: `invalid GitHub repository: ${repository}`
|
|
773
1012
|
};
|
|
774
1013
|
}
|
|
775
|
-
const payload = buildCheckRunPayload(report, options);
|
|
1014
|
+
const payload = buildCheckRunPayload(report, options, detailsUrl);
|
|
776
1015
|
const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
777
1016
|
const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
|
|
778
1017
|
const response = await fetch(requestUrl, {
|
|
@@ -787,6 +1026,7 @@ async function publishCheckRun(report, options = {}) {
|
|
|
787
1026
|
options.checkRunId === void 0 ? {
|
|
788
1027
|
name: options.name ?? DEFAULT_CHECK_NAME,
|
|
789
1028
|
head_sha: sha,
|
|
1029
|
+
...options.externalId ? { external_id: options.externalId } : {},
|
|
790
1030
|
...payload
|
|
791
1031
|
} : payload
|
|
792
1032
|
)
|
|
@@ -801,27 +1041,31 @@ async function publishCheckRun(report, options = {}) {
|
|
|
801
1041
|
return {
|
|
802
1042
|
status: options.checkRunId === void 0 ? "created" : "updated",
|
|
803
1043
|
id: data.id,
|
|
804
|
-
htmlUrl: data.html_url
|
|
1044
|
+
htmlUrl: data.html_url,
|
|
1045
|
+
sha
|
|
805
1046
|
};
|
|
806
1047
|
}
|
|
807
|
-
function buildCheckRunPayload(report, options) {
|
|
1048
|
+
function buildCheckRunPayload(report, options, detailsUrl) {
|
|
1049
|
+
const gate = options.gate ?? evaluateEvalGate(report);
|
|
808
1050
|
const annotations = buildCheckAnnotations(report, {
|
|
809
|
-
maxAnnotations: options.maxAnnotations
|
|
1051
|
+
maxAnnotations: options.maxAnnotations,
|
|
1052
|
+
gate
|
|
810
1053
|
});
|
|
811
|
-
const title = report.failures.length === 0 && report.status === "passed" ? "No eval failures" : report.failures.length === 0 ? "Vitest run failed" : `${report.failures.length} eval failure${report.failures.length === 1 ? "" : "s"}`;
|
|
812
1054
|
return {
|
|
813
1055
|
status: "completed",
|
|
814
|
-
conclusion:
|
|
1056
|
+
conclusion: gate.ok ? "success" : "failure",
|
|
815
1057
|
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1058
|
+
...detailsUrl ? { details_url: detailsUrl } : {},
|
|
816
1059
|
output: {
|
|
817
|
-
title,
|
|
1060
|
+
title: gate.title,
|
|
818
1061
|
summary: truncateCheckSummary(
|
|
819
1062
|
renderJobSummary(report, {
|
|
820
1063
|
...options,
|
|
821
1064
|
maxFailures: options.maxFailures ?? 5,
|
|
822
1065
|
maxReasonChars: options.maxReasonChars ?? 4e3,
|
|
823
1066
|
maxOutputChars: options.maxOutputChars ?? 2e3,
|
|
824
|
-
maxToolCalls: options.maxToolCalls ?? 10
|
|
1067
|
+
maxToolCalls: options.maxToolCalls ?? 10,
|
|
1068
|
+
gate
|
|
825
1069
|
})
|
|
826
1070
|
),
|
|
827
1071
|
annotations
|
|
@@ -920,11 +1164,17 @@ async function publishEvalReport(options) {
|
|
|
920
1164
|
})
|
|
921
1165
|
);
|
|
922
1166
|
const report = mergeEvalReports(reports);
|
|
1167
|
+
const gate = evaluateEvalGate(report, {
|
|
1168
|
+
failOnFailures: options.failOnFailures,
|
|
1169
|
+
minPassRate: options.minPassRate,
|
|
1170
|
+
minScoreAverage: options.minScoreAverage
|
|
1171
|
+
});
|
|
923
1172
|
const summary = renderJobSummary(report, {
|
|
924
1173
|
maxFailures: options.maxFailures,
|
|
925
1174
|
maxOutputChars: options.maxOutputChars,
|
|
926
1175
|
maxReasonChars: options.maxReasonChars,
|
|
927
|
-
maxToolCalls: options.maxToolCalls
|
|
1176
|
+
maxToolCalls: options.maxToolCalls,
|
|
1177
|
+
gate
|
|
928
1178
|
});
|
|
929
1179
|
if (options.summaryEnabled !== false) {
|
|
930
1180
|
if (options.summaryPath) {
|
|
@@ -935,8 +1185,13 @@ async function publishEvalReport(options) {
|
|
|
935
1185
|
}
|
|
936
1186
|
}
|
|
937
1187
|
if (options.annotations) {
|
|
1188
|
+
const gateCommand = renderGateWorkflowCommand(gate);
|
|
1189
|
+
if (gateCommand) {
|
|
1190
|
+
console.log(gateCommand);
|
|
1191
|
+
}
|
|
938
1192
|
for (const command of renderWorkflowCommands(report, {
|
|
939
|
-
maxAnnotations: options.maxAnnotations
|
|
1193
|
+
maxAnnotations: options.maxAnnotations,
|
|
1194
|
+
gate
|
|
940
1195
|
})) {
|
|
941
1196
|
console.log(command);
|
|
942
1197
|
}
|
|
@@ -954,10 +1209,17 @@ async function publishEvalReport(options) {
|
|
|
954
1209
|
name: options.checkName,
|
|
955
1210
|
repository: options.repository,
|
|
956
1211
|
sha: options.sha,
|
|
957
|
-
|
|
1212
|
+
detailsUrl: options.detailsUrl,
|
|
1213
|
+
externalId: options.externalId,
|
|
1214
|
+
token: options.token,
|
|
1215
|
+
gate
|
|
958
1216
|
});
|
|
959
1217
|
if (checkRun.status === "skipped") {
|
|
960
1218
|
options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
|
|
1219
|
+
} else if (checkRun.htmlUrl) {
|
|
1220
|
+
console.log(`published check run: ${checkRun.htmlUrl}`);
|
|
1221
|
+
} else if (checkRun.id !== void 0) {
|
|
1222
|
+
console.log(`published check run id: ${checkRun.id}`);
|
|
961
1223
|
}
|
|
962
1224
|
} catch (error) {
|
|
963
1225
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -967,12 +1229,21 @@ async function publishEvalReport(options) {
|
|
|
967
1229
|
options.warn?.(message);
|
|
968
1230
|
}
|
|
969
1231
|
}
|
|
1232
|
+
const gateFailed = gate.enforced && !gate.ok;
|
|
1233
|
+
const wantsSoftFail = options.softFail ?? options.checkRun === true;
|
|
1234
|
+
const softFail = wantsSoftFail && checkRunPublished(checkRun);
|
|
1235
|
+
const shouldFail = gateFailed && !softFail;
|
|
970
1236
|
return {
|
|
971
1237
|
report,
|
|
972
1238
|
resultFiles,
|
|
1239
|
+
gate,
|
|
1240
|
+
shouldFail,
|
|
973
1241
|
checkRun
|
|
974
1242
|
};
|
|
975
1243
|
}
|
|
1244
|
+
function checkRunPublished(checkRun) {
|
|
1245
|
+
return checkRun?.status === "created" || checkRun?.status === "updated";
|
|
1246
|
+
}
|
|
976
1247
|
|
|
977
1248
|
// src/cli.ts
|
|
978
1249
|
main().catch((error) => {
|
|
@@ -996,6 +1267,10 @@ async function main() {
|
|
|
996
1267
|
checkRunId: options.checkRunId,
|
|
997
1268
|
checkName: options.checkName,
|
|
998
1269
|
failOnCheckError: options.failOnCheckError,
|
|
1270
|
+
failOnFailures: options.failOnFailures,
|
|
1271
|
+
softFail: options.softFail,
|
|
1272
|
+
minPassRate: options.minPassRate,
|
|
1273
|
+
minScoreAverage: options.minScoreAverage,
|
|
999
1274
|
maxAnnotations: options.maxAnnotations,
|
|
1000
1275
|
maxFailures: options.maxFailures,
|
|
1001
1276
|
repository: options.repository,
|
|
@@ -1003,7 +1278,8 @@ async function main() {
|
|
|
1003
1278
|
token: options.token,
|
|
1004
1279
|
warn
|
|
1005
1280
|
});
|
|
1006
|
-
if (
|
|
1281
|
+
if (result.shouldFail) {
|
|
1282
|
+
console.error(result.gate.message);
|
|
1007
1283
|
process.exitCode = 1;
|
|
1008
1284
|
}
|
|
1009
1285
|
}
|
|
@@ -1025,13 +1301,17 @@ function usage() {
|
|
|
1025
1301
|
" --annotations Emit GitHub workflow-command annotations",
|
|
1026
1302
|
" --no-annotations Disable workflow-command annotations",
|
|
1027
1303
|
" --check-run Publish a GitHub Check Run when configured",
|
|
1028
|
-
" --fail-on-failures Exit non-zero when
|
|
1304
|
+
" --fail-on-failures Exit non-zero when any eval case failed",
|
|
1305
|
+
" --min-pass-rate <0-1> Exit non-zero when eval pass rate is below this floor",
|
|
1306
|
+
" --min-score-average <0-1> Exit non-zero when average score is below this floor",
|
|
1307
|
+
" --soft-fail Keep exit 0 when a published Check Run owns gate status",
|
|
1308
|
+
" --no-soft-fail Always exit non-zero when an enforced gate fails",
|
|
1029
1309
|
" --fail-on-check-error Fail when Check Run publishing fails",
|
|
1030
1310
|
" --check-run-id <id> Update an existing Check Run",
|
|
1031
1311
|
" --check-name <name> Check Run name (default: vitest-evals)",
|
|
1032
1312
|
" --token <token> GitHub token (default: GITHUB_TOKEN)",
|
|
1033
1313
|
" --repo <owner/repo> GitHub repository (default: GITHUB_REPOSITORY)",
|
|
1034
|
-
" --sha <sha> Git commit SHA (default: GITHUB_SHA)",
|
|
1314
|
+
" --sha <sha> Git commit SHA (default: PR head, then GITHUB_SHA)",
|
|
1035
1315
|
" --workspace <path> Workspace path for relative annotation files",
|
|
1036
1316
|
" --max-annotations <n> Maximum annotations to emit",
|
|
1037
1317
|
" --max-failures <n> Maximum failures to include in details"
|