pr-shepherd 1.0.0 → 1.1.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/poll-summary-formatter.mjs +3 -0
- package/bin/cli/rule-auto-resolve-lines.d.mts +3 -0
- package/bin/cli/rule-auto-resolve-lines.mjs +12 -0
- package/bin/cli/stack-layer-view.d.mts +2 -1
- package/bin/cli/stack-overview.mjs +3 -0
- package/bin/commands/check-activity.d.mts +8 -0
- package/bin/commands/check-activity.mjs +26 -0
- package/bin/commands/check-annotations.mjs +2 -1
- package/bin/commands/check-wait-detectors.mjs +11 -0
- package/bin/commands/check.mjs +6 -1
- package/bin/commands/poll-summary-signature.d.mts +6 -0
- package/bin/commands/poll-summary-signature.mjs +19 -2
- package/bin/commands/poll-summary.mjs +7 -0
- package/bin/commands/ready-delay.d.mts +1 -0
- package/bin/commands/ready-delay.mjs +8 -3
- package/bin/commands/rule-auto-resolve-format.d.mts +7 -0
- package/bin/commands/rule-auto-resolve-format.mjs +11 -4
- package/bin/commands/rule-auto-resolve.d.mts +14 -2
- package/bin/comments/thread-visibility.d.mts +6 -0
- package/bin/comments/thread-visibility.mjs +30 -15
- package/bin/github/batch-parse-suites.d.mts +14 -0
- package/bin/github/batch-parse-suites.mjs +4 -1
- package/bin/github/batch-parser-helpers.mjs +3 -0
- package/bin/github/check-annotation-cache.d.mts +5 -0
- package/bin/github/check-annotation-cache.mjs +6 -3
- package/bin/github/fingerprint.mjs +4 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +5 -0
- package/bin/github/poll-summary-annotation-probe.mjs +17 -3
- package/bin/github/poll-summary-annotations.d.mts +14 -0
- package/bin/github/poll-summary-annotations.mjs +75 -0
- package/bin/github/poll-summary-blocking-reviewer.d.mts +2 -0
- package/bin/github/poll-summary-blocking-reviewer.mjs +9 -0
- package/bin/github/poll-summary-checks.d.mts +1 -0
- package/bin/github/poll-summary-checks.mjs +20 -2
- package/bin/github/poll-summary-fingerprint.d.mts +7 -0
- package/bin/github/poll-summary-fingerprint.mjs +7 -0
- package/bin/github/poll-summary-projector.d.mts +3 -1
- package/bin/github/poll-summary-projector.mjs +13 -24
- package/bin/github/poll-summary-queue-removal.d.mts +2 -0
- package/bin/github/poll-summary-queue-removal.mjs +11 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-review-parity.d.mts +24 -0
- package/bin/github/poll-summary-review-parity.mjs +82 -0
- package/bin/github/poll-summary-review.d.mts +11 -2
- package/bin/github/poll-summary-review.mjs +87 -59
- package/bin/github/poll-summary-row-error.d.mts +16 -0
- package/bin/github/poll-summary-row-error.mjs +74 -0
- package/bin/github/poll-summary-rule-auto-resolve.d.mts +32 -0
- package/bin/github/poll-summary-rule-auto-resolve.mjs +119 -0
- package/bin/github/poll-summary-rule-handoff.d.mts +7 -0
- package/bin/github/poll-summary-rule-handoff.mjs +20 -0
- package/bin/github/poll-summary-rule-items.d.mts +20 -0
- package/bin/github/poll-summary-rule-items.mjs +28 -0
- package/bin/github/poll-summary.mjs +15 -7
- package/bin/github/rest-reader-core.mjs +13 -2
- package/bin/github/rest-summary-read.mjs +12 -1
- package/bin/types/github.d.mts +2 -0
- package/bin/types/poll-summary.d.mts +23 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
|
@@ -3,6 +3,7 @@ import { withPollSummaryInstructions } from "../commands/poll-summary-instructio
|
|
|
3
3
|
import { formatStackOverview, projectStackOverview } from "./stack-overview.mjs";
|
|
4
4
|
import { formatTransportEvidence } from "./transport-formatter.mjs";
|
|
5
5
|
import { formatNextCheckLines } from "./next-check-format.mjs";
|
|
6
|
+
import { formatRuleAutoResolveLines } from "./rule-auto-resolve-lines.mjs";
|
|
6
7
|
export function formatPollSummaryResult(result) {
|
|
7
8
|
if (result.selection.kind === "stack")
|
|
8
9
|
return formatStackOverview(projectStackOverview(result));
|
|
@@ -69,12 +70,14 @@ function formatItem(item) {
|
|
|
69
70
|
...(review
|
|
70
71
|
? [` - review: ${formatCounts(review, review.incomplete ? ", incomplete" : "")}`]
|
|
71
72
|
: []),
|
|
73
|
+
...formatRuleAutoResolveLines(item.ruleAutoResolve),
|
|
72
74
|
...(item.queueRemoval
|
|
73
75
|
? [
|
|
74
76
|
` - queue removal: reason \`${item.queueRemoval.reason ?? "UNKNOWN"}\` · at \`${item.queueRemoval.createdAtUnix}\`${item.queueRemoval.actor ? ` · actor \`@${item.queueRemoval.actor}\`` : ""}${item.queueRemoval.beforeCommitOid ? ` · commit \`${item.queueRemoval.beforeCommitOid}\`` : ""}${item.queueRemoval.beforeCommitParentOids?.length ? ` · parents \`${item.queueRemoval.beforeCommitParentOids.join(",")}\`` : ""}`,
|
|
75
77
|
]
|
|
76
78
|
: []),
|
|
77
79
|
` - reasons: ${item.reasons.map((reason) => `\`${reason}\``).join(", ")}`,
|
|
80
|
+
...(item.error ? [` - error: ${item.error.replace(/\s*[\r\n]+\s*/g, " ")}`] : []),
|
|
78
81
|
...(item.transport ? [` - transport \`${item.transport}\``] : []),
|
|
79
82
|
...(item.transportUnavailable ?? []).map(({ field, reason }) => ` - unavailable \`${field}\`: ${reason}`),
|
|
80
83
|
...(item.pollCommand
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PollSummaryRuleAutoResolve } from "../types.mts";
|
|
2
|
+
/** Row lines for an aggregate row or stack layer: the one-PR `## Classification auto-resolve` content. */
|
|
3
|
+
export declare function formatRuleAutoResolveLines(event: PollSummaryRuleAutoResolve | undefined): string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Row lines for an aggregate row or stack layer: the one-PR `## Classification auto-resolve` content. */
|
|
2
|
+
export function formatRuleAutoResolveLines(event) {
|
|
3
|
+
if (!event)
|
|
4
|
+
return [];
|
|
5
|
+
const link = (kind, id, url) => ` - ${kind} \`${id}\`${url ? ` ${url}` : ""}`;
|
|
6
|
+
return [
|
|
7
|
+
` - classification auto-resolve: ${event.summary}`,
|
|
8
|
+
...(event.threads ?? []).map((thread) => link("thread", thread.id, thread.url)),
|
|
9
|
+
...(event.minimized ?? []).map((item) => link(item.kind, item.id, item.url)),
|
|
10
|
+
...(event.errors ?? []).map((error) => ` - ${error}`),
|
|
11
|
+
];
|
|
12
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PollSummaryItem } from "../types.mts";
|
|
1
|
+
import type { PollSummaryItem, PollSummaryRuleAutoResolve } from "../types.mts";
|
|
2
2
|
/** One lean stack row shared by Markdown, JSON, and MCP. */
|
|
3
3
|
export interface StackLayerView {
|
|
4
4
|
transport?: "rest";
|
|
@@ -26,6 +26,7 @@ export interface StackLayerView {
|
|
|
26
26
|
actionable?: number;
|
|
27
27
|
checksIncomplete?: true;
|
|
28
28
|
reviewIncomplete?: true;
|
|
29
|
+
ruleAutoResolve?: PollSummaryRuleAutoResolve;
|
|
29
30
|
queueRemoval?: {
|
|
30
31
|
reason: string | null;
|
|
31
32
|
actor?: string;
|
|
@@ -3,6 +3,7 @@ import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
|
3
3
|
import { formatTransportEvidence } from "./transport-formatter.mjs";
|
|
4
4
|
import { formatNextCheckLines } from "./next-check-format.mjs";
|
|
5
5
|
import { formatPrUrl } from "../pr-reference.mjs";
|
|
6
|
+
import { formatRuleAutoResolveLines } from "./rule-auto-resolve-lines.mjs";
|
|
6
7
|
/** Lean stack payload shared by Markdown, JSON, and MCP. */
|
|
7
8
|
export function projectStackOverview(result) {
|
|
8
9
|
const selection = result.selection;
|
|
@@ -59,6 +60,7 @@ function projectLayer(item, stale) {
|
|
|
59
60
|
(item.review?.actionable ?? 0) > 0 && { actionable: item.review?.actionable }),
|
|
60
61
|
...(item.checks?.incomplete && { checksIncomplete: true }),
|
|
61
62
|
...(item.review?.incomplete && { reviewIncomplete: true }),
|
|
63
|
+
...(item.ruleAutoResolve && { ruleAutoResolve: item.ruleAutoResolve }),
|
|
62
64
|
...(removal && {
|
|
63
65
|
queueRemoval: {
|
|
64
66
|
reason: removal.reason,
|
|
@@ -156,6 +158,7 @@ function formatLayerLines(layer, index, overview) {
|
|
|
156
158
|
return [
|
|
157
159
|
`- ${layer.url === formatPrUrl(overview.repo, layer.pr) ? `PR #${layer.pr}: ${layer.title}` : `[PR #${layer.pr}: ${layer.title}](${layer.url})`}${facts.length > 0 ? ` — ${facts.join(" · ")}` : ""}`,
|
|
158
160
|
` - ${details.join(" · ")}`,
|
|
161
|
+
...formatRuleAutoResolveLines(layer.ruleAutoResolve),
|
|
159
162
|
...(layer.transport ? [` - transport \`${layer.transport}\``] : []),
|
|
160
163
|
...(layer.transportUnavailable ?? []).map(({ field, reason }) => ` - unavailable \`${field}\`: ${reason}`),
|
|
161
164
|
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BatchPartition } from "../classify/apply.mts";
|
|
2
|
+
import type { PrActivitySummary } from "../types.mts";
|
|
3
|
+
/**
|
|
4
|
+
* Drop rule-suppressed items from the activity digest, matching what `check` hides from the
|
|
5
|
+
* agent. IDs whose automatic minimize/resolve was denied stay visible: `check` surfaces those
|
|
6
|
+
* once so the caller can act. `reviewRoundCount` is raw review history and stays unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function filterSuppressedActivity(activity: PrActivitySummary | undefined, partition: BatchPartition, denied: Iterable<string>): PrActivitySummary | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop rule-suppressed items from the activity digest, matching what `check` hides from the
|
|
3
|
+
* agent. IDs whose automatic minimize/resolve was denied stay visible: `check` surfaces those
|
|
4
|
+
* once so the caller can act. `reviewRoundCount` is raw review history and stays unchanged.
|
|
5
|
+
*/
|
|
6
|
+
export function filterSuppressedActivity(activity, partition, denied) {
|
|
7
|
+
if (!activity)
|
|
8
|
+
return activity;
|
|
9
|
+
const deniedIds = new Set(denied);
|
|
10
|
+
const hidden = (id, suppressed) => suppressed.has(id) && !deniedIds.has(id);
|
|
11
|
+
const reviewItemsSinceLatestCommit = activity.reviewItemsSinceLatestCommit.filter((item) => {
|
|
12
|
+
switch (item.kind) {
|
|
13
|
+
case "pr-comment":
|
|
14
|
+
return !hidden(item.id, partition.suppressedCommentIds);
|
|
15
|
+
case "review-thread-comment":
|
|
16
|
+
return !item.threadId || !hidden(item.threadId, partition.suppressedThreadIds);
|
|
17
|
+
case "review-summary":
|
|
18
|
+
return !hidden(item.id, partition.suppressedReviewSummaryIds);
|
|
19
|
+
case "changes-requested-review":
|
|
20
|
+
return !partition.suppressedChangesRequestedIds.has(item.id);
|
|
21
|
+
default:
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return { ...activity, reviewItemsSinceLatestCommit };
|
|
26
|
+
}
|
|
@@ -42,7 +42,8 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber,
|
|
|
42
42
|
...buckets.filtered,
|
|
43
43
|
...buckets.ignored,
|
|
44
44
|
].filter(shouldFetchCheckAnnotations);
|
|
45
|
-
const
|
|
45
|
+
const expectedCounts = new Map(candidates.flatMap((c) => c.id != null && c.annotationCount ? [[c.id, c.annotationCount]] : []));
|
|
46
|
+
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber, cacheOpts && { ...cacheOpts, expectedCounts });
|
|
46
47
|
const byId = new Map(annotated.flatMap((c) => (c.id != null ? [[c.id, c]] : [])));
|
|
47
48
|
const apply = (list) => list.map((c) => {
|
|
48
49
|
const next = (c.id == null ? undefined : byId.get(c.id)) ?? c;
|
|
@@ -30,6 +30,17 @@ function detectorsCover(stored) {
|
|
|
30
30
|
* Read the PR's REST change detectors: the pull, the head's check runs, check suites and
|
|
31
31
|
* statuses, the three feedback lists, and the base branch. Every read is conditional, so an
|
|
32
32
|
* unchanged resource answers a free 304.
|
|
33
|
+
*
|
|
34
|
+
* The feedback lists are deliberately not rule-aware (#635 gap 7). An edited bot comment, even
|
|
35
|
+
* a rule-suppressed or minimized one, answers 200 and wakes the tick. The wake costs one
|
|
36
|
+
* charged list read plus one `BatchPr` first page (a complete fingerprint needs no more),
|
|
37
|
+
* the same point a fingerprint hit spends. Once the digest is re-recorded, a later eligible
|
|
38
|
+
* tick can replay for free again, but only if every detector answers 304 with that digest and
|
|
39
|
+
* the stored snapshot is within `poll.reconcile-seconds`; otherwise it goes to `BatchPr`. Being
|
|
40
|
+
* rule-aware would need the loaded rules, a persisted set of hidden comment ids (REST has no
|
|
41
|
+
* `isMinimized`), and a content digest in place of the ETag. That is more code, and more risk
|
|
42
|
+
* of sleeping through a real change (a rule-suppressed comment edited so it no longer matches,
|
|
43
|
+
* an edited minimized human comment, which `check` surfaces), than the one point it saves.
|
|
33
44
|
*/
|
|
34
45
|
async function readDetectors(prNumber, repo, report) {
|
|
35
46
|
const prefix = restRepoPath(repo);
|
package/bin/commands/check.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers
|
|
|
33
33
|
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
34
34
|
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./iterate/thread-mutation-routing.mjs";
|
|
35
35
|
import { loadClassifyRules } from "../classify/loader.mjs";
|
|
36
|
+
import { filterSuppressedActivity } from "./check-activity.mjs";
|
|
36
37
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
37
38
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
38
39
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
@@ -449,7 +450,11 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
449
450
|
...(batchData.allowedMergeMethods && {
|
|
450
451
|
allowedMergeMethods: batchData.allowedMergeMethods,
|
|
451
452
|
}),
|
|
452
|
-
activity: batchData.activity,
|
|
453
|
+
activity: filterSuppressedActivity(batchData.activity, partition, [
|
|
454
|
+
...deniedRuleAutoResolveCommentIds,
|
|
455
|
+
...deniedRuleAutoResolveThreadIds,
|
|
456
|
+
...deniedRuleAutoResolveReviewSummaryIds,
|
|
457
|
+
]),
|
|
453
458
|
...(hasQueueState && {
|
|
454
459
|
mergeQueue: {
|
|
455
460
|
enabled: Boolean(batchData.isMergeQueueEnabled),
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { PollSummaryResult } from "../types.mts";
|
|
2
|
+
/**
|
|
3
|
+
* Decision-relevant state only. Raw review totals and the ignored, skipped, filtered, and
|
|
4
|
+
* superseded check counts move when rules suppress a bot notice or a bot reposts a status
|
|
5
|
+
* check, which would reprint quiet polls and keep an idle stack from reaching `stall-timeout`.
|
|
6
|
+
* Post-rule `actionable` and the check transitions that drive routing stay in.
|
|
7
|
+
*/
|
|
2
8
|
export declare function summaryStatusSignature(result: PollSummaryResult): string;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision-relevant state only. Raw review totals and the ignored, skipped, filtered, and
|
|
3
|
+
* superseded check counts move when rules suppress a bot notice or a bot reposts a status
|
|
4
|
+
* check, which would reprint quiet polls and keep an idle stack from reaching `stall-timeout`.
|
|
5
|
+
* Post-rule `actionable` and the check transitions that drive routing stay in.
|
|
6
|
+
*/
|
|
1
7
|
export function summaryStatusSignature(result) {
|
|
2
8
|
return JSON.stringify({
|
|
3
9
|
nextAction: result.nextAction,
|
|
@@ -9,8 +15,19 @@ export function summaryStatusSignature(result) {
|
|
|
9
15
|
mergeable: item.mergeable,
|
|
10
16
|
mergeStateStatus: item.mergeStateStatus,
|
|
11
17
|
reviewDecision: item.reviewDecision,
|
|
12
|
-
checks: item.checks
|
|
13
|
-
|
|
18
|
+
checks: item.checks && {
|
|
19
|
+
passing: item.checks.passing,
|
|
20
|
+
failing: item.checks.failing,
|
|
21
|
+
inProgress: item.checks.inProgress,
|
|
22
|
+
incomplete: item.checks.incomplete,
|
|
23
|
+
annotated: item.checks.annotated,
|
|
24
|
+
unreportedRequired: item.checks.unreportedRequired,
|
|
25
|
+
actionsWorkflowInProgress: item.checks.actionsWorkflowInProgress,
|
|
26
|
+
},
|
|
27
|
+
review: item.review && {
|
|
28
|
+
actionable: item.review.actionable,
|
|
29
|
+
incomplete: item.review.incomplete,
|
|
30
|
+
},
|
|
14
31
|
})),
|
|
15
32
|
});
|
|
16
33
|
}
|
|
@@ -82,6 +82,7 @@ async function runAggregatePollCore(opts) {
|
|
|
82
82
|
try {
|
|
83
83
|
lastTick = await readPollSummaryTick(opts);
|
|
84
84
|
last = lastTick.result;
|
|
85
|
+
writeAggregateRuleAutoResolve(last);
|
|
85
86
|
rateLimitRetry.reset();
|
|
86
87
|
}
|
|
87
88
|
catch (error) {
|
|
@@ -192,6 +193,12 @@ async function runAggregatePollCore(opts) {
|
|
|
192
193
|
await sleep(sleepMs);
|
|
193
194
|
}
|
|
194
195
|
}
|
|
196
|
+
/** An auto-resolve is a one-tick event; forward it so a swallowed tick still records it. */
|
|
197
|
+
function writeAggregateRuleAutoResolve(result) {
|
|
198
|
+
for (const item of result.prs)
|
|
199
|
+
if (item.ruleAutoResolve?.summary)
|
|
200
|
+
process.stderr.write(`#${item.pr} ${item.ruleAutoResolve.summary}\n`);
|
|
201
|
+
}
|
|
195
202
|
function presentProbedCancel(result, merge) {
|
|
196
203
|
// The stack planner already cancels an all-merged stack and escalates a
|
|
197
204
|
// closed-unmerged layer. Do not rewrite that into a successful stop.
|
|
@@ -30,6 +30,7 @@ interface ReadyDelayState {
|
|
|
30
30
|
export declare function updateReadyDelay(prNumber: number, isReady: boolean, readyDelaySeconds: number, owner: string, repo: string, options: {
|
|
31
31
|
headSha: string;
|
|
32
32
|
alreadyElapsed?: boolean;
|
|
33
|
+
summary?: boolean;
|
|
33
34
|
}): Promise<ReadyDelayState>;
|
|
34
35
|
/** Delete the ready-delay marker once an elapsed delay has been consumed. */
|
|
35
36
|
export declare function clearReadyDelay(prNumber: number, owner: string, repo: string): Promise<void>;
|
|
@@ -21,7 +21,7 @@ import { resolvePrStatePath } from "../state/base.mjs";
|
|
|
21
21
|
* the loop and tells one-shot agents to stop.
|
|
22
22
|
*/
|
|
23
23
|
export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, owner, repo, options) {
|
|
24
|
-
const markerPath = readySincePath(prNumber, owner, repo);
|
|
24
|
+
const markerPath = readySincePath(prNumber, owner, repo, options.summary);
|
|
25
25
|
if (!isReady) {
|
|
26
26
|
// Reset the timer.
|
|
27
27
|
await safeUnlink(markerPath);
|
|
@@ -75,8 +75,13 @@ export async function readyDelayElapsed(prNumber, owner, repo, readyDelaySeconds
|
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
76
76
|
// Helpers
|
|
77
77
|
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The one-PR session owns `ready-since.txt`. The aggregate summary's coarser
|
|
80
|
+
* view keeps its own `ready-since-summary.txt` so it can never reset or start
|
|
81
|
+
* the authoritative timer.
|
|
82
|
+
*/
|
|
83
|
+
function readySincePath(pr, owner, repo, summary = false) {
|
|
84
|
+
return resolvePrStatePath({ owner, repo, pr }, summary ? "ready-since-summary.txt" : "ready-since.txt");
|
|
80
85
|
}
|
|
81
86
|
/** The marker's start time, or null when it is missing, malformed, or bound to another head. */
|
|
82
87
|
async function readReadySince(path, headSha) {
|
|
@@ -3,6 +3,13 @@ export declare function uniqueReasons(reasons: readonly string[]): string[];
|
|
|
3
3
|
export declare function decorateAutoResolveError(error: string, reasons: readonly string[]): string;
|
|
4
4
|
export declare function reasonsForError(error: string, ids: readonly string[], ruleReasons: ReadonlyMap<string, readonly string[]>): string[];
|
|
5
5
|
export declare function ruleAutoResolveFromReport(report: ShepherdReport): RuleAutoResolveReport | undefined;
|
|
6
|
+
/** The one report shape iterate and the aggregate summary both surface. */
|
|
7
|
+
export declare function buildRuleAutoResolveReport(input: {
|
|
8
|
+
threads: AutoResolvedThread[];
|
|
9
|
+
minimized: AutoMinimizedItem[];
|
|
10
|
+
errors: string[];
|
|
11
|
+
errorReasons: string[];
|
|
12
|
+
}): RuleAutoResolveReport | undefined;
|
|
6
13
|
export declare function projectRuleAutoResolve(event: RuleAutoResolveReport): Record<string, unknown>;
|
|
7
14
|
export declare function insertRuleAutoResolveSection(text: string, event: RuleAutoResolveReport | undefined): string;
|
|
8
15
|
export declare function stripReplayedRuleAutoResolve(report: ShepherdReport): ShepherdReport;
|
|
@@ -57,9 +57,16 @@ function splitJoined(reason) {
|
|
|
57
57
|
return reason.split("; ").filter(Boolean);
|
|
58
58
|
}
|
|
59
59
|
export function ruleAutoResolveFromReport(report) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
return buildRuleAutoResolveReport({
|
|
61
|
+
threads: report.threads.autoResolved,
|
|
62
|
+
minimized: report.comments.autoMinimized ?? [],
|
|
63
|
+
errors: report.threads.autoResolveErrors,
|
|
64
|
+
errorReasons: report.threads.autoResolveErrorReasons ?? [],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** The one report shape iterate and the aggregate summary both surface. */
|
|
68
|
+
export function buildRuleAutoResolveReport(input) {
|
|
69
|
+
const { threads, minimized, errors } = input;
|
|
63
70
|
if (threads.length === 0 && minimized.length === 0 && errors.length === 0)
|
|
64
71
|
return undefined;
|
|
65
72
|
const comments = minimized.filter((item) => item.kind === "pr-comment").length;
|
|
@@ -70,7 +77,7 @@ export function ruleAutoResolveFromReport(report) {
|
|
|
70
77
|
...threads.flatMap((thread) => splitJoined(thread.ruleReason)),
|
|
71
78
|
...minimized.flatMap((item) => splitJoined(item.ruleReason)),
|
|
72
79
|
])
|
|
73
|
-
: uniqueReasons(
|
|
80
|
+
: uniqueReasons(input.errorReasons);
|
|
74
81
|
const body = ruleAutoResolveBody({ threads: threads.length, comments, reviewSummaries }) ??
|
|
75
82
|
`auto-resolve failed for ${plural(errors.length, "mutation")}`;
|
|
76
83
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BatchPartition } from "../classify/apply.mts";
|
|
2
|
-
import type { AutoMinimizedItem, AutoResolvedThread,
|
|
2
|
+
import type { AutoMinimizedItem, AutoResolvedThread, ReviewThread } from "../types.mts";
|
|
3
3
|
export interface RuleAutoResolveApplied {
|
|
4
4
|
threadIds: string[];
|
|
5
5
|
commentIds: string[];
|
|
@@ -9,8 +9,20 @@ export interface RuleAutoResolveApplied {
|
|
|
9
9
|
autoResolveErrors: string[];
|
|
10
10
|
errorReasons: string[];
|
|
11
11
|
}
|
|
12
|
+
interface MinimizableItem {
|
|
13
|
+
id: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
viewerCanMinimize?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** The part of a PR read the mutations need. A full batch and a compact summary both satisfy it. */
|
|
18
|
+
export interface RuleAutoResolveBatch {
|
|
19
|
+
comments: readonly MinimizableItem[];
|
|
20
|
+
reviewSummaries: readonly MinimizableItem[];
|
|
21
|
+
reviewThreads: readonly ReviewThread[];
|
|
22
|
+
}
|
|
12
23
|
export declare function applySuppressedRuleAutoResolve(input: {
|
|
13
24
|
enabled: boolean;
|
|
14
25
|
partition: BatchPartition;
|
|
15
|
-
batch:
|
|
26
|
+
batch: RuleAutoResolveBatch;
|
|
16
27
|
}): Promise<RuleAutoResolveApplied>;
|
|
28
|
+
export {};
|
|
@@ -8,5 +8,11 @@ interface ThreadVisibility {
|
|
|
8
8
|
firstLookThreads: FirstLookThread[];
|
|
9
9
|
toMarkSeen: ReviewThread[];
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether an unresolved thread with a known outdated state keeps returning every tick once seen,
|
|
13
|
+
* given an authorized mutation. The compact summary calls this so its count cannot drift from
|
|
14
|
+
* `classifyThreadVisibility`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function unresolvedThreadRepeats(thread: ReviewThread, ended: boolean, botUsernames: NormalizedBotUsernames, policy: ResolveOtherHumanThreads): boolean;
|
|
11
17
|
export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames, repeatableThreadIds?: ReadonlySet<string>, resolveOtherHumanThreads?: ResolveOtherHumanThreads): ThreadVisibility;
|
|
12
18
|
export {};
|
|
@@ -3,6 +3,9 @@ import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
|
3
3
|
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, } from "./authors.mjs";
|
|
4
4
|
import { threadEndedByShepherd } from "./marker.mjs";
|
|
5
5
|
import { shouldResolveOtherHumanThread } from "./thread-resolve-policy.mjs";
|
|
6
|
+
function isOrdinaryHuman(thread, botUsernames) {
|
|
7
|
+
return isHumanAuthor(thread) && !isConfiguredBotAuthor(thread, botUsernames);
|
|
8
|
+
}
|
|
6
9
|
function withEdited(thread, edited) {
|
|
7
10
|
return edited ? { ...thread, edited: true } : thread;
|
|
8
11
|
}
|
|
@@ -20,9 +23,33 @@ function classifyFirstLookThread(thread, seenMap, firstLookStatus) {
|
|
|
20
23
|
return null;
|
|
21
24
|
return { ...visible, firstLookStatus };
|
|
22
25
|
}
|
|
26
|
+
/** An unresolved, current thread whose author class makes it repeat while a mutation is authorized. */
|
|
27
|
+
function repeatsAsActive(thread, ended, botUsernames, policy) {
|
|
28
|
+
if (thread.isOutdated !== false || thread.isMinimized === true || ended)
|
|
29
|
+
return false;
|
|
30
|
+
return (isConfiguredBotAuthor(thread, botUsernames) ||
|
|
31
|
+
isViewerAuthoredHuman(thread, botUsernames) ||
|
|
32
|
+
(isOrdinaryHuman(thread, botUsernames) && policy === "always"));
|
|
33
|
+
}
|
|
34
|
+
/** An unresolved thread that is resolution-only work: outdated, minimized or ended by Shepherd. */
|
|
35
|
+
function isResolutionOnlyCandidate(thread, ended, botUsernames, policy) {
|
|
36
|
+
if (!ended)
|
|
37
|
+
return thread.isOutdated === true || thread.isMinimized === true;
|
|
38
|
+
if (!isOrdinaryHuman(thread, botUsernames))
|
|
39
|
+
return true;
|
|
40
|
+
return (isViewerAuthoredHuman(thread, botUsernames) || shouldResolveOtherHumanThread(thread, policy));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether an unresolved thread with a known outdated state keeps returning every tick once seen,
|
|
44
|
+
* given an authorized mutation. The compact summary calls this so its count cannot drift from
|
|
45
|
+
* `classifyThreadVisibility`.
|
|
46
|
+
*/
|
|
47
|
+
export function unresolvedThreadRepeats(thread, ended, botUsernames, policy) {
|
|
48
|
+
return (repeatsAsActive(thread, ended, botUsernames, policy) ||
|
|
49
|
+
isResolutionOnlyCandidate(thread, ended, botUsernames, policy));
|
|
50
|
+
}
|
|
23
51
|
export function classifyThreadVisibility(threads, seenMap, botUsernames = new Set(), repeatableThreadIds, resolveOtherHumanThreads = "none") {
|
|
24
52
|
const shouldRepeat = (thread) => repeatableThreadIds?.has(thread.id) ?? true;
|
|
25
|
-
const isOrdinaryHuman = (thread) => isHumanAuthor(thread) && !isConfiguredBotAuthor(thread, botUsernames);
|
|
26
53
|
const unknownStatusThreads = threads.filter((thread) => thread.isResolved === undefined || thread.isOutdated === undefined);
|
|
27
54
|
const unresolvedThreads = threads.filter((thread) => thread.isResolved === false && thread.isOutdated !== undefined);
|
|
28
55
|
const activeThreads = unresolvedThreads
|
|
@@ -30,25 +57,13 @@ export function classifyThreadVisibility(threads, seenMap, botUsernames = new Se
|
|
|
30
57
|
.flatMap((t) => {
|
|
31
58
|
if (threadEndedByShepherd(t))
|
|
32
59
|
return [];
|
|
33
|
-
|
|
34
|
-
isViewerAuthoredHuman(t, botUsernames) ||
|
|
35
|
-
(isOrdinaryHuman(t) && resolveOtherHumanThreads === "always");
|
|
36
|
-
if (repeatAuthor && shouldRepeat(t))
|
|
60
|
+
if (repeatsAsActive(t, false, botUsernames, resolveOtherHumanThreads) && shouldRepeat(t))
|
|
37
61
|
return [t];
|
|
38
62
|
const visible = classifyVisibleThread(t, seenMap);
|
|
39
63
|
return visible ? [visible] : [];
|
|
40
64
|
});
|
|
41
65
|
const resolutionOnlyThreads = unresolvedThreads
|
|
42
|
-
.filter((t) =>
|
|
43
|
-
const endedByShepherd = threadEndedByShepherd(t);
|
|
44
|
-
if (endedByShepherd) {
|
|
45
|
-
if (!isOrdinaryHuman(t))
|
|
46
|
-
return true;
|
|
47
|
-
return (isViewerAuthoredHuman(t, botUsernames) ||
|
|
48
|
-
shouldResolveOtherHumanThread(t, resolveOtherHumanThreads));
|
|
49
|
-
}
|
|
50
|
-
return t.isOutdated || t.isMinimized;
|
|
51
|
-
})
|
|
66
|
+
.filter((t) => isResolutionOnlyCandidate(t, threadEndedByShepherd(t), botUsernames, resolveOtherHumanThreads))
|
|
52
67
|
.flatMap((t) => {
|
|
53
68
|
const visible = classifyVisibleThread(t, seenMap);
|
|
54
69
|
if (visible)
|
|
@@ -5,6 +5,20 @@ export declare function parseCheckSuitesComplete(raw: RawPr): boolean;
|
|
|
5
5
|
/** True only when the head commit's suite page finished and listed nothing. */
|
|
6
6
|
export declare function parseHeadCheckSuitesEmpty(raw: RawPr): boolean;
|
|
7
7
|
export declare function parseSuiteStartupFailures(raw: RawPr): CheckRun[];
|
|
8
|
+
/** One failing check per STARTUP_FAILURE workflow-run suite; shared by the one-PR and summary reads. */
|
|
9
|
+
export declare function startupFailureChecksFromSuites(suites: {
|
|
10
|
+
nodes: Array<{
|
|
11
|
+
conclusion: string | null;
|
|
12
|
+
workflowRun: {
|
|
13
|
+
databaseId?: number | null;
|
|
14
|
+
event: string | null;
|
|
15
|
+
url?: string | null;
|
|
16
|
+
workflow?: {
|
|
17
|
+
name: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
} | null;
|
|
20
|
+
}>;
|
|
21
|
+
} | null | undefined): CheckRun[];
|
|
8
22
|
/** `headWorkflowSuites`, omitted when the head has none. */
|
|
9
23
|
export declare function workflowSuites(raw: RawPr): {
|
|
10
24
|
headWorkflowSuites?: WorkflowSuiteSnapshot[];
|
|
@@ -22,7 +22,10 @@ function parseHeadWorkflowSuites(raw) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
export function parseSuiteStartupFailures(raw) {
|
|
25
|
-
|
|
25
|
+
return startupFailureChecksFromSuites(raw.commits.nodes[0]?.commit.checkSuites);
|
|
26
|
+
}
|
|
27
|
+
/** One failing check per STARTUP_FAILURE workflow-run suite; shared by the one-PR and summary reads. */
|
|
28
|
+
export function startupFailureChecksFromSuites(suites) {
|
|
26
29
|
if (!suites)
|
|
27
30
|
return [];
|
|
28
31
|
return suites.nodes.flatMap((node) => {
|
|
@@ -48,6 +48,9 @@ export function mapCheckRunNode(node) {
|
|
|
48
48
|
...(completedAtUnix !== undefined && { completedAtUnix }),
|
|
49
49
|
...(updatedAtUnix !== undefined && { updatedAtUnix }),
|
|
50
50
|
...(summary !== undefined && { summary }),
|
|
51
|
+
...((node.annotations?.totalCount ?? 0) > 0 && {
|
|
52
|
+
annotationCount: node.annotations.totalCount,
|
|
53
|
+
}),
|
|
51
54
|
...(((node.annotations?.totalCount ?? 0) > 0 || (node.annotations?.nodes.length ?? 0) > 0) && {
|
|
52
55
|
hasAnnotations: true,
|
|
53
56
|
}),
|
|
@@ -3,6 +3,11 @@ import type { CheckAnnotation } from "../types.mts";
|
|
|
3
3
|
export interface AnnotationCacheOptions {
|
|
4
4
|
stateKey: StateKey;
|
|
5
5
|
headSha?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Annotation total GitHub reports per check run right now. A cached list whose length
|
|
8
|
+
* differs is stale (a publisher added annotations to a completed run) and is not reused.
|
|
9
|
+
*/
|
|
10
|
+
expectedCounts?: ReadonlyMap<string, number>;
|
|
6
11
|
}
|
|
7
12
|
export declare function readFreshCheckAnnotations(checkRunId: string, cacheOpts: AnnotationCacheOptions | undefined): Promise<CheckAnnotation[] | undefined>;
|
|
8
13
|
export declare function storeCheckAnnotations(checkRunId: string, annotations: CheckAnnotation[], cacheOpts: AnnotationCacheOptions | undefined): Promise<void>;
|
|
@@ -12,9 +12,12 @@ export async function readFreshCheckAnnotations(checkRunId, cacheOpts) {
|
|
|
12
12
|
if (!cacheOpts)
|
|
13
13
|
return undefined;
|
|
14
14
|
const cached = await loadDerived(cacheOpts.stateKey, cacheName(checkRunId));
|
|
15
|
-
if (cached
|
|
16
|
-
return
|
|
17
|
-
|
|
15
|
+
if (!cached || Date.now() - cached.storedAt >= ANNOTATION_CACHE_MAX_AGE_MS)
|
|
16
|
+
return undefined;
|
|
17
|
+
const expected = cacheOpts.expectedCounts?.get(checkRunId);
|
|
18
|
+
if (expected !== undefined && cached.value.length !== expected)
|
|
19
|
+
return undefined;
|
|
20
|
+
return cached.value;
|
|
18
21
|
}
|
|
19
22
|
export async function storeCheckAnnotations(checkRunId, annotations, cacheOpts) {
|
|
20
23
|
if (!cacheOpts)
|
|
@@ -12,6 +12,10 @@ export function fingerprintFromRaw(raw, viewerPermission = null, viewerLogin = n
|
|
|
12
12
|
isMergeQueueEnabled: Boolean(raw.isMergeQueueEnabled),
|
|
13
13
|
mergePolicy: mergePolicyFingerprint(raw),
|
|
14
14
|
commentCount: raw.comments.totalCount ?? raw.comments.nodes.length,
|
|
15
|
+
// Raw `updatedAt` on purpose, even for bot or rule-suppressed comments (#635 gap 7). A miss
|
|
16
|
+
// continues the same BatchPr request into the full snapshot, so an edited bot comment costs
|
|
17
|
+
// no extra request over a hit, while excluding it would need rule evaluation here and would
|
|
18
|
+
// risk hiding an edit that un-suppresses the comment or edits a minimized human comment.
|
|
15
19
|
commentRevisions: commentRevisions(raw.comments.nodes),
|
|
16
20
|
threadCount: raw.reviewThreads.totalCount ?? raw.reviewThreads.nodes.length,
|
|
17
21
|
reviewCount: raw.allReviews?.totalCount ?? 0,
|
|
@@ -19,3 +19,8 @@ export declare function hydrateReadyAnnotationProbe(pr: RawSummaryPr, repo: Repo
|
|
|
19
19
|
actionable?: number;
|
|
20
20
|
incomplete?: true;
|
|
21
21
|
}): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Load annotation totals once per summary row, from the same probe the READY receipt
|
|
24
|
+
* hash uses. False when a page could not be read, so no total is trustworthy.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hydrateAnnotationTotals(pr: RawSummaryPr, repo: RepoInfo): Promise<boolean>;
|
|
@@ -7,6 +7,7 @@ import { fingerprintRawSummaryPr } from "./poll-summary-fingerprint.mjs";
|
|
|
7
7
|
import { POLL_SUMMARY_ANNOTATION_PROBE_QUERY } from "./queries.mjs";
|
|
8
8
|
const PROBE_UNAVAILABLE = Symbol.for("prShepherd.annotationProbeUnavailable");
|
|
9
9
|
const COMPLETE_FROM_BATCH = new WeakSet();
|
|
10
|
+
const HYDRATED = new WeakSet();
|
|
10
11
|
/** Annotation totals copied from the same full-snapshot request; no follow-up probe is needed. */
|
|
11
12
|
export function markReadyAnnotationProbeComplete(pr) {
|
|
12
13
|
COMPLETE_FROM_BATCH.add(pr);
|
|
@@ -36,9 +37,22 @@ export async function hydrateReadyAnnotationProbe(pr, repo, review) {
|
|
|
36
37
|
const checks = summarizePollSummaryChecks(pr);
|
|
37
38
|
if (!isCurrentSummaryReady(pr, checks, review))
|
|
38
39
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
await hydrateAnnotationTotals(pr, repo);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load annotation totals once per summary row, from the same probe the READY receipt
|
|
44
|
+
* hash uses. False when a page could not be read, so no total is trustworthy.
|
|
45
|
+
*/
|
|
46
|
+
export async function hydrateAnnotationTotals(pr, repo) {
|
|
47
|
+
if (COMPLETE_FROM_BATCH.has(pr))
|
|
48
|
+
return true;
|
|
49
|
+
if (HYDRATED.has(pr))
|
|
50
|
+
return !annotationProbeUnavailable(pr);
|
|
51
|
+
HYDRATED.add(pr);
|
|
52
|
+
if (await hydrateCommitAnnotations(pr, repo))
|
|
53
|
+
return true;
|
|
54
|
+
pr[PROBE_UNAVAILABLE] = true;
|
|
55
|
+
return false;
|
|
42
56
|
}
|
|
43
57
|
async function hydrateCommitAnnotations(pr, repo) {
|
|
44
58
|
const commit = pr.commits.nodes[0]?.commit;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PollSummaryChecks, PollSummaryItem } from "../types.mts";
|
|
2
|
+
import type { RepoInfo } from "./client.mts";
|
|
3
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
4
|
+
type Routed = Pick<PollSummaryItem, "action" | "reasons">;
|
|
5
|
+
/**
|
|
6
|
+
* The one-PR check treats unseen annotations on any non-SUCCESS check (failing, skipped,
|
|
7
|
+
* filtered, ignored or neutral) as actionable work. The compact summary only has annotation
|
|
8
|
+
* totals, and only for a row that otherwise claims readiness, so it loads them then and reads
|
|
9
|
+
* the annotations of just the non-SUCCESS checks that have any. A row with unseen annotations,
|
|
10
|
+
* or whose annotations cannot be read, goes to its one-PR `pollCommand` instead of
|
|
11
|
+
* claiming `appears-ready`, `cancel`, `mark_ready` or `merge`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyUnseenCheckAnnotations(raw: RawSummaryPr, repo: RepoInfo, routed: Routed, checks: PollSummaryChecks, seen: ReadonlyMap<string, unknown>, required: readonly string[]): Promise<Routed>;
|
|
14
|
+
export {};
|