@jphutchins/code-review 0.1.0-alpha.31 → 0.1.0-alpha.33

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
@@ -1068,9 +1068,9 @@ var formatErrors = (errors) => errors.map(describeValidationError);
1068
1068
  var declaredVersion = (raw) => typeof raw === "object" && raw !== null && "schema_version" in raw ? typeof raw.schema_version === "string" ? raw.schema_version : void 0 : void 0;
1069
1069
  var supportedVersions = (kind) => tableFor(kind).map((entry) => entry.minor);
1070
1070
  var defaultVersion = (kind) => {
1071
- const latest = tableFor(kind).find((entry) => entry.latest);
1072
- if (!latest) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
1073
- return latest.defaultVersion;
1071
+ const latest2 = tableFor(kind).find((entry) => entry.latest);
1072
+ if (!latest2) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
1073
+ return latest2.defaultVersion;
1074
1074
  };
1075
1075
  var bundledSchemaPath = (relativePath) => resolve$1(import.meta.dirname, "..", "schema", relativePath);
1076
1076
  var schemaPathFor = (kind, version) => {
@@ -1105,27 +1105,61 @@ var resolvers = {
1105
1105
  prices: (raw) => resolveSingleVersion("prices", raw)
1106
1106
  };
1107
1107
  var resolve = (kind, raw) => resolvers[kind](raw);
1108
- var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
1109
- 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) => {
1110
1128
  const child = execFile(
1111
- "gh",
1112
- ["api", ...args],
1113
- { 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
+ },
1114
1138
  (err, stdout, stderr) => {
1115
- if (err) {
1116
- const stderrStr = typeof stderr === "string" && stderr.trim() ? stderr.trim() : "";
1117
- const errStr = err instanceof Error ? err.message : "unknown error";
1118
- reject(new Error(`gh api ${describeEndpoint(args)} failed: ${stderrStr || errStr}`));
1119
- } else {
1120
- resolve3(stdout);
1121
- }
1139
+ if (err)
1140
+ reject(
1141
+ new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
1142
+ );
1143
+ else resolve3(stdout);
1122
1144
  }
1123
1145
  );
1124
- if (stdin !== void 0) {
1125
- child.stdin?.end(stdin);
1146
+ if (spec.stdin !== void 0) {
1147
+ child.stdin?.on("error", () => void 0);
1148
+ child.stdin?.end(spec.stdin);
1126
1149
  }
1127
1150
  });
1128
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
+
1129
1163
  // src/pr.ts
1130
1164
  var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
1131
1165
  var parseCandidates = (stdout) => parseJsonl(stdout);
@@ -1725,6 +1759,134 @@ var announce = async (input, ghApi = runGhApi) => {
1725
1759
  ghApi
1726
1760
  );
1727
1761
  };
1762
+ var incompleteBody = (headSha, runUrl, existingBody) => {
1763
+ const notice = `${DEFAULT_MARKER}
1764
+
1765
+ \u26A0\uFE0F **Code review did not complete** for \`${headSha.slice(0, 7)}\` \u2014 the review job failed ([run](${runUrl})). Re-request the review; do not treat this round as spent.`;
1766
+ const carried = existingBody ? carryForwardMarkers(existingBody) : "";
1767
+ return carried ? `${notice}
1768
+
1769
+ ${carried}` : notice;
1770
+ };
1771
+ var reportIncomplete = async (input, ghApi = runGhApi) => {
1772
+ const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
1773
+ const resolution = resolvePr(candidates, input.headBranch);
1774
+ if (resolution.kind !== "open") {
1775
+ process.stderr.write(
1776
+ `No open PR for ${input.headSha} \u2014 nothing to report (${resolution.kind})
1777
+ `
1778
+ );
1779
+ return;
1780
+ }
1781
+ const existing = await findBotComment(
1782
+ input.repo,
1783
+ resolution.prNumber,
1784
+ input.botLogin,
1785
+ DEFAULT_MARKER,
1786
+ ghApi
1787
+ );
1788
+ if (existing !== null && parseReviewComplete(existing.body)) {
1789
+ process.stderr.write(`Sticky already reflects a completed review \u2014 leaving it in place
1790
+ `);
1791
+ return;
1792
+ }
1793
+ if (existing !== null && !existing.body.includes(input.runUrl)) {
1794
+ process.stderr.write(`Sticky belongs to another run \u2014 leaving it in place
1795
+ `);
1796
+ return;
1797
+ }
1798
+ await upsertSticky(
1799
+ input.repo,
1800
+ resolution.prNumber,
1801
+ existing,
1802
+ incompleteBody(input.headSha, input.runUrl, existing?.body),
1803
+ ghApi
1804
+ );
1805
+ };
1806
+
1807
+ // src/checkrun.ts
1808
+ var CHECK_RUN_NAME = "Code review";
1809
+ var latest = (checks) => checks.reduce(
1810
+ (best, c) => best === null || c.id > best.id ? c : best,
1811
+ null
1812
+ );
1813
+ var isOpen = (status) => status === "in_progress" || status === "queued";
1814
+ var settled = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
1815
+ var decideCheckAction = (checks, intent) => {
1816
+ const head = latest(checks);
1817
+ switch (intent) {
1818
+ case "in_progress":
1819
+ return head !== null && isOpen(head.status) ? { kind: "noop", reason: "a check is already in progress for this head" } : { kind: "create", status: "in_progress" };
1820
+ case "neutral":
1821
+ if (head === null) return { kind: "create", status: "completed", conclusion: "neutral" };
1822
+ return head.status === "completed" && head.conclusion === "neutral" ? { kind: "noop", reason: "the check already records this completed review" } : { kind: "patch", id: head.id, status: "completed", conclusion: "neutral" };
1823
+ case "failure":
1824
+ if (head === null) return { kind: "create", status: "completed", conclusion: "failure" };
1825
+ if (head.status === "completed" && head.conclusion !== null && settled.has(head.conclusion))
1826
+ return { kind: "noop", reason: "a completed review already recorded this head" };
1827
+ return head.status === "completed" && head.conclusion === "failure" ? { kind: "noop", reason: "the check already records this failure" } : { kind: "patch", id: head.id, status: "completed", conclusion: "failure" };
1828
+ }
1829
+ };
1830
+ var CHECK_JQ = ".check_runs[] | {id: .id, status: .status, conclusion: .conclusion}";
1831
+ var fetchChecks = async (repo, headSha, ghApi) => parseJsonl(
1832
+ await ghApi([
1833
+ `repos/${repo}/commits/${headSha}/check-runs?check_name=${encodeURIComponent(CHECK_RUN_NAME)}&per_page=100`,
1834
+ "--paginate",
1835
+ "--jq",
1836
+ CHECK_JQ
1837
+ ])
1838
+ );
1839
+ var output = (intent, runUrl) => {
1840
+ switch (intent) {
1841
+ case "in_progress":
1842
+ return {
1843
+ title: "Code review in progress",
1844
+ summary: `The review is running \u2014 [see the run](${runUrl}).`
1845
+ };
1846
+ case "neutral":
1847
+ return {
1848
+ title: "Code review complete",
1849
+ summary: `The review was posted \u2014 [see the run](${runUrl}).`
1850
+ };
1851
+ case "failure":
1852
+ return {
1853
+ title: "Code review did not complete",
1854
+ summary: `The review job failed \u2014 [see the run](${runUrl}). Re-request the review; do not treat this round as spent.`
1855
+ };
1856
+ }
1857
+ };
1858
+ var checkRun = async (input, ghApi = runGhApi) => {
1859
+ const action = decideCheckAction(
1860
+ await fetchChecks(input.repo, input.headSha, ghApi),
1861
+ input.intent
1862
+ );
1863
+ if (action.kind === "noop") {
1864
+ process.stderr.write(`code-review check-run: ${action.reason} \u2014 leaving it
1865
+ `);
1866
+ return;
1867
+ }
1868
+ const body = action.kind === "create" ? {
1869
+ name: CHECK_RUN_NAME,
1870
+ head_sha: input.headSha,
1871
+ status: action.status,
1872
+ details_url: input.runUrl,
1873
+ ...action.conclusion ? { conclusion: action.conclusion } : {},
1874
+ output: output(input.intent, input.runUrl)
1875
+ } : {
1876
+ status: action.status,
1877
+ conclusion: action.conclusion,
1878
+ details_url: input.runUrl,
1879
+ output: output(input.intent, input.runUrl)
1880
+ };
1881
+ const endpoint = action.kind === "create" ? [`--method`, `POST`, `repos/${input.repo}/check-runs`, `--input`, `-`] : [
1882
+ `--method`,
1883
+ `PATCH`,
1884
+ `repos/${input.repo}/check-runs/${String(action.id)}`,
1885
+ `--input`,
1886
+ `-`
1887
+ ];
1888
+ await ghApi(endpoint, JSON.stringify(body));
1889
+ };
1728
1890
  var DURATION_RE = /^(\d+)(h|m|s)$/;
1729
1891
  var USD_RE = /^\$(\d+(?:\.\d+)?)$/;
1730
1892
  var toSeconds = (n, unit) => unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
@@ -1931,12 +2093,12 @@ var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
1931
2093
  `
1932
2094
  );
1933
2095
  }
1934
- const latest = runs.filter((r) => r.name === workflowName).reduce(
2096
+ const latest2 = runs.filter((r) => r.name === workflowName).reduce(
1935
2097
  (best, r) => best === null || r.run_number > best.run_number ? r : best,
1936
2098
  null
1937
2099
  );
1938
2100
  return {
1939
- run: latest === null ? null : { id: latest.id, status: latest.status ?? "unknown", conclusion: latest.conclusion },
2101
+ run: latest2 === null ? null : { id: latest2.id, status: latest2.status ?? "unknown", conclusion: latest2.conclusion },
1940
2102
  seenNames: [...new Set(runs.flatMap((r) => r.name === null ? [] : [r.name]))]
1941
2103
  };
1942
2104
  };
@@ -1988,21 +2150,11 @@ stacked=${String(result.stacked)}
1988
2150
  `;
1989
2151
  }
1990
2152
  };
1991
- var runGit = (args) => new Promise((resolve3, reject) => {
1992
- execFile(
1993
- "git",
1994
- [...args],
1995
- { encoding: "utf-8", maxBuffer: 100 * 1024 * 1024 },
1996
- (err, stdout, stderr) => {
1997
- if (err) {
1998
- const stderrStr = typeof stderr === "string" && stderr.trim() ? stderr.trim() : "";
1999
- const errStr = err instanceof Error ? err.message : "unknown error";
2000
- reject(new Error(`git ${args.join(" ")} failed: ${stderrStr || errStr}`));
2001
- } else {
2002
- resolve3(stdout);
2003
- }
2004
- }
2005
- );
2153
+ var runGit = (args) => execFileWithTimeout({
2154
+ command: "git",
2155
+ args,
2156
+ label: `git ${args.join(" ")}`,
2157
+ timeoutMs: subprocessTimeoutMs()
2006
2158
  });
2007
2159
  var PrMetaCodec = t.type({
2008
2160
  changed_files: t.number,
@@ -2698,7 +2850,7 @@ var renderCmd = defineCommand({
2698
2850
  const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
2699
2851
  const template = readFileSync(templatePath, "utf-8");
2700
2852
  const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
2701
- const output = render({
2853
+ const output2 = render({
2702
2854
  findings,
2703
2855
  envelope,
2704
2856
  prices,
@@ -2710,7 +2862,7 @@ var renderCmd = defineCommand({
2710
2862
  testReport,
2711
2863
  postedAt: formatUtc(/* @__PURE__ */ new Date())
2712
2864
  });
2713
- process.stdout.write(output);
2865
+ process.stdout.write(output2);
2714
2866
  }
2715
2867
  });
2716
2868
  var inlineCmd = defineCommand({
@@ -2900,7 +3052,7 @@ var budgetHookCmd = defineCommand({
2900
3052
  const prices = args.prices ? tryReadPrices(args.prices) : null;
2901
3053
  const spentUsd = prices !== null && usage ? computeCost(usage.models, prices).totalCostUSD : null;
2902
3054
  const wallMs = args.wall ? parseWallMs(args.wall) : null;
2903
- const output = evaluateBudgetHook(input, {
3055
+ const output2 = evaluateBudgetHook(input, {
2904
3056
  spentUsd,
2905
3057
  budgetUsd: parseBudgetUsd(args["budget-usd"]),
2906
3058
  elapsedMs: anchoredElapsedMs({
@@ -2924,7 +3076,7 @@ var budgetHookCmd = defineCommand({
2924
3076
  });
2925
3077
  if (asRecord(input)?.["hook_event_name"] === "PostToolBatch" && !isSubagentHookInput(input))
2926
3078
  snapshotIfValid(draftPath);
2927
- process.stdout.write(`${JSON.stringify(output)}
3079
+ process.stdout.write(`${JSON.stringify(output2)}
2928
3080
  `);
2929
3081
  } catch (err) {
2930
3082
  process.stderr.write(`code-review budget-hook: degrading to no-op \u2014 ${errMsg(err)}
@@ -3290,7 +3442,7 @@ var noticeCmd = defineCommand({
3290
3442
  run: ({ args }) => {
3291
3443
  if (!isNoticeKind(args.kind)) {
3292
3444
  process.stderr.write(
3293
- `::warning::code-review notice: unrecognized kind "${args.kind}" \u2014 the pinned CLI is older than the workflow calling it; rendering a generic incomplete notice
3445
+ `::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
3294
3446
  `
3295
3447
  );
3296
3448
  process.stdout.write(`${JSON.stringify(buildUnknownNoticeEnvelope(args.kind), null, 2)}
@@ -3738,6 +3890,92 @@ var announceCmd = defineCommand({
3738
3890
  );
3739
3891
  }
3740
3892
  });
3893
+ var isCheckIntent = (s) => s === "in_progress" || s === "neutral" || s === "failure";
3894
+ var checkRunCmd = defineCommand({
3895
+ meta: {
3896
+ name: "check-run",
3897
+ description: "Upsert the native 'Code review' check-run on the head SHA \u2014 the attribution surface that appears in the PR's own checks list and (writing to the base repo) works for fork PRs too. `in_progress` at review start, `neutral` when the review completes, `failure` when it didn't. Forward-only: `failure` never overwrites a completed review."
3898
+ },
3899
+ args: {
3900
+ repo: { type: "string", description: "Repository (owner/name)", required: true },
3901
+ "head-sha": {
3902
+ type: "string",
3903
+ description: "Head SHA the check-run is anchored to",
3904
+ required: true
3905
+ },
3906
+ status: {
3907
+ type: "positional",
3908
+ description: "One of: in_progress, neutral, failure",
3909
+ required: true
3910
+ },
3911
+ "run-url": {
3912
+ type: "string",
3913
+ description: "Workflow run URL the check-run's details link to",
3914
+ required: true
3915
+ }
3916
+ },
3917
+ run: async ({ args }) => {
3918
+ if (!isCheckIntent(args.status)) {
3919
+ process.stderr.write(
3920
+ `::warning::code-review check-run: unrecognized status "${annotationSafe(args.status)}" \u2014 expected in_progress, neutral, or failure; skipping
3921
+ `
3922
+ );
3923
+ return;
3924
+ }
3925
+ await checkRun({
3926
+ repo: args.repo,
3927
+ headSha: args["head-sha"],
3928
+ intent: args.status,
3929
+ runUrl: args["run-url"]
3930
+ }).catch(
3931
+ (err) => process.stderr.write(
3932
+ `::warning::code-review check-run: could not upsert the check-run (${annotationSafe(errMsg(err))}) \u2014 continuing (attribution aid)
3933
+ `
3934
+ )
3935
+ );
3936
+ }
3937
+ });
3938
+ var reportIncompleteCmd = defineCommand({
3939
+ meta: {
3940
+ name: "report-incomplete",
3941
+ description: "Post (or update) the sticky when a review job hard-failed and posted nothing \u2014 an attributed 'did not complete' notice linking the run, telling the reader to re-request. Never buries a completed review, and never overwrites a superseding run's live in-progress placeholder. (A cancelled review is left to the superseding run that took over.)"
3942
+ },
3943
+ args: {
3944
+ repo: { type: "string", description: "Repository (owner/name)", required: true },
3945
+ "head-sha": {
3946
+ type: "string",
3947
+ description: "Trusted head SHA to resolve the PR (from workflow_run.head_sha)",
3948
+ required: true
3949
+ },
3950
+ "run-url": {
3951
+ type: "string",
3952
+ description: "Workflow run URL the notice links to",
3953
+ required: true
3954
+ },
3955
+ "bot-login": {
3956
+ type: "string",
3957
+ description: "Bot login to trust for the sticky comment upsert (default: github-actions[bot])"
3958
+ },
3959
+ "head-branch": {
3960
+ type: "string",
3961
+ description: "Head branch to disambiguate the PR when multiple share a commit"
3962
+ }
3963
+ },
3964
+ run: async ({ args }) => {
3965
+ await reportIncomplete({
3966
+ repo: args.repo,
3967
+ headSha: args["head-sha"],
3968
+ botLogin: args["bot-login"] || "github-actions[bot]",
3969
+ runUrl: args["run-url"],
3970
+ headBranch: args["head-branch"]
3971
+ }).catch(
3972
+ (err) => process.stderr.write(
3973
+ `::warning::code-review report-incomplete: could not post the failure notice (${annotationSafe(errMsg(err))}) \u2014 continuing
3974
+ `
3975
+ )
3976
+ );
3977
+ }
3978
+ });
3741
3979
  var requireCeilingSec = (raw) => {
3742
3980
  if (raw === void 0) return null;
3743
3981
  const ms = parseWallMs(raw);
@@ -3911,6 +4149,8 @@ var main = defineCommand({
3911
4149
  inline: inlineCmd,
3912
4150
  post: postCmd,
3913
4151
  announce: announceCmd,
4152
+ "check-run": checkRunCmd,
4153
+ "report-incomplete": reportIncompleteCmd,
3914
4154
  cost: costCmd,
3915
4155
  "check-cost": checkCostCmd,
3916
4156
  validate: validateCmd,