pr-shepherd 0.30.1 → 0.31.0
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/.claude-plugin/plugin.json +1 -1
- package/bin/cli/fix-formatter-extra.mjs +25 -0
- package/bin/cli/fix-formatter.mjs +4 -20
- package/bin/cli/iterate-lean.mjs +3 -0
- package/bin/commands/iterate/fix-code.mjs +5 -2
- package/bin/commands/iterate/helpers.mjs +3 -1
- package/bin/commands/iterate/index.mjs +5 -5
- package/bin/commands/iterate/reruns.mjs +64 -3
- package/bin/config/load.mjs +7 -0
- package/bin/config.json +2 -1
- package/bin/github/batch-parsers.mjs +2 -0
- package/bin/github/gql/batch-pr.gql +3 -0
- package/bin/types/protected-run.mjs +1 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { blockquote } from "./list-formatters.mjs";
|
|
2
|
+
export function renderCheckAnnotation(a) {
|
|
3
|
+
const loc = `${a.path}:${renderAnnotationRange(a)}`;
|
|
4
|
+
const link = a.blobUrl ? ` [↗](${a.blobUrl})` : "";
|
|
5
|
+
const title = a.title ? ` — ${a.title}` : "";
|
|
6
|
+
const lines = [`- \`${a.id}\`${link} \`${loc}\` [${a.level}]${title}`];
|
|
7
|
+
if (a.message.trim() !== "")
|
|
8
|
+
lines.push(blockquote(a.message));
|
|
9
|
+
if (a.rawDetails !== undefined && a.rawDetails.trim() !== "")
|
|
10
|
+
lines.push(blockquote(a.rawDetails));
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
export function renderProtectedRun(run) {
|
|
14
|
+
const label = run.workflowName
|
|
15
|
+
? `${run.workflowName} (${run.checkNames.join(", ")})`
|
|
16
|
+
: run.checkNames.join(", ");
|
|
17
|
+
return `- \`${run.runId}\` — \`${label}\` [matched: \`${run.matchedPattern}\`]`;
|
|
18
|
+
}
|
|
19
|
+
function renderAnnotationRange(a) {
|
|
20
|
+
if (a.startLine === null && a.endLine === null)
|
|
21
|
+
return "?";
|
|
22
|
+
const start = a.startLine ?? a.endLine;
|
|
23
|
+
const end = a.endLine ?? a.startLine;
|
|
24
|
+
return start === end ? String(start) : `${start}-${end}`;
|
|
25
|
+
}
|
|
@@ -3,6 +3,7 @@ import { joinSections } from "../util/markdown.mjs";
|
|
|
3
3
|
import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
|
|
4
4
|
import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, renderThreadConversation, blockquote, } from "./list-formatters.mjs";
|
|
5
5
|
import { numberInstructions } from "./iterate-instructions.mjs";
|
|
6
|
+
import { renderCheckAnnotation, renderProtectedRun } from "./fix-formatter-extra.mjs";
|
|
6
7
|
export function formatFixCodeResult(header, result) {
|
|
7
8
|
const sections = [header];
|
|
8
9
|
if (result.fix.threads.length > 0) {
|
|
@@ -116,6 +117,9 @@ export function formatFixCodeResult(header, result) {
|
|
|
116
117
|
sections.push("## In-progress runs");
|
|
117
118
|
sections.push(result.fix.inProgressRunIds.map((id) => `- \`${id}\``).join("\n"));
|
|
118
119
|
}
|
|
120
|
+
if (result.fix.protectedRuns.length > 0) {
|
|
121
|
+
sections.push("## Protected runs", result.fix.protectedRuns.map(renderProtectedRun).join("\n"));
|
|
122
|
+
}
|
|
119
123
|
if (result.cancelled.length > 0) {
|
|
120
124
|
sections.push("## Cancelled runs");
|
|
121
125
|
sections.push(result.cancelled.map((id) => `- \`${id}\``).join("\n"));
|
|
@@ -132,29 +136,9 @@ export function formatFixCodeResult(header, result) {
|
|
|
132
136
|
sections.push(numberInstructions(result.fix.instructions));
|
|
133
137
|
return joinSections(sections);
|
|
134
138
|
}
|
|
135
|
-
function renderCheckAnnotation(a) {
|
|
136
|
-
const loc = `${a.path}:${renderAnnotationRange(a)}`;
|
|
137
|
-
const link = a.blobUrl ? ` [↗](${a.blobUrl})` : "";
|
|
138
|
-
const title = a.title ? ` — ${a.title}` : "";
|
|
139
|
-
const lines = [`- \`${a.id}\`${link} \`${loc}\` [${a.level}]${title}`];
|
|
140
|
-
if (a.message.trim() !== "")
|
|
141
|
-
lines.push(blockquote(a.message));
|
|
142
|
-
if (a.rawDetails !== undefined && a.rawDetails.trim() !== "")
|
|
143
|
-
lines.push(blockquote(a.rawDetails));
|
|
144
|
-
return lines.join("\n");
|
|
145
|
-
}
|
|
146
139
|
function indentBlockquote(body, indent) {
|
|
147
140
|
return blockquote(body)
|
|
148
141
|
.split("\n")
|
|
149
142
|
.map((line) => `${indent}${line}`)
|
|
150
143
|
.join("\n");
|
|
151
144
|
}
|
|
152
|
-
function renderAnnotationRange(a) {
|
|
153
|
-
if (a.startLine === null && a.endLine === null)
|
|
154
|
-
return "?";
|
|
155
|
-
const start = a.startLine ?? a.endLine;
|
|
156
|
-
const end = a.endLine ?? a.startLine;
|
|
157
|
-
if (start === end)
|
|
158
|
-
return String(start);
|
|
159
|
-
return `${start}-${end}`;
|
|
160
|
-
}
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -114,6 +114,9 @@ export function projectIterateLean(result, opts) {
|
|
|
114
114
|
...(result.fix.inProgressRunIds.length > 0 && {
|
|
115
115
|
inProgressRunIds: result.fix.inProgressRunIds,
|
|
116
116
|
}),
|
|
117
|
+
...(result.fix.protectedRuns.length > 0 && {
|
|
118
|
+
protectedRuns: result.fix.protectedRuns,
|
|
119
|
+
}),
|
|
117
120
|
...(result.fix.checks.length > 0 && { checks: result.fix.checks }),
|
|
118
121
|
...(result.fix.changesRequestedReviews.length > 0 && {
|
|
119
122
|
changesRequestedReviews: result.fix.changesRequestedReviews,
|
|
@@ -7,7 +7,7 @@ import { checkEscalateTriggers, validateBaseBranch, buildEscalateSuggestion, bui
|
|
|
7
7
|
import { buildResolveCommand } from "./classify.mjs";
|
|
8
8
|
import { buildFixInstructions } from "./render.mjs";
|
|
9
9
|
import { applyStallGuard } from "./stall.mjs";
|
|
10
|
-
import { tryCancelRun,
|
|
10
|
+
import { tryCancelRun, buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./helpers.mjs";
|
|
11
11
|
import { annotationMarkerBody } from "../check-annotations.mjs";
|
|
12
12
|
import { threadTranscriptBody } from "../../threads/transcript.mjs";
|
|
13
13
|
import { isHumanAuthor, isConfiguredBotAuthor } from "../../comments/authors.mjs";
|
|
@@ -29,6 +29,7 @@ function nextFixAttempts(stored, headSha, threads) {
|
|
|
29
29
|
export async function handleFixCode(ctx) {
|
|
30
30
|
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, ruleAutoResolveThreadIds, } = ctx;
|
|
31
31
|
const failingChecks = report.checks.failing;
|
|
32
|
+
const { protectedRunIds, protectedRuns } = buildRunProtection([...failingChecks, ...report.checks.inProgress], opts.neverCancelRuns);
|
|
32
33
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
33
34
|
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha, report.threads.actionable);
|
|
34
35
|
const botCrReviews = report.changesRequestedReviews.filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames));
|
|
@@ -78,7 +79,7 @@ export async function handleFixCode(ctx) {
|
|
|
78
79
|
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
79
80
|
let cancelled = [];
|
|
80
81
|
if (!opts.noAutoCancelActionable) {
|
|
81
|
-
const runIds =
|
|
82
|
+
const runIds = buildAutoCancelRunIdsWithOptions(report, { protectedRunIds });
|
|
82
83
|
const results = await Promise.all(runIds.map((id) => tryCancelRun(id, repoOwner, repoName)));
|
|
83
84
|
cancelled = results.filter((id) => id !== null);
|
|
84
85
|
}
|
|
@@ -101,6 +102,7 @@ export async function handleFixCode(ctx) {
|
|
|
101
102
|
const inProgressRunIds = pushLikely
|
|
102
103
|
? buildInProgressRunIds(report, cancelledSet, {
|
|
103
104
|
suppressProtectedFreshReruns: false,
|
|
105
|
+
protectedRunIds,
|
|
104
106
|
})
|
|
105
107
|
: [];
|
|
106
108
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
@@ -156,6 +158,7 @@ export async function handleFixCode(ctx) {
|
|
|
156
158
|
firstLookThreads,
|
|
157
159
|
firstLookComments,
|
|
158
160
|
inProgressRunIds,
|
|
161
|
+
protectedRuns,
|
|
159
162
|
},
|
|
160
163
|
cancelled,
|
|
161
164
|
};
|
|
@@ -2,7 +2,7 @@ import { execFile as execFileCb } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { rest } from "../../github/http.mjs";
|
|
4
4
|
const execFile = promisify(execFileCb);
|
|
5
|
-
export {
|
|
5
|
+
export { buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./reruns.mjs";
|
|
6
6
|
export function buildSummary(report) {
|
|
7
7
|
return {
|
|
8
8
|
passing: report.checks.passing.length,
|
|
@@ -24,6 +24,7 @@ export function buildRelevantChecks(report) {
|
|
|
24
24
|
conclusion,
|
|
25
25
|
runId: c.runId,
|
|
26
26
|
detailsUrl: c.detailsUrl || null,
|
|
27
|
+
...(c.workflowName !== undefined && { workflowName: c.workflowName }),
|
|
27
28
|
summary: c.summary,
|
|
28
29
|
},
|
|
29
30
|
];
|
|
@@ -55,6 +56,7 @@ export function buildActiveChecks(report) {
|
|
|
55
56
|
status: c.status,
|
|
56
57
|
runId: c.runId,
|
|
57
58
|
detailsUrl: c.detailsUrl || null,
|
|
59
|
+
...(c.workflowName !== undefined && { workflowName: c.workflowName }),
|
|
58
60
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
59
61
|
}));
|
|
60
62
|
}
|
|
@@ -16,12 +16,12 @@ export async function runIterate(opts) {
|
|
|
16
16
|
const readyDelaySeconds = opts.readyDelaySeconds ?? config.watch.readyDelayMinutes * 60;
|
|
17
17
|
const stallTimeoutSeconds = opts.stallTimeoutSeconds ?? config.iterate.stallTimeoutMinutes * 60;
|
|
18
18
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
19
|
-
if (prNumber === null)
|
|
19
|
+
if (prNumber === null)
|
|
20
20
|
throw new Error("No open PR found for current branch. Pass a PR number explicitly.");
|
|
21
|
-
|
|
22
|
-
const optsWithPr = { ...opts, prNumber };
|
|
21
|
+
const neverCancelRuns = opts.neverCancelRuns ?? config.actions.neverCancelRuns;
|
|
23
22
|
const report = await runCheck({
|
|
24
|
-
...
|
|
23
|
+
...opts,
|
|
24
|
+
prNumber,
|
|
25
25
|
autoResolve: config.actions.autoResolveOutdated,
|
|
26
26
|
autoMinimizeSuppressed: config.actions.autoMinimizeSuppressed,
|
|
27
27
|
});
|
|
@@ -121,7 +121,7 @@ export async function runIterate(opts) {
|
|
|
121
121
|
return handleFixCode({
|
|
122
122
|
base,
|
|
123
123
|
report,
|
|
124
|
-
opts,
|
|
124
|
+
opts: { ...opts, prNumber, neverCancelRuns },
|
|
125
125
|
headSha,
|
|
126
126
|
stallKey,
|
|
127
127
|
prNumber,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
1
2
|
function matchesRerunCheck(failure, check) {
|
|
2
3
|
if (failure.runId !== null && check.runId !== null) {
|
|
3
4
|
return failure.runId === check.runId && failure.name === check.name;
|
|
@@ -26,12 +27,12 @@ function protectedFreshRerunIds(report) {
|
|
|
26
27
|
.map((check) => check.runId)
|
|
27
28
|
.filter((id) => id !== null));
|
|
28
29
|
}
|
|
29
|
-
export function
|
|
30
|
+
export function buildAutoCancelRunIdsWithOptions(report, opts = {}) {
|
|
30
31
|
return [
|
|
31
32
|
...new Set(report.checks.failing
|
|
32
33
|
.filter((check) => !hasProtectedFreshRerun(check, report.checks.inProgress))
|
|
33
34
|
.map((check) => check.runId)
|
|
34
|
-
.filter((id) => id !== null)),
|
|
35
|
+
.filter((id) => id !== null && !(opts.protectedRunIds?.has(id) ?? false))),
|
|
35
36
|
];
|
|
36
37
|
}
|
|
37
38
|
export function buildInProgressRunIds(report, cancelledSet, opts = {}) {
|
|
@@ -39,6 +40,66 @@ export function buildInProgressRunIds(report, cancelledSet, opts = {}) {
|
|
|
39
40
|
return [
|
|
40
41
|
...new Set(report.checks.inProgress
|
|
41
42
|
.map((check) => check.runId)
|
|
42
|
-
.filter((id) => id !== null &&
|
|
43
|
+
.filter((id) => id !== null &&
|
|
44
|
+
!cancelledSet.has(id) &&
|
|
45
|
+
!protectedRunIds.has(id) &&
|
|
46
|
+
!(opts.protectedRunIds?.has(id) ?? false))),
|
|
43
47
|
];
|
|
44
48
|
}
|
|
49
|
+
export function buildRunProtection(checks, patterns = []) {
|
|
50
|
+
if (patterns.length === 0)
|
|
51
|
+
return { protectedRunIds: new Set(), protectedRuns: [] };
|
|
52
|
+
const matchers = patterns.map((pattern) => ({
|
|
53
|
+
pattern,
|
|
54
|
+
isMatch: picomatch(pattern, { nocase: true }),
|
|
55
|
+
}));
|
|
56
|
+
const byRunId = new Map();
|
|
57
|
+
for (const check of checks) {
|
|
58
|
+
if (check.runId === null)
|
|
59
|
+
continue;
|
|
60
|
+
const match = findProtectionMatch(check, matchers);
|
|
61
|
+
if (match === null)
|
|
62
|
+
continue;
|
|
63
|
+
addProtectedRun(byRunId, check.runId, check, match);
|
|
64
|
+
}
|
|
65
|
+
const protectedRuns = [...byRunId.values()];
|
|
66
|
+
return {
|
|
67
|
+
protectedRunIds: new Set(protectedRuns.map((run) => run.runId)),
|
|
68
|
+
protectedRuns,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function addProtectedRun(byRunId, runId, check, matchedPattern) {
|
|
72
|
+
const existing = byRunId.get(runId);
|
|
73
|
+
if (existing) {
|
|
74
|
+
updateProtectedRun(existing, check);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
byRunId.set(runId, createProtectedRun(runId, check, matchedPattern));
|
|
78
|
+
}
|
|
79
|
+
function updateProtectedRun(run, check) {
|
|
80
|
+
if (!run.checkNames.includes(check.name))
|
|
81
|
+
run.checkNames.push(check.name);
|
|
82
|
+
if (run.workflowName === undefined && check.workflowName !== undefined) {
|
|
83
|
+
run.workflowName = check.workflowName;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function createProtectedRun(runId, check, matchedPattern) {
|
|
87
|
+
return {
|
|
88
|
+
runId,
|
|
89
|
+
matchedPattern,
|
|
90
|
+
checkNames: [check.name],
|
|
91
|
+
...(check.workflowName !== undefined && { workflowName: check.workflowName }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function findProtectionMatch(check, matchers) {
|
|
95
|
+
const candidates = [
|
|
96
|
+
check.workflowName,
|
|
97
|
+
"jobName" in check ? check.jobName : undefined,
|
|
98
|
+
check.name,
|
|
99
|
+
].filter((value) => value !== undefined && value.trim() !== "");
|
|
100
|
+
for (const matcher of matchers) {
|
|
101
|
+
if (candidates.some((candidate) => matcher.isMatch(candidate)))
|
|
102
|
+
return matcher.pattern;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
package/bin/config/load.mjs
CHANGED
|
@@ -57,6 +57,12 @@ function parseIgnoreChecks(value) {
|
|
|
57
57
|
}
|
|
58
58
|
return value;
|
|
59
59
|
}
|
|
60
|
+
function parseNeverCancelRuns(value) {
|
|
61
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
62
|
+
throw new Error(`Invalid config: actions.neverCancelRuns must be an array of strings`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
60
66
|
const defaults = builtins;
|
|
61
67
|
const configCache = new Map();
|
|
62
68
|
export function loadConfig() {
|
|
@@ -74,6 +80,7 @@ export function loadConfig() {
|
|
|
74
80
|
const config = deepMerge(defaults, parsed);
|
|
75
81
|
config.botUsernames = parseBotUsernames(config.botUsernames);
|
|
76
82
|
config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
|
|
83
|
+
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
77
84
|
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
78
85
|
configCache.set(cwd, config);
|
|
79
86
|
return config;
|
package/bin/config.json
CHANGED
|
@@ -82,6 +82,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
82
82
|
const checks = rawCheckNodes.flatMap((node) => {
|
|
83
83
|
if (node.__typename === "CheckRun") {
|
|
84
84
|
const event = node.checkSuite?.workflowRun?.event ?? null;
|
|
85
|
+
const workflowName = node.checkSuite?.workflowRun?.workflow?.name?.trim() || undefined;
|
|
85
86
|
const runId = extractRunId(node.detailsUrl);
|
|
86
87
|
const summary = extractCheckRunSummary(node.title, node.summary);
|
|
87
88
|
const rawCreatedAt = node.checkSuite?.workflowRun?.createdAt ?? node.checkSuite?.createdAt;
|
|
@@ -100,6 +101,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
100
101
|
detailsUrl: node.detailsUrl ?? "",
|
|
101
102
|
event,
|
|
102
103
|
runId,
|
|
104
|
+
...(workflowName !== undefined && { workflowName }),
|
|
103
105
|
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
104
106
|
...(startedAtUnix !== undefined && { startedAtUnix }),
|
|
105
107
|
...(completedAtUnix !== undefined && { completedAtUnix }),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@vitest/coverage-v8": "^4.1.4",
|
|
39
39
|
"husky": "^9.1.7",
|
|
40
40
|
"knip": "^6.14.1",
|
|
41
|
-
"oxfmt": "^0.
|
|
41
|
+
"oxfmt": "^0.53.0",
|
|
42
42
|
"oxlint": "^1.60.0",
|
|
43
43
|
"typescript": "^6.0.3",
|
|
44
44
|
"vitest": "^4.1.4"
|