@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 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, Copilot review of the head, the latest verdict from a
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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "../chunk-PJOTYSEK.js";
4
+ } from "../chunk-K5K64YZP.js";
5
5
 
6
6
  // src/bin/pr-lifecycle.ts
7
7
  process.exitCode = await runCli(
@@ -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 === true) {
661
- return "review-requested";
665
+ if (facts.pendingBotReviewers.length > 0 && policy.skipCopilotReview !== true) {
666
+ return "awaiting-bot-review";
662
667
  }
663
- const reviewedCommits = countingCommits(facts);
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.includes(DEPENDABOT_REBASING_NOTICE)) {
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.login === COPILOT_REVIEWER_LOGIN || review.state === "APPROVED" || review.state === "CHANGES_REQUESTED" || review.body.includes("skill-meta")
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.includes(DEPENDABOT_REBASING_NOTICE)) {
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
- reviews: reviews.map((review) => toReviewFact(review, permissions))
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 { facts, nodeId, botEligibility, lineage } = await gatherFacts(client, pr, policy, options);
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 a Copilot review
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 a Copilot review before `review-requested`, for repos without
200
- * Copilot code review (where the wait would never end). Off by default.
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 a Copilot review\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)";
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-ci", "awaiting-copilot", "blocked-base-red", "changes-requested", "ci-failing", "closed", "draft", "escalation-needed", "fix-required", "review-requested"];
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 mode) and never touches
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, plan, and (unless dry-run)
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, COPILOT_REVIEWER_LOGIN, type ChangedFile, type CliDeps, type CliIo, type CollaboratorPermission, type CommitData, DEPENDABOT_LOGIN, DEPENDABOT_REBASE_COMMAND, DEPENDABOT_REBASING_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, 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, isRebasePending, isTrustedAuthor, isVersionOnlyPatch, parseArgs, parseDependabotUpdateType, parseGitVersion, parseVerdict, planReconcile, postTokenAdvisory, rebaseRequestMarker, reconcilePullRequest, runCli, toReconcileJson, withoutReleaseActions };
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
- COPILOT_REVIEWER_LOGIN,
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-PJOTYSEK.js";
56
+ } from "./chunk-K5K64YZP.js";
54
57
  export {
55
58
  ACTOR_TYPES,
56
59
  AUTO_MERGE_LABEL,
57
60
  BRANCH_UPDATERS,
58
- COPILOT_REVIEWER_LOGIN,
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": "7.0.0",
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",