pr-shepherd 0.54.0 → 0.55.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 +3 -3
- package/bin/checks/unreported-required.d.mts +29 -0
- package/bin/checks/unreported-required.mjs +52 -0
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +31 -31
- package/bin/classify/rule-action.d.mts +8 -0
- package/bin/classify/rule-action.mjs +29 -0
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/help-command-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +6 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +3 -3
- package/bin/cli/iterate-branch-segment.d.mts +3 -0
- package/bin/cli/iterate-branch-segment.mjs +13 -0
- package/bin/cli/iterate-formatter.mjs +34 -22
- package/bin/cli/iterate-instructions.mjs +6 -0
- package/bin/cli/iterate-lean.mjs +16 -0
- package/bin/cli/poll-summary-emitter.mjs +4 -1
- package/bin/cli/poll-summary-formatter.mjs +10 -3
- package/bin/cli/stack-overview.d.mts +48 -0
- package/bin/cli/stack-overview.mjs +138 -0
- package/bin/commands/check-fingerprint.mjs +2 -1
- package/bin/commands/check-status.d.mts +1 -1
- package/bin/commands/check-status.mjs +8 -3
- package/bin/commands/check-unreported.d.mts +26 -0
- package/bin/commands/check-unreported.mjs +73 -0
- package/bin/commands/check.mjs +45 -38
- package/bin/commands/iterate/base.mjs +9 -0
- package/bin/commands/iterate/conflicting-head-ci.d.mts +18 -0
- package/bin/commands/iterate/conflicting-head-ci.mjs +32 -0
- package/bin/commands/iterate/escalate.mjs +4 -0
- package/bin/commands/iterate/fix-code.mjs +30 -1
- package/bin/commands/iterate/index.mjs +69 -3
- package/bin/commands/iterate/merge-state.mjs +12 -12
- package/bin/commands/iterate/merge.d.mts +12 -2
- package/bin/commands/iterate/merge.mjs +32 -2
- package/bin/commands/iterate/native-stack-rebase.d.mts +8 -2
- package/bin/commands/iterate/native-stack-rebase.mjs +14 -4
- package/bin/commands/iterate/stack-trunk-conflict.d.mts +13 -0
- package/bin/commands/iterate/stack-trunk-conflict.mjs +51 -0
- package/bin/commands/iterate/unreported-required.d.mts +18 -0
- package/bin/commands/iterate/unreported-required.mjs +122 -0
- package/bin/commands/poll-summary.mjs +1 -0
- package/bin/commands/poll.mjs +9 -2
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
- package/bin/commands/rule-auto-resolve-format.mjs +151 -0
- package/bin/commands/rule-auto-resolve.d.mts +21 -0
- package/bin/commands/rule-auto-resolve.mjs +113 -0
- package/bin/commands/stack-drain.d.mts +0 -5
- package/bin/commands/stack-drain.mjs +26 -16
- package/bin/commands/stack-merge-flag.d.mts +7 -0
- package/bin/commands/stack-merge-flag.mjs +6 -0
- package/bin/config/load.d.mts +3 -0
- package/bin/config/load.mjs +9 -2
- package/bin/config/merge-method.d.mts +27 -0
- package/bin/config/merge-method.mjs +56 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +2 -1
- package/bin/github/batch-parse-suites.d.mts +5 -0
- package/bin/github/batch-parse-suites.mjs +20 -0
- package/bin/github/batch-parsers.d.mts +1 -1
- package/bin/github/batch-parsers.mjs +7 -0
- package/bin/github/batch-raw-types.d.mts +5 -0
- package/bin/github/batch.d.mts +5 -0
- package/bin/github/batch.mjs +10 -1
- package/bin/github/errors.d.mts +6 -0
- package/bin/github/errors.mjs +23 -5
- package/bin/github/fingerprint-fields.d.mts +1 -0
- package/bin/github/fingerprint-fields.mjs +1 -1
- package/bin/github/gql/batch-pr-page.gql +2 -0
- package/bin/github/gql/batch-pr.gql +5 -0
- package/bin/github/gql/commit-check-suites.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +6 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
- package/bin/github/gql/poll-summary-fragment.gql +16 -0
- package/bin/github/gql/pr-merge-policy.gql +1 -44
- package/bin/github/gql/ref-rules-query.gql +18 -0
- package/bin/github/gql/ref-rules.gql +46 -0
- package/bin/github/gql/upper-layer-conflict-target.gql +36 -0
- package/bin/github/graphql-internal-retry.d.mts +1 -1
- package/bin/github/graphql-internal-retry.mjs +4 -3
- package/bin/github/merge-queue-checks.mjs +7 -3
- package/bin/github/merge-target-rules.d.mts +20 -0
- package/bin/github/merge-target-rules.mjs +54 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
- package/bin/github/poll-summary-annotation-probe.mjs +88 -0
- package/bin/github/poll-summary-fingerprint.mjs +1 -1
- package/bin/github/poll-summary-projector.d.mts +1 -1
- package/bin/github/poll-summary-projector.mjs +19 -12
- package/bin/github/poll-summary-queue-removal.mjs +7 -3
- package/bin/github/poll-summary-raw.d.mts +16 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-route.mjs +5 -0
- package/bin/github/poll-summary-unreported.d.mts +6 -0
- package/bin/github/poll-summary-unreported.mjs +51 -0
- package/bin/github/poll-summary.d.mts +1 -0
- package/bin/github/poll-summary.mjs +28 -3
- package/bin/github/queries.d.mts +12 -0
- package/bin/github/queries.mjs +14 -2
- package/bin/github/queue-removal-freshness.d.mts +20 -5
- package/bin/github/queue-removal-freshness.mjs +37 -10
- package/bin/github/stack-read.d.mts +4 -0
- package/bin/github/stack-read.mjs +13 -1
- package/bin/mcp/server.mjs +6 -1
- package/bin/state/ci-retrigger.d.mts +25 -0
- package/bin/state/ci-retrigger.mjs +48 -0
- package/bin/state/conflicting-head-seen.d.mts +13 -0
- package/bin/state/conflicting-head-seen.mjs +44 -0
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +11 -0
- package/bin/types/iterate.d.mts +19 -3
- package/bin/types/poll-summary.d.mts +13 -0
- package/bin/types/report.d.mts +34 -1
- package/package.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
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
package/bin/commands/check.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { fetchPrBatch } from "../github/batch.mjs";
|
|
|
2
2
|
import { queueRemovalAppliesToHead } from "../github/queue-removal-freshness.mjs";
|
|
3
3
|
import { storePrFingerprint } from "../state/pr-fingerprint.mjs";
|
|
4
4
|
import { tryReuseFingerprintReport } from "./check-fingerprint.mjs";
|
|
5
|
+
import { collectUnreportedRequired, refreshCachedUnreported } from "./check-unreported.mjs";
|
|
5
6
|
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
6
7
|
import { classifyChecks, getCiVerdict } from "../checks/classify.mjs";
|
|
7
8
|
import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
|
|
@@ -17,7 +18,7 @@ import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs"
|
|
|
17
18
|
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
18
19
|
import { classifyThreadVisibility } from "../comments/thread-visibility.mjs";
|
|
19
20
|
import { classifyReviewsForDisplay, classifyChangesRequestedReviewsForDisplay, } from "../comments/review-visibility.mjs";
|
|
20
|
-
import {
|
|
21
|
+
import { applySuppressedRuleAutoResolve } from "./rule-auto-resolve.mjs";
|
|
21
22
|
import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers.mjs";
|
|
22
23
|
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
23
24
|
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./iterate/thread-mutation-routing.mjs";
|
|
@@ -37,7 +38,7 @@ export async function runCheck(opts) {
|
|
|
37
38
|
if (reuseFingerprint) {
|
|
38
39
|
const cached = await tryReuseFingerprintReport(prNumber, repo, stateKey, config);
|
|
39
40
|
if (cached)
|
|
40
|
-
return cached;
|
|
41
|
+
return refreshCachedUnreported(cached, repo);
|
|
41
42
|
}
|
|
42
43
|
const paginateApprovedReviews = config.iterate.minimizeApprovals;
|
|
43
44
|
const result = await fetchPrBatch(prNumber, repo, { paginateApprovedReviews });
|
|
@@ -67,8 +68,10 @@ export async function runCheck(opts) {
|
|
|
67
68
|
// it as stale/updated rather than as still current, so Shepherd doesn't escalate
|
|
68
69
|
// `merge-queue-removed` permanently on data it can no longer check. A squash or rebase
|
|
69
70
|
// queue commit has one parent and does not list the PR head; that removal stays current
|
|
70
|
-
// until
|
|
71
|
-
//
|
|
71
|
+
// until this head reached the PR after it. The push time is the earliest pull_request
|
|
72
|
+
// check on the head, with committer time as the fallback when no check time is
|
|
73
|
+
// available. The raw removal fields still render in the merge-queue header regardless
|
|
74
|
+
// of this flag.
|
|
72
75
|
const headCommittedAtUnix = batchData.activity?.latestCommitCommittedAtUnix;
|
|
73
76
|
const headUpdatedAfterRemoval = Boolean(latestRemoval &&
|
|
74
77
|
!queueRemovalAppliesToHead({
|
|
@@ -76,6 +79,9 @@ export async function runCheck(opts) {
|
|
|
76
79
|
headOid: batchData.headRefOid,
|
|
77
80
|
...(headCommittedAtUnix !== undefined &&
|
|
78
81
|
headCommittedAtUnix !== null && { headCommittedAtUnix }),
|
|
82
|
+
...(batchData.headPushedAtUnix !== undefined && {
|
|
83
|
+
headPushedAtUnix: batchData.headPushedAtUnix,
|
|
84
|
+
}),
|
|
79
85
|
removedAtUnix: latestRemoval.createdAtUnix,
|
|
80
86
|
}));
|
|
81
87
|
const queueRawChecks = batchData.isInMergeQueue
|
|
@@ -154,14 +160,23 @@ export async function runCheck(opts) {
|
|
|
154
160
|
visibleChangesRequestedIds.has(review.id));
|
|
155
161
|
}).length;
|
|
156
162
|
const approvedReviews = approvedReviewVisibility.visible;
|
|
157
|
-
|
|
163
|
+
const unreported = await collectUnreportedRequired({
|
|
164
|
+
batchData,
|
|
165
|
+
checks: allChecks,
|
|
166
|
+
suites: result.headWorkflowSuites ?? [],
|
|
167
|
+
owner: repo.owner,
|
|
168
|
+
name: repo.name,
|
|
169
|
+
pr: prNumber,
|
|
170
|
+
relevantEvents: config.checks.ciTriggerEvents,
|
|
171
|
+
});
|
|
172
|
+
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount, unreported.hasUnreportedRequired);
|
|
158
173
|
// Resolve any pending mergeability refresh (and the resulting MERGED/CLOSED short-circuit)
|
|
159
174
|
// before deciding what to persist below — deferWhileQueued must see the same final,
|
|
160
175
|
// possibly-refreshed mergeStatus that commands/iterate/index.mts acts on, not the pre-refresh
|
|
161
176
|
// snapshot. A conflict newly discovered by this REST read is exactly the kind of check-driven
|
|
162
177
|
// signal that keeps a queued PR out of the deferral path.
|
|
163
178
|
if (status === "READY" && !didRefreshMergeability) {
|
|
164
|
-
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount);
|
|
179
|
+
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount, unreported.hasUnreportedRequired);
|
|
165
180
|
batchData = refreshed.batchData;
|
|
166
181
|
mergeStatus = refreshed.mergeStatus;
|
|
167
182
|
status = refreshed.status;
|
|
@@ -236,7 +251,13 @@ export async function runCheck(opts) {
|
|
|
236
251
|
ruleAutoResolveCommentIds: partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize === true),
|
|
237
252
|
ruleAutoResolveReviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => batchData.reviewSummaries.find((review) => review.id === id)?.viewerCanMinimize === true),
|
|
238
253
|
};
|
|
239
|
-
const { threadIds: authorizedRuleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await
|
|
254
|
+
const { threadIds: authorizedRuleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, autoResolved, autoMinimized, autoResolveErrors, errorReasons, } = await applySuppressedRuleAutoResolve({
|
|
255
|
+
enabled: opts.autoMinimizeSuppressed === true,
|
|
256
|
+
partition: authorizedPartition,
|
|
257
|
+
batch: batchData,
|
|
258
|
+
prNumber,
|
|
259
|
+
repo,
|
|
260
|
+
});
|
|
240
261
|
const visibleMutationThreadIds = new Set([...threadVisibility.activeThreads, ...threadVisibility.resolutionOnlyThreads].map((thread) => thread.id));
|
|
241
262
|
const ruleAutoResolveThreadIds = [
|
|
242
263
|
...authorizedRuleAutoResolveThreadIds,
|
|
@@ -254,11 +275,13 @@ export async function runCheck(opts) {
|
|
|
254
275
|
pr: prNumber,
|
|
255
276
|
nodeId: batchData.nodeId,
|
|
256
277
|
headSha: batchData.headRefOid,
|
|
278
|
+
headRefName: unreported.headRefName,
|
|
257
279
|
repo: `${repo.owner}/${repo.name}`,
|
|
258
280
|
...(batchData.viewerAuthorization && { viewerAuthorization: batchData.viewerAuthorization }),
|
|
259
281
|
status,
|
|
260
282
|
baseBranch: batchData.baseRefName,
|
|
261
283
|
...(batchData.baseRefOid && { baseRefOid: batchData.baseRefOid }),
|
|
284
|
+
...(result.headCheckSuitesEmpty && { headCheckSuitesEmpty: true }),
|
|
262
285
|
mergeStatus,
|
|
263
286
|
checks: {
|
|
264
287
|
passing: merged.passing,
|
|
@@ -275,8 +298,9 @@ export async function runCheck(opts) {
|
|
|
275
298
|
threads: {
|
|
276
299
|
actionable: threadVisibility.activeThreads,
|
|
277
300
|
resolutionOnly: threadVisibility.resolutionOnlyThreads,
|
|
278
|
-
autoResolved
|
|
279
|
-
autoResolveErrors
|
|
301
|
+
autoResolved,
|
|
302
|
+
autoResolveErrors,
|
|
303
|
+
...(errorReasons.length > 0 && { autoResolveErrorReasons: errorReasons }),
|
|
280
304
|
firstLook: threadVisibility.firstLookThreads,
|
|
281
305
|
...(ruleAutoResolveThreadIds.length > 0
|
|
282
306
|
? { ruleAutoResolveIds: ruleAutoResolveThreadIds }
|
|
@@ -286,6 +310,7 @@ export async function runCheck(opts) {
|
|
|
286
310
|
actionable: visibleCommentClassification.actionable,
|
|
287
311
|
minimizeIds: [...visibleCommentClassification.minimizeIds, ...ruleAutoResolveCommentIds],
|
|
288
312
|
firstLook: firstLookComments,
|
|
313
|
+
...(autoMinimized.length > 0 && { autoMinimized }),
|
|
289
314
|
},
|
|
290
315
|
changesRequestedReviews,
|
|
291
316
|
reviewSummaries: seenSummaries,
|
|
@@ -296,6 +321,9 @@ export async function runCheck(opts) {
|
|
|
296
321
|
? { ruleAutoResolveReviewSummaryIds }
|
|
297
322
|
: undefined),
|
|
298
323
|
branchProtection: batchData.branchProtection,
|
|
324
|
+
...(batchData.allowedMergeMethods && {
|
|
325
|
+
allowedMergeMethods: batchData.allowedMergeMethods,
|
|
326
|
+
}),
|
|
299
327
|
activity: batchData.activity,
|
|
300
328
|
...(hasQueueState && {
|
|
301
329
|
mergeQueue: {
|
|
@@ -315,38 +343,17 @@ export async function runCheck(opts) {
|
|
|
315
343
|
...(headUpdatedAfterRemoval && { headUpdatedAfterRemoval: true }),
|
|
316
344
|
},
|
|
317
345
|
}),
|
|
346
|
+
...(unreported.unreportedRequiredChecks && {
|
|
347
|
+
unreportedRequiredChecks: unreported.unreportedRequiredChecks,
|
|
348
|
+
}),
|
|
349
|
+
...(unreported.trunkBehindBy !== undefined && { trunkBehindBy: unreported.trunkBehindBy }),
|
|
350
|
+
...(unreported.actionsWorkflowInProgress && {
|
|
351
|
+
actionsWorkflowInProgress: true,
|
|
352
|
+
}),
|
|
353
|
+
...(unreported.stackBottomPr !== undefined && { stackBottomPr: unreported.stackBottomPr }),
|
|
318
354
|
};
|
|
319
355
|
if (result.fingerprint) {
|
|
320
356
|
await storePrFingerprint(stateKey, result.fingerprint, report, config);
|
|
321
357
|
}
|
|
322
358
|
return report;
|
|
323
359
|
}
|
|
324
|
-
async function remainingRuleAutoResolveIds(partition, autoMinimizeSuppressed = false) {
|
|
325
|
-
const consumedIds = autoMinimizeSuppressed
|
|
326
|
-
? await selfApplySuppressedRuleAutoResolve(partition)
|
|
327
|
-
: { minimized: new Set(), resolvedThreads: new Set() };
|
|
328
|
-
return {
|
|
329
|
-
threadIds: partition.ruleAutoResolveThreadIds.filter((id) => !consumedIds.resolvedThreads.has(id)),
|
|
330
|
-
commentIds: partition.ruleAutoResolveCommentIds.filter((id) => !consumedIds.minimized.has(id)),
|
|
331
|
-
reviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => !consumedIds.minimized.has(id)),
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
async function selfApplySuppressedRuleAutoResolve(partition) {
|
|
335
|
-
const minimizeIds = [
|
|
336
|
-
...partition.ruleAutoResolveCommentIds.filter((id) => partition.suppressedCommentIds.has(id)),
|
|
337
|
-
...partition.ruleAutoResolveReviewSummaryIds.filter((id) => partition.suppressedReviewSummaryIds.has(id)),
|
|
338
|
-
];
|
|
339
|
-
const threadIds = partition.ruleAutoResolveThreadIds.filter((id) => partition.suppressedThreadIds.has(id));
|
|
340
|
-
const [minimized, resolved] = await Promise.all([
|
|
341
|
-
minimizeIds.length > 0
|
|
342
|
-
? autoMinimizeComments(minimizeIds)
|
|
343
|
-
: Promise.resolve({ minimized: [], errors: [] }),
|
|
344
|
-
threadIds.length > 0
|
|
345
|
-
? autoResolveThreads(threadIds)
|
|
346
|
-
: Promise.resolve({ resolved: [], errors: [] }),
|
|
347
|
-
]);
|
|
348
|
-
return {
|
|
349
|
-
minimized: new Set(minimized.minimized),
|
|
350
|
-
resolvedThreads: new Set(resolved.resolved),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ruleAutoResolveFromReport } from "../rule-auto-resolve-format.mjs";
|
|
1
2
|
import { buildActiveChecks, buildRelevantChecks, buildSummary, buildSuppressedCheckFields, } from "./helpers.mjs";
|
|
2
3
|
export function buildIterateBase(report, readyState) {
|
|
4
|
+
const ruleAutoResolve = ruleAutoResolveFromReport(report);
|
|
3
5
|
return {
|
|
4
6
|
pr: report.pr,
|
|
5
7
|
repo: report.repo,
|
|
@@ -21,6 +23,13 @@ export function buildIterateBase(report, readyState) {
|
|
|
21
23
|
...buildSuppressedCheckFields(report),
|
|
22
24
|
activity: report.activity,
|
|
23
25
|
mergeQueue: report.mergeQueue,
|
|
26
|
+
...(report.unreportedRequiredChecks &&
|
|
27
|
+
report.unreportedRequiredChecks.length > 0 && {
|
|
28
|
+
unreportedRequiredChecks: report.unreportedRequiredChecks,
|
|
29
|
+
}),
|
|
30
|
+
...(report.trunkBehindBy !== undefined &&
|
|
31
|
+
report.trunkBehindBy > 0 && { trunkBehindBy: report.trunkBehindBy }),
|
|
24
32
|
...(report.fingerprintReused === true && { fingerprintReused: true }),
|
|
33
|
+
...(ruleAutoResolve && { ruleAutoResolve }),
|
|
25
34
|
};
|
|
26
35
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function conflictingHeadCiNote(input: {
|
|
2
|
+
hasConflicts: boolean;
|
|
3
|
+
headCheckSuitesEmpty: boolean;
|
|
4
|
+
checkRunCount: number;
|
|
5
|
+
/** Unix seconds when Shepherd first saw this head. Omit when that time is unknown. */
|
|
6
|
+
firstSeenAtUnix: number | null | undefined;
|
|
7
|
+
nowMs: number;
|
|
8
|
+
}): string | undefined;
|
|
9
|
+
export declare function countReportedChecks(checks: {
|
|
10
|
+
passing: readonly unknown[];
|
|
11
|
+
failing: readonly unknown[];
|
|
12
|
+
inProgress: readonly unknown[];
|
|
13
|
+
skipped: readonly unknown[];
|
|
14
|
+
filtered: readonly unknown[];
|
|
15
|
+
ignored?: readonly unknown[];
|
|
16
|
+
}): number;
|
|
17
|
+
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
18
|
+
export declare function insertConflictingHeadCiNote(instructions: string[], note: string | undefined): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** How long a conflicting head may sit with no CI before Shepherd says none will start. */
|
|
2
|
+
const CONFLICTING_HEAD_CI_GRACE_MS = 2 * 60 * 1000;
|
|
3
|
+
const NO_CI_NOTE = "GitHub did not start pull_request workflows for this conflicting head: no check suites or workflow runs have appeared for at least 2 minutes. Missing CI is a consequence of the conflict, not an outage, and pushing this same head again will not start them.";
|
|
4
|
+
export function conflictingHeadCiNote(input) {
|
|
5
|
+
if (!input.hasConflicts || !input.headCheckSuitesEmpty || input.checkRunCount > 0) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
if (typeof input.firstSeenAtUnix !== "number")
|
|
9
|
+
return undefined;
|
|
10
|
+
const ageMs = input.nowMs - input.firstSeenAtUnix * 1000;
|
|
11
|
+
if (!Number.isFinite(ageMs) || ageMs < CONFLICTING_HEAD_CI_GRACE_MS)
|
|
12
|
+
return undefined;
|
|
13
|
+
return NO_CI_NOTE;
|
|
14
|
+
}
|
|
15
|
+
export function countReportedChecks(checks) {
|
|
16
|
+
return (checks.passing.length +
|
|
17
|
+
checks.failing.length +
|
|
18
|
+
checks.inProgress.length +
|
|
19
|
+
checks.skipped.length +
|
|
20
|
+
checks.filtered.length +
|
|
21
|
+
(checks.ignored?.length ?? 0));
|
|
22
|
+
}
|
|
23
|
+
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
24
|
+
export function insertConflictingHeadCiNote(instructions, note) {
|
|
25
|
+
if (!note)
|
|
26
|
+
return;
|
|
27
|
+
const conflictAt = instructions.findIndex((step) => step.includes("merge conflicts"));
|
|
28
|
+
if (conflictAt >= 0)
|
|
29
|
+
instructions.splice(conflictAt + 1, 0, note);
|
|
30
|
+
else
|
|
31
|
+
instructions.unshift(note);
|
|
32
|
+
}
|
|
@@ -240,6 +240,10 @@ export function buildEscalateSuggestion(triggers, detail) {
|
|
|
240
240
|
const duration = detail ?? "60 minutes";
|
|
241
241
|
return `No progress detected for ${duration} — state has not changed. This is a manual checkpoint: inspect the PR and apply a manual fix before resuming.`;
|
|
242
242
|
}
|
|
243
|
+
if (triggers.includes("required-checks-unreported")) {
|
|
244
|
+
const names = detail ? ` (${detail})` : "";
|
|
245
|
+
return `Required checks are still unreported after one close/reopen of this head${names}. Another reopen will not create a job the workflow does not emit. Path filters are the usual cause.`;
|
|
246
|
+
}
|
|
243
247
|
if (triggers.includes("stall-state-unavailable")) {
|
|
244
248
|
const reason = detail ?? "unknown error";
|
|
245
249
|
return `The stall timer could not be saved (${reason}). Fix PR_SHEPHERD_STATE_DIR or the directory permissions, then resume. Automated polling is paused because a stuck loop would not be detected.`;
|
|
@@ -8,6 +8,9 @@ import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./thre
|
|
|
8
8
|
import { buildFixInstructions } from "./render.mjs";
|
|
9
9
|
import { buildReleasedBlockerInstruction } from "./check-instructions.mjs";
|
|
10
10
|
import { buildNativeStackLayerRebase } from "./native-stack-rebase.mjs";
|
|
11
|
+
import { lookupUpperLayerTrunkConflict } from "./stack-trunk-conflict.mjs";
|
|
12
|
+
import { conflictingHeadCiNote, countReportedChecks, insertConflictingHeadCiNote, } from "./conflicting-head-ci.mjs";
|
|
13
|
+
import { conflictingHeadFirstSeenUnix } from "../../state/conflicting-head-seen.mjs";
|
|
11
14
|
import { applyStallGuard } from "./stall.mjs";
|
|
12
15
|
import { annotationMarkerBody, checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
13
16
|
import { threadTranscriptBody } from "../../threads/transcript.mjs";
|
|
@@ -282,9 +285,21 @@ export async function handleFixCode(ctx) {
|
|
|
282
285
|
}
|
|
283
286
|
const firstLookThreads = report.threads.firstLook;
|
|
284
287
|
const firstLookComments = report.comments.firstLook;
|
|
288
|
+
const stack = report.mergeStatus.mergeRequirements?.stack;
|
|
289
|
+
const headRef = report.headSha;
|
|
290
|
+
// Only an upper layer can be dirty against trunk while already containing its parent.
|
|
291
|
+
const trunkConflict = hasConflicts && stack && headRef && baseLookup.branch !== stack.baseRefName
|
|
292
|
+
? await lookupUpperLayerTrunkConflict({
|
|
293
|
+
owner: repoOwner,
|
|
294
|
+
name: repoName,
|
|
295
|
+
pr: prNumber,
|
|
296
|
+
headRef,
|
|
297
|
+
trunk: stack.baseRefName,
|
|
298
|
+
})
|
|
299
|
+
: undefined;
|
|
285
300
|
// Conflicts, and a behind branch whose rerun already failed, both ask for a branch update.
|
|
286
301
|
const stackRebase = hasConflicts || (isBehind && exhaustedAttempts.length > 0)
|
|
287
|
-
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch },
|
|
302
|
+
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict)
|
|
288
303
|
: undefined;
|
|
289
304
|
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true, exhaustedAttempts.length > 0, stackRebase);
|
|
290
305
|
if (failingAgentChecks.some((check) => releasedCheckNames.has(check.name))) {
|
|
@@ -296,9 +311,23 @@ export async function handleFixCode(ctx) {
|
|
|
296
311
|
if (repairInstructions && repairInstructions.length > 0) {
|
|
297
312
|
instructions.unshift(...repairInstructions);
|
|
298
313
|
}
|
|
314
|
+
const checkRunCount = countReportedChecks(report.checks);
|
|
315
|
+
const suitesEmpty = report.headCheckSuitesEmpty === true;
|
|
316
|
+
const nowMs = Date.now();
|
|
317
|
+
const firstSeenAtUnix = hasConflicts && suitesEmpty && checkRunCount === 0 && report.headSha
|
|
318
|
+
? await conflictingHeadFirstSeenUnix({ owner: repoOwner, repo: repoName, pr: prNumber }, report.headSha, nowMs)
|
|
319
|
+
: undefined;
|
|
320
|
+
insertConflictingHeadCiNote(instructions, conflictingHeadCiNote({
|
|
321
|
+
hasConflicts,
|
|
322
|
+
headCheckSuitesEmpty: suitesEmpty,
|
|
323
|
+
checkRunCount,
|
|
324
|
+
firstSeenAtUnix,
|
|
325
|
+
nowMs,
|
|
326
|
+
}));
|
|
299
327
|
const prospectiveResult = {
|
|
300
328
|
...base,
|
|
301
329
|
baseBranch: baseLookup.branch,
|
|
330
|
+
...(trunkConflict && { stackTrunkConflict: trunkConflict.trunk }),
|
|
302
331
|
action: "fix_code",
|
|
303
332
|
fix: {
|
|
304
333
|
threads,
|
|
@@ -18,6 +18,7 @@ import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
|
18
18
|
import { withIterateApiUsage } from "./run.mjs";
|
|
19
19
|
import { fetchRawSummaryPr } from "../../github/poll-summary.mjs";
|
|
20
20
|
import { summarizePollSummaryPr } from "../../github/poll-summary-projector.mjs";
|
|
21
|
+
import { annotationProbeUnavailable } from "../../github/poll-summary-annotation-probe.mjs";
|
|
21
22
|
import { fingerprintRawSummaryPr } from "../../github/poll-summary-fingerprint.mjs";
|
|
22
23
|
import { currentQueueRemovalEvent } from "../../github/poll-summary-queue-removal.mjs";
|
|
23
24
|
import { isCurrentSummaryReady } from "../../github/poll-summary-readiness.mjs";
|
|
@@ -25,6 +26,7 @@ import { clearReadyReceipt, isReadyReceiptCurrent, readReadyReceipt, writeReadyR
|
|
|
25
26
|
import { stackDraftHold } from "./parent-first.mjs";
|
|
26
27
|
import { findStaleNativeStackAncestry } from "./stale-ancestry.mjs";
|
|
27
28
|
import { annotateBlockedWait, resolveCheckBlockerGate } from "./check-blocker-gate.mjs";
|
|
29
|
+
import { buildUnreportedFixResult, planUnreportedRequired } from "./unreported-required.mjs";
|
|
28
30
|
export function runIterate(opts) {
|
|
29
31
|
return withIterateApiUsage(opts, () => runIterateCore(opts));
|
|
30
32
|
}
|
|
@@ -122,6 +124,16 @@ async function runIterateCore(opts) {
|
|
|
122
124
|
// readiness evidence, so a rerun (e.g. with --merge) does not wait again.
|
|
123
125
|
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName, { headSha, alreadyElapsed: receiptCurrent });
|
|
124
126
|
const base = buildIterateBase(reportForWork, readyState);
|
|
127
|
+
const unreportedPlan = await planUnreportedRequired({
|
|
128
|
+
report,
|
|
129
|
+
base,
|
|
130
|
+
stateKey: stallKey,
|
|
131
|
+
headSha,
|
|
132
|
+
otherAutonomousWork: hasActionableWork || staleAncestry !== null,
|
|
133
|
+
});
|
|
134
|
+
if (unreportedPlan.escalate)
|
|
135
|
+
return unreportedPlan.escalate;
|
|
136
|
+
const unreportedRepair = unreportedPlan.repairInstructions ?? [];
|
|
125
137
|
// Checks (including merge-queue synthetic-commit checks) and hard conflicts are signals
|
|
126
138
|
// GitHub itself is already acting on — the queue will eject the PR for these regardless of
|
|
127
139
|
// what Shepherd does, so they always surface immediately. Only review threads/comments/
|
|
@@ -148,9 +160,36 @@ async function runIterateCore(opts) {
|
|
|
148
160
|
surfacedApprovals,
|
|
149
161
|
botUsernames,
|
|
150
162
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
163
|
+
...((unreportedRepair.length > 0 || staleAncestry !== null) && {
|
|
164
|
+
repairInstructions: [...unreportedRepair, ...(staleAncestry?.instructions ?? [])],
|
|
165
|
+
}),
|
|
166
|
+
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (unreportedRepair.length > 0 && staleAncestry) {
|
|
170
|
+
return handleFixCode({
|
|
171
|
+
base,
|
|
172
|
+
report: reportForWork,
|
|
173
|
+
opts: { ...opts, prNumber, neverCancelRuns },
|
|
174
|
+
headSha,
|
|
175
|
+
stallKey,
|
|
176
|
+
prNumber,
|
|
177
|
+
stallTimeoutSeconds,
|
|
178
|
+
repoOwner,
|
|
179
|
+
repoName,
|
|
180
|
+
reviewSummaryIds,
|
|
181
|
+
firstLookSummaries,
|
|
182
|
+
editedSummaries,
|
|
183
|
+
surfacedApprovals,
|
|
184
|
+
botUsernames,
|
|
185
|
+
releasedCheckNames: blockerGate?.releasedNames,
|
|
186
|
+
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
151
187
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
152
188
|
});
|
|
153
189
|
}
|
|
190
|
+
if (unreportedRepair.length > 0) {
|
|
191
|
+
return annotateBlockedWait(await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, buildUnreportedFixResult(base, report, unreportedRepair), reportForWork, reviewSummaryIds), blockerGate);
|
|
192
|
+
}
|
|
154
193
|
const mergeStateResult = await handleActiveMergeState({
|
|
155
194
|
enabled: opts.merge,
|
|
156
195
|
active: activeMerge,
|
|
@@ -182,7 +221,7 @@ async function runIterateCore(opts) {
|
|
|
182
221
|
surfacedApprovals,
|
|
183
222
|
botUsernames,
|
|
184
223
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
185
|
-
repairInstructions: staleAncestry.instructions,
|
|
224
|
+
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
186
225
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
187
226
|
});
|
|
188
227
|
}
|
|
@@ -220,6 +259,17 @@ async function runIterateCore(opts) {
|
|
|
220
259
|
log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
|
|
221
260
|
};
|
|
222
261
|
}
|
|
262
|
+
if (isCleanReadyState &&
|
|
263
|
+
readyState.isReady &&
|
|
264
|
+
!readyState.shouldCancel &&
|
|
265
|
+
readyState.remainingSeconds > 0) {
|
|
266
|
+
await clearStallState(stallKey);
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
action: "ready",
|
|
270
|
+
log: `READY: PR #${base.pr} is ready — ${readyState.remainingSeconds}s of ready-delay remaining — ${buildWaitLog(base).slice("WAIT: ".length)}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
223
273
|
const hold = stackDraftHold(report, autoMarkReady);
|
|
224
274
|
const wait = {
|
|
225
275
|
...base,
|
|
@@ -238,14 +288,19 @@ async function recordReadyReceipt(key, report) {
|
|
|
238
288
|
return false;
|
|
239
289
|
try {
|
|
240
290
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
241
|
-
|
|
242
|
-
if (fingerprint === null ||
|
|
291
|
+
if (fingerprintRawSummaryPr(raw) === null ||
|
|
243
292
|
raw.state !== "OPEN" ||
|
|
244
293
|
raw.isDraft ||
|
|
245
294
|
raw.headRefOid !== report.headSha ||
|
|
246
295
|
raw.baseRefOid !== report.baseRefOid)
|
|
247
296
|
return false;
|
|
297
|
+
// Summarize hydrates annotation totals onto the snapshot before the stored hash.
|
|
248
298
|
const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
|
|
299
|
+
if (annotationProbeUnavailable(raw))
|
|
300
|
+
return false;
|
|
301
|
+
const fingerprint = fingerprintRawSummaryPr(raw);
|
|
302
|
+
if (fingerprint === null)
|
|
303
|
+
return false;
|
|
249
304
|
if (!isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {}))
|
|
250
305
|
return false;
|
|
251
306
|
const removalEvent = currentQueueRemovalEvent(raw);
|
|
@@ -290,6 +345,17 @@ async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
|
290
345
|
}
|
|
291
346
|
try {
|
|
292
347
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
348
|
+
const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
|
|
349
|
+
if (annotationProbeUnavailable(raw)) {
|
|
350
|
+
const sameHead = raw.headRefOid === report.headSha;
|
|
351
|
+
const sameBase = retainQueuedReceipt || raw.baseRefOid === report.baseRefOid;
|
|
352
|
+
const stillReady = isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {});
|
|
353
|
+
if (!sameHead || !sameBase || !stillReady) {
|
|
354
|
+
await clearReadyReceipt(key);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
293
359
|
// Queue predecessors can advance the target branch without changing this
|
|
294
360
|
// PR's source. Compare against the receipt's base only while both fresh
|
|
295
361
|
// views still place the PR in the queue.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
2
2
|
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
3
|
-
import { buildMergeCommandPlan } from "./merge.mjs";
|
|
3
|
+
import { buildMergeCommandPlan, unavailableMergeResult } from "./merge.mjs";
|
|
4
4
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
5
5
|
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
6
6
|
import { inlineCode } from "../../util/markdown.mjs";
|
|
@@ -49,17 +49,17 @@ export function buildReadyMergeOutcome(enabled, readyElapsed, base, report) {
|
|
|
49
49
|
return buildStackedRouteResult(base, report, stack);
|
|
50
50
|
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
51
51
|
report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
52
|
+
const plan = buildMergeCommandPlan({
|
|
53
|
+
pr: report.pr,
|
|
54
|
+
repo: report.repo,
|
|
55
|
+
nodeId: report.nodeId,
|
|
56
|
+
headSha: report.headSha ?? "unknown",
|
|
57
|
+
queue,
|
|
58
|
+
...(report.allowedMergeMethods && { allowedMergeMethods: report.allowedMergeMethods }),
|
|
59
|
+
});
|
|
60
|
+
if ("unavailable" in plan)
|
|
61
|
+
return unavailableMergeResult(base, report, plan.unavailable);
|
|
62
|
+
return { ...base, action: "merge", merge: plan };
|
|
63
63
|
}
|
|
64
64
|
/** Raw counts of non-CI actionable work held back for one queued-PR wait tick. Omitted (all zero) when empty. */
|
|
65
65
|
function buildDeferredWork(input) {
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MergeMethod } from "../../config/merge-method.mts";
|
|
2
|
+
import type { IterateResult, IterateResultBase, MergeCommandPlan } from "../../types.mts";
|
|
2
3
|
interface MergePlanInput {
|
|
3
4
|
pr: number;
|
|
4
5
|
repo: string;
|
|
5
6
|
nodeId: string;
|
|
6
7
|
headSha: string;
|
|
7
8
|
queue: boolean;
|
|
9
|
+
/** Omitted when the batch did not select repository merge settings. */
|
|
10
|
+
allowedMergeMethods?: readonly MergeMethod[];
|
|
8
11
|
}
|
|
9
|
-
export
|
|
12
|
+
export interface MergeMethodUnavailable {
|
|
13
|
+
unavailable: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildMergeCommandPlan(input: MergePlanInput): MergeCommandPlan | MergeMethodUnavailable;
|
|
10
16
|
export declare function renderMergeCommand(command: {
|
|
11
17
|
argv: string[];
|
|
12
18
|
}): string;
|
|
19
|
+
export declare function unavailableMergeResult(base: IterateResultBase, report: {
|
|
20
|
+
pr: number;
|
|
21
|
+
repo: string;
|
|
22
|
+
}, unavailable: string): IterateResult;
|
|
13
23
|
export {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
2
|
import { findMergeStrategies } from "../../config/merge-command-args.mjs";
|
|
3
|
+
import { chooseMergeMethod, configuredMergeMethod, } from "../../config/merge-method.mjs";
|
|
4
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
3
5
|
import { renderShellCommand } from "../../cli/runner.mjs";
|
|
6
|
+
import { buildEscalateHumanMessage } from "./escalate.mjs";
|
|
4
7
|
const ENQUEUE_MUTATION = "mutation EnqueuePullRequest($pullRequestId: ID!, $expectedHeadOid: GitObjectID!) { enqueuePullRequest(input: { pullRequestId: $pullRequestId, expectedHeadOid: $expectedHeadOid }) { mergeQueueEntry { id } } }";
|
|
5
8
|
export function buildMergeCommandPlan(input) {
|
|
6
9
|
const base = [
|
|
@@ -32,9 +35,17 @@ export function buildMergeCommandPlan(input) {
|
|
|
32
35
|
},
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
const
|
|
38
|
+
const mergeConfig = loadConfig().merge;
|
|
39
|
+
const configuredArgs = mergeConfig?.commandArgs ?? [];
|
|
40
|
+
const decision = chooseMergeMethod({
|
|
41
|
+
allowed: input.allowedMergeMethods,
|
|
42
|
+
configured: configuredMergeMethod(mergeConfig ?? {}),
|
|
43
|
+
fallback: "merge",
|
|
44
|
+
});
|
|
45
|
+
if ("unavailable" in decision)
|
|
46
|
+
return decision;
|
|
36
47
|
const hasStrategy = findMergeStrategies(configuredArgs).length > 0;
|
|
37
|
-
const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs,
|
|
48
|
+
const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs, `--${decision.method}`];
|
|
38
49
|
return {
|
|
39
50
|
mode: "auto",
|
|
40
51
|
command: { argv: [...base, "--auto", ...commandArgs] },
|
|
@@ -44,3 +55,22 @@ export function buildMergeCommandPlan(input) {
|
|
|
44
55
|
export function renderMergeCommand(command) {
|
|
45
56
|
return renderShellCommand(command.argv);
|
|
46
57
|
}
|
|
58
|
+
export function unavailableMergeResult(base, report, unavailable) {
|
|
59
|
+
const escalateBase = {
|
|
60
|
+
triggers: ["merge-method-unavailable"],
|
|
61
|
+
unresolvedThreads: [],
|
|
62
|
+
ambiguousComments: [],
|
|
63
|
+
changesRequestedReviews: [],
|
|
64
|
+
suggestion: unavailable,
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
action: "escalate",
|
|
69
|
+
escalate: {
|
|
70
|
+
...escalateBase,
|
|
71
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, formatPrUrl(report.repo, report.pr), {
|
|
72
|
+
merge: true,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -7,6 +7,8 @@ export type NativeStackRebaseStart = {
|
|
|
7
7
|
parentBranch: string;
|
|
8
8
|
} | {
|
|
9
9
|
bottomPr: number;
|
|
10
|
+
} | {
|
|
11
|
+
trunk: string;
|
|
10
12
|
};
|
|
11
13
|
/**
|
|
12
14
|
* One gh-stack rebase step. A native stack layer must not be rebased or merged from its base
|
|
@@ -22,12 +24,16 @@ export declare function buildNativeStackRebaseInstruction(repo: string, stackNum
|
|
|
22
24
|
* workflow keeps failing); undefined outside a stack.
|
|
23
25
|
* A layer whose PR targets the stack's trunk (`stack.baseRefName`) is the bottom open layer —
|
|
24
26
|
* position 1, or a higher layer GitHub retargeted after every layer below it merged. Any other
|
|
25
|
-
* layer is an upper layer whose parent is its own PR base branch.
|
|
27
|
+
* layer is an upper layer whose parent is its own PR base branch. `trunkConflict` means that
|
|
28
|
+
* upper layer already contains its parent, so the rebase starts at the bottom open layer.
|
|
26
29
|
*/
|
|
27
30
|
export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
28
31
|
number: number;
|
|
29
32
|
baseBranch: string;
|
|
30
|
-
}, stack: StackStatus | undefined
|
|
33
|
+
}, stack: StackStatus | undefined, trunkConflict?: {
|
|
34
|
+
trunk: string;
|
|
35
|
+
bottomPr?: number;
|
|
36
|
+
}): string | undefined;
|
|
31
37
|
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
32
38
|
export declare function buildConflictInstruction(stackRebase: string | undefined): string;
|
|
33
39
|
/** Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack. */
|
|
@@ -12,7 +12,9 @@ export function buildNativeStackRebaseInstruction(repo, stackNumber, start) {
|
|
|
12
12
|
`check out the parent stack branch \`${start.parentBranch}\``,
|
|
13
13
|
"gh stack rebase --upstack --no-trunk",
|
|
14
14
|
]
|
|
15
|
-
:
|
|
15
|
+
: "bottomPr" in start
|
|
16
|
+
? [`check out the head branch of PR #${start.bottomPr}`, "gh stack rebase"]
|
|
17
|
+
: [`check out the bottom open layer whose base is \`${start.trunk}\``, "gh stack rebase"];
|
|
16
18
|
const prepare = `if \`gh stack\` does not track stack #${stackNumber} locally, import it with \`gh stack checkout ${stackNumber}\`, then confirm every layer's local branch is at its PR's head commit — a stale local layer would overwrite that PR's newer commits on push`;
|
|
17
19
|
return `From a clean checkout of \`${repo}\`, ${prepare}. Then ${checkout} and run \`${command}\`; if it stops on a conflict, resolve it and run \`gh stack rebase --continue\`.`;
|
|
18
20
|
}
|
|
@@ -21,12 +23,20 @@ export function buildNativeStackRebaseInstruction(repo, stackNumber, start) {
|
|
|
21
23
|
* workflow keeps failing); undefined outside a stack.
|
|
22
24
|
* A layer whose PR targets the stack's trunk (`stack.baseRefName`) is the bottom open layer —
|
|
23
25
|
* position 1, or a higher layer GitHub retargeted after every layer below it merged. Any other
|
|
24
|
-
* layer is an upper layer whose parent is its own PR base branch.
|
|
26
|
+
* layer is an upper layer whose parent is its own PR base branch. `trunkConflict` means that
|
|
27
|
+
* upper layer already contains its parent, so the rebase starts at the bottom open layer.
|
|
25
28
|
*/
|
|
26
|
-
export function buildNativeStackLayerRebase(repo, pr, stack) {
|
|
29
|
+
export function buildNativeStackLayerRebase(repo, pr, stack, trunkConflict) {
|
|
27
30
|
if (!stack)
|
|
28
31
|
return undefined;
|
|
29
|
-
|
|
32
|
+
const start = trunkConflict
|
|
33
|
+
? trunkConflict.bottomPr !== undefined
|
|
34
|
+
? { bottomPr: trunkConflict.bottomPr }
|
|
35
|
+
: { trunk: trunkConflict.trunk }
|
|
36
|
+
: pr.baseBranch === stack.baseRefName
|
|
37
|
+
? { bottomPr: pr.number }
|
|
38
|
+
: { parentBranch: pr.baseBranch };
|
|
39
|
+
return buildNativeStackRebaseInstruction(repo, stack.number, start);
|
|
30
40
|
}
|
|
31
41
|
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
32
42
|
export function buildConflictInstruction(stackRebase) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** An upper layer already contains its parent, so the dirty state is against trunk. */
|
|
2
|
+
export interface UpperLayerTrunkConflict {
|
|
3
|
+
trunk: string;
|
|
4
|
+
/** Open layer whose PR base is the trunk. Omitted when that layer is not on this page. */
|
|
5
|
+
bottomPr?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function lookupUpperLayerTrunkConflict(input: {
|
|
8
|
+
owner: string;
|
|
9
|
+
name: string;
|
|
10
|
+
pr: number;
|
|
11
|
+
headRef: string;
|
|
12
|
+
trunk: string;
|
|
13
|
+
}): Promise<UpperLayerTrunkConflict | undefined>;
|