@jphutchins/code-review 0.1.0-alpha.27 → 0.1.0-alpha.29
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 +119 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/comment.eta +5 -2
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
|
9
9
|
import _addFormats from 'ajv-formats';
|
|
10
10
|
import * as t from 'io-ts';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
|
+
import { performance } from 'perf_hooks';
|
|
12
13
|
import { PathReporter } from 'io-ts/lib/PathReporter.js';
|
|
13
14
|
|
|
14
15
|
// src/cost.ts
|
|
@@ -185,6 +186,8 @@ var parseReviewedSha = (body) => {
|
|
|
185
186
|
const sha = /<!-- reviewed-sha: ([0-9a-fA-F]{40}) -->/.exec(body)?.[1]?.toLowerCase();
|
|
186
187
|
return sha && sha !== ZERO_SHA ? sha : null;
|
|
187
188
|
};
|
|
189
|
+
var REVIEW_COMPLETE_MARKER = "<!-- review-complete -->";
|
|
190
|
+
var parseReviewComplete = (body) => body.includes(REVIEW_COMPLETE_MARKER);
|
|
188
191
|
var parseFindingsMarker = (body) => {
|
|
189
192
|
const match = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body);
|
|
190
193
|
const b64 = match?.[1];
|
|
@@ -239,6 +242,8 @@ var computeSeverityCounts = (findings) => findings.reduce(
|
|
|
239
242
|
var render = (input) => {
|
|
240
243
|
const eta = new Eta({ autoTrim: false });
|
|
241
244
|
const usageAvailable = input.envelope !== null;
|
|
245
|
+
const hasUsage = input.envelope !== null && input.envelope.models.length > 0;
|
|
246
|
+
const incomplete = input.incomplete ?? input.envelope?.incomplete ?? false;
|
|
242
247
|
const costReport = input.envelope ? computeCost(input.envelope.models, input.prices) : null;
|
|
243
248
|
const pricesProvided = input.pricesProvided ?? true;
|
|
244
249
|
const route = input.route ?? input.envelope?.route ?? null;
|
|
@@ -248,6 +253,8 @@ var render = (input) => {
|
|
|
248
253
|
findings: input.findings,
|
|
249
254
|
envelope: input.envelope,
|
|
250
255
|
usageAvailable,
|
|
256
|
+
hasUsage,
|
|
257
|
+
incomplete,
|
|
251
258
|
costReport,
|
|
252
259
|
pricesProvided,
|
|
253
260
|
route,
|
|
@@ -601,7 +608,12 @@ var ResultEnvelopeCodec = t.intersection([
|
|
|
601
608
|
t.partial({
|
|
602
609
|
vendor_cost_usd: t.union([t.number, t.null]),
|
|
603
610
|
route: t.string,
|
|
604
|
-
effort: t.string
|
|
611
|
+
effort: t.string,
|
|
612
|
+
// The run produced a notice rather than a completed review (security-gate block, agent kill, no
|
|
613
|
+
// recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
|
|
614
|
+
// is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
|
|
615
|
+
// to bury a completed review under it. Absent ⇒ a completed review.
|
|
616
|
+
incomplete: t.boolean
|
|
605
617
|
})
|
|
606
618
|
]);
|
|
607
619
|
var ModelPricesCodec = t.type({
|
|
@@ -958,6 +970,35 @@ var formatMarkdown = (md) => {
|
|
|
958
970
|
};
|
|
959
971
|
var pad2 = (n) => String(n).padStart(2, "0");
|
|
960
972
|
var formatUtc = (d) => `${String(d.getUTCFullYear())}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())} UTC`;
|
|
973
|
+
|
|
974
|
+
// src/notice.ts
|
|
975
|
+
var isNoticeKind = (s) => s === "security-blocked" || s === "setup-failed" || s === "diff-apply-failed" || s === "no-output";
|
|
976
|
+
var blockquote = (text) => text.replaceAll("\n", "\n> ");
|
|
977
|
+
var noticeSummary = (kind, reasons) => {
|
|
978
|
+
switch (kind) {
|
|
979
|
+
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.";
|
|
985
|
+
case "setup-failed":
|
|
986
|
+
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
|
+
case "diff-apply-failed":
|
|
988
|
+
return "### \u26A0\uFE0F Could not apply the diff\n\nThe PR diff could not be applied to the checked-out base commit, so the review was skipped rather than run against an unmodified tree. See workflow logs.";
|
|
989
|
+
case "no-output":
|
|
990
|
+
return "### \u26A0\uFE0F Review did not complete\n\nThe diff passed triage but the review produced no output. See workflow logs.";
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
var buildNoticeEnvelope = (kind, reasons) => ({
|
|
994
|
+
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
995
|
+
findings: noticeFindings(noticeSummary(kind, reasons)),
|
|
996
|
+
models: [],
|
|
997
|
+
turns: 0,
|
|
998
|
+
duration_ms: 0,
|
|
999
|
+
vendor_cost_usd: null,
|
|
1000
|
+
incomplete: true
|
|
1001
|
+
});
|
|
961
1002
|
var identity = (decoded) => decoded;
|
|
962
1003
|
var findingsTable = [
|
|
963
1004
|
{
|
|
@@ -1416,6 +1457,15 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1416
1457
|
DEFAULT_MARKER,
|
|
1417
1458
|
ghApi
|
|
1418
1459
|
);
|
|
1460
|
+
const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
|
|
1461
|
+
const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
|
|
1462
|
+
const leaveInPlace = () => {
|
|
1463
|
+
process.stderr.write(
|
|
1464
|
+
`Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place
|
|
1465
|
+
`
|
|
1466
|
+
);
|
|
1467
|
+
process.exit(0);
|
|
1468
|
+
};
|
|
1419
1469
|
const prices = JSON.parse(readFileSync(input.pricesPath, "utf-8"));
|
|
1420
1470
|
const decodedPrices = PriceMapCodec.decode(prices);
|
|
1421
1471
|
if (decodedPrices._tag === "Left") {
|
|
@@ -1427,6 +1477,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1427
1477
|
render({
|
|
1428
1478
|
findings: noticeFindings(`### \u26A0\uFE0F ${message}`),
|
|
1429
1479
|
envelope: null,
|
|
1480
|
+
incomplete: true,
|
|
1430
1481
|
prices: decodedPrices.right,
|
|
1431
1482
|
pricesProvided: input.pricesProvided,
|
|
1432
1483
|
template,
|
|
@@ -1439,6 +1490,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1439
1490
|
})
|
|
1440
1491
|
);
|
|
1441
1492
|
if (isEmptyDiff(diff)) {
|
|
1493
|
+
if (wouldBuryCompleted(true)) leaveInPlace();
|
|
1442
1494
|
await upsertSticky(
|
|
1443
1495
|
input.repo,
|
|
1444
1496
|
prNumber,
|
|
@@ -1450,6 +1502,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1450
1502
|
}
|
|
1451
1503
|
const findingsResult = loadFindings(input.findingsPath);
|
|
1452
1504
|
if (findingsResult.kind !== "ok") {
|
|
1505
|
+
if (wouldBuryCompleted(true)) leaveInPlace();
|
|
1453
1506
|
await upsertSticky(
|
|
1454
1507
|
input.repo,
|
|
1455
1508
|
prNumber,
|
|
@@ -1486,6 +1539,8 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1486
1539
|
);
|
|
1487
1540
|
process.exit(0);
|
|
1488
1541
|
}
|
|
1542
|
+
const thisIncomplete = envelope.incomplete === true;
|
|
1543
|
+
if (wouldBuryCompleted(thisIncomplete)) leaveInPlace();
|
|
1489
1544
|
const findingsMarker = findingsPointer(findings, input.jsonUrl);
|
|
1490
1545
|
const {
|
|
1491
1546
|
comments: rawComments,
|
|
@@ -1509,6 +1564,7 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1509
1564
|
const commonRenderInput = {
|
|
1510
1565
|
findings,
|
|
1511
1566
|
envelope,
|
|
1567
|
+
incomplete: thisIncomplete,
|
|
1512
1568
|
prices: decodedPrices.right,
|
|
1513
1569
|
pricesProvided: input.pricesProvided,
|
|
1514
1570
|
template,
|
|
@@ -1623,7 +1679,7 @@ var announce = async (input, ghApi = runGhApi) => {
|
|
|
1623
1679
|
DEFAULT_MARKER,
|
|
1624
1680
|
ghApi
|
|
1625
1681
|
);
|
|
1626
|
-
if (existing !== null && parseReviewedSha(existing.body) === input.headSha.toLowerCase()) {
|
|
1682
|
+
if (existing !== null && parseReviewComplete(existing.body) && parseReviewedSha(existing.body) === input.headSha.toLowerCase()) {
|
|
1627
1683
|
process.stderr.write(
|
|
1628
1684
|
`Sticky already reflects a completed review of ${input.headSha} \u2014 leaving it in place
|
|
1629
1685
|
`
|
|
@@ -1829,15 +1885,21 @@ var RunCodec = t.type({
|
|
|
1829
1885
|
conclusion: t.union([t.string, t.null]),
|
|
1830
1886
|
run_number: t.number
|
|
1831
1887
|
});
|
|
1832
|
-
var
|
|
1888
|
+
var RUN_JQ = ".workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, run_number: .run_number}";
|
|
1833
1889
|
var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
|
|
1834
|
-
const
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1890
|
+
const endpoint = `repos/${repo}/actions/runs?head_sha=${headSha}&per_page=100`;
|
|
1891
|
+
const rows = parseJsonl(await ghApi([endpoint, "--paginate", "--jq", RUN_JQ]));
|
|
1892
|
+
const decoded = rows.map((row) => RunCodec.decode(row));
|
|
1893
|
+
const runs = decoded.flatMap((d) => d._tag === "Right" ? [d.right] : []);
|
|
1894
|
+
const dropped = decoded.length - runs.length;
|
|
1895
|
+
if (dropped > 0) {
|
|
1896
|
+
const firstDrift = decoded.find((d) => d._tag === "Left");
|
|
1897
|
+
const detail = firstDrift === void 0 ? "" : ` (${PathReporter.report(firstDrift).join("; ")})`;
|
|
1898
|
+
process.stderr.write(
|
|
1899
|
+
`Warning: ${String(dropped)} of ${String(rows.length)} workflow-run row(s) from ${endpoint} failed to decode${detail} \u2014 excluded from the lookup
|
|
1900
|
+
`
|
|
1839
1901
|
);
|
|
1840
|
-
|
|
1902
|
+
}
|
|
1841
1903
|
const latest = runs.filter((r) => r.name === workflowName).reduce(
|
|
1842
1904
|
(best, r) => best === null || r.run_number > best.run_number ? r : best,
|
|
1843
1905
|
null
|
|
@@ -1848,21 +1910,34 @@ var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
|
|
|
1848
1910
|
};
|
|
1849
1911
|
};
|
|
1850
1912
|
var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi, sleep: defaultSleep, elapsedMs: monotonicElapsed() }) => {
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1913
|
+
const safeResolve = async () => {
|
|
1914
|
+
try {
|
|
1915
|
+
return await resolveCiRun(repo, headSha, options.workflowName, deps.ghApi);
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
process.stderr.write(
|
|
1918
|
+
`Warning: CI-run lookup for ${headSha} failed (${errMsg(err)}) \u2014 retrying until the timeout
|
|
1919
|
+
`
|
|
1920
|
+
);
|
|
1921
|
+
return { run: null, seenNames: [] };
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
const poll = async (lastSeenNames, lastRunId) => {
|
|
1925
|
+
const { run, seenNames } = await safeResolve();
|
|
1926
|
+
if (run !== null && run.status === "completed" && run.conclusion !== null)
|
|
1927
|
+
return { kind: "concluded", conclusion: run.conclusion, runId: run.id };
|
|
1928
|
+
const runId = run === null ? lastRunId : run.id;
|
|
1929
|
+
const names = seenNames.length > 0 ? seenNames : lastSeenNames;
|
|
1855
1930
|
if (deps.elapsedMs() >= options.timeoutMs)
|
|
1856
|
-
return { kind: "timed-out", runId
|
|
1931
|
+
return { kind: "timed-out", runId, seenNames: names };
|
|
1857
1932
|
await deps.sleep(options.pollIntervalMs);
|
|
1858
|
-
return poll();
|
|
1933
|
+
return poll(names, runId);
|
|
1859
1934
|
};
|
|
1860
|
-
return poll();
|
|
1935
|
+
return poll([], null);
|
|
1861
1936
|
};
|
|
1862
1937
|
var defaultSleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1863
1938
|
var monotonicElapsed = () => {
|
|
1864
|
-
const start =
|
|
1865
|
-
return () =>
|
|
1939
|
+
const start = performance.now();
|
|
1940
|
+
return () => performance.now() - start;
|
|
1866
1941
|
};
|
|
1867
1942
|
var renderCiOutputs = (outcome) => outcome.kind === "concluded" ? `ci_settled=true
|
|
1868
1943
|
ci_conclusion=${outcome.conclusion}
|
|
@@ -2345,6 +2420,7 @@ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) =>
|
|
|
2345
2420
|
findings: noticeFindings(`### \u26A0\uFE0F Review did not complete
|
|
2346
2421
|
|
|
2347
2422
|
${outcome.reason}`),
|
|
2423
|
+
incomplete: true,
|
|
2348
2424
|
...telemetry
|
|
2349
2425
|
};
|
|
2350
2426
|
}
|
|
@@ -3105,6 +3181,30 @@ var adaptCmd = defineCommand({
|
|
|
3105
3181
|
`);
|
|
3106
3182
|
}
|
|
3107
3183
|
});
|
|
3184
|
+
var noticeCmd = defineCommand({
|
|
3185
|
+
meta: {
|
|
3186
|
+
name: "notice",
|
|
3187
|
+
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"
|
|
3188
|
+
},
|
|
3189
|
+
args: {
|
|
3190
|
+
kind: {
|
|
3191
|
+
type: "positional",
|
|
3192
|
+
description: "One of: security-blocked, setup-failed, diff-apply-failed, no-output",
|
|
3193
|
+
required: true
|
|
3194
|
+
},
|
|
3195
|
+
reasons: {
|
|
3196
|
+
type: "string",
|
|
3197
|
+
description: "security-blocked only: the triage's fail-closed reason string (empty/omitted \u21D2 the no-reason wording)"
|
|
3198
|
+
}
|
|
3199
|
+
},
|
|
3200
|
+
run: ({ args }) => {
|
|
3201
|
+
const kind = isNoticeKind(args.kind) ? args.kind : fail(
|
|
3202
|
+
`Unknown notice kind "${args.kind}" \u2014 expected one of: security-blocked, setup-failed, diff-apply-failed, no-output`
|
|
3203
|
+
);
|
|
3204
|
+
process.stdout.write(`${JSON.stringify(buildNoticeEnvelope(kind, args.reasons), null, 2)}
|
|
3205
|
+
`);
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3108
3208
|
var isExtractSchemaKind = (s) => s === "findings" || s === "triage";
|
|
3109
3209
|
var requireExtractSchemaKind = (name) => isExtractSchemaKind(name) ? name : fail(`Unknown kind "${name}" for extract \u2014 expected one of: findings, triage`);
|
|
3110
3210
|
var failClosedTriage = (outcome) => ({
|
|
@@ -3708,6 +3808,7 @@ var main = defineCommand({
|
|
|
3708
3808
|
validate: validateCmd,
|
|
3709
3809
|
"seed-draft": seedDraftCmd,
|
|
3710
3810
|
adapt: adaptCmd,
|
|
3811
|
+
notice: noticeCmd,
|
|
3711
3812
|
extract: extractCmd,
|
|
3712
3813
|
"validate-patches": validatePatchesCmd,
|
|
3713
3814
|
"print-schema": printSchemaCmd,
|