pr-shepherd 0.21.0 → 0.23.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/README.md +87 -124
- package/bin/checks/triage.mjs +1 -0
- package/bin/cli/args.mjs +2 -0
- package/bin/cli/fix-formatter.mjs +38 -9
- package/bin/cli/formatters.mjs +3 -45
- package/bin/cli/handlers.mjs +22 -1
- package/bin/cli/help-command-pages.mjs +27 -2
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/list-formatters.mjs +55 -4
- package/bin/cli/mark-files-as-viewed-flags.mjs +34 -0
- package/bin/cli/mark-files-as-viewed-formatter.mjs +52 -0
- package/bin/cli/mutate-formatter.mjs +50 -0
- package/bin/cli-parser.mjs +11 -2
- package/bin/cli-parser.test-support.mjs +6 -1
- package/bin/commands/check-annotations.mjs +40 -0
- package/bin/commands/check.mjs +25 -57
- package/bin/commands/check.test-support.mjs +9 -2
- package/bin/commands/commit-suggestion-instruction.mjs +1 -1
- package/bin/commands/iterate/classify.mjs +25 -30
- package/bin/commands/iterate/escalate.mjs +16 -2
- package/bin/commands/iterate/fix-code.mjs +29 -18
- package/bin/commands/iterate/helpers.mjs +1 -0
- package/bin/commands/iterate/render.mjs +9 -3
- package/bin/commands/iterate/stall.mjs +43 -2
- package/bin/commands/iterate-test-support.mjs +1 -1
- package/bin/commands/iterate-thread-test-support.mjs +18 -0
- package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +10 -1
- package/bin/commands/mark-files-as-viewed.mjs +220 -0
- package/bin/commands/resolve-mutate.mjs +41 -4
- package/bin/commands/resolve.mjs +16 -76
- package/bin/commands/resolve.test-support.mjs +2 -0
- package/bin/commands/shepherd-journal.mjs +1 -1
- package/bin/comments/authors.mjs +14 -0
- package/bin/comments/minimize-policy.mjs +6 -3
- package/bin/comments/pending-ops.mjs +6 -0
- package/bin/comments/resolve.mjs +28 -11
- package/bin/comments/resolve.test-support.mjs +6 -1
- package/bin/comments/review-visibility.mjs +14 -0
- package/bin/comments/thread-visibility.mjs +60 -0
- package/bin/comments/visible-comments.mjs +1 -1
- package/bin/config.json +1 -1
- package/bin/github/batch-parser-helpers.mjs +3 -4
- package/bin/github/batch-parsers.mjs +35 -8
- package/bin/github/batch.mjs +2 -0
- package/bin/github/check-annotations.mjs +75 -0
- package/bin/github/client.test-support.mjs +55 -0
- package/bin/github/gql/batch-pr.gql +15 -2
- package/bin/github/gql/check-run-annotations.gql +32 -0
- package/bin/github/gql/review-thread-comments.gql +27 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/github/thread-comments.mjs +34 -0
- package/bin/reporters/agent.mjs +27 -0
- package/bin/state/seen-comments.test-support.mjs +19 -0
- package/bin/threads/transcript.mjs +31 -0
- package/bin/types/agent-thread.mjs +1 -0
- package/bin/types/check-annotations.mjs +1 -0
- package/bin/types/review-thread.mjs +1 -0
- package/bin/types.mjs +3 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +31 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +1 -1
package/bin/commands/resolve.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
2
2
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
3
|
-
import { getOutdatedThreads } from "../comments/outdated.mjs";
|
|
4
|
-
import { autoResolveOutdated } from "../comments/resolve.mjs";
|
|
5
3
|
import { loadConfig } from "../config/load.mjs";
|
|
6
4
|
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
7
5
|
import { extractSuggestion } from "../suggestions/extract.mjs";
|
|
8
6
|
import { buildFetchInstructions } from "./resolve-instructions.mjs";
|
|
9
7
|
import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs";
|
|
8
|
+
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
9
|
+
import { classifyThreadVisibility } from "../comments/thread-visibility.mjs";
|
|
10
|
+
import { classifyReviewsForDisplay } from "../comments/review-visibility.mjs";
|
|
10
11
|
export { runResolveMutate } from "./resolve-mutate.mjs";
|
|
11
12
|
export async function runResolveFetch(opts) {
|
|
12
13
|
const repo = await getRepoInfo();
|
|
@@ -16,39 +17,9 @@ export async function runResolveFetch(opts) {
|
|
|
16
17
|
}
|
|
17
18
|
const { data } = await fetchPrBatch(prNumber, repo);
|
|
18
19
|
const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
|
|
19
|
-
const unresolvedThreads = data.reviewThreads.filter((t) => !t.isResolved);
|
|
20
|
-
const outdatedCandidates = data.reviewThreads.filter((t) => t.isOutdated);
|
|
21
|
-
const resolvedCandidates = data.reviewThreads.filter((t) => t.isResolved && !t.isOutdated);
|
|
22
|
-
const minimizedThreadCandidates = data.reviewThreads.filter((t) => t.isMinimized && !t.isResolved && !t.isOutdated);
|
|
23
20
|
const minimizedCommentCandidates = data.comments.filter((c) => c.isMinimized);
|
|
24
21
|
const seenMap = await loadSeenMap(stateKey);
|
|
25
|
-
const
|
|
26
|
-
const editedOutdated = [];
|
|
27
|
-
for (const t of outdatedCandidates) {
|
|
28
|
-
const cls = classifyItem(t.id, t.body, seenMap);
|
|
29
|
-
if (cls === "new")
|
|
30
|
-
unseenOutdated.push(t);
|
|
31
|
-
else if (cls === "edited")
|
|
32
|
-
editedOutdated.push(t);
|
|
33
|
-
}
|
|
34
|
-
const unseenResolved = [];
|
|
35
|
-
const editedResolved = [];
|
|
36
|
-
for (const t of resolvedCandidates) {
|
|
37
|
-
const cls = classifyItem(t.id, t.body, seenMap);
|
|
38
|
-
if (cls === "new")
|
|
39
|
-
unseenResolved.push(t);
|
|
40
|
-
else if (cls === "edited")
|
|
41
|
-
editedResolved.push(t);
|
|
42
|
-
}
|
|
43
|
-
const unseenMinimizedThreads = [];
|
|
44
|
-
const editedMinimizedThreads = [];
|
|
45
|
-
for (const t of minimizedThreadCandidates) {
|
|
46
|
-
const cls = classifyItem(t.id, t.body, seenMap);
|
|
47
|
-
if (cls === "new")
|
|
48
|
-
unseenMinimizedThreads.push(t);
|
|
49
|
-
else if (cls === "edited")
|
|
50
|
-
editedMinimizedThreads.push(t);
|
|
51
|
-
}
|
|
22
|
+
const threadVisibility = classifyThreadVisibility(data.reviewThreads, seenMap);
|
|
52
23
|
const unseenMinimizedComments = [];
|
|
53
24
|
const editedMinimizedComments = [];
|
|
54
25
|
for (const c of minimizedCommentCandidates) {
|
|
@@ -58,52 +29,19 @@ export async function runResolveFetch(opts) {
|
|
|
58
29
|
else if (cls === "edited")
|
|
59
30
|
editedMinimizedComments.push(c);
|
|
60
31
|
}
|
|
61
|
-
const outdated = getOutdatedThreads(unresolvedThreads);
|
|
62
|
-
const autoResolvedIds = new Set();
|
|
63
|
-
if (outdated.length > 0) {
|
|
64
|
-
const { resolved: resolvedIds, errors } = await autoResolveOutdated(outdated.map((t) => t.id));
|
|
65
|
-
for (const id of resolvedIds)
|
|
66
|
-
autoResolvedIds.add(id);
|
|
67
|
-
if (errors.length > 0) {
|
|
68
|
-
process.stderr.write(`pr-shepherd: auto-resolve outdated threads failed (continuing): ${errors.join(", ")}\n`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const activeThreads = unresolvedThreads.filter((t) => !t.isOutdated && !t.isMinimized);
|
|
72
|
-
const resolutionOnlyThreads = unresolvedThreads.filter((t) => !autoResolvedIds.has(t.id) && (t.isOutdated || t.isMinimized));
|
|
73
32
|
const cfg = loadConfig();
|
|
74
33
|
const visibleCommentClassification = classifyVisibleComments(data.comments, seenMap, cfg.iterate?.minimizeComments);
|
|
75
|
-
const
|
|
34
|
+
const changesRequestedReviewVisibility = classifyReviewsForDisplay(data.changesRequestedReviews, seenMap);
|
|
35
|
+
const reviewSummaryVisibility = cfg.resolve.fetchReviewSummaries
|
|
36
|
+
? classifyReviewsForDisplay(data.reviewSummaries, seenMap)
|
|
37
|
+
: { visible: [], toMarkSeen: [] };
|
|
38
|
+
const actionableThreads = threadVisibility.activeThreads.map(({ isResolved: _r, isOutdated: _o, ...rest }) => {
|
|
76
39
|
const thread = rest;
|
|
77
40
|
const suggestion = extractSuggestion(rest);
|
|
78
41
|
if (suggestion)
|
|
79
42
|
thread.suggestion = suggestion;
|
|
80
43
|
return thread;
|
|
81
44
|
});
|
|
82
|
-
const firstLookThreads = [
|
|
83
|
-
...unseenOutdated.map((t) => ({
|
|
84
|
-
...t,
|
|
85
|
-
firstLookStatus: "outdated",
|
|
86
|
-
autoResolved: autoResolvedIds.has(t.id),
|
|
87
|
-
})),
|
|
88
|
-
...editedOutdated.map((t) => ({
|
|
89
|
-
...t,
|
|
90
|
-
firstLookStatus: "outdated",
|
|
91
|
-
autoResolved: autoResolvedIds.has(t.id),
|
|
92
|
-
edited: true,
|
|
93
|
-
})),
|
|
94
|
-
...unseenResolved.map((t) => ({ ...t, firstLookStatus: "resolved" })),
|
|
95
|
-
...editedResolved.map((t) => ({
|
|
96
|
-
...t,
|
|
97
|
-
firstLookStatus: "resolved",
|
|
98
|
-
edited: true,
|
|
99
|
-
})),
|
|
100
|
-
...unseenMinimizedThreads.map((t) => ({ ...t, firstLookStatus: "minimized" })),
|
|
101
|
-
...editedMinimizedThreads.map((t) => ({
|
|
102
|
-
...t,
|
|
103
|
-
firstLookStatus: "minimized",
|
|
104
|
-
edited: true,
|
|
105
|
-
})),
|
|
106
|
-
];
|
|
107
45
|
const firstLookComments = [
|
|
108
46
|
...unseenMinimizedComments.map((c) => ({ ...c, firstLookStatus: "minimized" })),
|
|
109
47
|
...editedMinimizedComments.map((c) => ({
|
|
@@ -114,19 +52,21 @@ export async function runResolveFetch(opts) {
|
|
|
114
52
|
];
|
|
115
53
|
// Mark new and edited items as seen (best-effort — markSeen never throws).
|
|
116
54
|
await Promise.allSettled([
|
|
117
|
-
...
|
|
55
|
+
...threadVisibility.toMarkSeen.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
|
|
118
56
|
...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
119
57
|
...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
58
|
+
...changesRequestedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
59
|
+
...reviewSummaryVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
120
60
|
]);
|
|
121
61
|
const result = {
|
|
122
62
|
prNumber,
|
|
123
63
|
actionableThreads,
|
|
124
|
-
resolutionOnlyThreads,
|
|
125
|
-
firstLookThreads,
|
|
64
|
+
resolutionOnlyThreads: threadVisibility.resolutionOnlyThreads,
|
|
65
|
+
firstLookThreads: threadVisibility.firstLookThreads,
|
|
126
66
|
actionableComments: visibleCommentClassification.actionable,
|
|
127
67
|
firstLookComments,
|
|
128
|
-
changesRequestedReviews:
|
|
129
|
-
reviewSummaries:
|
|
68
|
+
changesRequestedReviews: changesRequestedReviewVisibility.visible,
|
|
69
|
+
reviewSummaries: reviewSummaryVisibility.visible,
|
|
130
70
|
commitSuggestionsEnabled: cfg.actions.commitSuggestions,
|
|
131
71
|
};
|
|
132
72
|
return { ...result, instructions: buildFetchInstructions(prNumber, result) };
|
|
@@ -102,8 +102,10 @@ function makeComment(overrides = {}) {
|
|
|
102
102
|
export function registerHooks() {
|
|
103
103
|
beforeEach(() => {
|
|
104
104
|
vi.clearAllMocks();
|
|
105
|
+
mockFetchPrBatch.mockResolvedValue({ data: makeBatchData() });
|
|
105
106
|
mockAutoResolveOutdated.mockResolvedValue({ resolved: [], errors: [] });
|
|
106
107
|
mockApplyResolveOptions.mockResolvedValue({
|
|
108
|
+
repliedThreads: [],
|
|
107
109
|
resolvedThreads: [],
|
|
108
110
|
minimizedComments: [],
|
|
109
111
|
dismissedReviews: [],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const SHEPHERD_JOURNAL_SECTION = "## Shepherd Journal";
|
|
2
2
|
export const SHEPHERD_JOURNAL_SECTION_PATTERN = /^##\s+Shepherd\s+Journal$/;
|
|
3
3
|
export const SHEPHERD_JOURNAL_APPEND_HINT = "If this section already exists, append your entries under it instead of creating a duplicate heading.";
|
|
4
|
-
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` — you are seeing these for the first time.
|
|
4
|
+
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` — you are seeing these for the first time. Eligible non-human IDs, when present, are already included in the resolve command's `--minimize-comment-ids`; if any warrants a Shepherd Journal note, append it before running resolve.";
|
|
5
5
|
export function buildShepherdJournalInstruction(prNumber, itemReferenceGuidance) {
|
|
6
6
|
return [
|
|
7
7
|
`For any large decisions or rejections you made this iteration, add or update a \`${SHEPHERD_JOURNAL_SECTION}\` section in the PR description (\`gh pr edit ${prNumber} --body …\`) summarizing each decision.`,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function isBotLogin(login) {
|
|
2
|
+
return (login ?? "").toLowerCase().includes("[bot]");
|
|
3
|
+
}
|
|
4
|
+
export function normalizeAuthorType(typeName, login) {
|
|
5
|
+
if (isBotLogin(login))
|
|
6
|
+
return "Bot";
|
|
7
|
+
if (typeName === "User" || typeName === "Bot")
|
|
8
|
+
return typeName;
|
|
9
|
+
return "Unknown";
|
|
10
|
+
}
|
|
11
|
+
export function isHumanAuthor(author) {
|
|
12
|
+
const login = author.author ?? author.login ?? "";
|
|
13
|
+
return author.authorType === "User" && !isBotLogin(login);
|
|
14
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import { isHumanAuthor } from "./authors.mjs";
|
|
2
|
+
export function shouldMinimizeAuthor(authorType, policy, author) {
|
|
3
|
+
if (isHumanAuthor({ author, authorType }))
|
|
4
|
+
return false;
|
|
2
5
|
switch (policy) {
|
|
3
6
|
case undefined:
|
|
4
7
|
case "all":
|
|
5
|
-
return
|
|
8
|
+
return authorType !== "User";
|
|
6
9
|
case "bots":
|
|
7
10
|
return authorType === "Bot";
|
|
8
11
|
case "users":
|
|
9
|
-
return
|
|
12
|
+
return false;
|
|
10
13
|
case "none":
|
|
11
14
|
return false;
|
|
12
15
|
default:
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export function setPendingOps(result, ops) {
|
|
2
2
|
const resolved = new Set(result.resolvedThreads);
|
|
3
|
+
const replied = new Set(result.repliedThreads);
|
|
3
4
|
const minimized = new Set(result.minimizedComments);
|
|
4
5
|
const dismissed = new Set(result.dismissedReviews);
|
|
6
|
+
const unrepliedThreads = ops
|
|
7
|
+
.filter((op) => op.kind === "p" && !replied.has(op.id))
|
|
8
|
+
.map((op) => op.id);
|
|
5
9
|
const unresolvedThreads = ops
|
|
6
10
|
.filter((op) => op.kind === "r" && !resolved.has(op.id))
|
|
7
11
|
.map((op) => op.id);
|
|
@@ -11,6 +15,8 @@ export function setPendingOps(result, ops) {
|
|
|
11
15
|
const undismissedReviews = ops
|
|
12
16
|
.filter((op) => op.kind === "d" && !dismissed.has(op.id))
|
|
13
17
|
.map((op) => op.id);
|
|
18
|
+
if (unrepliedThreads.length > 0)
|
|
19
|
+
result.unrepliedThreads = unrepliedThreads;
|
|
14
20
|
if (unresolvedThreads.length > 0)
|
|
15
21
|
result.unresolvedThreads = unresolvedThreads;
|
|
16
22
|
if (unminimizedComments.length > 0)
|
package/bin/comments/resolve.mjs
CHANGED
|
@@ -25,12 +25,14 @@ function dismissReviewNonDismissibleMessage(id) {
|
|
|
25
25
|
}
|
|
26
26
|
export async function applyResolveOptions(pr, repo, opts) {
|
|
27
27
|
const resolveThreadIds = dedupeIds(opts.resolveThreadIds ?? []);
|
|
28
|
+
const replyThreadIds = dedupeIds(opts.replyThreadIds ?? []);
|
|
28
29
|
const minimizeCommentIds = opts.minimizeCommentIds ?? [];
|
|
29
30
|
const dismissReviewIds = dedupeIds(opts.dismissReviewIds ?? []);
|
|
30
31
|
const minimizeCommentIdSet = new Set(minimizeCommentIds);
|
|
31
32
|
const filteredDismissReviewIds = dismissReviewIds.filter((id) => !minimizeCommentIdSet.has(id));
|
|
32
33
|
const overlappingDismissIds = dismissReviewIds.filter((id) => minimizeCommentIdSet.has(id));
|
|
33
34
|
const result = {
|
|
35
|
+
repliedThreads: [],
|
|
34
36
|
resolvedThreads: [],
|
|
35
37
|
minimizedComments: [],
|
|
36
38
|
dismissedReviews: [],
|
|
@@ -42,31 +44,35 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
42
44
|
result.skippedDismissals.push(id);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
|
-
if (filteredDismissReviewIds.length > 0 && !opts.dismissMessage) {
|
|
46
|
-
throw new Error("--message is required when dismissing reviews");
|
|
47
|
+
if ((filteredDismissReviewIds.length > 0 || replyThreadIds.length > 0) && !opts.dismissMessage) {
|
|
48
|
+
throw new Error("--message is required when replying to threads or dismissing reviews");
|
|
47
49
|
}
|
|
48
50
|
if (opts.requireSha) {
|
|
49
51
|
// Verify GitHub received the commit before resolving — prevents auto-merge
|
|
50
52
|
// before reviewers see the fix.
|
|
51
53
|
await waitForSha(pr, repo, opts.requireSha);
|
|
52
54
|
}
|
|
53
|
-
await bulkApply(resolveThreadIds, minimizeCommentIds, filteredDismissReviewIds, opts.dismissMessage ?? "", result);
|
|
55
|
+
await bulkApply(replyThreadIds, resolveThreadIds, minimizeCommentIds, filteredDismissReviewIds, opts.dismissMessage ?? "", result);
|
|
54
56
|
return result;
|
|
55
57
|
}
|
|
56
58
|
export async function autoResolveOutdated(threadIds) {
|
|
57
59
|
const result = {
|
|
60
|
+
repliedThreads: [],
|
|
58
61
|
resolvedThreads: [],
|
|
59
62
|
minimizedComments: [],
|
|
60
63
|
dismissedReviews: [],
|
|
61
64
|
errors: [],
|
|
62
65
|
};
|
|
63
|
-
await bulkApply(threadIds, [], [], "", result);
|
|
66
|
+
await bulkApply([], threadIds, [], [], "", result);
|
|
64
67
|
return { resolved: result.resolvedThreads, errors: result.errors };
|
|
65
68
|
}
|
|
66
69
|
// Keep mutation batches small so rate-limit stops leave a precise pending list.
|
|
67
70
|
const BULK_CHUNK_SIZE = 10;
|
|
68
|
-
function buildBulkMutation(resolveIds, minimizeIds, dismissIds, dismissMessage) {
|
|
71
|
+
function buildBulkMutation(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage) {
|
|
69
72
|
const ops = [];
|
|
73
|
+
for (let i = 0; i < replyIds.length; i++) {
|
|
74
|
+
ops.push(` p${i}: addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: ${JSON.stringify(replyIds[i])}, body: ${JSON.stringify(dismissMessage)} }) { comment { id } }`);
|
|
75
|
+
}
|
|
70
76
|
for (let i = 0; i < resolveIds.length; i++) {
|
|
71
77
|
ops.push(` r${i}: resolveReviewThread(input: { threadId: ${JSON.stringify(resolveIds[i])} }) { thread { isResolved } }`);
|
|
72
78
|
}
|
|
@@ -78,8 +84,9 @@ function buildBulkMutation(resolveIds, minimizeIds, dismissIds, dismissMessage)
|
|
|
78
84
|
}
|
|
79
85
|
return `mutation BulkApply {\n${ops.join("\n")}\n}`;
|
|
80
86
|
}
|
|
81
|
-
async function bulkApply(resolveIds, minimizeIds, dismissIds, dismissMessage, result) {
|
|
87
|
+
async function bulkApply(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage, result) {
|
|
82
88
|
const allOps = [
|
|
89
|
+
...replyIds.map((id) => ({ kind: "p", id })),
|
|
83
90
|
...resolveIds.map((id) => ({ kind: "r", id })),
|
|
84
91
|
...minimizeIds.map((id) => ({ kind: "m", id })),
|
|
85
92
|
...dismissIds.map((id) => ({ kind: "d", id })),
|
|
@@ -87,17 +94,15 @@ async function bulkApply(resolveIds, minimizeIds, dismissIds, dismissMessage, re
|
|
|
87
94
|
for (let i = 0; i < allOps.length; i += BULK_CHUNK_SIZE) {
|
|
88
95
|
const chunk = allOps.slice(i, i + BULK_CHUNK_SIZE);
|
|
89
96
|
// eslint-disable-next-line no-await-in-loop
|
|
90
|
-
const stopped = await bulkApplyChunk(chunk.filter((o) => o.kind === "r").map((o) => o.id), chunk.filter((o) => o.kind === "m").map((o) => o.id), chunk.filter((o) => o.kind === "d").map((o) => o.id), dismissMessage, result, i + BULK_CHUNK_SIZE < allOps.length);
|
|
97
|
+
const stopped = await bulkApplyChunk(chunk.filter((o) => o.kind === "r").map((o) => o.id), chunk.filter((o) => o.kind === "p").map((o) => o.id), chunk.filter((o) => o.kind === "m").map((o) => o.id), chunk.filter((o) => o.kind === "d").map((o) => o.id), dismissMessage, result, i + BULK_CHUNK_SIZE < allOps.length);
|
|
91
98
|
if (stopped) {
|
|
92
99
|
setPendingOps(result, allOps.slice(i));
|
|
93
100
|
return;
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
|
-
async function bulkApplyChunk(resolveIds, minimizeIds, dismissIds, dismissMessage, result, hasPendingAfter) {
|
|
98
|
-
|
|
99
|
-
return false;
|
|
100
|
-
const doc = buildBulkMutation(resolveIds, minimizeIds, dismissIds, dismissMessage);
|
|
104
|
+
async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dismissMessage, result, hasPendingAfter) {
|
|
105
|
+
const doc = buildBulkMutation(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage);
|
|
101
106
|
let data = {};
|
|
102
107
|
let graphQlErrors = [];
|
|
103
108
|
let rateLimitStop;
|
|
@@ -122,6 +127,8 @@ async function bulkApplyChunk(resolveIds, minimizeIds, dismissIds, dismissMessag
|
|
|
122
127
|
result.rateLimit = stop;
|
|
123
128
|
return true;
|
|
124
129
|
}
|
|
130
|
+
for (const id of replyIds)
|
|
131
|
+
result.errors.push(`${id}: ${msg}`);
|
|
125
132
|
for (const id of resolveIds)
|
|
126
133
|
result.errors.push(`${id}: ${msg}`);
|
|
127
134
|
for (const id of minimizeIds)
|
|
@@ -130,6 +137,16 @@ async function bulkApplyChunk(resolveIds, minimizeIds, dismissIds, dismissMessag
|
|
|
130
137
|
result.errors.push(`${id}: ${msg}`);
|
|
131
138
|
return false;
|
|
132
139
|
}
|
|
140
|
+
for (let i = 0; i < replyIds.length; i++) {
|
|
141
|
+
const id = replyIds[i];
|
|
142
|
+
if (id === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
const p = data[`p${i}`];
|
|
145
|
+
if (p?.comment?.id)
|
|
146
|
+
result.repliedThreads.push(id);
|
|
147
|
+
else if (!suppressCurrentChunkErrors)
|
|
148
|
+
result.errors.push(`${id}: reply returned null or comment not created`);
|
|
149
|
+
}
|
|
133
150
|
for (let i = 0; i < resolveIds.length; i++) {
|
|
134
151
|
const r = data[`r${i}`];
|
|
135
152
|
if (r?.thread?.isResolved === true)
|
|
@@ -15,9 +15,14 @@ const REPO = { owner: "owner", name: "repo" };
|
|
|
15
15
|
function makeBulkResponse(doc) {
|
|
16
16
|
const str = typeof doc === "string" ? doc : "";
|
|
17
17
|
const data = {};
|
|
18
|
-
for (const
|
|
18
|
+
for (const match of str.matchAll(/^\s+([a-z]\d+):/gm)) {
|
|
19
|
+
const alias = match[1];
|
|
20
|
+
if (alias === undefined)
|
|
21
|
+
continue;
|
|
19
22
|
if (alias.startsWith("r"))
|
|
20
23
|
data[alias] = { thread: { isResolved: true } };
|
|
24
|
+
else if (alias.startsWith("p"))
|
|
25
|
+
data[alias] = { comment: { id: `${alias}-comment` } };
|
|
21
26
|
else if (alias.startsWith("m"))
|
|
22
27
|
data[alias] = { minimizedComment: { isMinimized: true } };
|
|
23
28
|
else if (alias.startsWith("d"))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { classifyItem } from "../state/seen-comments.mjs";
|
|
2
|
+
export function classifyReviewsForDisplay(reviews, seenMap) {
|
|
3
|
+
const visible = [];
|
|
4
|
+
const toMarkSeen = [];
|
|
5
|
+
for (const review of reviews) {
|
|
6
|
+
const cls = classifyItem(review.id, review.body, seenMap);
|
|
7
|
+
if (cls === "unchanged")
|
|
8
|
+
continue;
|
|
9
|
+
const rendered = cls === "edited" ? { ...review, edited: true } : review;
|
|
10
|
+
visible.push(rendered);
|
|
11
|
+
toMarkSeen.push(review);
|
|
12
|
+
}
|
|
13
|
+
return { visible, toMarkSeen };
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { classifyItem } from "../state/seen-comments.mjs";
|
|
2
|
+
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
3
|
+
function withEdited(thread, edited) {
|
|
4
|
+
return edited ? { ...thread, edited: true } : thread;
|
|
5
|
+
}
|
|
6
|
+
function classifyVisibleThread(thread, seenMap) {
|
|
7
|
+
const cls = classifyItem(thread.id, threadTranscriptBody(thread), seenMap);
|
|
8
|
+
if (cls === "unchanged")
|
|
9
|
+
return null;
|
|
10
|
+
return withEdited(thread, cls === "edited");
|
|
11
|
+
}
|
|
12
|
+
function classifyFirstLookThread(thread, seenMap, firstLookStatus) {
|
|
13
|
+
const visible = classifyVisibleThread(thread, seenMap);
|
|
14
|
+
if (visible === null)
|
|
15
|
+
return null;
|
|
16
|
+
return { ...visible, firstLookStatus };
|
|
17
|
+
}
|
|
18
|
+
export function classifyThreadVisibility(threads, seenMap) {
|
|
19
|
+
const unresolvedThreads = threads.filter((t) => !t.isResolved);
|
|
20
|
+
const activeThreads = unresolvedThreads
|
|
21
|
+
.filter((t) => !t.isOutdated && !t.isMinimized)
|
|
22
|
+
.flatMap((t) => {
|
|
23
|
+
const visible = classifyVisibleThread(t, seenMap);
|
|
24
|
+
return visible ? [visible] : [];
|
|
25
|
+
});
|
|
26
|
+
const resolutionOnlyThreads = unresolvedThreads
|
|
27
|
+
.filter((t) => t.isOutdated || t.isMinimized)
|
|
28
|
+
.flatMap((t) => {
|
|
29
|
+
const visible = classifyVisibleThread(t, seenMap);
|
|
30
|
+
return visible ? [visible] : [];
|
|
31
|
+
});
|
|
32
|
+
const firstLookThreads = [
|
|
33
|
+
...threads.flatMap((t) => {
|
|
34
|
+
if (!t.isOutdated)
|
|
35
|
+
return [];
|
|
36
|
+
const visible = classifyFirstLookThread(t, seenMap, "outdated");
|
|
37
|
+
return visible ? [visible] : [];
|
|
38
|
+
}),
|
|
39
|
+
...threads.flatMap((t) => {
|
|
40
|
+
if (!t.isResolved || t.isOutdated)
|
|
41
|
+
return [];
|
|
42
|
+
const visible = classifyFirstLookThread(t, seenMap, "resolved");
|
|
43
|
+
return visible ? [visible] : [];
|
|
44
|
+
}),
|
|
45
|
+
...threads.flatMap((t) => {
|
|
46
|
+
if (!t.isMinimized || t.isResolved || t.isOutdated)
|
|
47
|
+
return [];
|
|
48
|
+
const visible = classifyFirstLookThread(t, seenMap, "minimized");
|
|
49
|
+
return visible ? [visible] : [];
|
|
50
|
+
}),
|
|
51
|
+
];
|
|
52
|
+
const seenMarkerIds = new Set();
|
|
53
|
+
const toMarkSeen = [...activeThreads, ...resolutionOnlyThreads, ...firstLookThreads].filter((thread) => {
|
|
54
|
+
if (seenMarkerIds.has(thread.id))
|
|
55
|
+
return false;
|
|
56
|
+
seenMarkerIds.add(thread.id);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
return { activeThreads, resolutionOnlyThreads, firstLookThreads, toMarkSeen };
|
|
60
|
+
}
|
|
@@ -5,7 +5,7 @@ export function classifyVisibleComments(comments, seenMap, minimizeComments) {
|
|
|
5
5
|
const minimizeIds = [];
|
|
6
6
|
const toMarkSeen = [];
|
|
7
7
|
for (const c of comments.filter((comment) => !comment.isMinimized)) {
|
|
8
|
-
if (shouldMinimizeAuthor(c.authorType, minimizeComments)) {
|
|
8
|
+
if (shouldMinimizeAuthor(c.authorType, minimizeComments, c.author)) {
|
|
9
9
|
actionable.push(c);
|
|
10
10
|
minimizeIds.push(c.id);
|
|
11
11
|
continue;
|
package/bin/config.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return "Unknown";
|
|
1
|
+
import { normalizeAuthorType } from "../comments/authors.mjs";
|
|
2
|
+
export function mapAuthorType(typeName, login) {
|
|
3
|
+
return normalizeAuthorType(typeName, login);
|
|
5
4
|
}
|
|
6
5
|
export function parseCreatedAt(iso) {
|
|
7
6
|
const ms = new Date(iso).getTime();
|
|
@@ -10,26 +10,36 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
10
10
|
}));
|
|
11
11
|
const reviewThreads = rawThreadPages.map((t) => {
|
|
12
12
|
const comment = t.comments.nodes[0];
|
|
13
|
+
const comments = t.comments.nodes.map((c) => ({
|
|
14
|
+
id: c.id,
|
|
15
|
+
isMinimized: c.isMinimized,
|
|
16
|
+
author: c.author?.login ?? "unknown",
|
|
17
|
+
authorType: mapAuthorType(c.author?.__typename, c.author?.login),
|
|
18
|
+
body: c.body,
|
|
19
|
+
url: c.url,
|
|
20
|
+
createdAtUnix: parseCreatedAt(c.createdAt),
|
|
21
|
+
}));
|
|
13
22
|
return {
|
|
14
23
|
id: t.id,
|
|
15
24
|
isResolved: t.isResolved,
|
|
16
25
|
isOutdated: t.isOutdated,
|
|
17
26
|
isMinimized: comment?.isMinimized ?? false,
|
|
18
|
-
path: comment?.path ?? null,
|
|
19
|
-
line: comment?.line ?? null,
|
|
20
|
-
startLine: comment?.startLine ?? null,
|
|
27
|
+
path: t.path ?? comment?.path ?? null,
|
|
28
|
+
line: t.line ?? comment?.line ?? null,
|
|
29
|
+
startLine: t.startLine ?? comment?.startLine ?? null,
|
|
21
30
|
author: comment?.author?.login ?? "unknown",
|
|
22
|
-
authorType: mapAuthorType(comment?.author?.__typename),
|
|
31
|
+
authorType: mapAuthorType(comment?.author?.__typename, comment?.author?.login),
|
|
23
32
|
body: comment?.body ?? "",
|
|
24
33
|
url: comment?.url ?? "",
|
|
25
34
|
createdAtUnix: comment ? parseCreatedAt(comment.createdAt) : 0,
|
|
35
|
+
comments,
|
|
26
36
|
};
|
|
27
37
|
});
|
|
28
38
|
const comments = rawCommentNodes.map((c) => ({
|
|
29
39
|
id: c.id,
|
|
30
40
|
isMinimized: c.isMinimized,
|
|
31
41
|
author: c.author?.login ?? "unknown",
|
|
32
|
-
authorType: mapAuthorType(c.author?.__typename),
|
|
42
|
+
authorType: mapAuthorType(c.author?.__typename, c.author?.login),
|
|
33
43
|
body: c.body,
|
|
34
44
|
url: c.url,
|
|
35
45
|
createdAtUnix: parseCreatedAt(c.createdAt),
|
|
@@ -37,7 +47,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
37
47
|
const changesRequestedReviews = rawReviewNodes.map((r) => ({
|
|
38
48
|
id: r.id,
|
|
39
49
|
author: r.author?.login ?? "unknown",
|
|
40
|
-
authorType: mapAuthorType(r.author?.__typename),
|
|
50
|
+
authorType: mapAuthorType(r.author?.__typename, r.author?.login),
|
|
41
51
|
body: r.body,
|
|
42
52
|
}));
|
|
43
53
|
const reviewSummaries = rawReviewSummaryNodes
|
|
@@ -45,7 +55,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
45
55
|
.map((r) => ({
|
|
46
56
|
id: r.id,
|
|
47
57
|
author: r.author?.login ?? "unknown",
|
|
48
|
-
authorType: mapAuthorType(r.author?.__typename),
|
|
58
|
+
authorType: mapAuthorType(r.author?.__typename, r.author?.login),
|
|
49
59
|
body: r.body,
|
|
50
60
|
}));
|
|
51
61
|
// APPROVED reviews often have empty bodies (clicking "Approve" without a comment), so
|
|
@@ -56,7 +66,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
56
66
|
.map((r) => ({
|
|
57
67
|
id: r.id,
|
|
58
68
|
author: r.author?.login ?? "unknown",
|
|
59
|
-
authorType: mapAuthorType(r.author?.__typename),
|
|
69
|
+
authorType: mapAuthorType(r.author?.__typename, r.author?.login),
|
|
60
70
|
body: r.body,
|
|
61
71
|
}));
|
|
62
72
|
const checks = rawCheckNodes.flatMap((node) => {
|
|
@@ -64,14 +74,28 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
64
74
|
const event = node.checkSuite?.workflowRun?.event ?? null;
|
|
65
75
|
const runId = extractRunId(node.detailsUrl);
|
|
66
76
|
const summary = extractCheckRunSummary(node.title, node.summary);
|
|
77
|
+
const rawCreatedAt = node.checkSuite
|
|
78
|
+
? (node.checkSuite.workflowRun?.createdAt ?? node.checkSuite.createdAt)
|
|
79
|
+
: undefined;
|
|
80
|
+
const rawUpdatedAt = node.checkSuite
|
|
81
|
+
? (node.checkSuite.workflowRun?.updatedAt ?? node.checkSuite.updatedAt)
|
|
82
|
+
: undefined;
|
|
83
|
+
const createdAtUnix = rawCreatedAt ? parseCreatedAt(rawCreatedAt) : undefined;
|
|
84
|
+
const startedAtUnix = node.startedAt ? parseCreatedAt(node.startedAt) : undefined;
|
|
85
|
+
const updatedAtUnix = rawUpdatedAt ? parseCreatedAt(rawUpdatedAt) : undefined;
|
|
67
86
|
return [
|
|
68
87
|
{
|
|
88
|
+
id: node.id,
|
|
69
89
|
name: node.name,
|
|
70
90
|
status: node.status,
|
|
71
91
|
conclusion: node.conclusion,
|
|
92
|
+
source: "check_run",
|
|
72
93
|
detailsUrl: node.detailsUrl ?? "",
|
|
73
94
|
event,
|
|
74
95
|
runId,
|
|
96
|
+
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
97
|
+
...(startedAtUnix !== undefined && { startedAtUnix }),
|
|
98
|
+
...(updatedAtUnix !== undefined && { updatedAtUnix }),
|
|
75
99
|
...(summary !== undefined && { summary }),
|
|
76
100
|
},
|
|
77
101
|
];
|
|
@@ -79,14 +103,17 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
79
103
|
if (node.__typename === "StatusContext") {
|
|
80
104
|
const { status, conclusion } = mapStatusContextState(node.state);
|
|
81
105
|
const summary = node.description?.trim() || undefined;
|
|
106
|
+
const createdAtUnix = node.createdAt ? parseCreatedAt(node.createdAt) : undefined;
|
|
82
107
|
return [
|
|
83
108
|
{
|
|
84
109
|
name: node.context,
|
|
85
110
|
status,
|
|
86
111
|
conclusion,
|
|
112
|
+
source: "status_context",
|
|
87
113
|
detailsUrl: node.targetUrl ?? "",
|
|
88
114
|
event: null,
|
|
89
115
|
runId: null,
|
|
116
|
+
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
90
117
|
...(summary !== undefined && { summary }),
|
|
91
118
|
},
|
|
92
119
|
];
|
package/bin/github/batch.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { graphql, graphqlWithRateLimit } from "./client.mjs";
|
|
2
2
|
import { paginateForward, paginateBackward } from "./pagination.mjs";
|
|
3
|
+
import { hydrateThreadCommentPages } from "./thread-comments.mjs";
|
|
3
4
|
import { BATCH_PR_QUERY } from "./queries.mjs";
|
|
4
5
|
import { parseRawPr } from "./batch-parsers.mjs";
|
|
5
6
|
/**
|
|
@@ -36,6 +37,7 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
|
|
|
36
37
|
// extra contains pages before the first page.
|
|
37
38
|
rawThreadPages = [...extra, ...rawThreadPages];
|
|
38
39
|
}
|
|
40
|
+
rawThreadPages = await hydrateThreadCommentPages(rawThreadPages);
|
|
39
41
|
// Paginate comments backward if the first page is incomplete.
|
|
40
42
|
let rawCommentNodes = raw.comments.nodes;
|
|
41
43
|
if (raw.comments.pageInfo.hasPreviousPage && raw.comments.pageInfo.startCursor) {
|