@vitest-evals/github-reporter 0.16.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 CHANGED
@@ -31,10 +31,19 @@ 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.
38
47
 
39
48
  ## Score and Pass-Rate Gates
40
49
 
@@ -49,9 +58,11 @@ workflow annotations and warns instead of failing.
49
58
 
50
59
  - `fail-on-failures: true` requires every eval case to pass
51
60
  - `min-pass-rate` and `min-score-average` set aggregate floors in the `0`-`1` range
52
- - `status`, Check Run conclusion/title, and step exit follow the gate
61
+ - `status` and Check Run conclusion/title follow the gate
53
62
  - quality misses become warnings when the gate still passes
54
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
55
66
  - use `evals-failed` / `pass-rate` for raw tallies (`pass-rate` is a 0-1 ratio)
56
67
 
57
68
  ## Sharded Reports
@@ -80,10 +91,12 @@ final reducer job:
80
91
  | `results` | `vitest-results.json` | Vitest JSON result files. Supports paths, `*` and `**` globs, and newline-separated entries. |
81
92
  | `publish-summary` | `true` | Write a GitHub Actions job summary. |
82
93
  | `publish-annotations` | `true` | Emit GitHub workflow annotations for failed evals. |
83
- | `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`. |
84
95
  | `check-name` | `vitest-evals` | Name of the GitHub Check Run. |
85
96
  | `github-token` | `${{ github.token }}` | Token used for Check Run publishing. |
97
+ | `sha` | PR head, else `GITHUB_SHA` | Commit SHA for the Check Run. |
86
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. |
87
100
  | `min-pass-rate` | unset | Minimum fraction of eval cases that must pass (`0`-`1`). |
88
101
  | `min-score-average` | unset | Minimum average eval score across scored cases (`0`-`1`). |
89
102
  | `max-annotations` | unset | Maximum number of failure annotations to publish. Check Run annotations are capped at 50 by GitHub. |
package/dist/cli.js CHANGED
@@ -41,6 +41,12 @@ 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;
44
50
  case "--min-pass-rate":
45
51
  options.minPassRate = readRatio(args, ++index, arg);
46
52
  break;
@@ -645,6 +651,9 @@ function formatFloorSuffix(minPassRate, minScoreAverage) {
645
651
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
646
652
  }
647
653
 
654
+ // src/github.ts
655
+ var import_node_fs = require("fs");
656
+
648
657
  // src/summary.ts
649
658
  var DEFAULT_MAX_FAILURES = 20;
650
659
  var DEFAULT_MAX_REASON_CHARS = 8e3;
@@ -945,10 +954,44 @@ function formatCaseUsage(testCase) {
945
954
  var DEFAULT_CHECK_NAME = "vitest-evals";
946
955
  var MAX_CHECK_SUMMARY_LENGTH = 64e3;
947
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
+ }
948
988
  async function publishCheckRun(report, options = {}) {
949
989
  const token = options.token ?? process.env.GITHUB_TOKEN;
950
990
  const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
951
- const sha = options.sha ?? process.env.GITHUB_SHA;
991
+ const sha = resolveCheckSha(process.env, { sha: options.sha });
992
+ const detailsUrl = resolveCheckDetailsUrl(process.env, {
993
+ detailsUrl: options.detailsUrl
994
+ });
952
995
  if (!token) {
953
996
  return { status: "skipped", reason: "missing GITHUB_TOKEN" };
954
997
  }
@@ -956,7 +999,10 @@ async function publishCheckRun(report, options = {}) {
956
999
  return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
957
1000
  }
958
1001
  if (!sha && options.checkRunId === void 0) {
959
- return { status: "skipped", reason: "missing GITHUB_SHA" };
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
+ };
960
1006
  }
961
1007
  const [owner, repo] = repository.split("/");
962
1008
  if (!owner || !repo) {
@@ -965,7 +1011,7 @@ async function publishCheckRun(report, options = {}) {
965
1011
  reason: `invalid GitHub repository: ${repository}`
966
1012
  };
967
1013
  }
968
- const payload = buildCheckRunPayload(report, options);
1014
+ const payload = buildCheckRunPayload(report, options, detailsUrl);
969
1015
  const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
970
1016
  const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
971
1017
  const response = await fetch(requestUrl, {
@@ -980,6 +1026,7 @@ async function publishCheckRun(report, options = {}) {
980
1026
  options.checkRunId === void 0 ? {
981
1027
  name: options.name ?? DEFAULT_CHECK_NAME,
982
1028
  head_sha: sha,
1029
+ ...options.externalId ? { external_id: options.externalId } : {},
983
1030
  ...payload
984
1031
  } : payload
985
1032
  )
@@ -994,10 +1041,11 @@ async function publishCheckRun(report, options = {}) {
994
1041
  return {
995
1042
  status: options.checkRunId === void 0 ? "created" : "updated",
996
1043
  id: data.id,
997
- htmlUrl: data.html_url
1044
+ htmlUrl: data.html_url,
1045
+ sha
998
1046
  };
999
1047
  }
1000
- function buildCheckRunPayload(report, options) {
1048
+ function buildCheckRunPayload(report, options, detailsUrl) {
1001
1049
  const gate = options.gate ?? evaluateEvalGate(report);
1002
1050
  const annotations = buildCheckAnnotations(report, {
1003
1051
  maxAnnotations: options.maxAnnotations,
@@ -1007,6 +1055,7 @@ function buildCheckRunPayload(report, options) {
1007
1055
  status: "completed",
1008
1056
  conclusion: gate.ok ? "success" : "failure",
1009
1057
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
1058
+ ...detailsUrl ? { details_url: detailsUrl } : {},
1010
1059
  output: {
1011
1060
  title: gate.title,
1012
1061
  summary: truncateCheckSummary(
@@ -1160,11 +1209,17 @@ async function publishEvalReport(options) {
1160
1209
  name: options.checkName,
1161
1210
  repository: options.repository,
1162
1211
  sha: options.sha,
1212
+ detailsUrl: options.detailsUrl,
1213
+ externalId: options.externalId,
1163
1214
  token: options.token,
1164
1215
  gate
1165
1216
  });
1166
1217
  if (checkRun.status === "skipped") {
1167
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}`);
1168
1223
  }
1169
1224
  } catch (error) {
1170
1225
  const message = error instanceof Error ? error.message : String(error);
@@ -1174,14 +1229,21 @@ async function publishEvalReport(options) {
1174
1229
  options.warn?.(message);
1175
1230
  }
1176
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;
1177
1236
  return {
1178
1237
  report,
1179
1238
  resultFiles,
1180
1239
  gate,
1181
- shouldFail: gate.enforced && !gate.ok,
1240
+ shouldFail,
1182
1241
  checkRun
1183
1242
  };
1184
1243
  }
1244
+ function checkRunPublished(checkRun) {
1245
+ return checkRun?.status === "created" || checkRun?.status === "updated";
1246
+ }
1185
1247
 
1186
1248
  // src/cli.ts
1187
1249
  main().catch((error) => {
@@ -1206,6 +1268,7 @@ async function main() {
1206
1268
  checkName: options.checkName,
1207
1269
  failOnCheckError: options.failOnCheckError,
1208
1270
  failOnFailures: options.failOnFailures,
1271
+ softFail: options.softFail,
1209
1272
  minPassRate: options.minPassRate,
1210
1273
  minScoreAverage: options.minScoreAverage,
1211
1274
  maxAnnotations: options.maxAnnotations,
@@ -1241,12 +1304,14 @@ function usage() {
1241
1304
  " --fail-on-failures Exit non-zero when any eval case failed",
1242
1305
  " --min-pass-rate <0-1> Exit non-zero when eval pass rate is below this floor",
1243
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",
1244
1309
  " --fail-on-check-error Fail when Check Run publishing fails",
1245
1310
  " --check-run-id <id> Update an existing Check Run",
1246
1311
  " --check-name <name> Check Run name (default: vitest-evals)",
1247
1312
  " --token <token> GitHub token (default: GITHUB_TOKEN)",
1248
1313
  " --repo <owner/repo> GitHub repository (default: GITHUB_REPOSITORY)",
1249
- " --sha <sha> Git commit SHA (default: GITHUB_SHA)",
1314
+ " --sha <sha> Git commit SHA (default: PR head, then GITHUB_SHA)",
1250
1315
  " --workspace <path> Workspace path for relative annotation files",
1251
1316
  " --max-annotations <n> Maximum annotations to emit",
1252
1317
  " --max-failures <n> Maximum failures to include in details"