@rmartz/pr-lifecycle 7.0.0 → 9.0.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/README.md +1 -1
- package/dist/bin/pr-lifecycle.js +1 -1
- package/dist/{chunk-PJOTYSEK.js → chunk-K5K64YZP.js} +81 -20
- package/dist/index.d.ts +70 -11
- package/dist/index.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An **event-driven pull-request lifecycle reconciler** for GitHub Actions. On every
|
|
4
4
|
relevant GitHub event it recomputes a PR's lifecycle state from the PR's current
|
|
5
|
-
facts — draft status,
|
|
5
|
+
facts — draft status, pending bot review requests, the latest verdict from a
|
|
6
6
|
trusted author and the head SHA it reviewed — converges the PR's labels to match,
|
|
7
7
|
and **arms GitHub-native auto-merge when the PR reaches `approved`**. The
|
|
8
8
|
consumer's branch ruleset (required checks) then decides when the merge lands.
|
package/dist/bin/pr-lifecycle.js
CHANGED
|
@@ -455,7 +455,9 @@ function createHttpClient(options) {
|
|
|
455
455
|
isCrossRepository: pull.head.repo?.id !== pull.base.repo.id,
|
|
456
456
|
mergeable: pull.mergeable ?? void 0,
|
|
457
457
|
mergeState: pull.mergeable_state,
|
|
458
|
-
changedFileCount: pull.changed_files
|
|
458
|
+
changedFileCount: pull.changed_files,
|
|
459
|
+
createdAt: pull.created_at,
|
|
460
|
+
requestedBotReviewers: (pull.requested_reviewers ?? []).filter((reviewer) => reviewer.type === "Bot").map((reviewer) => reviewer.login)
|
|
459
461
|
};
|
|
460
462
|
},
|
|
461
463
|
async getBranchHeadSha(branch) {
|
|
@@ -512,6 +514,10 @@ function createHttpClient(options) {
|
|
|
512
514
|
async removeLabel(pr, name) {
|
|
513
515
|
await request("DELETE", `${repoPath}/issues/${pr}/labels/${encodeURIComponent(name)}`);
|
|
514
516
|
},
|
|
517
|
+
async getLastReadyForReviewAt(pr) {
|
|
518
|
+
const events = await paginate(`${repoPath}/issues/${pr}/events`);
|
|
519
|
+
return events.filter((event) => event.event === "ready_for_review").at(-1)?.created_at;
|
|
520
|
+
},
|
|
515
521
|
async listIssueComments(pr) {
|
|
516
522
|
const comments = await paginate(`${repoPath}/issues/${pr}/comments`);
|
|
517
523
|
return comments.map((comment) => comment.body ?? "");
|
|
@@ -621,8 +627,8 @@ function currentVerdict(facts, policy) {
|
|
|
621
627
|
// src/state.ts
|
|
622
628
|
var LIFECYCLE_STATES = [
|
|
623
629
|
"approved",
|
|
630
|
+
"awaiting-bot-review",
|
|
624
631
|
"awaiting-ci",
|
|
625
|
-
"awaiting-copilot",
|
|
626
632
|
"blocked-base-red",
|
|
627
633
|
"changes-requested",
|
|
628
634
|
"ci-failing",
|
|
@@ -632,7 +638,6 @@ var LIFECYCLE_STATES = [
|
|
|
632
638
|
"fix-required",
|
|
633
639
|
"review-requested"
|
|
634
640
|
];
|
|
635
|
-
var COPILOT_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
|
|
636
641
|
var WIP_PATTERN = /\[wip\]/i;
|
|
637
642
|
function computeState(facts, policy) {
|
|
638
643
|
if (facts.status !== "open") {
|
|
@@ -657,14 +662,10 @@ function computeState(facts, policy) {
|
|
|
657
662
|
if (facts.ciStatus === "pending") {
|
|
658
663
|
return "awaiting-ci";
|
|
659
664
|
}
|
|
660
|
-
if (policy.skipCopilotReview
|
|
661
|
-
return "review
|
|
665
|
+
if (facts.pendingBotReviewers.length > 0 && policy.skipCopilotReview !== true) {
|
|
666
|
+
return "awaiting-bot-review";
|
|
662
667
|
}
|
|
663
|
-
|
|
664
|
-
const copilotReviewedHead = facts.reviews.some(
|
|
665
|
-
(review) => review.author.login === COPILOT_REVIEWER_LOGIN && reviewedCommits.has(review.commitSha)
|
|
666
|
-
);
|
|
667
|
-
return copilotReviewedHead ? "review-requested" : "awaiting-copilot";
|
|
668
|
+
return "review-requested";
|
|
668
669
|
}
|
|
669
670
|
|
|
670
671
|
// src/plan.ts
|
|
@@ -677,6 +678,7 @@ var LIFECYCLE_LABELS = [
|
|
|
677
678
|
"review requested"
|
|
678
679
|
];
|
|
679
680
|
var AUTO_MERGE_LABEL = "auto-merge enabled";
|
|
681
|
+
var DEPENDABOT_REBASING_LABEL = "dependabot rebasing";
|
|
680
682
|
var UPDATE_REQUIRED_LABEL = "update required";
|
|
681
683
|
function lifecycleLabels(state) {
|
|
682
684
|
switch (state) {
|
|
@@ -692,8 +694,8 @@ function lifecycleLabels(state) {
|
|
|
692
694
|
return ["fix required", "ci failing"];
|
|
693
695
|
case "review-requested":
|
|
694
696
|
return ["review requested"];
|
|
697
|
+
case "awaiting-bot-review":
|
|
695
698
|
case "awaiting-ci":
|
|
696
|
-
case "awaiting-copilot":
|
|
697
699
|
case "blocked-base-red":
|
|
698
700
|
case "closed":
|
|
699
701
|
case "draft":
|
|
@@ -732,8 +734,11 @@ function planReconcile(facts, policy) {
|
|
|
732
734
|
autoMerge = facts.immediatelyMergeable ? "merge" : "arm";
|
|
733
735
|
}
|
|
734
736
|
}
|
|
735
|
-
const owned = [...LIFECYCLE_LABELS];
|
|
737
|
+
const owned = [...LIFECYCLE_LABELS, DEPENDABOT_REBASING_LABEL];
|
|
736
738
|
const desired = new Set(lifecycleLabels(state));
|
|
739
|
+
if (facts.updater === "dependabot" && facts.dependabotRebasing) {
|
|
740
|
+
desired.add(DEPENDABOT_REBASING_LABEL);
|
|
741
|
+
}
|
|
737
742
|
if (arming) {
|
|
738
743
|
owned.push(AUTO_MERGE_LABEL);
|
|
739
744
|
if (shouldArm && autoMerge !== "merge") {
|
|
@@ -762,6 +767,10 @@ function withoutReleaseActions(plan) {
|
|
|
762
767
|
// src/github/dependabot-rebase.ts
|
|
763
768
|
var DEPENDABOT_REBASE_COMMAND = "@dependabot rebase";
|
|
764
769
|
var DEPENDABOT_REBASING_NOTICE = "Dependabot is rebasing this PR";
|
|
770
|
+
var DEPENDABOT_RECREATING_NOTICE = "Dependabot is recreating this PR";
|
|
771
|
+
function isDependabotRebasing(prBody) {
|
|
772
|
+
return prBody.includes(DEPENDABOT_REBASING_NOTICE) || prBody.includes(DEPENDABOT_RECREATING_NOTICE);
|
|
773
|
+
}
|
|
765
774
|
function rebaseRequestMarker(headSha) {
|
|
766
775
|
return `<!-- pr-lifecycle:dependabot-rebase head=${headSha} -->`;
|
|
767
776
|
}
|
|
@@ -771,7 +780,7 @@ function buildRebaseRequestBody(headSha) {
|
|
|
771
780
|
${rebaseRequestMarker(headSha)}`;
|
|
772
781
|
}
|
|
773
782
|
function isRebasePending(prBody, comments, headSha) {
|
|
774
|
-
if (prBody
|
|
783
|
+
if (isDependabotRebasing(prBody)) {
|
|
775
784
|
return true;
|
|
776
785
|
}
|
|
777
786
|
const marker = rebaseRequestMarker(headSha);
|
|
@@ -806,6 +815,11 @@ var OWNED_LABEL_DEFINITIONS = {
|
|
|
806
815
|
color: "B60205",
|
|
807
816
|
description: "The PR's required CI (excluding gate checks) is failing on its head."
|
|
808
817
|
},
|
|
818
|
+
"dependabot rebasing": {
|
|
819
|
+
name: DEPENDABOT_REBASING_LABEL,
|
|
820
|
+
color: "0366D6",
|
|
821
|
+
description: "Dependabot is rebasing or recreating this PR; its branch is about to change."
|
|
822
|
+
},
|
|
809
823
|
"fix required": {
|
|
810
824
|
name: "fix required",
|
|
811
825
|
color: "D93F0B",
|
|
@@ -1017,7 +1031,7 @@ var MAX_CHAIN_STEPS = 20;
|
|
|
1017
1031
|
function reviewedAncestors(input) {
|
|
1018
1032
|
return new Set(
|
|
1019
1033
|
input.reviews.filter((review) => review.commitSha !== input.headSha).filter(
|
|
1020
|
-
(review) => review.
|
|
1034
|
+
(review) => review.state === "APPROVED" || review.state === "CHANGES_REQUESTED" || review.body.includes("skill-meta")
|
|
1021
1035
|
).map((review) => review.commitSha)
|
|
1022
1036
|
);
|
|
1023
1037
|
}
|
|
@@ -1259,7 +1273,7 @@ async function gatherRebasePending(client, pr, pull, updater, policy) {
|
|
|
1259
1273
|
if (updater !== "dependabot") {
|
|
1260
1274
|
return false;
|
|
1261
1275
|
}
|
|
1262
|
-
if (pull.body
|
|
1276
|
+
if (isDependabotRebasing(pull.body)) {
|
|
1263
1277
|
return true;
|
|
1264
1278
|
}
|
|
1265
1279
|
const couldUpdate = policy.autoUpdate === true && pull.state === "open" && pull.labels.includes(UPDATE_REQUIRED_LABEL);
|
|
@@ -1283,6 +1297,7 @@ async function gatherFacts(client, pr, policy = {}, options = {}) {
|
|
|
1283
1297
|
nodeId: pull.nodeId,
|
|
1284
1298
|
botEligibility,
|
|
1285
1299
|
lineage,
|
|
1300
|
+
createdAt: pull.createdAt,
|
|
1286
1301
|
facts: {
|
|
1287
1302
|
status: pull.merged ? "merged" : pull.state,
|
|
1288
1303
|
isDraft: pull.draft,
|
|
@@ -1298,11 +1313,47 @@ async function gatherFacts(client, pr, policy = {}, options = {}) {
|
|
|
1298
1313
|
cleanAncestors: lineage?.cleanAncestors ?? [],
|
|
1299
1314
|
updater,
|
|
1300
1315
|
rebasePending,
|
|
1301
|
-
|
|
1316
|
+
dependabotRebasing: updater === "dependabot" && isDependabotRebasing(pull.body),
|
|
1317
|
+
reviews: reviews.map((review) => toReviewFact(review, permissions)),
|
|
1318
|
+
pendingBotReviewers: pull.requestedBotReviewers
|
|
1302
1319
|
}
|
|
1303
1320
|
};
|
|
1304
1321
|
}
|
|
1305
1322
|
|
|
1323
|
+
// src/github/settle.ts
|
|
1324
|
+
var DEFAULT_SETTLE_MS = 3e4;
|
|
1325
|
+
function realSleep(ms) {
|
|
1326
|
+
return new Promise((resolve) => {
|
|
1327
|
+
setTimeout(resolve, ms);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
async function settleDelay(client, pr, gathered, policy, settleMs, now) {
|
|
1331
|
+
const { facts } = gathered;
|
|
1332
|
+
if (policy.skipCopilotReview === true || computeState(facts, policy) !== "review-requested") {
|
|
1333
|
+
return 0;
|
|
1334
|
+
}
|
|
1335
|
+
if (facts.labels.includes("review requested")) {
|
|
1336
|
+
return 0;
|
|
1337
|
+
}
|
|
1338
|
+
const readyAt = await client.getLastReadyForReviewAt(pr);
|
|
1339
|
+
const reviewableSince = Math.max(
|
|
1340
|
+
Date.parse(gathered.createdAt),
|
|
1341
|
+
readyAt === void 0 ? 0 : Date.parse(readyAt)
|
|
1342
|
+
);
|
|
1343
|
+
const elapsed = now - reviewableSince;
|
|
1344
|
+
if (!Number.isFinite(elapsed)) {
|
|
1345
|
+
return settleMs;
|
|
1346
|
+
}
|
|
1347
|
+
return Math.min(settleMs, Math.max(0, settleMs - elapsed));
|
|
1348
|
+
}
|
|
1349
|
+
function resolveSettle(options = {}) {
|
|
1350
|
+
return {
|
|
1351
|
+
settleMs: options.settleMs ?? DEFAULT_SETTLE_MS,
|
|
1352
|
+
now: options.now ?? Date.now,
|
|
1353
|
+
sleep: options.sleep ?? realSleep
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1306
1357
|
// src/github/reconcile.ts
|
|
1307
1358
|
var REFUSE_RELEASE = {
|
|
1308
1359
|
createIssueComment: () => Promise.reject(new Error("rebase requests need a release token")),
|
|
@@ -1311,7 +1362,14 @@ var REFUSE_RELEASE = {
|
|
|
1311
1362
|
updateBranch: () => Promise.reject(new Error("updating needs a release token"))
|
|
1312
1363
|
};
|
|
1313
1364
|
async function reconcilePullRequest(client, pr, policy, options = {}) {
|
|
1314
|
-
const
|
|
1365
|
+
const settle = resolveSettle(options.settle);
|
|
1366
|
+
let gathered = await gatherFacts(client, pr, policy, options);
|
|
1367
|
+
const settledMs = await settleDelay(client, pr, gathered, policy, settle.settleMs, settle.now());
|
|
1368
|
+
if (settledMs > 0) {
|
|
1369
|
+
await settle.sleep(settledMs);
|
|
1370
|
+
gathered = await gatherFacts(client, pr, policy, options);
|
|
1371
|
+
}
|
|
1372
|
+
const { facts, nodeId, botEligibility, lineage } = gathered;
|
|
1315
1373
|
const planned = planReconcile(facts, policy);
|
|
1316
1374
|
const unavailable = options.release === "unavailable";
|
|
1317
1375
|
const plan = unavailable ? withoutReleaseActions(planned) : planned;
|
|
@@ -1322,7 +1380,7 @@ async function reconcilePullRequest(client, pr, policy, options = {}) {
|
|
|
1322
1380
|
const wanted = planned.autoMerge;
|
|
1323
1381
|
const skippedAutoMerge = unavailable && (wanted === "arm" || wanted === "merge") ? wanted : void 0;
|
|
1324
1382
|
const skippedUpdate = unavailable && planned.update !== "none" ? planned.update : void 0;
|
|
1325
|
-
return { plan, botEligibility, lineage, skippedAutoMerge, skippedUpdate };
|
|
1383
|
+
return { plan, botEligibility, lineage, skippedAutoMerge, skippedUpdate, settledMs };
|
|
1326
1384
|
}
|
|
1327
1385
|
|
|
1328
1386
|
// src/github/token-advisory.ts
|
|
@@ -1362,7 +1420,7 @@ reconcile options:
|
|
|
1362
1420
|
--auto-update Update approved PRs merge-safety flags 'update required'
|
|
1363
1421
|
(Dependabot PRs are asked to rebase, never updated)
|
|
1364
1422
|
--trusted-authors <a,b> Only these logins (with write access) may cast verdicts
|
|
1365
|
-
--skip-copilot-review Don't wait for
|
|
1423
|
+
--skip-copilot-review Don't wait for requested bot reviewers (deprecated)
|
|
1366
1424
|
--hold-checks <a,b> Required checks whose pending is a hold (default: pr-policy)
|
|
1367
1425
|
--ignore-checks <a,b> Required checks CI never counts (default: merge-safety)
|
|
1368
1426
|
--dry-run Compute the plan without writing anything
|
|
@@ -1472,15 +1530,17 @@ export {
|
|
|
1472
1530
|
isTrustedAuthor,
|
|
1473
1531
|
currentVerdict,
|
|
1474
1532
|
LIFECYCLE_STATES,
|
|
1475
|
-
COPILOT_REVIEWER_LOGIN,
|
|
1476
1533
|
computeState,
|
|
1477
1534
|
LIFECYCLE_LABELS,
|
|
1478
1535
|
AUTO_MERGE_LABEL,
|
|
1536
|
+
DEPENDABOT_REBASING_LABEL,
|
|
1479
1537
|
UPDATE_REQUIRED_LABEL,
|
|
1480
1538
|
planReconcile,
|
|
1481
1539
|
withoutReleaseActions,
|
|
1482
1540
|
DEPENDABOT_REBASE_COMMAND,
|
|
1483
1541
|
DEPENDABOT_REBASING_NOTICE,
|
|
1542
|
+
DEPENDABOT_RECREATING_NOTICE,
|
|
1543
|
+
isDependabotRebasing,
|
|
1484
1544
|
rebaseRequestMarker,
|
|
1485
1545
|
buildRebaseRequestBody,
|
|
1486
1546
|
isRebasePending,
|
|
@@ -1492,6 +1552,7 @@ export {
|
|
|
1492
1552
|
MAX_CHAIN_STEPS,
|
|
1493
1553
|
gatherLineage,
|
|
1494
1554
|
gatherFacts,
|
|
1555
|
+
DEFAULT_SETTLE_MS,
|
|
1495
1556
|
reconcilePullRequest,
|
|
1496
1557
|
TOKEN_ADVISORY_MARKER,
|
|
1497
1558
|
RELEASE_TOKEN_DOCS_URL,
|
package/dist/index.d.ts
CHANGED
|
@@ -178,7 +178,19 @@ interface PullRequestFacts {
|
|
|
178
178
|
* error. Always `false` for other PRs.
|
|
179
179
|
*/
|
|
180
180
|
rebasePending: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* For a Dependabot PR: the PR body says Dependabot is rebasing or recreating the
|
|
183
|
+
* branch right now. Unlike `rebasePending`, our own request doesn't count — only
|
|
184
|
+
* Dependabot's notice, which it removes when done. Drives the
|
|
185
|
+
* `dependabot rebasing` label. Always `false` for other PRs.
|
|
186
|
+
*/
|
|
187
|
+
dependabotRebasing: boolean;
|
|
181
188
|
reviews: readonly ReviewFact[];
|
|
189
|
+
/**
|
|
190
|
+
* Logins of the bots (e.g. Copilot) whose review is requested and not yet
|
|
191
|
+
* submitted. Users and teams are left out: the lifecycle never waits on people.
|
|
192
|
+
*/
|
|
193
|
+
pendingBotReviewers: readonly string[];
|
|
182
194
|
}
|
|
183
195
|
interface ReconcilePolicy {
|
|
184
196
|
/**
|
|
@@ -196,8 +208,10 @@ interface ReconcilePolicy {
|
|
|
196
208
|
*/
|
|
197
209
|
autoUpdate?: boolean;
|
|
198
210
|
/**
|
|
199
|
-
* Don't wait for
|
|
200
|
-
*
|
|
211
|
+
* Don't wait for requested bot reviewers (e.g. Copilot) before
|
|
212
|
+
* `review-requested`. Rarely needed: a bot that won't review is never requested.
|
|
213
|
+
* Off by default. Deprecated: the wait now ends on its own; kept so existing
|
|
214
|
+
* callers keep working.
|
|
201
215
|
*/
|
|
202
216
|
skipCopilotReview?: boolean;
|
|
203
217
|
/**
|
|
@@ -243,6 +257,10 @@ interface PullRequestData {
|
|
|
243
257
|
mergeState: string | undefined;
|
|
244
258
|
/** How many files the PR changes (`changed_files`), to tell a full file list from a truncated one. */
|
|
245
259
|
changedFileCount: number;
|
|
260
|
+
/** ISO-8601 time the PR was opened. */
|
|
261
|
+
createdAt: string;
|
|
262
|
+
/** Logins of requested reviewers that are bots (users and teams left out). */
|
|
263
|
+
requestedBotReviewers: string[];
|
|
246
264
|
}
|
|
247
265
|
/** A check-run on a commit (the latest run per name). */
|
|
248
266
|
interface CheckRunData {
|
|
@@ -316,6 +334,8 @@ interface GitHubClient {
|
|
|
316
334
|
addLabels(pr: number, names: readonly string[]): Promise<void>;
|
|
317
335
|
/** Throws a GitHubApiError with status 404 when the label is not on the PR. */
|
|
318
336
|
removeLabel(pr: number, name: string): Promise<void>;
|
|
337
|
+
/** When the PR was last marked ready for review (ISO-8601), if ever. */
|
|
338
|
+
getLastReadyForReviewAt(pr: number): Promise<string | undefined>;
|
|
319
339
|
/** Bodies of the PR's conversation comments, oldest first. */
|
|
320
340
|
listIssueComments(pr: number): Promise<string[]>;
|
|
321
341
|
createIssueComment(pr: number, body: string): Promise<void>;
|
|
@@ -400,7 +420,7 @@ interface CliDeps {
|
|
|
400
420
|
/** Runs git for approval carry-over; tests inject one. Defaults to the real binary. */
|
|
401
421
|
git?: GitRunner;
|
|
402
422
|
}
|
|
403
|
-
declare const USAGE = "Usage: ai-pr-lifecycle <command> [options]\n\nCommands:\n reconcile Recompute a PR's lifecycle state and converge its labels\n help Show this message\n\nreconcile options:\n --repo <owner/repo> Repository (required)\n --pr <number> Pull request number (required)\n --arm-auto-merge Arm/disarm native auto-merge from the state\n --auto-update Update approved PRs merge-safety flags 'update required'\n (Dependabot PRs are asked to rebase, never updated)\n --trusted-authors <a,b> Only these logins (with write access) may cast verdicts\n --skip-copilot-review Don't wait for
|
|
423
|
+
declare const USAGE = "Usage: ai-pr-lifecycle <command> [options]\n\nCommands:\n reconcile Recompute a PR's lifecycle state and converge its labels\n help Show this message\n\nreconcile options:\n --repo <owner/repo> Repository (required)\n --pr <number> Pull request number (required)\n --arm-auto-merge Arm/disarm native auto-merge from the state\n --auto-update Update approved PRs merge-safety flags 'update required'\n (Dependabot PRs are asked to rebase, never updated)\n --trusted-authors <a,b> Only these logins (with write access) may cast verdicts\n --skip-copilot-review Don't wait for requested bot reviewers (deprecated)\n --hold-checks <a,b> Required checks whose pending is a hold (default: pr-policy)\n --ignore-checks <a,b> Required checks CI never counts (default: merge-safety)\n --dry-run Compute the plan without writing anything\n --json Print one JSON object (schemaVersion 1) on stdout\n --no-token-advisory Don't comment on the PR when arming lacks a release token\n\nEnvironment:\n GITHUB_TOKEN Token for reads and writes (required)\n PR_LIFECYCLE_TOKEN\n Real-actor token for arming, merging, and updating;\n without it, labels are kept but those are skipped\n GITHUB_API_URL API base URL (GitHub Enterprise Server)";
|
|
404
424
|
declare function runCli(argv: readonly string[], io: CliIo, deps: CliDeps): Promise<number>;
|
|
405
425
|
|
|
406
426
|
/**
|
|
@@ -431,20 +451,25 @@ declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
|
431
451
|
* Lifecycle state, computed from facts in a fixed priority order. See
|
|
432
452
|
* docs/reconciler-design.md §State.
|
|
433
453
|
*/
|
|
434
|
-
declare const LIFECYCLE_STATES: readonly ["approved", "awaiting-
|
|
454
|
+
declare const LIFECYCLE_STATES: readonly ["approved", "awaiting-bot-review", "awaiting-ci", "blocked-base-red", "changes-requested", "ci-failing", "closed", "draft", "escalation-needed", "fix-required", "review-requested"];
|
|
435
455
|
type LifecycleState = (typeof LIFECYCLE_STATES)[number];
|
|
436
|
-
declare const COPILOT_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
|
|
437
456
|
declare function computeState(facts: PullRequestFacts, policy: ReconcilePolicy): LifecycleState;
|
|
438
457
|
|
|
439
458
|
/**
|
|
440
459
|
* The reconcile plan: the minimal label and auto-merge changes that converge a PR
|
|
441
460
|
* to its computed lifecycle state. Labels are output only — the core owns the
|
|
442
|
-
* lifecycle labels (plus `auto-merge enabled` in arming
|
|
443
|
-
* any other label. See docs/reconciler-design.md §Plan.
|
|
461
|
+
* lifecycle labels and `dependabot rebasing` (plus `auto-merge enabled` in arming
|
|
462
|
+
* mode) and never touches any other label. See docs/reconciler-design.md §Plan.
|
|
444
463
|
*/
|
|
445
464
|
declare const LIFECYCLE_LABELS: readonly ["approved", "changes requested", "escalation needed", "fix required", "ci failing", "review requested"];
|
|
446
465
|
type LifecycleLabel = (typeof LIFECYCLE_LABELS)[number];
|
|
447
466
|
declare const AUTO_MERGE_LABEL = "auto-merge enabled";
|
|
467
|
+
/**
|
|
468
|
+
* Shown while Dependabot rebases or recreates its PR, so workflows can hold off
|
|
469
|
+
* until the branch settles. Owned like the lifecycle labels, but independent of
|
|
470
|
+
* the state.
|
|
471
|
+
*/
|
|
472
|
+
declare const DEPENDABOT_REBASING_LABEL = "dependabot rebasing";
|
|
448
473
|
/** merge-safety's label for a PR whose base moved in a way that matters. Read, never written. */
|
|
449
474
|
declare const UPDATE_REQUIRED_LABEL = "update required";
|
|
450
475
|
/**
|
|
@@ -536,6 +561,8 @@ interface GatheredPullRequest {
|
|
|
536
561
|
botEligibility: BotEligibility;
|
|
537
562
|
/** How far approval carry-over verified, for reporting (absent when off). */
|
|
538
563
|
lineage: Lineage | undefined;
|
|
564
|
+
/** When the PR was opened (ISO-8601), for the settle wait (settle.ts). */
|
|
565
|
+
createdAt: string;
|
|
539
566
|
}
|
|
540
567
|
interface GatherOptions {
|
|
541
568
|
/**
|
|
@@ -550,7 +577,28 @@ interface GatherOptions {
|
|
|
550
577
|
}
|
|
551
578
|
declare function gatherFacts(client: GitHubClient, pr: number, policy?: ReconcilePolicy, options?: GatherOptions): Promise<GatheredPullRequest>;
|
|
552
579
|
|
|
580
|
+
/**
|
|
581
|
+
* The settle wait: a PR that just became reviewable isn't moved to
|
|
582
|
+
* `review-requested` until bot reviewers have had time to be requested. Copilot's
|
|
583
|
+
* auto-review request lands a few seconds after the PR opens (observed: up to 5s),
|
|
584
|
+
* so a run that reads the PR sooner would see no request and skip the wait. The
|
|
585
|
+
* wait happens inside the run, never as "stay put and wait for the next event":
|
|
586
|
+
* when no bot is coming, no further event may ever fire. See
|
|
587
|
+
* docs/reconciler-design.md §Waiting for bot reviewers.
|
|
588
|
+
*/
|
|
589
|
+
/** Long enough to cover the observed request delay several times over. */
|
|
590
|
+
declare const DEFAULT_SETTLE_MS = 30000;
|
|
591
|
+
interface SettleOptions {
|
|
592
|
+
/** How long after a PR becomes reviewable to wait for bot requests. */
|
|
593
|
+
settleMs?: number;
|
|
594
|
+
/** Clock and timer, injectable for tests. */
|
|
595
|
+
now?: () => number;
|
|
596
|
+
sleep?: (ms: number) => Promise<void>;
|
|
597
|
+
}
|
|
598
|
+
|
|
553
599
|
interface ReconcileOptions extends GatherOptions {
|
|
600
|
+
/** The settle wait for a PR that just became reviewable (settle.ts). */
|
|
601
|
+
settle?: SettleOptions;
|
|
554
602
|
/** Compute and return the plan without writing anything. */
|
|
555
603
|
dryRun?: boolean;
|
|
556
604
|
/**
|
|
@@ -570,10 +618,12 @@ interface ReconcileResult {
|
|
|
570
618
|
skippedAutoMerge: 'arm' | 'merge' | undefined;
|
|
571
619
|
/** The branch update the plan wanted but skipped because `release` was unavailable. */
|
|
572
620
|
skippedUpdate: Exclude<UpdateAction, 'none'> | undefined;
|
|
621
|
+
/** How long the run waited for bot review requests before gathering again (ms). */
|
|
622
|
+
settledMs: number;
|
|
573
623
|
}
|
|
574
624
|
/**
|
|
575
|
-
* One full reconcile pass for a PR: gather its facts
|
|
576
|
-
* execute. Returns the plan, the bot-eligibility verdict, and the carry-over
|
|
625
|
+
* One full reconcile pass for a PR: gather its facts (waiting to settle first if it
|
|
626
|
+
* just became reviewable), plan, and (unless dry-run) execute. Returns the plan, the bot-eligibility verdict, and the carry-over
|
|
577
627
|
* result so callers can report what changed and why.
|
|
578
628
|
*/
|
|
579
629
|
declare function reconcilePullRequest(client: GitHubClient, pr: number, policy: ReconcilePolicy, options?: ReconcileOptions): Promise<ReconcileResult>;
|
|
@@ -639,6 +689,14 @@ declare function formatSummary(target: ReconcileTarget, result: ReconcileResult)
|
|
|
639
689
|
declare const DEPENDABOT_REBASE_COMMAND = "@dependabot rebase";
|
|
640
690
|
/** Dependabot puts this in the PR body while a rebase runs, and removes it after. */
|
|
641
691
|
declare const DEPENDABOT_REBASING_NOTICE = "Dependabot is rebasing this PR";
|
|
692
|
+
/** The same notice for `@dependabot recreate`, which force-pushes a fresh branch. */
|
|
693
|
+
declare const DEPENDABOT_RECREATING_NOTICE = "Dependabot is recreating this PR";
|
|
694
|
+
/**
|
|
695
|
+
* Whether Dependabot says, in the PR body, that it is rebasing or recreating the
|
|
696
|
+
* branch right now. Only Dependabot's own notice counts — not our request marker,
|
|
697
|
+
* which means "asked", not "running".
|
|
698
|
+
*/
|
|
699
|
+
declare function isDependabotRebasing(prBody: string): boolean;
|
|
642
700
|
/** Hidden marker recording which head a request was made for. */
|
|
643
701
|
declare function rebaseRequestMarker(headSha: string): string;
|
|
644
702
|
/** The command on its own first line, as Dependabot parses it, then the marker. */
|
|
@@ -672,7 +730,7 @@ declare function executePlan(client: GitHubClient, target: PlanTarget, plan: Rec
|
|
|
672
730
|
* a consumer repo is missing; an existing label's color and description are never
|
|
673
731
|
* changed.
|
|
674
732
|
*/
|
|
675
|
-
declare const OWNED_LABEL_DEFINITIONS: Record<LifecycleLabel | typeof AUTO_MERGE_LABEL, LabelDefinition>;
|
|
733
|
+
declare const OWNED_LABEL_DEFINITIONS: Record<LifecycleLabel | typeof AUTO_MERGE_LABEL | typeof DEPENDABOT_REBASING_LABEL, LabelDefinition>;
|
|
676
734
|
|
|
677
735
|
/**
|
|
678
736
|
* The advisory comment posted when a PR is ready to arm or merge but no release
|
|
@@ -707,6 +765,7 @@ interface ParsedVerdict {
|
|
|
707
765
|
declare function parseVerdict(review: ReviewFact): ParsedVerdict | undefined;
|
|
708
766
|
/** Whether an author may cast a counting verdict under the policy. */
|
|
709
767
|
declare function isTrustedAuthor(author: ReviewAuthor, policy: ReconcilePolicy): boolean;
|
|
768
|
+
/** The latest verdict that counts for the PR's current head, if any. */
|
|
710
769
|
declare function currentVerdict(facts: PullRequestFacts, policy: ReconcilePolicy): Verdict | undefined;
|
|
711
770
|
|
|
712
|
-
export { ACTOR_TYPES, AUTO_MERGE_LABEL, type ActorType, type AutoMergeAction, BRANCH_UPDATERS, type BotEligibility, type BotPrCommit, type BotPrFacts, type BotPrType, type BranchUpdater,
|
|
771
|
+
export { ACTOR_TYPES, AUTO_MERGE_LABEL, type ActorType, type AutoMergeAction, BRANCH_UPDATERS, type BotEligibility, type BotPrCommit, type BotPrFacts, type BotPrType, type BranchUpdater, type ChangedFile, type CliDeps, type CliIo, type CollaboratorPermission, type CommitData, DEFAULT_SETTLE_MS, DEPENDABOT_LOGIN, DEPENDABOT_REBASE_COMMAND, DEPENDABOT_REBASING_LABEL, DEPENDABOT_REBASING_NOTICE, DEPENDABOT_RECREATING_NOTICE, DEPENDABOT_UPDATE_TYPES, type DependabotUpdateType, type GatherOptions, type GatheredPullRequest, GitHubApiError, type GitHubClient, type GitResult, type GitRunner, type HttpClientOptions, LIFECYCLE_LABELS, LIFECYCLE_STATES, type LabelDefinition, type LifecycleLabel, type LifecycleState, type Lineage, type LineageInput, MAX_CHAIN_STEPS, type MergeStep, type MergeVerifier, OWNED_LABEL_DEFINITIONS, PR_STATUSES, type ParsedArgs, type ParsedVerdict, type PlanTarget, type PullRequestData, type PullRequestFacts, type PullRequestStatus, RELEASE_PLEASE_BRANCH_PREFIX, RELEASE_TOKEN_DOCS_URL, REPO_PERMISSIONS, REVIEW_STATES, type ReconcileArgs, type ReconcileJson, type ReconcileOptions, type ReconcilePlan, type ReconcilePolicy, type ReconcileResult, type ReconcileTarget, type ReleaseActions, type ReleaseDiffVerdict, type RepoPermission, type RepoSource, type ReviewAuthor, type ReviewData, type ReviewFact, type ReviewState, SCHEMA_VERSION, type SettleOptions, TOKEN_ADVISORY_MARKER, UPDATE_REQUIRED_LABEL, USAGE, type UpdateAction, VERDICTS, type Verdict, buildRebaseRequestBody, buildTokenAdvisoryBody, classifyBotPr, classifyReleaseDiff, computeState, createGitRunner, createHttpClient, createMergeVerifier, currentVerdict, executePlan, formatSummary, gatherFacts, gatherLineage, isApiStatus, isDependabotRebasing, isRebasePending, isTrustedAuthor, isVersionOnlyPatch, parseArgs, parseDependabotUpdateType, parseGitVersion, parseVerdict, planReconcile, postTokenAdvisory, rebaseRequestMarker, reconcilePullRequest, runCli, toReconcileJson, withoutReleaseActions };
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
ACTOR_TYPES,
|
|
3
3
|
AUTO_MERGE_LABEL,
|
|
4
4
|
BRANCH_UPDATERS,
|
|
5
|
-
|
|
5
|
+
DEFAULT_SETTLE_MS,
|
|
6
6
|
DEPENDABOT_LOGIN,
|
|
7
7
|
DEPENDABOT_REBASE_COMMAND,
|
|
8
|
+
DEPENDABOT_REBASING_LABEL,
|
|
8
9
|
DEPENDABOT_REBASING_NOTICE,
|
|
10
|
+
DEPENDABOT_RECREATING_NOTICE,
|
|
9
11
|
DEPENDABOT_UPDATE_TYPES,
|
|
10
12
|
GitHubApiError,
|
|
11
13
|
LIFECYCLE_LABELS,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
gatherFacts,
|
|
37
39
|
gatherLineage,
|
|
38
40
|
isApiStatus,
|
|
41
|
+
isDependabotRebasing,
|
|
39
42
|
isRebasePending,
|
|
40
43
|
isTrustedAuthor,
|
|
41
44
|
isVersionOnlyPatch,
|
|
@@ -50,15 +53,17 @@ import {
|
|
|
50
53
|
runCli,
|
|
51
54
|
toReconcileJson,
|
|
52
55
|
withoutReleaseActions
|
|
53
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-K5K64YZP.js";
|
|
54
57
|
export {
|
|
55
58
|
ACTOR_TYPES,
|
|
56
59
|
AUTO_MERGE_LABEL,
|
|
57
60
|
BRANCH_UPDATERS,
|
|
58
|
-
|
|
61
|
+
DEFAULT_SETTLE_MS,
|
|
59
62
|
DEPENDABOT_LOGIN,
|
|
60
63
|
DEPENDABOT_REBASE_COMMAND,
|
|
64
|
+
DEPENDABOT_REBASING_LABEL,
|
|
61
65
|
DEPENDABOT_REBASING_NOTICE,
|
|
66
|
+
DEPENDABOT_RECREATING_NOTICE,
|
|
62
67
|
DEPENDABOT_UPDATE_TYPES,
|
|
63
68
|
GitHubApiError,
|
|
64
69
|
LIFECYCLE_LABELS,
|
|
@@ -89,6 +94,7 @@ export {
|
|
|
89
94
|
gatherFacts,
|
|
90
95
|
gatherLineage,
|
|
91
96
|
isApiStatus,
|
|
97
|
+
isDependabotRebasing,
|
|
92
98
|
isRebasePending,
|
|
93
99
|
isTrustedAuthor,
|
|
94
100
|
isVersionOnlyPatch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmartz/pr-lifecycle",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Event-driven PR lifecycle reconciler: recomputes a pull request's lifecycle labels from its current facts on every GitHub event and arms native auto-merge on approval.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ci",
|