@jphutchins/code-review 0.1.0-alpha.26 → 0.1.0-alpha.28
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 +245 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
@@ -195,6 +196,13 @@ var parseFindingsMarker = (body) => {
|
|
|
195
196
|
return null;
|
|
196
197
|
}
|
|
197
198
|
};
|
|
199
|
+
var carryForwardMarkers = (body) => {
|
|
200
|
+
const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
|
|
201
|
+
const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
|
|
202
|
+
const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
|
|
203
|
+
${findings}` : void 0;
|
|
204
|
+
return [findingsBlock, reviewedSha].filter((m) => m !== void 0).join("\n\n");
|
|
205
|
+
};
|
|
198
206
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
199
207
|
var projectPatch = (patch) => {
|
|
200
208
|
if (patch === void 0) return { kind: "none" };
|
|
@@ -1590,6 +1598,47 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1590
1598
|
}
|
|
1591
1599
|
}
|
|
1592
1600
|
};
|
|
1601
|
+
var announceBody = (headSha, runUrl, existingBody) => {
|
|
1602
|
+
const notice = `${DEFAULT_MARKER}
|
|
1603
|
+
|
|
1604
|
+
\u{1F504} **Code review in progress** for \`${headSha.slice(0, 7)}\` \u2014 see the [workflow run](${runUrl}) for progress; this comment is updated with the review when it completes.`;
|
|
1605
|
+
const carried = existingBody ? carryForwardMarkers(existingBody) : "";
|
|
1606
|
+
return carried ? `${notice}
|
|
1607
|
+
|
|
1608
|
+
${carried}` : notice;
|
|
1609
|
+
};
|
|
1610
|
+
var announce = async (input, ghApi = runGhApi) => {
|
|
1611
|
+
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
1612
|
+
const resolution = resolvePr(candidates, input.headBranch);
|
|
1613
|
+
if (resolution.kind !== "open") {
|
|
1614
|
+
process.stderr.write(
|
|
1615
|
+
`No open PR for ${input.headSha} \u2014 nothing to announce (${resolution.kind})
|
|
1616
|
+
`
|
|
1617
|
+
);
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const existing = await findBotComment(
|
|
1621
|
+
input.repo,
|
|
1622
|
+
resolution.prNumber,
|
|
1623
|
+
input.botLogin,
|
|
1624
|
+
DEFAULT_MARKER,
|
|
1625
|
+
ghApi
|
|
1626
|
+
);
|
|
1627
|
+
if (existing !== null && parseReviewedSha(existing.body) === input.headSha.toLowerCase()) {
|
|
1628
|
+
process.stderr.write(
|
|
1629
|
+
`Sticky already reflects a completed review of ${input.headSha} \u2014 leaving it in place
|
|
1630
|
+
`
|
|
1631
|
+
);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
await upsertSticky(
|
|
1635
|
+
input.repo,
|
|
1636
|
+
resolution.prNumber,
|
|
1637
|
+
existing,
|
|
1638
|
+
announceBody(input.headSha, input.runUrl, existing?.body),
|
|
1639
|
+
ghApi
|
|
1640
|
+
);
|
|
1641
|
+
};
|
|
1593
1642
|
var DURATION_RE = /^(\d+)(h|m|s)$/;
|
|
1594
1643
|
var USD_RE = /^\$(\d+(?:\.\d+)?)$/;
|
|
1595
1644
|
var toSeconds = (n, unit) => unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
|
|
@@ -1781,15 +1830,21 @@ var RunCodec = t.type({
|
|
|
1781
1830
|
conclusion: t.union([t.string, t.null]),
|
|
1782
1831
|
run_number: t.number
|
|
1783
1832
|
});
|
|
1784
|
-
var
|
|
1833
|
+
var RUN_JQ = ".workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, run_number: .run_number}";
|
|
1785
1834
|
var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
|
|
1786
|
-
const
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1835
|
+
const endpoint = `repos/${repo}/actions/runs?head_sha=${headSha}&per_page=100`;
|
|
1836
|
+
const rows = parseJsonl(await ghApi([endpoint, "--paginate", "--jq", RUN_JQ]));
|
|
1837
|
+
const decoded = rows.map((row) => RunCodec.decode(row));
|
|
1838
|
+
const runs = decoded.flatMap((d) => d._tag === "Right" ? [d.right] : []);
|
|
1839
|
+
const dropped = decoded.length - runs.length;
|
|
1840
|
+
if (dropped > 0) {
|
|
1841
|
+
const firstDrift = decoded.find((d) => d._tag === "Left");
|
|
1842
|
+
const detail = firstDrift === void 0 ? "" : ` (${PathReporter.report(firstDrift).join("; ")})`;
|
|
1843
|
+
process.stderr.write(
|
|
1844
|
+
`Warning: ${String(dropped)} of ${String(rows.length)} workflow-run row(s) from ${endpoint} failed to decode${detail} \u2014 excluded from the lookup
|
|
1845
|
+
`
|
|
1791
1846
|
);
|
|
1792
|
-
|
|
1847
|
+
}
|
|
1793
1848
|
const latest = runs.filter((r) => r.name === workflowName).reduce(
|
|
1794
1849
|
(best, r) => best === null || r.run_number > best.run_number ? r : best,
|
|
1795
1850
|
null
|
|
@@ -1800,21 +1855,34 @@ var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
|
|
|
1800
1855
|
};
|
|
1801
1856
|
};
|
|
1802
1857
|
var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi, sleep: defaultSleep, elapsedMs: monotonicElapsed() }) => {
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1858
|
+
const safeResolve = async () => {
|
|
1859
|
+
try {
|
|
1860
|
+
return await resolveCiRun(repo, headSha, options.workflowName, deps.ghApi);
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
process.stderr.write(
|
|
1863
|
+
`Warning: CI-run lookup for ${headSha} failed (${errMsg(err)}) \u2014 retrying until the timeout
|
|
1864
|
+
`
|
|
1865
|
+
);
|
|
1866
|
+
return { run: null, seenNames: [] };
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
const poll = async (lastSeenNames, lastRunId) => {
|
|
1870
|
+
const { run, seenNames } = await safeResolve();
|
|
1871
|
+
if (run !== null && run.status === "completed" && run.conclusion !== null)
|
|
1872
|
+
return { kind: "concluded", conclusion: run.conclusion, runId: run.id };
|
|
1873
|
+
const runId = run === null ? lastRunId : run.id;
|
|
1874
|
+
const names = seenNames.length > 0 ? seenNames : lastSeenNames;
|
|
1807
1875
|
if (deps.elapsedMs() >= options.timeoutMs)
|
|
1808
|
-
return { kind: "timed-out", runId
|
|
1876
|
+
return { kind: "timed-out", runId, seenNames: names };
|
|
1809
1877
|
await deps.sleep(options.pollIntervalMs);
|
|
1810
|
-
return poll();
|
|
1878
|
+
return poll(names, runId);
|
|
1811
1879
|
};
|
|
1812
|
-
return poll();
|
|
1880
|
+
return poll([], null);
|
|
1813
1881
|
};
|
|
1814
1882
|
var defaultSleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1815
1883
|
var monotonicElapsed = () => {
|
|
1816
|
-
const start =
|
|
1817
|
-
return () =>
|
|
1884
|
+
const start = performance.now();
|
|
1885
|
+
return () => performance.now() - start;
|
|
1818
1886
|
};
|
|
1819
1887
|
var renderCiOutputs = (outcome) => outcome.kind === "concluded" ? `ci_settled=true
|
|
1820
1888
|
ci_conclusion=${outcome.conclusion}
|
|
@@ -1855,12 +1923,17 @@ var PrMetaCodec = t.type({
|
|
|
1855
1923
|
title: t.string,
|
|
1856
1924
|
body: t.union([t.string, t.null])
|
|
1857
1925
|
});
|
|
1858
|
-
var IssueCommentCodec = t.
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
})
|
|
1863
|
-
|
|
1926
|
+
var IssueCommentCodec = t.intersection([
|
|
1927
|
+
t.type({
|
|
1928
|
+
id: t.number,
|
|
1929
|
+
body: t.union([t.string, t.null]),
|
|
1930
|
+
user: t.type({ login: t.string })
|
|
1931
|
+
}),
|
|
1932
|
+
t.partial({
|
|
1933
|
+
created_at: t.union([t.string, t.null]),
|
|
1934
|
+
author_association: t.union([t.string, t.null])
|
|
1935
|
+
})
|
|
1936
|
+
]);
|
|
1864
1937
|
var JobCodec = t.type({ id: t.number, conclusion: t.union([t.string, t.null]) });
|
|
1865
1938
|
var JobsResponseCodec = t.type({ jobs: t.array(JobCodec) });
|
|
1866
1939
|
var fetchPrMeta = async (repo, prNumber, ghApi) => {
|
|
@@ -1882,18 +1955,92 @@ var fetchApiDiff = async (repo, prNumber, ghApi) => {
|
|
|
1882
1955
|
return null;
|
|
1883
1956
|
}
|
|
1884
1957
|
};
|
|
1885
|
-
var
|
|
1958
|
+
var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association}";
|
|
1959
|
+
var REVIEW_COMMENT_JQ = ".[] | {body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association, path: .path, line: .line}";
|
|
1960
|
+
var REVIEW_JQ = ".[] | {body: .body, user: {login: .user.login}, submitted_at: .submitted_at, author_association: .author_association, state: .state}";
|
|
1961
|
+
var ReviewCommentCodec = t.intersection([
|
|
1962
|
+
t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
|
|
1963
|
+
t.partial({
|
|
1964
|
+
created_at: t.union([t.string, t.null]),
|
|
1965
|
+
author_association: t.union([t.string, t.null]),
|
|
1966
|
+
path: t.union([t.string, t.null]),
|
|
1967
|
+
line: t.union([t.number, t.null])
|
|
1968
|
+
})
|
|
1969
|
+
]);
|
|
1970
|
+
var ReviewCodec = t.intersection([
|
|
1971
|
+
t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
|
|
1972
|
+
t.partial({
|
|
1973
|
+
submitted_at: t.union([t.string, t.null]),
|
|
1974
|
+
author_association: t.union([t.string, t.null]),
|
|
1975
|
+
state: t.union([t.string, t.null])
|
|
1976
|
+
})
|
|
1977
|
+
]);
|
|
1978
|
+
var fetchJsonlRows = async (ghApi, endpoint, jq) => {
|
|
1886
1979
|
try {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
} catch {
|
|
1980
|
+
return parseJsonl(await ghApi([endpoint, "--paginate", "--jq", jq]));
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
process.stderr.write(
|
|
1983
|
+
`Warning: could not fetch ${endpoint} (${errMsg(err)}) \u2014 omitting it from the review context
|
|
1984
|
+
`
|
|
1985
|
+
);
|
|
1894
1986
|
return null;
|
|
1895
1987
|
}
|
|
1896
1988
|
};
|
|
1989
|
+
var decodeArrayOrNull = (codec, rows) => {
|
|
1990
|
+
if (rows === null) return null;
|
|
1991
|
+
return rows.flatMap((row) => {
|
|
1992
|
+
const decoded = codec.decode(row);
|
|
1993
|
+
return decoded._tag === "Right" ? [decoded.right] : [];
|
|
1994
|
+
});
|
|
1995
|
+
};
|
|
1996
|
+
var priorReviewFrom = (comments, botLogin) => {
|
|
1997
|
+
const byBot = comments.filter((c) => c.user.login === botLogin);
|
|
1998
|
+
const last = byBot[byBot.length - 1];
|
|
1999
|
+
return last ? { id: last.id, body: last.body } : null;
|
|
2000
|
+
};
|
|
2001
|
+
var MAX_CONVERSATION_COMMENTS = 50;
|
|
2002
|
+
var MAX_CONVERSATION_BODY_CHARS = 4e3;
|
|
2003
|
+
var clip = (body) => {
|
|
2004
|
+
if (body.length <= MAX_CONVERSATION_BODY_CHARS) return body;
|
|
2005
|
+
const cut = body.slice(0, MAX_CONVERSATION_BODY_CHARS);
|
|
2006
|
+
const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
2007
|
+
return `${safe}
|
|
2008
|
+
\u2026 [truncated]`;
|
|
2009
|
+
};
|
|
2010
|
+
var boundedHuman = (items, botLogin, label, project) => {
|
|
2011
|
+
const human = items.filter(
|
|
2012
|
+
(a) => a.user.login !== botLogin && typeof a.body === "string" && a.body.trim() !== ""
|
|
2013
|
+
);
|
|
2014
|
+
const kept = human.slice(-MAX_CONVERSATION_COMMENTS);
|
|
2015
|
+
if (kept.length < human.length) {
|
|
2016
|
+
process.stderr.write(
|
|
2017
|
+
`Note: PR has ${String(human.length)} ${label} \u2014 feeding the review the most recent ${String(MAX_CONVERSATION_COMMENTS)}
|
|
2018
|
+
`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
return kept.map(project);
|
|
2022
|
+
};
|
|
2023
|
+
var issueCommentsFrom = (comments, botLogin) => boundedHuman(comments, botLogin, "discussion comments", (c) => ({
|
|
2024
|
+
author: c.user.login,
|
|
2025
|
+
author_association: c.author_association ?? null,
|
|
2026
|
+
created_at: c.created_at ?? null,
|
|
2027
|
+
body: clip(c.body)
|
|
2028
|
+
}));
|
|
2029
|
+
var reviewCommentsFrom = (comments, botLogin) => boundedHuman(comments, botLogin, "inline review comments", (c) => ({
|
|
2030
|
+
author: c.user.login,
|
|
2031
|
+
author_association: c.author_association ?? null,
|
|
2032
|
+
created_at: c.created_at ?? null,
|
|
2033
|
+
path: c.path ?? null,
|
|
2034
|
+
line: c.line ?? null,
|
|
2035
|
+
body: clip(c.body)
|
|
2036
|
+
}));
|
|
2037
|
+
var reviewsFrom = (reviews, botLogin) => boundedHuman(reviews, botLogin, "review submissions", (r) => ({
|
|
2038
|
+
author: r.user.login,
|
|
2039
|
+
author_association: r.author_association ?? null,
|
|
2040
|
+
submitted_at: r.submitted_at ?? null,
|
|
2041
|
+
state: r.state ?? null,
|
|
2042
|
+
body: clip(r.body)
|
|
2043
|
+
}));
|
|
1897
2044
|
var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
1898
2045
|
const stdout = await ghApi([`repos/${repo}/actions/runs/${runId}/jobs`]);
|
|
1899
2046
|
const decoded = JobsResponseCodec.decode(JSON.parse(stdout));
|
|
@@ -1943,11 +2090,31 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
1943
2090
|
join(input.outDir, "pr_context.json"),
|
|
1944
2091
|
JSON.stringify({ title: meta.title, body: meta.body })
|
|
1945
2092
|
);
|
|
1946
|
-
const
|
|
2093
|
+
const [issueRows, reviewCommentRows, reviewRows] = await Promise.all([
|
|
2094
|
+
fetchJsonlRows(ghApi, `repos/${input.repo}/issues/${String(prNumber)}/comments`, COMMENT_JQ),
|
|
2095
|
+
fetchJsonlRows(
|
|
2096
|
+
ghApi,
|
|
2097
|
+
`repos/${input.repo}/pulls/${String(prNumber)}/comments`,
|
|
2098
|
+
REVIEW_COMMENT_JQ
|
|
2099
|
+
),
|
|
2100
|
+
fetchJsonlRows(ghApi, `repos/${input.repo}/pulls/${String(prNumber)}/reviews`, REVIEW_JQ)
|
|
2101
|
+
]);
|
|
2102
|
+
const issueComments = decodeArrayOrNull(IssueCommentCodec, issueRows);
|
|
2103
|
+
const reviewComments = decodeArrayOrNull(ReviewCommentCodec, reviewCommentRows);
|
|
2104
|
+
const reviews = decodeArrayOrNull(ReviewCodec, reviewRows);
|
|
2105
|
+
const prior = issueComments === null ? null : priorReviewFrom(issueComments, input.botLogin);
|
|
1947
2106
|
writeFileSync(
|
|
1948
2107
|
join(input.outDir, "prior_review.json"),
|
|
1949
2108
|
prior === null ? "null" : JSON.stringify(prior)
|
|
1950
2109
|
);
|
|
2110
|
+
writeFileSync(
|
|
2111
|
+
join(input.outDir, "pr_conversation.json"),
|
|
2112
|
+
JSON.stringify({
|
|
2113
|
+
issue_comments: issueComments === null ? [] : issueCommentsFrom(issueComments, input.botLogin),
|
|
2114
|
+
review_comments: reviewComments === null ? [] : reviewCommentsFrom(reviewComments, input.botLogin),
|
|
2115
|
+
reviews: reviews === null ? [] : reviewsFrom(reviews, input.botLogin)
|
|
2116
|
+
})
|
|
2117
|
+
);
|
|
1951
2118
|
if (input.conclusion === "failure") {
|
|
1952
2119
|
await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi);
|
|
1953
2120
|
}
|
|
@@ -3204,7 +3371,7 @@ var stopGateCmd = defineCommand({
|
|
|
3204
3371
|
var gatherCmd = defineCommand({
|
|
3205
3372
|
meta: {
|
|
3206
3373
|
name: "gather",
|
|
3207
|
-
description: "Resolve the PR from the CI head SHA and gather review inputs (diff with git-diff fallback, PR context, prior bot review, failing-job logs) as files for the review agent"
|
|
3374
|
+
description: "Resolve the PR from the CI head SHA and gather review inputs (diff with git-diff fallback, PR context, prior bot review, untrusted PR conversation to triage, failing-job logs) as files for the review agent"
|
|
3208
3375
|
},
|
|
3209
3376
|
args: {
|
|
3210
3377
|
repo: { type: "string", description: "Repository (owner/name)", required: true },
|
|
@@ -3338,6 +3505,51 @@ var postCmd = defineCommand({
|
|
|
3338
3505
|
});
|
|
3339
3506
|
}
|
|
3340
3507
|
});
|
|
3508
|
+
var announceCmd = defineCommand({
|
|
3509
|
+
meta: {
|
|
3510
|
+
name: "announce",
|
|
3511
|
+
description: "Post (or update) the sticky the moment a review starts \u2014 an in-progress placeholder linking the run \u2014 so a workflow_run review, which runs from the default branch and is otherwise invisible on the PR, is visibly under way. Preserves a prior sticky's embedded findings + reviewed-sha markers so the re-review seed survives the swap."
|
|
3512
|
+
},
|
|
3513
|
+
args: {
|
|
3514
|
+
"head-sha": {
|
|
3515
|
+
type: "string",
|
|
3516
|
+
description: "Trusted head SHA to resolve the PR (from workflow_run.head_sha)",
|
|
3517
|
+
required: true
|
|
3518
|
+
},
|
|
3519
|
+
repo: {
|
|
3520
|
+
type: "string",
|
|
3521
|
+
description: "Repository (owner/name)",
|
|
3522
|
+
required: true
|
|
3523
|
+
},
|
|
3524
|
+
"run-url": {
|
|
3525
|
+
type: "string",
|
|
3526
|
+
description: "Workflow run URL the placeholder links to",
|
|
3527
|
+
required: true
|
|
3528
|
+
},
|
|
3529
|
+
"bot-login": {
|
|
3530
|
+
type: "string",
|
|
3531
|
+
description: "Bot login to trust for the sticky comment upsert (default: github-actions[bot])"
|
|
3532
|
+
},
|
|
3533
|
+
"head-branch": {
|
|
3534
|
+
type: "string",
|
|
3535
|
+
description: "Head branch to disambiguate the PR when multiple share a commit"
|
|
3536
|
+
}
|
|
3537
|
+
},
|
|
3538
|
+
run: async ({ args }) => {
|
|
3539
|
+
await announce({
|
|
3540
|
+
repo: args.repo,
|
|
3541
|
+
headSha: args["head-sha"],
|
|
3542
|
+
botLogin: args["bot-login"] || "github-actions[bot]",
|
|
3543
|
+
runUrl: args["run-url"],
|
|
3544
|
+
headBranch: args["head-branch"]
|
|
3545
|
+
}).catch(
|
|
3546
|
+
(err) => process.stderr.write(
|
|
3547
|
+
`code-review announce: could not post the in-progress sticky (${errMsg(err)}) \u2014 continuing (cosmetic)
|
|
3548
|
+
`
|
|
3549
|
+
)
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
});
|
|
3341
3553
|
var requireCeilingSec = (raw) => {
|
|
3342
3554
|
if (raw === void 0) return null;
|
|
3343
3555
|
const ms = parseWallMs(raw);
|
|
@@ -3510,6 +3722,7 @@ var main = defineCommand({
|
|
|
3510
3722
|
render: renderCmd,
|
|
3511
3723
|
inline: inlineCmd,
|
|
3512
3724
|
post: postCmd,
|
|
3725
|
+
announce: announceCmd,
|
|
3513
3726
|
cost: costCmd,
|
|
3514
3727
|
"check-cost": checkCostCmd,
|
|
3515
3728
|
validate: validateCmd,
|