@jphutchins/code-review 0.1.0-alpha.25 → 0.1.0-alpha.27
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 +225 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -195,6 +195,13 @@ var parseFindingsMarker = (body) => {
|
|
|
195
195
|
return null;
|
|
196
196
|
}
|
|
197
197
|
};
|
|
198
|
+
var carryForwardMarkers = (body) => {
|
|
199
|
+
const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
|
|
200
|
+
const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
|
|
201
|
+
const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
|
|
202
|
+
${findings}` : void 0;
|
|
203
|
+
return [findingsBlock, reviewedSha].filter((m) => m !== void 0).join("\n\n");
|
|
204
|
+
};
|
|
198
205
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
199
206
|
var projectPatch = (patch) => {
|
|
200
207
|
if (patch === void 0) return { kind: "none" };
|
|
@@ -1055,18 +1062,27 @@ var runGhApi = (args, stdin, env) => new Promise((resolve3, reject) => {
|
|
|
1055
1062
|
});
|
|
1056
1063
|
|
|
1057
1064
|
// src/pr.ts
|
|
1065
|
+
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
|
|
1066
|
+
var parseCandidates = (stdout) => parseJsonl(stdout);
|
|
1058
1067
|
var fetchPrCandidates = async (repo, headSha, ghApi) => {
|
|
1059
|
-
const
|
|
1060
|
-
`repos/${repo}/commits/${headSha}/pulls`,
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1068
|
+
const direct = parseCandidates(
|
|
1069
|
+
await ghApi([`repos/${repo}/commits/${headSha}/pulls`, "--jq", CANDIDATE_JQ])
|
|
1070
|
+
);
|
|
1071
|
+
if (direct.length > 0) return direct;
|
|
1072
|
+
const open = parseCandidates(
|
|
1073
|
+
await ghApi([
|
|
1074
|
+
`repos/${repo}/pulls?state=open&per_page=100`,
|
|
1075
|
+
"--paginate",
|
|
1076
|
+
"--jq",
|
|
1077
|
+
CANDIDATE_JQ
|
|
1078
|
+
])
|
|
1079
|
+
);
|
|
1080
|
+
return open.filter((c) => c.headSha === headSha);
|
|
1065
1081
|
};
|
|
1066
1082
|
var resolvePr = (candidates, headBranch) => {
|
|
1067
1083
|
if (candidates.length === 0) return { kind: "none" };
|
|
1068
1084
|
const scoped = candidates.length > 1 && headBranch ? candidates.filter((c) => c.headRef === headBranch) : candidates;
|
|
1069
|
-
const chosen = scoped[0] ?? candidates[0];
|
|
1085
|
+
const chosen = scoped.find((c) => c.state === "open") ?? scoped[0] ?? candidates[0];
|
|
1070
1086
|
if (chosen === void 0) return { kind: "none" };
|
|
1071
1087
|
return chosen.state === "open" ? { kind: "open", prNumber: chosen.number } : { kind: "not-open", prNumber: chosen.number, state: chosen.state };
|
|
1072
1088
|
};
|
|
@@ -1581,6 +1597,47 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1581
1597
|
}
|
|
1582
1598
|
}
|
|
1583
1599
|
};
|
|
1600
|
+
var announceBody = (headSha, runUrl, existingBody) => {
|
|
1601
|
+
const notice = `${DEFAULT_MARKER}
|
|
1602
|
+
|
|
1603
|
+
\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.`;
|
|
1604
|
+
const carried = existingBody ? carryForwardMarkers(existingBody) : "";
|
|
1605
|
+
return carried ? `${notice}
|
|
1606
|
+
|
|
1607
|
+
${carried}` : notice;
|
|
1608
|
+
};
|
|
1609
|
+
var announce = async (input, ghApi = runGhApi) => {
|
|
1610
|
+
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
1611
|
+
const resolution = resolvePr(candidates, input.headBranch);
|
|
1612
|
+
if (resolution.kind !== "open") {
|
|
1613
|
+
process.stderr.write(
|
|
1614
|
+
`No open PR for ${input.headSha} \u2014 nothing to announce (${resolution.kind})
|
|
1615
|
+
`
|
|
1616
|
+
);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const existing = await findBotComment(
|
|
1620
|
+
input.repo,
|
|
1621
|
+
resolution.prNumber,
|
|
1622
|
+
input.botLogin,
|
|
1623
|
+
DEFAULT_MARKER,
|
|
1624
|
+
ghApi
|
|
1625
|
+
);
|
|
1626
|
+
if (existing !== null && parseReviewedSha(existing.body) === input.headSha.toLowerCase()) {
|
|
1627
|
+
process.stderr.write(
|
|
1628
|
+
`Sticky already reflects a completed review of ${input.headSha} \u2014 leaving it in place
|
|
1629
|
+
`
|
|
1630
|
+
);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
await upsertSticky(
|
|
1634
|
+
input.repo,
|
|
1635
|
+
resolution.prNumber,
|
|
1636
|
+
existing,
|
|
1637
|
+
announceBody(input.headSha, input.runUrl, existing?.body),
|
|
1638
|
+
ghApi
|
|
1639
|
+
);
|
|
1640
|
+
};
|
|
1584
1641
|
var DURATION_RE = /^(\d+)(h|m|s)$/;
|
|
1585
1642
|
var USD_RE = /^\$(\d+(?:\.\d+)?)$/;
|
|
1586
1643
|
var toSeconds = (n, unit) => unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
|
|
@@ -1846,12 +1903,17 @@ var PrMetaCodec = t.type({
|
|
|
1846
1903
|
title: t.string,
|
|
1847
1904
|
body: t.union([t.string, t.null])
|
|
1848
1905
|
});
|
|
1849
|
-
var IssueCommentCodec = t.
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
})
|
|
1854
|
-
|
|
1906
|
+
var IssueCommentCodec = t.intersection([
|
|
1907
|
+
t.type({
|
|
1908
|
+
id: t.number,
|
|
1909
|
+
body: t.union([t.string, t.null]),
|
|
1910
|
+
user: t.type({ login: t.string })
|
|
1911
|
+
}),
|
|
1912
|
+
t.partial({
|
|
1913
|
+
created_at: t.union([t.string, t.null]),
|
|
1914
|
+
author_association: t.union([t.string, t.null])
|
|
1915
|
+
})
|
|
1916
|
+
]);
|
|
1855
1917
|
var JobCodec = t.type({ id: t.number, conclusion: t.union([t.string, t.null]) });
|
|
1856
1918
|
var JobsResponseCodec = t.type({ jobs: t.array(JobCodec) });
|
|
1857
1919
|
var fetchPrMeta = async (repo, prNumber, ghApi) => {
|
|
@@ -1873,18 +1935,92 @@ var fetchApiDiff = async (repo, prNumber, ghApi) => {
|
|
|
1873
1935
|
return null;
|
|
1874
1936
|
}
|
|
1875
1937
|
};
|
|
1876
|
-
var
|
|
1938
|
+
var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association}";
|
|
1939
|
+
var REVIEW_COMMENT_JQ = ".[] | {body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association, path: .path, line: .line}";
|
|
1940
|
+
var REVIEW_JQ = ".[] | {body: .body, user: {login: .user.login}, submitted_at: .submitted_at, author_association: .author_association, state: .state}";
|
|
1941
|
+
var ReviewCommentCodec = t.intersection([
|
|
1942
|
+
t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
|
|
1943
|
+
t.partial({
|
|
1944
|
+
created_at: t.union([t.string, t.null]),
|
|
1945
|
+
author_association: t.union([t.string, t.null]),
|
|
1946
|
+
path: t.union([t.string, t.null]),
|
|
1947
|
+
line: t.union([t.number, t.null])
|
|
1948
|
+
})
|
|
1949
|
+
]);
|
|
1950
|
+
var ReviewCodec = t.intersection([
|
|
1951
|
+
t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
|
|
1952
|
+
t.partial({
|
|
1953
|
+
submitted_at: t.union([t.string, t.null]),
|
|
1954
|
+
author_association: t.union([t.string, t.null]),
|
|
1955
|
+
state: t.union([t.string, t.null])
|
|
1956
|
+
})
|
|
1957
|
+
]);
|
|
1958
|
+
var fetchJsonlRows = async (ghApi, endpoint, jq) => {
|
|
1877
1959
|
try {
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
} catch {
|
|
1960
|
+
return parseJsonl(await ghApi([endpoint, "--paginate", "--jq", jq]));
|
|
1961
|
+
} catch (err) {
|
|
1962
|
+
process.stderr.write(
|
|
1963
|
+
`Warning: could not fetch ${endpoint} (${errMsg(err)}) \u2014 omitting it from the review context
|
|
1964
|
+
`
|
|
1965
|
+
);
|
|
1885
1966
|
return null;
|
|
1886
1967
|
}
|
|
1887
1968
|
};
|
|
1969
|
+
var decodeArrayOrNull = (codec, rows) => {
|
|
1970
|
+
if (rows === null) return null;
|
|
1971
|
+
return rows.flatMap((row) => {
|
|
1972
|
+
const decoded = codec.decode(row);
|
|
1973
|
+
return decoded._tag === "Right" ? [decoded.right] : [];
|
|
1974
|
+
});
|
|
1975
|
+
};
|
|
1976
|
+
var priorReviewFrom = (comments, botLogin) => {
|
|
1977
|
+
const byBot = comments.filter((c) => c.user.login === botLogin);
|
|
1978
|
+
const last = byBot[byBot.length - 1];
|
|
1979
|
+
return last ? { id: last.id, body: last.body } : null;
|
|
1980
|
+
};
|
|
1981
|
+
var MAX_CONVERSATION_COMMENTS = 50;
|
|
1982
|
+
var MAX_CONVERSATION_BODY_CHARS = 4e3;
|
|
1983
|
+
var clip = (body) => {
|
|
1984
|
+
if (body.length <= MAX_CONVERSATION_BODY_CHARS) return body;
|
|
1985
|
+
const cut = body.slice(0, MAX_CONVERSATION_BODY_CHARS);
|
|
1986
|
+
const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
1987
|
+
return `${safe}
|
|
1988
|
+
\u2026 [truncated]`;
|
|
1989
|
+
};
|
|
1990
|
+
var boundedHuman = (items, botLogin, label, project) => {
|
|
1991
|
+
const human = items.filter(
|
|
1992
|
+
(a) => a.user.login !== botLogin && typeof a.body === "string" && a.body.trim() !== ""
|
|
1993
|
+
);
|
|
1994
|
+
const kept = human.slice(-MAX_CONVERSATION_COMMENTS);
|
|
1995
|
+
if (kept.length < human.length) {
|
|
1996
|
+
process.stderr.write(
|
|
1997
|
+
`Note: PR has ${String(human.length)} ${label} \u2014 feeding the review the most recent ${String(MAX_CONVERSATION_COMMENTS)}
|
|
1998
|
+
`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
return kept.map(project);
|
|
2002
|
+
};
|
|
2003
|
+
var issueCommentsFrom = (comments, botLogin) => boundedHuman(comments, botLogin, "discussion comments", (c) => ({
|
|
2004
|
+
author: c.user.login,
|
|
2005
|
+
author_association: c.author_association ?? null,
|
|
2006
|
+
created_at: c.created_at ?? null,
|
|
2007
|
+
body: clip(c.body)
|
|
2008
|
+
}));
|
|
2009
|
+
var reviewCommentsFrom = (comments, botLogin) => boundedHuman(comments, botLogin, "inline review comments", (c) => ({
|
|
2010
|
+
author: c.user.login,
|
|
2011
|
+
author_association: c.author_association ?? null,
|
|
2012
|
+
created_at: c.created_at ?? null,
|
|
2013
|
+
path: c.path ?? null,
|
|
2014
|
+
line: c.line ?? null,
|
|
2015
|
+
body: clip(c.body)
|
|
2016
|
+
}));
|
|
2017
|
+
var reviewsFrom = (reviews, botLogin) => boundedHuman(reviews, botLogin, "review submissions", (r) => ({
|
|
2018
|
+
author: r.user.login,
|
|
2019
|
+
author_association: r.author_association ?? null,
|
|
2020
|
+
submitted_at: r.submitted_at ?? null,
|
|
2021
|
+
state: r.state ?? null,
|
|
2022
|
+
body: clip(r.body)
|
|
2023
|
+
}));
|
|
1888
2024
|
var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
1889
2025
|
const stdout = await ghApi([`repos/${repo}/actions/runs/${runId}/jobs`]);
|
|
1890
2026
|
const decoded = JobsResponseCodec.decode(JSON.parse(stdout));
|
|
@@ -1934,11 +2070,31 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
1934
2070
|
join(input.outDir, "pr_context.json"),
|
|
1935
2071
|
JSON.stringify({ title: meta.title, body: meta.body })
|
|
1936
2072
|
);
|
|
1937
|
-
const
|
|
2073
|
+
const [issueRows, reviewCommentRows, reviewRows] = await Promise.all([
|
|
2074
|
+
fetchJsonlRows(ghApi, `repos/${input.repo}/issues/${String(prNumber)}/comments`, COMMENT_JQ),
|
|
2075
|
+
fetchJsonlRows(
|
|
2076
|
+
ghApi,
|
|
2077
|
+
`repos/${input.repo}/pulls/${String(prNumber)}/comments`,
|
|
2078
|
+
REVIEW_COMMENT_JQ
|
|
2079
|
+
),
|
|
2080
|
+
fetchJsonlRows(ghApi, `repos/${input.repo}/pulls/${String(prNumber)}/reviews`, REVIEW_JQ)
|
|
2081
|
+
]);
|
|
2082
|
+
const issueComments = decodeArrayOrNull(IssueCommentCodec, issueRows);
|
|
2083
|
+
const reviewComments = decodeArrayOrNull(ReviewCommentCodec, reviewCommentRows);
|
|
2084
|
+
const reviews = decodeArrayOrNull(ReviewCodec, reviewRows);
|
|
2085
|
+
const prior = issueComments === null ? null : priorReviewFrom(issueComments, input.botLogin);
|
|
1938
2086
|
writeFileSync(
|
|
1939
2087
|
join(input.outDir, "prior_review.json"),
|
|
1940
2088
|
prior === null ? "null" : JSON.stringify(prior)
|
|
1941
2089
|
);
|
|
2090
|
+
writeFileSync(
|
|
2091
|
+
join(input.outDir, "pr_conversation.json"),
|
|
2092
|
+
JSON.stringify({
|
|
2093
|
+
issue_comments: issueComments === null ? [] : issueCommentsFrom(issueComments, input.botLogin),
|
|
2094
|
+
review_comments: reviewComments === null ? [] : reviewCommentsFrom(reviewComments, input.botLogin),
|
|
2095
|
+
reviews: reviews === null ? [] : reviewsFrom(reviews, input.botLogin)
|
|
2096
|
+
})
|
|
2097
|
+
);
|
|
1942
2098
|
if (input.conclusion === "failure") {
|
|
1943
2099
|
await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi);
|
|
1944
2100
|
}
|
|
@@ -3195,7 +3351,7 @@ var stopGateCmd = defineCommand({
|
|
|
3195
3351
|
var gatherCmd = defineCommand({
|
|
3196
3352
|
meta: {
|
|
3197
3353
|
name: "gather",
|
|
3198
|
-
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"
|
|
3354
|
+
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"
|
|
3199
3355
|
},
|
|
3200
3356
|
args: {
|
|
3201
3357
|
repo: { type: "string", description: "Repository (owner/name)", required: true },
|
|
@@ -3329,6 +3485,51 @@ var postCmd = defineCommand({
|
|
|
3329
3485
|
});
|
|
3330
3486
|
}
|
|
3331
3487
|
});
|
|
3488
|
+
var announceCmd = defineCommand({
|
|
3489
|
+
meta: {
|
|
3490
|
+
name: "announce",
|
|
3491
|
+
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."
|
|
3492
|
+
},
|
|
3493
|
+
args: {
|
|
3494
|
+
"head-sha": {
|
|
3495
|
+
type: "string",
|
|
3496
|
+
description: "Trusted head SHA to resolve the PR (from workflow_run.head_sha)",
|
|
3497
|
+
required: true
|
|
3498
|
+
},
|
|
3499
|
+
repo: {
|
|
3500
|
+
type: "string",
|
|
3501
|
+
description: "Repository (owner/name)",
|
|
3502
|
+
required: true
|
|
3503
|
+
},
|
|
3504
|
+
"run-url": {
|
|
3505
|
+
type: "string",
|
|
3506
|
+
description: "Workflow run URL the placeholder links to",
|
|
3507
|
+
required: true
|
|
3508
|
+
},
|
|
3509
|
+
"bot-login": {
|
|
3510
|
+
type: "string",
|
|
3511
|
+
description: "Bot login to trust for the sticky comment upsert (default: github-actions[bot])"
|
|
3512
|
+
},
|
|
3513
|
+
"head-branch": {
|
|
3514
|
+
type: "string",
|
|
3515
|
+
description: "Head branch to disambiguate the PR when multiple share a commit"
|
|
3516
|
+
}
|
|
3517
|
+
},
|
|
3518
|
+
run: async ({ args }) => {
|
|
3519
|
+
await announce({
|
|
3520
|
+
repo: args.repo,
|
|
3521
|
+
headSha: args["head-sha"],
|
|
3522
|
+
botLogin: args["bot-login"] || "github-actions[bot]",
|
|
3523
|
+
runUrl: args["run-url"],
|
|
3524
|
+
headBranch: args["head-branch"]
|
|
3525
|
+
}).catch(
|
|
3526
|
+
(err) => process.stderr.write(
|
|
3527
|
+
`code-review announce: could not post the in-progress sticky (${errMsg(err)}) \u2014 continuing (cosmetic)
|
|
3528
|
+
`
|
|
3529
|
+
)
|
|
3530
|
+
);
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3332
3533
|
var requireCeilingSec = (raw) => {
|
|
3333
3534
|
if (raw === void 0) return null;
|
|
3334
3535
|
const ms = parseWallMs(raw);
|
|
@@ -3501,6 +3702,7 @@ var main = defineCommand({
|
|
|
3501
3702
|
render: renderCmd,
|
|
3502
3703
|
inline: inlineCmd,
|
|
3503
3704
|
post: postCmd,
|
|
3705
|
+
announce: announceCmd,
|
|
3504
3706
|
cost: costCmd,
|
|
3505
3707
|
"check-cost": checkCostCmd,
|
|
3506
3708
|
validate: validateCmd,
|