@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/dist/cli.mjs CHANGED
@@ -40,6 +40,18 @@ 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;
49
+ case "--min-pass-rate":
50
+ options.minPassRate = readRatio(args, ++index, arg);
51
+ break;
52
+ case "--min-score-average":
53
+ options.minScoreAverage = readRatio(args, ++index, arg);
54
+ break;
43
55
  case "--max-annotations":
44
56
  options.maxAnnotations = readInteger(args, ++index, arg);
45
57
  break;
@@ -99,6 +111,14 @@ function readInteger(args, index, flag) {
99
111
  }
100
112
  return Number(rawValue);
101
113
  }
114
+ function readRatio(args, index, flag) {
115
+ const rawValue = readValue(args, index, flag);
116
+ const parsed = Number(rawValue);
117
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
118
+ throw new Error(`Invalid ratio for ${flag}`);
119
+ }
120
+ return parsed;
121
+ }
102
122
 
103
123
  // src/report.ts
104
124
  import { appendFile } from "fs/promises";
@@ -190,9 +210,10 @@ var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
190
210
  var MAX_CHECK_FIELD_LENGTH = 64e3;
191
211
  function renderWorkflowCommands(report, options = {}) {
192
212
  const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
213
+ const command = caseAnnotationCommand(options.gate);
193
214
  return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
194
215
  (testCase) => formatWorkflowCommand({
195
- command: "error",
216
+ command,
196
217
  properties: {
197
218
  file: testCase.displayFile,
198
219
  line: String(testCase.location.line),
@@ -208,11 +229,12 @@ function buildCheckAnnotations(report, options = {}) {
208
229
  options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS,
209
230
  DEFAULT_MAX_CHECK_ANNOTATIONS
210
231
  );
232
+ const annotationLevel = caseAnnotationLevel(options.gate);
211
233
  return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
212
234
  path: testCase.displayFile,
213
235
  start_line: testCase.location.line,
214
236
  end_line: testCase.location.line,
215
- annotation_level: "failure",
237
+ annotation_level: annotationLevel,
216
238
  title: truncate(
217
239
  `${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
218
240
  255
@@ -224,6 +246,12 @@ function buildCheckAnnotations(report, options = {}) {
224
246
  raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
225
247
  }));
226
248
  }
249
+ function caseAnnotationCommand(gate) {
250
+ return gate?.ok ? "warning" : "error";
251
+ }
252
+ function caseAnnotationLevel(gate) {
253
+ return gate?.ok ? "warning" : "failure";
254
+ }
227
255
  function hasAnnotationLocation(testCase) {
228
256
  return Boolean(testCase.location);
229
257
  }
@@ -469,6 +497,167 @@ function toolCallCount(testCase) {
469
497
  return testCase.toolCalls.length;
470
498
  }
471
499
 
500
+ // src/gate.ts
501
+ function evaluateEvalGate(report, policy = {}) {
502
+ const minPassRate = resolveMinPassRate(policy);
503
+ const minScoreAverage = policy.minScoreAverage;
504
+ const enforced = minPassRate !== void 0 || minScoreAverage !== void 0 || policy.failOnFailures === true;
505
+ const passRate = computePassRate(report);
506
+ const counts = formatEvalCounts(report, passRate);
507
+ if (!enforced) {
508
+ const ok = report.status === "passed";
509
+ return {
510
+ ok,
511
+ status: ok ? "passed" : "failed",
512
+ enforced: false,
513
+ passRate,
514
+ title: defaultCheckTitle(report),
515
+ message: ok ? `eval report passed: ${counts}` : `eval report failed: ${counts}`
516
+ };
517
+ }
518
+ const nonEvalFailures = Math.max(
519
+ 0,
520
+ report.totals.failed - report.totals.evalFailed
521
+ );
522
+ if (nonEvalFailures > 0) {
523
+ return {
524
+ ok: false,
525
+ status: "failed",
526
+ enforced: true,
527
+ passRate,
528
+ title: "Eval report hard failure",
529
+ message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
530
+ };
531
+ }
532
+ if (report.totals.evalTotal === 0) {
533
+ return {
534
+ ok: false,
535
+ status: "failed",
536
+ enforced: true,
537
+ passRate: null,
538
+ title: "Eval report hard failure",
539
+ message: "no eval cases were reported"
540
+ };
541
+ }
542
+ if (report.status === "failed" && report.totals.failed === 0 && report.failures.length === 0) {
543
+ return {
544
+ ok: false,
545
+ status: "failed",
546
+ enforced: true,
547
+ passRate,
548
+ title: "Eval report hard failure",
549
+ message: `vitest run failed without counted test failures; ${counts}`
550
+ };
551
+ }
552
+ if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
553
+ return {
554
+ ok: false,
555
+ status: "failed",
556
+ enforced: true,
557
+ passRate,
558
+ title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
559
+ message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
560
+ };
561
+ }
562
+ if (minScoreAverage !== void 0) {
563
+ const average = report.score?.average;
564
+ if (average === void 0 || !Number.isFinite(average)) {
565
+ return {
566
+ ok: false,
567
+ status: "failed",
568
+ enforced: true,
569
+ passRate,
570
+ title: "Eval score gate failed",
571
+ message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
572
+ };
573
+ }
574
+ if (average + Number.EPSILON < minScoreAverage) {
575
+ return {
576
+ ok: false,
577
+ status: "failed",
578
+ enforced: true,
579
+ passRate,
580
+ title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
581
+ message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
582
+ };
583
+ }
584
+ }
585
+ return {
586
+ ok: true,
587
+ status: "passed",
588
+ enforced: true,
589
+ passRate,
590
+ title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
591
+ message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
592
+ };
593
+ }
594
+ function computePassRate(report) {
595
+ if (report.totals.evalTotal <= 0) {
596
+ return null;
597
+ }
598
+ return report.totals.evalPassed / report.totals.evalTotal;
599
+ }
600
+ function formatPercent(value) {
601
+ if (value == null || !Number.isFinite(value)) {
602
+ return "n/a";
603
+ }
604
+ return `${(value * 100).toFixed(1)}%`;
605
+ }
606
+ function renderGateWorkflowCommand(gate) {
607
+ if (gate.ok || !gate.enforced) {
608
+ return void 0;
609
+ }
610
+ return `::error title=${escapeCommandProperty(gate.title)}::${escapeCommandData(gate.message)}`;
611
+ }
612
+ function resolveMinPassRate(policy) {
613
+ const configured = policy.minPassRate;
614
+ if (policy.failOnFailures) {
615
+ if (configured === void 0) {
616
+ return 1;
617
+ }
618
+ return Math.max(configured, 1);
619
+ }
620
+ return configured;
621
+ }
622
+ function defaultCheckTitle(report) {
623
+ if (report.failures.length === 0 && report.status === "passed") {
624
+ return "No eval failures";
625
+ }
626
+ if (report.failures.length === 0) {
627
+ return "Vitest run failed";
628
+ }
629
+ return `${report.failures.length} eval failure${report.failures.length === 1 ? "" : "s"}`;
630
+ }
631
+ function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
632
+ if (minPassRate !== void 0) {
633
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
634
+ }
635
+ if (minScoreAverage !== void 0) {
636
+ return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
637
+ }
638
+ return defaultCheckTitle(report);
639
+ }
640
+ function formatEvalCounts(report, passRate) {
641
+ const scoreText = report.score?.average === void 0 ? "n/a" : formatScore(report.score.average);
642
+ const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
643
+ return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
644
+ report.totals.evalTotal
645
+ )} passed (${passRateText}), avg score ${scoreText}`;
646
+ }
647
+ function formatFloorSuffix(minPassRate, minScoreAverage) {
648
+ const parts = [];
649
+ if (minPassRate !== void 0) {
650
+ parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
651
+ }
652
+ if (minScoreAverage !== void 0) {
653
+ parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
654
+ }
655
+ return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
656
+ }
657
+
658
+ // src/github.ts
659
+ import { readFileSync } from "fs";
660
+
472
661
  // src/summary.ts
473
662
  var DEFAULT_MAX_FAILURES = 20;
474
663
  var DEFAULT_MAX_REASON_CHARS = 8e3;
@@ -489,20 +678,22 @@ function renderJobSummary(report, options = {}) {
489
678
  const lines = [
490
679
  "# vitest-evals",
491
680
  "",
492
- ...renderSummaryTable(report, nonEvalFailures),
681
+ ...renderSummaryTable(report, nonEvalFailures, options.gate),
493
682
  "",
494
683
  ...renderScoreDistribution(report),
495
684
  "## Results",
496
685
  ""
497
686
  ];
498
687
  if (report.failures.length > 0) {
499
- lines.push("### Failures", "");
688
+ const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
689
+ lines.push(failureHeading, "");
500
690
  failures.forEach((testCase, index) => {
501
691
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
502
692
  });
503
693
  if (report.failures.length > failures.length) {
694
+ const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
504
695
  lines.push(
505
- `${report.failures.length - failures.length} more failures omitted from this summary.`,
696
+ `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
506
697
  ""
507
698
  );
508
699
  }
@@ -518,9 +709,9 @@ function renderJobSummary(report, options = {}) {
518
709
  function formatCountLine(passed, failed, total) {
519
710
  return `${formatNumber(passed)} passed, ${formatNumber(failed)} failed, ${formatNumber(total)} total`;
520
711
  }
521
- function renderSummaryTable(report, nonEvalFailures) {
712
+ function renderSummaryTable(report, nonEvalFailures, gate) {
522
713
  const rows = [
523
- ["Status", report.status],
714
+ ["Status", gate?.status ?? report.status],
524
715
  [
525
716
  "Evals",
526
717
  formatCountLine(
@@ -530,9 +721,20 @@ function renderSummaryTable(report, nonEvalFailures) {
530
721
  )
531
722
  ]
532
723
  ];
724
+ if (gate?.passRate !== void 0 && gate.passRate !== null) {
725
+ rows.push(["Pass Rate", formatPercent(gate.passRate)]);
726
+ } else if (report.totals.evalTotal > 0) {
727
+ rows.push([
728
+ "Pass Rate",
729
+ formatPercent(report.totals.evalPassed / report.totals.evalTotal)
730
+ ]);
731
+ }
533
732
  if (report.score) {
534
733
  rows.push(["Score", formatScoreSummary(report.score)]);
535
734
  }
735
+ if (gate?.enforced) {
736
+ rows.push(["Gate", gate.message]);
737
+ }
536
738
  if (nonEvalFailures > 0) {
537
739
  rows.push([
538
740
  "Other Failures",
@@ -756,10 +958,44 @@ function formatCaseUsage(testCase) {
756
958
  var DEFAULT_CHECK_NAME = "vitest-evals";
757
959
  var MAX_CHECK_SUMMARY_LENGTH = 64e3;
758
960
  var CHECK_SUMMARY_TRUNCATION_SUFFIX = "\n\n[truncated for GitHub Check Run]\n";
961
+ function resolveCheckSha(env = process.env, options = {}) {
962
+ const explicit = options.sha?.trim() || env.GITHUB_PR_HEAD_SHA?.trim();
963
+ if (explicit) {
964
+ return explicit;
965
+ }
966
+ const eventPath = options.eventPath?.trim() || env.GITHUB_EVENT_PATH?.trim();
967
+ if (eventPath) {
968
+ try {
969
+ const event = JSON.parse(readFileSync(eventPath, "utf8"));
970
+ const headSha = event.pull_request?.head?.sha;
971
+ if (typeof headSha === "string" && headSha.trim()) {
972
+ return headSha.trim();
973
+ }
974
+ } catch {
975
+ }
976
+ }
977
+ return env.GITHUB_SHA?.trim() || void 0;
978
+ }
979
+ function resolveCheckDetailsUrl(env = process.env, options = {}) {
980
+ const explicit = options.detailsUrl?.trim();
981
+ if (explicit) {
982
+ return explicit;
983
+ }
984
+ const server = env.GITHUB_SERVER_URL?.replace(/\/$/, "");
985
+ const repository = env.GITHUB_REPOSITORY?.trim();
986
+ const runId = env.GITHUB_RUN_ID?.trim();
987
+ if (!server || !repository || !runId) {
988
+ return void 0;
989
+ }
990
+ return `${server}/${repository}/actions/runs/${runId}`;
991
+ }
759
992
  async function publishCheckRun(report, options = {}) {
760
993
  const token = options.token ?? process.env.GITHUB_TOKEN;
761
994
  const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
762
- const sha = options.sha ?? process.env.GITHUB_SHA;
995
+ const sha = resolveCheckSha(process.env, { sha: options.sha });
996
+ const detailsUrl = resolveCheckDetailsUrl(process.env, {
997
+ detailsUrl: options.detailsUrl
998
+ });
763
999
  if (!token) {
764
1000
  return { status: "skipped", reason: "missing GITHUB_TOKEN" };
765
1001
  }
@@ -767,7 +1003,10 @@ async function publishCheckRun(report, options = {}) {
767
1003
  return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
768
1004
  }
769
1005
  if (!sha && options.checkRunId === void 0) {
770
- return { status: "skipped", reason: "missing GITHUB_SHA" };
1006
+ return {
1007
+ status: "skipped",
1008
+ reason: "missing commit SHA (set --sha / options.sha, GITHUB_PR_HEAD_SHA, pull_request.head.sha, or GITHUB_SHA)"
1009
+ };
771
1010
  }
772
1011
  const [owner, repo] = repository.split("/");
773
1012
  if (!owner || !repo) {
@@ -776,7 +1015,7 @@ async function publishCheckRun(report, options = {}) {
776
1015
  reason: `invalid GitHub repository: ${repository}`
777
1016
  };
778
1017
  }
779
- const payload = buildCheckRunPayload(report, options);
1018
+ const payload = buildCheckRunPayload(report, options, detailsUrl);
780
1019
  const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
781
1020
  const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
782
1021
  const response = await fetch(requestUrl, {
@@ -791,6 +1030,7 @@ async function publishCheckRun(report, options = {}) {
791
1030
  options.checkRunId === void 0 ? {
792
1031
  name: options.name ?? DEFAULT_CHECK_NAME,
793
1032
  head_sha: sha,
1033
+ ...options.externalId ? { external_id: options.externalId } : {},
794
1034
  ...payload
795
1035
  } : payload
796
1036
  )
@@ -805,27 +1045,31 @@ async function publishCheckRun(report, options = {}) {
805
1045
  return {
806
1046
  status: options.checkRunId === void 0 ? "created" : "updated",
807
1047
  id: data.id,
808
- htmlUrl: data.html_url
1048
+ htmlUrl: data.html_url,
1049
+ sha
809
1050
  };
810
1051
  }
811
- function buildCheckRunPayload(report, options) {
1052
+ function buildCheckRunPayload(report, options, detailsUrl) {
1053
+ const gate = options.gate ?? evaluateEvalGate(report);
812
1054
  const annotations = buildCheckAnnotations(report, {
813
- maxAnnotations: options.maxAnnotations
1055
+ maxAnnotations: options.maxAnnotations,
1056
+ gate
814
1057
  });
815
- 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"}`;
816
1058
  return {
817
1059
  status: "completed",
818
- conclusion: report.status === "passed" ? "success" : "failure",
1060
+ conclusion: gate.ok ? "success" : "failure",
819
1061
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
1062
+ ...detailsUrl ? { details_url: detailsUrl } : {},
820
1063
  output: {
821
- title,
1064
+ title: gate.title,
822
1065
  summary: truncateCheckSummary(
823
1066
  renderJobSummary(report, {
824
1067
  ...options,
825
1068
  maxFailures: options.maxFailures ?? 5,
826
1069
  maxReasonChars: options.maxReasonChars ?? 4e3,
827
1070
  maxOutputChars: options.maxOutputChars ?? 2e3,
828
- maxToolCalls: options.maxToolCalls ?? 10
1071
+ maxToolCalls: options.maxToolCalls ?? 10,
1072
+ gate
829
1073
  })
830
1074
  ),
831
1075
  annotations
@@ -924,11 +1168,17 @@ async function publishEvalReport(options) {
924
1168
  })
925
1169
  );
926
1170
  const report = mergeEvalReports(reports);
1171
+ const gate = evaluateEvalGate(report, {
1172
+ failOnFailures: options.failOnFailures,
1173
+ minPassRate: options.minPassRate,
1174
+ minScoreAverage: options.minScoreAverage
1175
+ });
927
1176
  const summary = renderJobSummary(report, {
928
1177
  maxFailures: options.maxFailures,
929
1178
  maxOutputChars: options.maxOutputChars,
930
1179
  maxReasonChars: options.maxReasonChars,
931
- maxToolCalls: options.maxToolCalls
1180
+ maxToolCalls: options.maxToolCalls,
1181
+ gate
932
1182
  });
933
1183
  if (options.summaryEnabled !== false) {
934
1184
  if (options.summaryPath) {
@@ -939,8 +1189,13 @@ async function publishEvalReport(options) {
939
1189
  }
940
1190
  }
941
1191
  if (options.annotations) {
1192
+ const gateCommand = renderGateWorkflowCommand(gate);
1193
+ if (gateCommand) {
1194
+ console.log(gateCommand);
1195
+ }
942
1196
  for (const command of renderWorkflowCommands(report, {
943
- maxAnnotations: options.maxAnnotations
1197
+ maxAnnotations: options.maxAnnotations,
1198
+ gate
944
1199
  })) {
945
1200
  console.log(command);
946
1201
  }
@@ -958,10 +1213,17 @@ async function publishEvalReport(options) {
958
1213
  name: options.checkName,
959
1214
  repository: options.repository,
960
1215
  sha: options.sha,
961
- token: options.token
1216
+ detailsUrl: options.detailsUrl,
1217
+ externalId: options.externalId,
1218
+ token: options.token,
1219
+ gate
962
1220
  });
963
1221
  if (checkRun.status === "skipped") {
964
1222
  options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
1223
+ } else if (checkRun.htmlUrl) {
1224
+ console.log(`published check run: ${checkRun.htmlUrl}`);
1225
+ } else if (checkRun.id !== void 0) {
1226
+ console.log(`published check run id: ${checkRun.id}`);
965
1227
  }
966
1228
  } catch (error) {
967
1229
  const message = error instanceof Error ? error.message : String(error);
@@ -971,12 +1233,21 @@ async function publishEvalReport(options) {
971
1233
  options.warn?.(message);
972
1234
  }
973
1235
  }
1236
+ const gateFailed = gate.enforced && !gate.ok;
1237
+ const wantsSoftFail = options.softFail ?? options.checkRun === true;
1238
+ const softFail = wantsSoftFail && checkRunPublished(checkRun);
1239
+ const shouldFail = gateFailed && !softFail;
974
1240
  return {
975
1241
  report,
976
1242
  resultFiles,
1243
+ gate,
1244
+ shouldFail,
977
1245
  checkRun
978
1246
  };
979
1247
  }
1248
+ function checkRunPublished(checkRun) {
1249
+ return checkRun?.status === "created" || checkRun?.status === "updated";
1250
+ }
980
1251
 
981
1252
  // src/cli.ts
982
1253
  main().catch((error) => {
@@ -1000,6 +1271,10 @@ async function main() {
1000
1271
  checkRunId: options.checkRunId,
1001
1272
  checkName: options.checkName,
1002
1273
  failOnCheckError: options.failOnCheckError,
1274
+ failOnFailures: options.failOnFailures,
1275
+ softFail: options.softFail,
1276
+ minPassRate: options.minPassRate,
1277
+ minScoreAverage: options.minScoreAverage,
1003
1278
  maxAnnotations: options.maxAnnotations,
1004
1279
  maxFailures: options.maxFailures,
1005
1280
  repository: options.repository,
@@ -1007,7 +1282,8 @@ async function main() {
1007
1282
  token: options.token,
1008
1283
  warn
1009
1284
  });
1010
- if (options.failOnFailures && result.report.status === "failed") {
1285
+ if (result.shouldFail) {
1286
+ console.error(result.gate.message);
1011
1287
  process.exitCode = 1;
1012
1288
  }
1013
1289
  }
@@ -1029,13 +1305,17 @@ function usage() {
1029
1305
  " --annotations Emit GitHub workflow-command annotations",
1030
1306
  " --no-annotations Disable workflow-command annotations",
1031
1307
  " --check-run Publish a GitHub Check Run when configured",
1032
- " --fail-on-failures Exit non-zero when the combined report failed",
1308
+ " --fail-on-failures Exit non-zero when any eval case failed",
1309
+ " --min-pass-rate <0-1> Exit non-zero when eval pass rate is below this floor",
1310
+ " --min-score-average <0-1> Exit non-zero when average score is below this floor",
1311
+ " --soft-fail Keep exit 0 when a published Check Run owns gate status",
1312
+ " --no-soft-fail Always exit non-zero when an enforced gate fails",
1033
1313
  " --fail-on-check-error Fail when Check Run publishing fails",
1034
1314
  " --check-run-id <id> Update an existing Check Run",
1035
1315
  " --check-name <name> Check Run name (default: vitest-evals)",
1036
1316
  " --token <token> GitHub token (default: GITHUB_TOKEN)",
1037
1317
  " --repo <owner/repo> GitHub repository (default: GITHUB_REPOSITORY)",
1038
- " --sha <sha> Git commit SHA (default: GITHUB_SHA)",
1318
+ " --sha <sha> Git commit SHA (default: PR head, then GITHUB_SHA)",
1039
1319
  " --workspace <path> Workspace path for relative annotation files",
1040
1320
  " --max-annotations <n> Maximum annotations to emit",
1041
1321
  " --max-failures <n> Maximum failures to include in details"