@jphutchins/code-review 0.1.0-alpha.50 → 0.1.0-alpha.52
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 +6 -4
- package/dist/index.js +136 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/schema/VERSIONING.md +28 -1
- package/schema/prices.example.json +29 -5
- package/schema/prices.schema.json +73 -6
- package/templates/comment.eta +15 -2
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npx @jphutchins/code-review <subcommand>
|
|
|
37
37
|
|
|
38
38
|
| Subcommand | What it does |
|
|
39
39
|
| --- | --- |
|
|
40
|
-
| `post` | Post a complete review
|
|
40
|
+
| `post` | Post a complete review from findings + envelope + diff — the one-call path. The findings are listed in the sticky summary; `--inline` renders the in-diff ones as comments on the diff instead. Also renders the review into `$GITHUB_STEP_SUMMARY`: the sticky is overwritten each round, the run summary keeps each round as it stood |
|
|
41
41
|
| `gather` | Resolve the PR from the CI head SHA and gather the review inputs (diff with git-diff fallback, PR context, prior bot review, the prior review's answered-findings registry, failing-job logs) into the workspace for the agent |
|
|
42
42
|
| `parse-command` | Resolve a PR's head from its number and parse a ChatOps trigger comment (`/code-review [24m] [$1.00] <instructions>`) into review overrides — the on-demand comment trigger |
|
|
43
43
|
| `react` | Add/remove a GitHub comment reaction — the ChatOps acknowledgement (👀 on receipt, 🚀 on completion) |
|
|
@@ -96,8 +96,10 @@ links to:
|
|
|
96
96
|
|
|
97
97
|
- **`code-review-findings`** — the findings JSON + result envelope the comment job renders. The
|
|
98
98
|
sticky comment embeds this same JSON directly, base64-encoded, in an
|
|
99
|
-
`<!-- code-review:findings-json;base64 <base64> -->` HTML comment
|
|
100
|
-
|
|
99
|
+
`<!-- code-review:findings-json;base64 <base64> -->` HTML comment. The comment usually renders that
|
|
100
|
+
same review as prose, which is the cheaper read; decode the marker where the prose is not the
|
|
101
|
+
review, where the prose carries only part of it, or where the prose omits a field you need. The directive that rides ahead of the
|
|
102
|
+
marker states that rule.
|
|
101
103
|
Embedding in the comment (rather than only linking the artifact) keeps the pointer from expiring
|
|
102
104
|
with artifact retention; when the encoded findings are too large to embed, the sticky falls back to
|
|
103
105
|
a `<!-- code-review:findings-json <url> -->` link marker instead. The shared serializer is
|
|
@@ -152,7 +154,7 @@ links to:
|
|
|
152
154
|
mechanism frequencies carried in `convergence.rounds` (each entry's `codes` map), from which the
|
|
153
155
|
re-review seed re-derives the advisory `scope_metastasis` entry it hands the next-round agent. Each
|
|
154
156
|
inline review comment embeds only its own finding (a `schema_version` + one-finding fragment), and the
|
|
155
|
-
review-object body
|
|
157
|
+
review-object body links the sticky and the workflow run — so the **sticky is the sole documented decode surface** for
|
|
156
158
|
the whole-document marker; a decoding agent reads it there. The review body is written only after the
|
|
157
159
|
sticky exists (a failed sticky write aborts the run first), so it never carries the blob itself.
|
|
158
160
|
- **`code-review-transcript`** — the full Claude Code session transcripts for the triage and review
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
|
-
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync } from 'fs';
|
|
3
|
+
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
4
4
|
import { randomBytes } from 'crypto';
|
|
5
5
|
import { resolve as resolve$1, join, dirname, basename, extname } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
@@ -282,9 +282,16 @@ var NonEmptyPriceSlots = t.refinement(
|
|
|
282
282
|
(a) => a.length >= 1,
|
|
283
283
|
"NonEmptyPriceSlots"
|
|
284
284
|
);
|
|
285
|
+
var SlottedRequired = t.type({ slots: NonEmptyPriceSlots });
|
|
286
|
+
var SlottedOptional = t.partial({ weekend_slots: NonEmptyPriceSlots });
|
|
287
|
+
var SlottedShape = t.intersection([SlottedRequired, SlottedOptional]);
|
|
288
|
+
var SLOTTED_KEYS = /* @__PURE__ */ new Set([
|
|
289
|
+
...Object.keys(SlottedRequired.props),
|
|
290
|
+
...Object.keys(SlottedOptional.props)
|
|
291
|
+
]);
|
|
285
292
|
var SlottedModelPricesStrict = t.refinement(
|
|
286
|
-
|
|
287
|
-
(s) => Object.keys(s).every((k) => k
|
|
293
|
+
SlottedShape,
|
|
294
|
+
(s) => Object.keys(s).every((k) => SLOTTED_KEYS.has(k)),
|
|
288
295
|
"SlottedModelPricesStrict"
|
|
289
296
|
);
|
|
290
297
|
var SlottedModelPricesCodec = t.exact(SlottedModelPricesStrict);
|
|
@@ -339,6 +346,11 @@ var slotCovers = (slot, minute) => {
|
|
|
339
346
|
const to = hhmmToMinutes(slot.utc_to);
|
|
340
347
|
return from < to ? minute >= from && minute < to : minute >= from || minute < to;
|
|
341
348
|
};
|
|
349
|
+
var BEIJING_OFFSET_MINUTES = 8 * 60;
|
|
350
|
+
var isBeijingWeekend = (instant) => {
|
|
351
|
+
const day = new Date(instant.getTime() + BEIJING_OFFSET_MINUTES * 6e4).getUTCDay();
|
|
352
|
+
return day === 0 || day === 6;
|
|
353
|
+
};
|
|
342
354
|
var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
343
355
|
if (!("slots" in p)) return p;
|
|
344
356
|
if (pricedAt === void 0) {
|
|
@@ -348,10 +360,13 @@ var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
|
348
360
|
return null;
|
|
349
361
|
}
|
|
350
362
|
const minute = utcMinuteOfDay(pricedAt);
|
|
351
|
-
const
|
|
363
|
+
const weekendSlots = p.weekend_slots;
|
|
364
|
+
const useWeekend = weekendSlots != null && isBeijingWeekend(pricedAt);
|
|
365
|
+
const slots = useWeekend ? weekendSlots : p.slots;
|
|
366
|
+
const covering = slots.filter((s) => slotCovers(s, minute));
|
|
352
367
|
if (covering.length === 1) return covering[0] ?? null;
|
|
353
368
|
warn(
|
|
354
|
-
`code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots cover ${hhmmOf(minute)} UTC (expected exactly 1);
|
|
369
|
+
`code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots in \`${useWeekend ? "weekend_slots" : "slots"}\` cover ${hhmmOf(minute)} UTC (expected exactly 1); that array must partition the 24h day with no gap or overlap; cost for this model set to $0`
|
|
355
370
|
);
|
|
356
371
|
return null;
|
|
357
372
|
};
|
|
@@ -507,7 +522,7 @@ var severityEmoji = (s) => {
|
|
|
507
522
|
};
|
|
508
523
|
var EMBED_LIMIT = 42700;
|
|
509
524
|
var FINDINGS_SCHEMA_URL = "https://raw.githubusercontent.com/JPHutchins/code-review/main/schema/findings.schema.json";
|
|
510
|
-
var AGENTS_STOP_DIRECTIVE = `<!-- AGENTS: STOP \u2014 this comment
|
|
525
|
+
var AGENTS_STOP_DIRECTIVE = `<!-- AGENTS: STOP \u2014 this comment carries a code-review findings document in the marker, and usually renders that same review as prose too. Read the prose when it is there: it is the cheaper read. Decode the marker when the prose is not the review (this comment may be a status notice instead), when the prose is only part of it (findings anchored to diff lines stay on the diff, and a review too large to embed links its artifact here rather than carrying it \u2014 the workflow run's summary renders both of those whole), or when you need a field the prose does not render. Read the document's schema_version and fetch the schema for THAT version before acting \u2014 a schema's own $id is its canonical URL, and the current version is at ${FINDINGS_SCHEMA_URL} \u2014 then parse the WHOLE findings document, not only the fields you recognize. -->`;
|
|
511
526
|
var b64LengthOf = (document) => Buffer.from(JSON.stringify(document), "utf-8").toString("base64").length;
|
|
512
527
|
var encodeMarker = (document, jsonUrl, limit) => {
|
|
513
528
|
const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
|
|
@@ -848,15 +863,17 @@ ${findings}` : void 0;
|
|
|
848
863
|
return [findingsBlock, reviewedSha, reviewedRoute, completedAncestor, convergence, rounds, signal].filter((m) => m !== void 0).join("\n\n");
|
|
849
864
|
};
|
|
850
865
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
851
|
-
var projectPatch = (patch) => {
|
|
866
|
+
var projectPatch = (patch, surface) => {
|
|
852
867
|
if (patch === void 0) return { kind: "none" };
|
|
853
|
-
const lowered = patchToSuggestion(patch);
|
|
868
|
+
const lowered = surface === "diff-anchored" ? patchToSuggestion(patch) : void 0;
|
|
854
869
|
return typeof lowered === "string" ? { kind: "suggestion", text: escapeFence(lowered) } : { kind: "patch", raw: escapeFence(patch) };
|
|
855
870
|
};
|
|
856
871
|
var formatConfidence = (n) => n.toFixed(2);
|
|
857
|
-
var reviewBodyPointer = (headSha, stickyUrl) => {
|
|
872
|
+
var reviewBodyPointer = (headSha, stickyUrl, runUrl, runHasSummary) => {
|
|
858
873
|
const sha7 = headSha.slice(0, 7);
|
|
859
|
-
|
|
874
|
+
const summary = stickyUrl ? `the [summary comment](${stickyUrl})` : "the summary comment";
|
|
875
|
+
const run = runUrl ? ` See the [workflow run](${runUrl}) for ${runHasSummary ? "this round's review in full, its job log," : "the job log"} and the findings artifact.` : "";
|
|
876
|
+
return `\u{1F916} Automated code review for \`${sha7}\` \u2014 see ${summary} for the verdict, walkthrough, and cost.${run}`;
|
|
860
877
|
};
|
|
861
878
|
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
862
879
|
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
@@ -1195,7 +1212,7 @@ var sanitizeFinding = (f, answeredNotes) => {
|
|
|
1195
1212
|
...f,
|
|
1196
1213
|
title: escapePipes(f.title),
|
|
1197
1214
|
path: escapeCodeBackticks(f.path),
|
|
1198
|
-
patchProjection: projectPatch(f.patch),
|
|
1215
|
+
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1199
1216
|
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
|
|
1200
1217
|
};
|
|
1201
1218
|
};
|
|
@@ -1267,6 +1284,7 @@ var render = (input) => {
|
|
|
1267
1284
|
systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
|
|
1268
1285
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
1269
1286
|
inlineDisposition: input.inlineDisposition ?? null,
|
|
1287
|
+
unverifiedNoLogs: input.unverifiedNoLogs === true,
|
|
1270
1288
|
runUrl: input.runUrl ?? null,
|
|
1271
1289
|
jsonUrl: input.jsonUrl ?? null,
|
|
1272
1290
|
// The blob is the agent's complete document with the pipeline-stamped convergence field inside it
|
|
@@ -1365,7 +1383,7 @@ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRoo
|
|
|
1365
1383
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
1366
1384
|
eta.renderString(template, {
|
|
1367
1385
|
...f,
|
|
1368
|
-
patchProjection: projectPatch(f.patch),
|
|
1386
|
+
patchProjection: projectPatch(f.patch, "diff-anchored"),
|
|
1369
1387
|
severityEmoji,
|
|
1370
1388
|
formatConfidence,
|
|
1371
1389
|
modelsText,
|
|
@@ -2273,31 +2291,43 @@ var commentPayload = (c) => ({
|
|
|
2273
2291
|
...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
|
|
2274
2292
|
body: formatMarkdown(c.body)
|
|
2275
2293
|
});
|
|
2276
|
-
var postInlineReview = async (
|
|
2277
|
-
const pointer = reviewBodyPointer(
|
|
2294
|
+
var postInlineReview = async (pr, comments, inDiff, ghApi) => {
|
|
2295
|
+
const pointer = reviewBodyPointer(
|
|
2296
|
+
pr.headSha,
|
|
2297
|
+
pr.stickyUrl,
|
|
2298
|
+
pr.runUrl,
|
|
2299
|
+
(process.env["GITHUB_STEP_SUMMARY"] ?? "") !== ""
|
|
2300
|
+
);
|
|
2278
2301
|
const reviewBody = (withComments) => JSON.stringify({
|
|
2279
2302
|
body: pointer,
|
|
2280
|
-
commit_id: headSha,
|
|
2303
|
+
commit_id: pr.headSha,
|
|
2281
2304
|
event: "COMMENT",
|
|
2282
2305
|
comments: withComments ? comments.map(commentPayload) : []
|
|
2283
2306
|
});
|
|
2284
|
-
const reviewsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/reviews`, "--input", "-"];
|
|
2307
|
+
const reviewsEndpoint = [`repos/${pr.repo}/pulls/${String(pr.prNumber)}/reviews`, "--input", "-"];
|
|
2285
2308
|
try {
|
|
2286
2309
|
const stdout = await ghApi(reviewsEndpoint, reviewBody(true));
|
|
2287
2310
|
return { url: parseHtmlUrl(stdout), inlinePosted: comments.length, unposted: [] };
|
|
2288
2311
|
} catch (err) {
|
|
2289
2312
|
if (comments.length === 0) throw err;
|
|
2290
2313
|
process.stderr.write(
|
|
2291
|
-
`Warning: the batched inline review on PR #${String(prNumber)} was rejected (${errMsg(err)}) \u2014 posting the review body-only, then each comment individually to keep the ones GitHub accepts (issue #57)
|
|
2314
|
+
`Warning: the batched inline review on PR #${String(pr.prNumber)} was rejected (${errMsg(err)}) \u2014 posting the review body-only, then each comment individually to keep the ones GitHub accepts (issue #57)
|
|
2292
2315
|
`
|
|
2293
2316
|
);
|
|
2294
2317
|
const url = parseHtmlUrl(await ghApi(reviewsEndpoint, reviewBody(false)));
|
|
2295
|
-
const commentsEndpoint = [
|
|
2318
|
+
const commentsEndpoint = [
|
|
2319
|
+
`repos/${pr.repo}/pulls/${String(pr.prNumber)}/comments`,
|
|
2320
|
+
"--input",
|
|
2321
|
+
"-"
|
|
2322
|
+
];
|
|
2296
2323
|
const unposted = [];
|
|
2297
2324
|
let inlinePosted = 0;
|
|
2298
2325
|
for (const [i, c] of comments.entries()) {
|
|
2299
2326
|
try {
|
|
2300
|
-
await ghApi(
|
|
2327
|
+
await ghApi(
|
|
2328
|
+
commentsEndpoint,
|
|
2329
|
+
JSON.stringify({ commit_id: pr.headSha, ...commentPayload(c) })
|
|
2330
|
+
);
|
|
2301
2331
|
inlinePosted += 1;
|
|
2302
2332
|
} catch (e) {
|
|
2303
2333
|
const finding = inDiff[i];
|
|
@@ -2351,6 +2381,18 @@ var postComment = async (repo, prNumber, body, ghApi) => {
|
|
|
2351
2381
|
);
|
|
2352
2382
|
return parseCommentRef(stdout);
|
|
2353
2383
|
};
|
|
2384
|
+
var appendRunSummary = (summaryPath, body) => {
|
|
2385
|
+
if (summaryPath === void 0 || summaryPath === "") return;
|
|
2386
|
+
const rendered = body();
|
|
2387
|
+
try {
|
|
2388
|
+
appendFileSync(summaryPath, `
|
|
2389
|
+
${rendered}
|
|
2390
|
+
`);
|
|
2391
|
+
} catch (err) {
|
|
2392
|
+
process.stderr.write(`Warning: could not write the run summary: ${errMsg(err)}
|
|
2393
|
+
`);
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2354
2396
|
var upsertSticky = async (repo, prNumber, existing, body, ghApi) => {
|
|
2355
2397
|
if (existing !== null) {
|
|
2356
2398
|
const patched = await patchComment(repo, existing.id, body, ghApi);
|
|
@@ -2388,6 +2430,7 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
|
2388
2430
|
"--input",
|
|
2389
2431
|
"-"
|
|
2390
2432
|
],
|
|
2433
|
+
// GitHub caps a dismissal message at 140 chars.
|
|
2391
2434
|
JSON.stringify({ message: "Superseded by a new review for an updated commit." })
|
|
2392
2435
|
);
|
|
2393
2436
|
} catch (err) {
|
|
@@ -2545,7 +2588,7 @@ ${conv}`;
|
|
|
2545
2588
|
noticeBody(
|
|
2546
2589
|
`${DEFAULT_MARKER}
|
|
2547
2590
|
|
|
2548
|
-
\u26A0\uFE0F **CI-fix pass completed with no findings** for \`${input.headSha.slice(0, 7)}
|
|
2591
|
+
\u26A0\uFE0F **CI-fix pass completed with no findings** for \`${input.headSha.slice(0, 7)}\`${input.unverifiedNoLogs === true ? ' \u2014 **but no failing-job logs were available**, so it had only the diff to work from and "no findings" is not evidence of none' : ""} \u2014 the completed full review of \`${priorSha ? priorSha.slice(0, 7) : "an earlier commit"}\` is preserved below.${dropNote ? `
|
|
2549
2592
|
|
|
2550
2593
|
${dropNote}` : ""}`,
|
|
2551
2594
|
sticky.body
|
|
@@ -2560,7 +2603,7 @@ ${dropNote}` : ""}`,
|
|
|
2560
2603
|
throw new Error(`Price map at ${input.pricesPath} does not match the expected shape`);
|
|
2561
2604
|
}
|
|
2562
2605
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
2563
|
-
const
|
|
2606
|
+
const inlineRequested = input.inline === true;
|
|
2564
2607
|
const renderNotice = (message) => {
|
|
2565
2608
|
const findings2 = stampConvergence(incompleteFindings(`### \u26A0\uFE0F ${message}`), priorConv);
|
|
2566
2609
|
return formatMarkdown(
|
|
@@ -2677,19 +2720,27 @@ ${dropNote}` : ""}`,
|
|
|
2677
2720
|
convergenceRound: false,
|
|
2678
2721
|
testReport,
|
|
2679
2722
|
clocDiff,
|
|
2680
|
-
inlineDisposition: { kind: "no-envelope" },
|
|
2723
|
+
inlineDisposition: inlineRequested ? { kind: "no-envelope" } : { kind: "disabled" },
|
|
2681
2724
|
runUrl: input.runUrl,
|
|
2725
|
+
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
2682
2726
|
jsonUrl: input.jsonUrl,
|
|
2683
2727
|
findingsPointer: findingsBlob(stampedFindings2),
|
|
2684
2728
|
postedAt: input.postedAt
|
|
2685
2729
|
})
|
|
2686
2730
|
);
|
|
2687
2731
|
await upsertSticky(input.repo, prNumber, existingSticky, body, ghApi);
|
|
2732
|
+
appendRunSummary(process.env["GITHUB_STEP_SUMMARY"], () => body);
|
|
2733
|
+
if (inlineRequested) {
|
|
2734
|
+
process.stderr.write(
|
|
2735
|
+
"Warning: inline: true was requested, but the result envelope is missing \u2014 inline comments cannot be built; the findings are in the sticky and the run summary instead\n"
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2688
2738
|
process.stderr.write(
|
|
2689
|
-
"Result envelope missing or malformed \u2014 posted sticky summary without usage/cost data
|
|
2739
|
+
"Result envelope missing or malformed \u2014 posted sticky summary without usage/cost data\n"
|
|
2690
2740
|
);
|
|
2691
2741
|
process.exit(0);
|
|
2692
2742
|
}
|
|
2743
|
+
const inlineTemplate = inlineRequested ? readFileSync(input.inlineTemplatePath, "utf-8") : "";
|
|
2693
2744
|
const thisIncomplete = envelope.incomplete === true || isIncompleteFindings(findings);
|
|
2694
2745
|
if (wouldBuryCompleted(thisIncomplete)) {
|
|
2695
2746
|
logAnsweredDrops();
|
|
@@ -2703,14 +2754,14 @@ ${dropNote}` : ""}`,
|
|
|
2703
2754
|
comments: rawComments,
|
|
2704
2755
|
strays,
|
|
2705
2756
|
inDiff
|
|
2706
|
-
} = buildInlineComments(visibleFindings, diff, {
|
|
2757
|
+
} = inlineRequested ? buildInlineComments(visibleFindings, diff, {
|
|
2707
2758
|
inlineTemplate,
|
|
2708
2759
|
models: envelope.models.map((m) => m.model),
|
|
2709
2760
|
findings,
|
|
2710
2761
|
jsonUrl: input.jsonUrl,
|
|
2711
2762
|
sameRootNotes,
|
|
2712
2763
|
answeredNotes: reRaisedNotes
|
|
2713
|
-
});
|
|
2764
|
+
}) : { comments: [], strays: visibleFindings, inDiff: [] };
|
|
2714
2765
|
const { comments, longFiles } = checkLongSuggestions(rawComments);
|
|
2715
2766
|
for (const wf of longFiles) {
|
|
2716
2767
|
process.stderr.write(
|
|
@@ -2719,7 +2770,7 @@ ${dropNote}` : ""}`,
|
|
|
2719
2770
|
);
|
|
2720
2771
|
}
|
|
2721
2772
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
2722
|
-
const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
2773
|
+
const initialDisposition = !inlineRequested ? { kind: "disabled" } : comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
2723
2774
|
const currentCounts = computeSeverityCounts(findings.findings);
|
|
2724
2775
|
const currentCodes = computeCodeCounts(findings.findings, findings.systemic_problems ?? []);
|
|
2725
2776
|
const roundNumber = priorRoundCount + 1;
|
|
@@ -2767,6 +2818,7 @@ ${dropNote}` : ""}`,
|
|
|
2767
2818
|
strays,
|
|
2768
2819
|
suppressedNits,
|
|
2769
2820
|
runUrl: input.runUrl,
|
|
2821
|
+
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
2770
2822
|
jsonUrl: input.jsonUrl,
|
|
2771
2823
|
findingsPointer: findingsMarker,
|
|
2772
2824
|
postedAt: input.postedAt,
|
|
@@ -2805,16 +2857,20 @@ ${dropNote}` : ""}`,
|
|
|
2805
2857
|
inlinePosted,
|
|
2806
2858
|
unposted
|
|
2807
2859
|
} = await postInlineReview(
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2860
|
+
{
|
|
2861
|
+
repo: input.repo,
|
|
2862
|
+
prNumber,
|
|
2863
|
+
headSha: input.headSha,
|
|
2864
|
+
stickyUrl: stickyRef?.url,
|
|
2865
|
+
runUrl: input.runUrl
|
|
2866
|
+
},
|
|
2811
2867
|
comments,
|
|
2812
2868
|
inDiff,
|
|
2813
|
-
stickyRef?.url,
|
|
2814
2869
|
ghApi
|
|
2815
2870
|
);
|
|
2816
2871
|
process.stderr.write(
|
|
2817
|
-
`Posted a review with ${String(inlinePosted)} inline comment(s) on PR #${String(prNumber)}
|
|
2872
|
+
inlineRequested ? `Posted a review with ${String(inlinePosted)} inline comment(s) on PR #${String(prNumber)}
|
|
2873
|
+
` : `Posted a body-only review on PR #${String(prNumber)}; the findings are in the sticky
|
|
2818
2874
|
`
|
|
2819
2875
|
);
|
|
2820
2876
|
const priorReviewIds = botReviews.map((r) => r.id);
|
|
@@ -2844,6 +2900,14 @@ ${dropNote}` : ""}`,
|
|
|
2844
2900
|
);
|
|
2845
2901
|
}
|
|
2846
2902
|
}
|
|
2903
|
+
appendRunSummary(
|
|
2904
|
+
process.env["GITHUB_STEP_SUMMARY"],
|
|
2905
|
+
() => renderBody(
|
|
2906
|
+
{ kind: "whole-document", inlineCount: inlinePosted, rejectedCount: unanchoredCount },
|
|
2907
|
+
reviewUrl,
|
|
2908
|
+
visibleFindings
|
|
2909
|
+
)
|
|
2910
|
+
);
|
|
2847
2911
|
};
|
|
2848
2912
|
var noticeBody = (lead, existingBody) => {
|
|
2849
2913
|
const carried = existingBody ? carryForwardMarkers(existingBody) : "";
|
|
@@ -3216,6 +3280,8 @@ conclusion=${result.conclusion}
|
|
|
3216
3280
|
diff_size=${String(result.diffSize)}
|
|
3217
3281
|
stacked=${String(result.stacked)}
|
|
3218
3282
|
base_sha=${result.baseSha}
|
|
3283
|
+
staged_job_logs=${String(result.stagedJobLogs)}
|
|
3284
|
+
failing_jobs=${String(result.failingJobs)}
|
|
3219
3285
|
`;
|
|
3220
3286
|
}
|
|
3221
3287
|
};
|
|
@@ -3244,7 +3310,7 @@ var IssueCommentCodec = t.intersection([
|
|
|
3244
3310
|
})
|
|
3245
3311
|
]);
|
|
3246
3312
|
var JobCodec = t.type({ id: t.number, conclusion: t.union([t.string, t.null]) });
|
|
3247
|
-
var
|
|
3313
|
+
var JOBS_JQ = ".jobs[] | {id: .id, conclusion: .conclusion}";
|
|
3248
3314
|
var fetchPrMeta = async (repo, prNumber, ghApi) => {
|
|
3249
3315
|
const stdout = await ghApi([
|
|
3250
3316
|
`repos/${repo}/pulls/${String(prNumber)}`,
|
|
@@ -3391,16 +3457,23 @@ var reviewsFrom = (reviews, botLogin) => boundedHuman(reviews, botLogin, "review
|
|
|
3391
3457
|
state: r.state ?? null,
|
|
3392
3458
|
body: clip(r.body)
|
|
3393
3459
|
}));
|
|
3460
|
+
var MAX_STAGED_JOB_LOGS = 20;
|
|
3394
3461
|
var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
3395
|
-
const
|
|
3396
|
-
|
|
3462
|
+
const rows = parseJsonl(
|
|
3463
|
+
await ghApi([`repos/${repo}/actions/runs/${runId}/jobs`, "--paginate", "--jq", JOBS_JQ])
|
|
3464
|
+
);
|
|
3465
|
+
const decoded = t.array(JobCodec).decode(rows);
|
|
3397
3466
|
if (decoded._tag === "Left") {
|
|
3398
3467
|
throw new Error(`Jobs list for run ${runId} did not match the expected shape`);
|
|
3399
3468
|
}
|
|
3400
|
-
|
|
3469
|
+
const failing = decoded.right.filter((j) => j.conclusion === "failure");
|
|
3470
|
+
const selected = failing.slice(0, MAX_STAGED_JOB_LOGS);
|
|
3471
|
+
let staged = 0;
|
|
3472
|
+
for (const job of selected) {
|
|
3401
3473
|
try {
|
|
3402
3474
|
const log = await ghApi([`repos/${repo}/actions/jobs/${String(job.id)}/logs`]);
|
|
3403
3475
|
writeFileSync(join(outDir, `job_${String(job.id)}.log`), log);
|
|
3476
|
+
staged += 1;
|
|
3404
3477
|
} catch (err) {
|
|
3405
3478
|
process.stderr.write(
|
|
3406
3479
|
`Warning: failed to download logs for job ${String(job.id)}: ${errMsg(err)} \u2014 continuing with the logs retrieved so far
|
|
@@ -3408,6 +3481,19 @@ var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
|
3408
3481
|
);
|
|
3409
3482
|
}
|
|
3410
3483
|
}
|
|
3484
|
+
if (failing.length > selected.length) {
|
|
3485
|
+
process.stderr.write(
|
|
3486
|
+
`::warning::${annotationSafe(`${String(failing.length)} failing job(s) in run ${runId}; staged ${String(staged)} of the first ${String(selected.length)} log(s) \u2014 the review does not see the rest`)}
|
|
3487
|
+
`
|
|
3488
|
+
);
|
|
3489
|
+
}
|
|
3490
|
+
if (staged === 0) {
|
|
3491
|
+
process.stderr.write(
|
|
3492
|
+
`::warning::${annotationSafe(`No failing-job logs could be staged for run ${runId} (${String(failing.length)} failing job(s) reported) \u2014 the review has only the diff to work from`)}
|
|
3493
|
+
`
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
return { staged, failing: failing.length };
|
|
3411
3497
|
};
|
|
3412
3498
|
var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
3413
3499
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
@@ -3477,16 +3563,16 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
3477
3563
|
reviews: reviews === null ? [] : reviewsFrom(reviews, input.botLogin)
|
|
3478
3564
|
})
|
|
3479
3565
|
);
|
|
3480
|
-
|
|
3481
|
-
await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi);
|
|
3482
|
-
}
|
|
3566
|
+
const jobLogs = input.conclusion === "failure" ? await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi) : { staged: 0, failing: 0 };
|
|
3483
3567
|
return {
|
|
3484
3568
|
kind: "gathered",
|
|
3485
3569
|
pr: prNumber,
|
|
3486
3570
|
conclusion: input.conclusion,
|
|
3487
3571
|
diffSize: Buffer.byteLength(prDiff, "utf8"),
|
|
3488
3572
|
stacked,
|
|
3489
|
-
baseSha: meta.base_sha
|
|
3573
|
+
baseSha: meta.base_sha,
|
|
3574
|
+
stagedJobLogs: jobLogs.staged,
|
|
3575
|
+
failingJobs: jobLogs.failing
|
|
3490
3576
|
};
|
|
3491
3577
|
};
|
|
3492
3578
|
|
|
@@ -5146,7 +5232,7 @@ var postCmd = defineCommand({
|
|
|
5146
5232
|
},
|
|
5147
5233
|
"run-url": {
|
|
5148
5234
|
type: "string",
|
|
5149
|
-
description: "Workflow run URL (transcript/traces), rendered as a link in the LLM Disclosure aside"
|
|
5235
|
+
description: "Workflow run URL (transcript/traces), rendered as a link in the LLM Disclosure aside and in the review-object body"
|
|
5150
5236
|
},
|
|
5151
5237
|
"json-url": {
|
|
5152
5238
|
type: "string",
|
|
@@ -5159,6 +5245,14 @@ var postCmd = defineCommand({
|
|
|
5159
5245
|
"nit-visibility-floor": {
|
|
5160
5246
|
type: "string",
|
|
5161
5247
|
description: NIT_VISIBILITY_FLOOR_DESCRIPTION
|
|
5248
|
+
},
|
|
5249
|
+
inline: {
|
|
5250
|
+
type: "boolean",
|
|
5251
|
+
description: "Also render findings as inline review comments on the diff. Off by default: an inline thread cannot be revised by a later round, so stale threads accumulate. The review object is posted either way; with this off the sticky lists the findings instead"
|
|
5252
|
+
},
|
|
5253
|
+
"unverified-no-logs": {
|
|
5254
|
+
type: "boolean",
|
|
5255
|
+
description: "Mark the review unverified: the fast-fix route ran with no failing-job logs staged, so its findings came from the diff alone. The caller decides this \u2014 the logs are staged in the review job, not here"
|
|
5162
5256
|
}
|
|
5163
5257
|
},
|
|
5164
5258
|
run: async ({ args }) => {
|
|
@@ -5182,6 +5276,8 @@ var postCmd = defineCommand({
|
|
|
5182
5276
|
jsonUrl: args["json-url"],
|
|
5183
5277
|
convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
|
|
5184
5278
|
nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
|
|
5279
|
+
inline: args.inline,
|
|
5280
|
+
unverifiedNoLogs: args["unverified-no-logs"],
|
|
5185
5281
|
postedAt: formatUtc(/* @__PURE__ */ new Date()),
|
|
5186
5282
|
pricedAt: /* @__PURE__ */ new Date()
|
|
5187
5283
|
});
|