@jphutchins/code-review 0.1.0-alpha.30 → 0.1.0-alpha.32

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/index.js CHANGED
@@ -404,6 +404,7 @@ var renderStraysSection = (strays) => {
404
404
  };
405
405
  var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
406
406
  var errMsg = (e) => e instanceof Error ? e.message : String(e);
407
+ var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
407
408
  var tryParseJson = (text) => {
408
409
  try {
409
410
  return { ok: true, value: JSON.parse(text) };
@@ -972,16 +973,32 @@ var pad2 = (n) => String(n).padStart(2, "0");
972
973
  var formatUtc = (d) => `${String(d.getUTCFullYear())}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())} UTC`;
973
974
 
974
975
  // src/notice.ts
975
- var isNoticeKind = (s) => s === "security-blocked" || s === "setup-failed" || s === "checkout-failed" || s === "no-output";
976
+ var NOTICE_KINDS = [
977
+ "security-blocked",
978
+ "triage-error",
979
+ "setup-failed",
980
+ "checkout-failed",
981
+ "no-output"
982
+ ];
983
+ var isNoticeKind = (s) => NOTICE_KINDS.some((k) => k === s);
976
984
  var blockquote = (text) => text.replaceAll("\n", "\n> ");
985
+ var reasoned = (lead, noReason, reasons) => typeof reasons === "string" && reasons.trim() !== "" ? `${lead}
986
+
987
+ > ${blockquote(reasons)}` : noReason;
977
988
  var noticeSummary = (kind, reasons) => {
978
989
  switch (kind) {
979
990
  case "security-blocked":
980
- return reasons !== void 0 && reasons.trim() !== "" ? `### \u{1F6D1} Code review skipped by the security gate
981
-
982
- The diff was flagged as unsafe to apply and execute:
983
-
984
- > ${blockquote(reasons)}` : "### \u{1F6D1} Code review skipped by the security gate\n\nThe security triage returned an unsafe verdict without a reason. See workflow logs.";
991
+ return reasoned(
992
+ "### \u{1F6D1} Code review skipped by the security gate\n\nThe diff was flagged as unsafe to apply and execute:",
993
+ "### \u{1F6D1} Code review skipped by the security gate\n\nThe security triage returned an unsafe verdict without a reason. See workflow logs.",
994
+ reasons
995
+ );
996
+ case "triage-error":
997
+ return reasoned(
998
+ "### \u{1F6E0}\uFE0F Security gate could not evaluate\n\nThe security triage could not produce a verdict (operational error), so the review failed closed \u2014 this is an infrastructure failure, not a finding about this diff. Re-run to retry a transient fault; a persistent one is a configuration issue (see the workflow logs). The triage step reported:",
999
+ "### \u{1F6E0}\uFE0F Security gate could not evaluate\n\nThe security triage could not produce a verdict (operational error), so the review failed closed \u2014 this is an infrastructure failure, not a finding about this diff. Re-run to retry a transient fault; a persistent one is a configuration issue (see the workflow logs).",
1000
+ reasons
1001
+ );
985
1002
  case "setup-failed":
986
1003
  return "### \u{1F6E0}\uFE0F Review did not run\n\nThe review job failed before the security triage could run (e.g. dependency install or environment setup). See the workflow logs \u2014 this is an infrastructure failure, not a security verdict.";
987
1004
  case "checkout-failed":
@@ -990,15 +1007,21 @@ The diff was flagged as unsafe to apply and execute:
990
1007
  return "### \u26A0\uFE0F Review did not complete\n\nThe diff passed triage but the review produced no output. See workflow logs.";
991
1008
  }
992
1009
  };
993
- var buildNoticeEnvelope = (kind, reasons) => ({
1010
+ var noticeEnvelope = (summary) => ({
994
1011
  schema_version: DEFAULT_SCHEMA_VERSION,
995
- findings: noticeFindings(noticeSummary(kind, reasons)),
1012
+ findings: noticeFindings(summary),
996
1013
  models: [],
997
1014
  turns: 0,
998
1015
  duration_ms: 0,
999
1016
  vendor_cost_usd: null,
1000
1017
  incomplete: true
1001
1018
  });
1019
+ var buildNoticeEnvelope = (kind, reasons) => noticeEnvelope(noticeSummary(kind, reasons));
1020
+ var buildUnknownNoticeEnvelope = (kind) => noticeEnvelope(
1021
+ `### \u26A0\uFE0F Review could not be rendered
1022
+
1023
+ The workflow asked for an unrecognized notice kind (\`${kind}\`) \u2014 the pinned code-review CLI is older than the workflow calling it (check that its version matches). Failing closed. See the workflow logs.`
1024
+ );
1002
1025
  var identity = (decoded) => decoded;
1003
1026
  var findingsTable = [
1004
1027
  {
@@ -1082,33 +1105,75 @@ var resolvers = {
1082
1105
  prices: (raw) => resolveSingleVersion("prices", raw)
1083
1106
  };
1084
1107
  var resolve = (kind, raw) => resolvers[kind](raw);
1085
- var runGhApi = (args, stdin, env) => new Promise((resolve3, reject) => {
1108
+ var MAX_BUFFER = 100 * 1024 * 1024;
1109
+ var MAX_TIMEOUT_MS = 2147483647;
1110
+ var parseTimeoutMs = (raw, fallback) => {
1111
+ if (raw === void 0 || !/^\d+$/.test(raw)) return fallback;
1112
+ const parsed = Number(raw);
1113
+ return parsed > 0 && parsed <= MAX_TIMEOUT_MS ? parsed : fallback;
1114
+ };
1115
+ var SUBPROCESS_TIMEOUT_ENV = "CODE_REVIEW_SUBPROCESS_TIMEOUT_MS";
1116
+ var DEFAULT_SUBPROCESS_TIMEOUT_MS = 12e4;
1117
+ var subprocessTimeoutMs = () => parseTimeoutMs(process.env[SUBPROCESS_TIMEOUT_ENV], DEFAULT_SUBPROCESS_TIMEOUT_MS);
1118
+ var classifyExecError = (err, stderr, timeoutMs) => {
1119
+ const e = err;
1120
+ const stderrStr = stderr.trim();
1121
+ if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
1122
+ return `output exceeded ${String(MAX_BUFFER)} bytes (killed)`;
1123
+ if (e.killed === true)
1124
+ return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
1125
+ return stderrStr || errMsg(err);
1126
+ };
1127
+ var execFileWithTimeout = (spec) => new Promise((resolve3, reject) => {
1086
1128
  const child = execFile(
1087
- "gh",
1088
- ["api", ...args],
1089
- { env: { ...process.env, ...env }, encoding: "utf-8", maxBuffer: 100 * 1024 * 1024 },
1129
+ spec.command,
1130
+ [...spec.args],
1131
+ {
1132
+ ...spec.env ? { env: { ...process.env, ...spec.env } } : {},
1133
+ encoding: "utf-8",
1134
+ maxBuffer: MAX_BUFFER,
1135
+ timeout: spec.timeoutMs,
1136
+ killSignal: "SIGKILL"
1137
+ },
1090
1138
  (err, stdout, stderr) => {
1091
- if (err) {
1092
- const stderrStr = typeof stderr === "string" && stderr.trim() ? stderr.trim() : "";
1093
- const errStr = err instanceof Error ? err.message : "unknown error";
1094
- reject(new Error(`gh api failed: ${stderrStr || errStr}`));
1095
- } else {
1096
- resolve3(stdout);
1097
- }
1139
+ if (err)
1140
+ reject(
1141
+ new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
1142
+ );
1143
+ else resolve3(stdout);
1098
1144
  }
1099
1145
  );
1100
- if (stdin !== void 0) {
1101
- child.stdin?.end(stdin);
1146
+ if (spec.stdin !== void 0) {
1147
+ child.stdin?.on("error", () => void 0);
1148
+ child.stdin?.end(spec.stdin);
1102
1149
  }
1103
1150
  });
1104
1151
 
1152
+ // src/gh.ts
1153
+ var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
1154
+ var runGhApi = (args, stdin, env) => execFileWithTimeout({
1155
+ command: "gh",
1156
+ args: ["api", ...args],
1157
+ label: `gh api ${describeEndpoint(args)}`,
1158
+ timeoutMs: subprocessTimeoutMs(),
1159
+ env,
1160
+ stdin
1161
+ });
1162
+
1105
1163
  // src/pr.ts
1106
1164
  var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
1107
1165
  var parseCandidates = (stdout) => parseJsonl(stdout);
1166
+ var fetchDirectCandidates = async (repo, headSha, ghApi) => {
1167
+ try {
1168
+ return parseCandidates(
1169
+ await ghApi([`repos/${repo}/commits/${headSha}/pulls`, "--jq", CANDIDATE_JQ])
1170
+ );
1171
+ } catch {
1172
+ return [];
1173
+ }
1174
+ };
1108
1175
  var fetchPrCandidates = async (repo, headSha, ghApi) => {
1109
- const direct = parseCandidates(
1110
- await ghApi([`repos/${repo}/commits/${headSha}/pulls`, "--jq", CANDIDATE_JQ])
1111
- );
1176
+ const direct = await fetchDirectCandidates(repo, headSha, ghApi);
1112
1177
  if (direct.length > 0) return direct;
1113
1178
  const open = parseCandidates(
1114
1179
  await ghApi([
@@ -1957,21 +2022,11 @@ stacked=${String(result.stacked)}
1957
2022
  `;
1958
2023
  }
1959
2024
  };
1960
- var runGit = (args) => new Promise((resolve3, reject) => {
1961
- execFile(
1962
- "git",
1963
- [...args],
1964
- { encoding: "utf-8", maxBuffer: 100 * 1024 * 1024 },
1965
- (err, stdout, stderr) => {
1966
- if (err) {
1967
- const stderrStr = typeof stderr === "string" && stderr.trim() ? stderr.trim() : "";
1968
- const errStr = err instanceof Error ? err.message : "unknown error";
1969
- reject(new Error(`git ${args.join(" ")} failed: ${stderrStr || errStr}`));
1970
- } else {
1971
- resolve3(stdout);
1972
- }
1973
- }
1974
- );
2025
+ var runGit = (args) => execFileWithTimeout({
2026
+ command: "git",
2027
+ args,
2028
+ label: `git ${args.join(" ")}`,
2029
+ timeoutMs: subprocessTimeoutMs()
1975
2030
  });
1976
2031
  var PrMetaCodec = t.type({
1977
2032
  changed_files: t.number,
@@ -3240,25 +3295,36 @@ var adaptCmd = defineCommand({
3240
3295
  var noticeCmd = defineCommand({
3241
3296
  meta: {
3242
3297
  name: "notice",
3243
- description: "Emit an abstract envelope for a run that produced no completed review (security block, setup failure, unapplied diff, or empty run) \u2014 flagged incomplete so the commenter renders it honestly and won't bury a real review"
3298
+ description: "Emit an abstract envelope for a run that produced no completed review (security block, triage error, setup failure, checkout failure, or empty run) \u2014 flagged incomplete so the commenter renders it honestly and won't bury a real review"
3244
3299
  },
3245
3300
  args: {
3246
3301
  kind: {
3247
3302
  type: "positional",
3248
- description: "One of: security-blocked, setup-failed, checkout-failed, no-output",
3303
+ description: `One of: ${NOTICE_KINDS.join(", ")} (an unrecognized kind renders a generic incomplete notice rather than failing \u2014 the pinned CLI is older than the workflow)`,
3249
3304
  required: true
3250
3305
  },
3251
3306
  reasons: {
3252
3307
  type: "string",
3253
- description: "security-blocked only: the triage's fail-closed reason string (empty/omitted \u21D2 the no-reason wording)"
3308
+ description: "security-blocked / triage-error only: the triage's fail-closed reason string (empty/omitted \u21D2 the no-reason wording)"
3254
3309
  }
3255
3310
  },
3311
+ // An unrecognized kind degrades to a generic incomplete notice instead of exiting non-zero: a
3312
+ // `notice <kind>` call under the workflow's `set -euo pipefail` must never crash the assemble
3313
+ // step into posting nothing, and an unknown kind almost always means version skew, not a typo.
3256
3314
  run: ({ args }) => {
3257
- const kind = isNoticeKind(args.kind) ? args.kind : fail(
3258
- `Unknown notice kind "${args.kind}" \u2014 expected one of: security-blocked, setup-failed, checkout-failed, no-output`
3259
- );
3260
- process.stdout.write(`${JSON.stringify(buildNoticeEnvelope(kind, args.reasons), null, 2)}
3315
+ if (!isNoticeKind(args.kind)) {
3316
+ process.stderr.write(
3317
+ `::warning::code-review notice: unrecognized kind "${annotationSafe(args.kind)}" \u2014 the pinned CLI is older than the workflow calling it; rendering a generic incomplete notice
3318
+ `
3319
+ );
3320
+ process.stdout.write(`${JSON.stringify(buildUnknownNoticeEnvelope(args.kind), null, 2)}
3261
3321
  `);
3322
+ return;
3323
+ }
3324
+ process.stdout.write(
3325
+ `${JSON.stringify(buildNoticeEnvelope(args.kind, args.reasons), null, 2)}
3326
+ `
3327
+ );
3262
3328
  }
3263
3329
  });
3264
3330
  var isExtractSchemaKind = (s) => s === "findings" || s === "triage";
@@ -3685,9 +3751,13 @@ var announceCmd = defineCommand({
3685
3751
  runUrl: args["run-url"],
3686
3752
  headBranch: args["head-branch"]
3687
3753
  }).catch(
3688
- (err) => process.stderr.write(
3689
- `code-review announce: could not post the in-progress sticky (${errMsg(err)}) \u2014 continuing (cosmetic)
3754
+ (err) => (
3755
+ // `::warning::` so a persistently broken announce shows up in the run's annotations, not only
3756
+ // buried in the step log — `announce` otherwise reports success while having posted nothing.
3757
+ process.stderr.write(
3758
+ `::warning::code-review announce: could not post the in-progress sticky (${annotationSafe(errMsg(err))}) \u2014 continuing (cosmetic)
3690
3759
  `
3760
+ )
3691
3761
  )
3692
3762
  );
3693
3763
  }