@rmartz/pr-lifecycle 5.0.0 → 8.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.
@@ -48,8 +48,9 @@ pnpm run hygiene
48
48
 
49
49
  Versioned by [semantic-release](.releaserc.json): a push to `main` analyzes the
50
50
  Conventional-Commit history since the last `v*` tag and, when a release is
51
- warranted, publishes the package to GitHub Packages and creates the tag and
52
- GitHub Release.
51
+ warranted, publishes the package to npmjs (public, via OIDC trusted publishing
52
+ with provenance) and creates the tag and GitHub Release. Versions up to 5.0.0
53
+ were published to GitHub Packages and stay there for existing pins.
53
54
 
54
55
  ## License
55
56
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "../chunk-RQEBRPZU.js";
4
+ } from "../chunk-ENU2AAWK.js";
5
5
 
6
6
  // src/bin/pr-lifecycle.ts
7
7
  process.exitCode = await runCli(
@@ -1,7 +1,66 @@
1
+ // src/release-diff.ts
2
+ var MANIFEST = ".release-please-manifest.json";
3
+ var CHANGELOG = "CHANGELOG.md";
4
+ var PACKAGE_JSON = "package.json";
5
+ var RELEASE_STATUSES = /* @__PURE__ */ new Set(["added", "modified"]);
6
+ var VERSION_LINE = /^[ \t]*"version"[ \t]*:[ \t]*"[^"\\\n]*"[,\t ]*$/;
7
+ function basename(path) {
8
+ return path.slice(path.lastIndexOf("/") + 1);
9
+ }
10
+ function isVersionOnlyPatch(patch) {
11
+ let removed = 0;
12
+ let added = 0;
13
+ for (const line of patch.split("\n")) {
14
+ if (line.startsWith("@@") || line.startsWith("\\") || line.startsWith(" ") || line === "") {
15
+ continue;
16
+ }
17
+ const sign = line[0];
18
+ if (sign !== "+" && sign !== "-" || !VERSION_LINE.test(line.slice(1))) {
19
+ return false;
20
+ }
21
+ if (sign === "+") {
22
+ added += 1;
23
+ } else {
24
+ removed += 1;
25
+ }
26
+ }
27
+ return added === 1 && removed === 1;
28
+ }
29
+ function checkFile(file) {
30
+ if (!RELEASE_STATUSES.has(file.status)) {
31
+ return `${file.filename} was ${file.status}`;
32
+ }
33
+ const name = basename(file.filename);
34
+ if (file.filename === MANIFEST || name === CHANGELOG) {
35
+ return void 0;
36
+ }
37
+ if (name !== PACKAGE_JSON) {
38
+ return `${file.filename} is not a release file`;
39
+ }
40
+ if (file.patch === void 0) {
41
+ return `${file.filename} has no patch to verify`;
42
+ }
43
+ return isVersionOnlyPatch(file.patch) ? void 0 : `${file.filename} changes more than its version`;
44
+ }
45
+ function classifyReleaseDiff(files) {
46
+ if (files === void 0) {
47
+ return { release: false, reason: "changed files unavailable" };
48
+ }
49
+ if (files.length === 0) {
50
+ return { release: false, reason: "no changed files" };
51
+ }
52
+ for (const file of files) {
53
+ const problem = checkFile(file);
54
+ if (problem !== void 0) {
55
+ return { release: false, reason: problem };
56
+ }
57
+ }
58
+ return { release: true };
59
+ }
60
+
1
61
  // src/bot-eligibility.ts
2
62
  var DEPENDABOT_LOGIN = "dependabot[bot]";
3
63
  var RELEASE_PLEASE_BRANCH_PREFIX = "release-please--";
4
- var RELEASE_PLEASE_PENDING_LABEL = "autorelease: pending";
5
64
  var DEPENDABOT_UPDATE_TYPES = [
6
65
  "version-update:semver-patch",
7
66
  "version-update:semver-minor",
@@ -72,8 +131,13 @@ function classifyBotPr(facts) {
72
131
  if (facts.authorLogin === DEPENDABOT_LOGIN) {
73
132
  return classifyDependabot(facts.commits);
74
133
  }
75
- if (facts.headRef.startsWith(RELEASE_PLEASE_BRANCH_PREFIX) || facts.labels.includes(RELEASE_PLEASE_PENDING_LABEL)) {
76
- return result(true, "release-please release PR \u2014 eligible", "release-please");
134
+ if (facts.headRef.startsWith(RELEASE_PLEASE_BRANCH_PREFIX)) {
135
+ const diff = classifyReleaseDiff(facts.changedFiles);
136
+ return diff.release ? result(true, "release-please release PR \u2014 eligible", "release-please") : result(
137
+ false,
138
+ `release-please branch, but not a pure release (${diff.reason}) \u2014 held for review`,
139
+ "release-please"
140
+ );
77
141
  }
78
142
  return result(false, "not a recognized bot PR");
79
143
  }
@@ -90,6 +154,7 @@ function parseList(value) {
90
154
  function parseReconcile(args) {
91
155
  const values = {};
92
156
  let armAutoMerge = false;
157
+ let autoUpdate = false;
93
158
  let skipCopilotReview = false;
94
159
  let dryRun = false;
95
160
  let json = false;
@@ -100,6 +165,9 @@ function parseReconcile(args) {
100
165
  case "--arm-auto-merge":
101
166
  armAutoMerge = true;
102
167
  break;
168
+ case "--auto-update":
169
+ autoUpdate = true;
170
+ break;
103
171
  case "--skip-copilot-review":
104
172
  skipCopilotReview = true;
105
173
  break;
@@ -151,6 +219,7 @@ function parseReconcile(args) {
151
219
  pr: Number(prArg),
152
220
  policy: {
153
221
  armAutoMerge,
222
+ autoUpdate,
154
223
  skipCopilotReview,
155
224
  ...trustedAuthors === void 0 ? {} : { trustedAuthors },
156
225
  ...holdChecks === void 0 ? {} : { holdChecks },
@@ -181,7 +250,7 @@ function parseArgs(argv) {
181
250
  // src/cli/output.ts
182
251
  var SCHEMA_VERSION = 1;
183
252
  function toReconcileJson(target, result2) {
184
- const { plan, botEligibility, lineage, skippedAutoMerge } = result2;
253
+ const { plan, botEligibility, lineage, skippedAutoMerge, skippedUpdate } = result2;
185
254
  return {
186
255
  schemaVersion: SCHEMA_VERSION,
187
256
  repo: `${target.owner}/${target.repo}`,
@@ -199,7 +268,9 @@ function toReconcileJson(target, result2) {
199
268
  },
200
269
  carryOver: lineage === void 0 ? null : { cleanAncestors: lineage.cleanAncestors, stoppedBecause: lineage.stoppedBecause },
201
270
  // The CLI makes release actions unavailable only for a missing token.
202
- autoMergeSkipped: skippedAutoMerge === void 0 ? null : { action: skippedAutoMerge, reason: "token-missing" }
271
+ autoMergeSkipped: skippedAutoMerge === void 0 ? null : { action: skippedAutoMerge, reason: "token-missing" },
272
+ update: plan.update,
273
+ updateSkipped: skippedUpdate === void 0 ? null : { action: skippedUpdate, reason: "token-missing" }
203
274
  };
204
275
  }
205
276
  function formatSummary(target, result2) {
@@ -208,7 +279,9 @@ function formatSummary(target, result2) {
208
279
  ...plan.addLabels.map((label) => `+${label}`),
209
280
  ...plan.removeLabels.map((label) => `-${label}`),
210
281
  ...plan.autoMerge === "none" ? [] : [`auto-merge ${plan.autoMerge}`],
211
- ...result2.skippedAutoMerge === void 0 ? [] : [`auto-merge ${result2.skippedAutoMerge} skipped (no release token)`]
282
+ ...result2.skippedAutoMerge === void 0 ? [] : [`auto-merge ${result2.skippedAutoMerge} skipped (no release token)`],
283
+ ...plan.update === "none" ? [] : [`update ${plan.update}`],
284
+ ...result2.skippedUpdate === void 0 ? [] : [`update ${result2.skippedUpdate} skipped (no release token)`]
212
285
  ];
213
286
  const prefix = target.dryRun ? "[dry-run] " : "";
214
287
  const body = changes.length === 0 ? "no changes" : changes.join(", ");
@@ -239,38 +312,11 @@ var REVIEW_STATES = [
239
312
  "PENDING"
240
313
  ];
241
314
  var PR_STATUSES = ["closed", "merged", "open"];
315
+ var BRANCH_UPDATERS = ["dependabot", "github"];
242
316
 
243
- // src/github/rest-payloads.ts
244
- function toReviewState(state) {
245
- return REVIEW_STATES.find((known) => known === state) ?? "DISMISSED";
246
- }
247
- function toActorType(type) {
248
- return type === "User" ? "User" : "Bot";
249
- }
250
- function toReviewData(review) {
251
- return {
252
- id: review.id,
253
- login: review.user?.login,
254
- type: review.user === null ? "User" : toActorType(review.user.type),
255
- commitSha: review.commit_id,
256
- state: toReviewState(review.state),
257
- body: review.body ?? "",
258
- submittedAt: review.submitted_at ?? void 0
259
- };
260
- }
261
-
262
- // src/github/http-client.ts
317
+ // src/github/http-transport.ts
263
318
  var PAGE_SIZE = 100;
264
- var ENABLE_AUTO_MERGE = `mutation($id: ID!, $head: GitObjectID!) {
265
- enablePullRequestAutoMerge(input: { pullRequestId: $id, mergeMethod: SQUASH, expectedHeadOid: $head }) { clientMutationId }
266
- }`;
267
- var MERGE_PULL_REQUEST = `mutation($id: ID!, $head: GitObjectID!) {
268
- mergePullRequest(input: { pullRequestId: $id, mergeMethod: SQUASH, expectedHeadOid: $head }) { clientMutationId }
269
- }`;
270
- var DISABLE_AUTO_MERGE = `mutation($id: ID!) {
271
- disablePullRequestAutoMerge(input: { pullRequestId: $id }) { clientMutationId }
272
- }`;
273
- function createHttpClient(options) {
319
+ function createTransport(options) {
274
320
  let apiUrl = options.apiUrl ?? "https://api.github.com";
275
321
  while (apiUrl.endsWith("/")) {
276
322
  apiUrl = apiUrl.slice(0, -1);
@@ -315,7 +361,79 @@ function createHttpClient(options) {
315
361
  throw new GitHubApiError(200, `GraphQL error: ${messages}`);
316
362
  }
317
363
  }
364
+ return { repoPath, request, paginate, graphql };
365
+ }
366
+
367
+ // src/github/http-checks.ts
368
+ function createCheckMethods({ repoPath, request, paginate }) {
369
+ return {
370
+ async getRequiredStatusChecks(branch) {
371
+ const rules = await paginate(
372
+ `${repoPath}/rules/branches/${encodeURIComponent(branch)}`
373
+ );
374
+ const contexts = rules.filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks ?? []).map((check) => check.context);
375
+ return [...new Set(contexts)];
376
+ },
377
+ async listCheckRuns(sha) {
378
+ const runs = [];
379
+ for (let page = 1; ; page += 1) {
380
+ const batch = await request(
381
+ "GET",
382
+ `${repoPath}/commits/${sha}/check-runs?per_page=${PAGE_SIZE}&page=${page}`
383
+ );
384
+ runs.push(...batch.check_runs);
385
+ if (batch.check_runs.length < PAGE_SIZE) {
386
+ break;
387
+ }
388
+ }
389
+ return runs.map((run) => ({
390
+ name: run.name,
391
+ status: run.status,
392
+ conclusion: run.conclusion,
393
+ completedAt: run.completed_at
394
+ }));
395
+ },
396
+ async listCommitStatuses(sha) {
397
+ const combined = await request("GET", `${repoPath}/commits/${sha}/status`);
398
+ return combined.statuses.map((status) => ({ context: status.context, state: status.state }));
399
+ }
400
+ };
401
+ }
402
+
403
+ // src/github/rest-payloads.ts
404
+ function toReviewState(state) {
405
+ return REVIEW_STATES.find((known) => known === state) ?? "DISMISSED";
406
+ }
407
+ function toActorType(type) {
408
+ return type === "User" ? "User" : "Bot";
409
+ }
410
+ function toReviewData(review) {
411
+ return {
412
+ id: review.id,
413
+ login: review.user?.login,
414
+ type: review.user === null ? "User" : toActorType(review.user.type),
415
+ commitSha: review.commit_id,
416
+ state: toReviewState(review.state),
417
+ body: review.body ?? "",
418
+ submittedAt: review.submitted_at ?? void 0
419
+ };
420
+ }
421
+
422
+ // src/github/http-client.ts
423
+ var ENABLE_AUTO_MERGE = `mutation($id: ID!, $head: GitObjectID!) {
424
+ enablePullRequestAutoMerge(input: { pullRequestId: $id, mergeMethod: SQUASH, expectedHeadOid: $head }) { clientMutationId }
425
+ }`;
426
+ var MERGE_PULL_REQUEST = `mutation($id: ID!, $head: GitObjectID!) {
427
+ mergePullRequest(input: { pullRequestId: $id, mergeMethod: SQUASH, expectedHeadOid: $head }) { clientMutationId }
428
+ }`;
429
+ var DISABLE_AUTO_MERGE = `mutation($id: ID!) {
430
+ disablePullRequestAutoMerge(input: { pullRequestId: $id }) { clientMutationId }
431
+ }`;
432
+ function createHttpClient(options) {
433
+ const transport = createTransport(options);
434
+ const { repoPath, request, paginate, graphql } = transport;
318
435
  return {
436
+ ...createCheckMethods(transport),
319
437
  async getPullRequest(pr) {
320
438
  const pull = await request("GET", `${repoPath}/pulls/${pr}`);
321
439
  return {
@@ -324,6 +442,7 @@ function createHttpClient(options) {
324
442
  merged: pull.merged,
325
443
  draft: pull.draft === true,
326
444
  title: pull.title,
445
+ body: pull.body ?? "",
327
446
  headSha: pull.head.sha,
328
447
  labels: pull.labels.map((label) => label.name),
329
448
  autoMergeEnabled: pull.auto_merge !== null && pull.auto_merge !== void 0,
@@ -335,16 +454,12 @@ function createHttpClient(options) {
335
454
  // repo is treated as a fork, so it can never look like a trusted branch.
336
455
  isCrossRepository: pull.head.repo?.id !== pull.base.repo.id,
337
456
  mergeable: pull.mergeable ?? void 0,
338
- mergeState: pull.mergeable_state
457
+ mergeState: pull.mergeable_state,
458
+ changedFileCount: pull.changed_files,
459
+ createdAt: pull.created_at,
460
+ requestedBotReviewers: (pull.requested_reviewers ?? []).filter((reviewer) => reviewer.type === "Bot").map((reviewer) => reviewer.login)
339
461
  };
340
462
  },
341
- async getRequiredStatusChecks(branch) {
342
- const rules = await paginate(
343
- `${repoPath}/rules/branches/${encodeURIComponent(branch)}`
344
- );
345
- const contexts = rules.filter((rule) => rule.type === "required_status_checks").flatMap((rule) => rule.parameters?.required_status_checks ?? []).map((check) => check.context);
346
- return [...new Set(contexts)];
347
- },
348
463
  async getBranchHeadSha(branch) {
349
464
  const result2 = await request(
350
465
  "GET",
@@ -352,25 +467,6 @@ function createHttpClient(options) {
352
467
  );
353
468
  return result2.commit.sha;
354
469
  },
355
- async listCheckRuns(sha) {
356
- const runs = [];
357
- for (let page = 1; ; page += 1) {
358
- const batch = await request(
359
- "GET",
360
- `${repoPath}/commits/${sha}/check-runs?per_page=${PAGE_SIZE}&page=${page}`
361
- );
362
- runs.push(...batch.check_runs);
363
- if (batch.check_runs.length < PAGE_SIZE) {
364
- break;
365
- }
366
- }
367
- return runs.map((run) => ({
368
- name: run.name,
369
- status: run.status,
370
- conclusion: run.conclusion,
371
- completedAt: run.completed_at
372
- }));
373
- },
374
470
  async getCommit(sha) {
375
471
  const commit = await request("GET", `${repoPath}/git/commits/${sha}`);
376
472
  return { treeSha: commit.tree.sha, parents: commit.parents.map((parent) => parent.sha) };
@@ -379,9 +475,13 @@ function createHttpClient(options) {
379
475
  const result2 = await request("GET", `${repoPath}/compare/${base}...${head}?per_page=1`);
380
476
  return { status: result2.status, mergeBaseSha: result2.merge_base_commit.sha };
381
477
  },
382
- async listCommitStatuses(sha) {
383
- const combined = await request("GET", `${repoPath}/commits/${sha}/status`);
384
- return combined.statuses.map((status) => ({ context: status.context, state: status.state }));
478
+ async listPullRequestFiles(pr) {
479
+ const files = await paginate(`${repoPath}/pulls/${pr}/files`);
480
+ return files.map((file) => ({
481
+ filename: file.filename,
482
+ status: file.status,
483
+ patch: file.patch
484
+ }));
385
485
  },
386
486
  async listCommits(pr) {
387
487
  const commits = await paginate(`${repoPath}/pulls/${pr}/commits`);
@@ -414,6 +514,10 @@ function createHttpClient(options) {
414
514
  async removeLabel(pr, name) {
415
515
  await request("DELETE", `${repoPath}/issues/${pr}/labels/${encodeURIComponent(name)}`);
416
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
+ },
417
521
  async listIssueComments(pr) {
418
522
  const comments = await paginate(`${repoPath}/issues/${pr}/comments`);
419
523
  return comments.map((comment) => comment.body ?? "");
@@ -427,6 +531,11 @@ function createHttpClient(options) {
427
531
  async mergePullRequest(pullRequestNodeId, expectedHeadOid) {
428
532
  await graphql(MERGE_PULL_REQUEST, { id: pullRequestNodeId, head: expectedHeadOid });
429
533
  },
534
+ async updateBranch(pr, expectedHeadSha) {
535
+ await request("PUT", `${repoPath}/pulls/${pr}/update-branch`, {
536
+ expected_head_sha: expectedHeadSha
537
+ });
538
+ },
430
539
  async disableAutoMerge(pullRequestNodeId) {
431
540
  await graphql(DISABLE_AUTO_MERGE, { id: pullRequestNodeId });
432
541
  }
@@ -518,8 +627,8 @@ function currentVerdict(facts, policy) {
518
627
  // src/state.ts
519
628
  var LIFECYCLE_STATES = [
520
629
  "approved",
630
+ "awaiting-bot-review",
521
631
  "awaiting-ci",
522
- "awaiting-copilot",
523
632
  "blocked-base-red",
524
633
  "changes-requested",
525
634
  "ci-failing",
@@ -529,7 +638,6 @@ var LIFECYCLE_STATES = [
529
638
  "fix-required",
530
639
  "review-requested"
531
640
  ];
532
- var COPILOT_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
533
641
  var WIP_PATTERN = /\[wip\]/i;
534
642
  function computeState(facts, policy) {
535
643
  if (facts.status !== "open") {
@@ -554,14 +662,10 @@ function computeState(facts, policy) {
554
662
  if (facts.ciStatus === "pending") {
555
663
  return "awaiting-ci";
556
664
  }
557
- if (policy.skipCopilotReview === true) {
558
- return "review-requested";
665
+ if (facts.pendingBotReviewers.length > 0 && policy.skipCopilotReview !== true) {
666
+ return "awaiting-bot-review";
559
667
  }
560
- const reviewedCommits = countingCommits(facts);
561
- const copilotReviewedHead = facts.reviews.some(
562
- (review) => review.author.login === COPILOT_REVIEWER_LOGIN && reviewedCommits.has(review.commitSha)
563
- );
564
- return copilotReviewedHead ? "review-requested" : "awaiting-copilot";
668
+ return "review-requested";
565
669
  }
566
670
 
567
671
  // src/plan.ts
@@ -574,6 +678,7 @@ var LIFECYCLE_LABELS = [
574
678
  "review requested"
575
679
  ];
576
680
  var AUTO_MERGE_LABEL = "auto-merge enabled";
681
+ var UPDATE_REQUIRED_LABEL = "update required";
577
682
  function lifecycleLabels(state) {
578
683
  switch (state) {
579
684
  case "approved":
@@ -588,18 +693,35 @@ function lifecycleLabels(state) {
588
693
  return ["fix required", "ci failing"];
589
694
  case "review-requested":
590
695
  return ["review requested"];
696
+ case "awaiting-bot-review":
591
697
  case "awaiting-ci":
592
- case "awaiting-copilot":
593
698
  case "blocked-base-red":
594
699
  case "closed":
595
700
  case "draft":
596
701
  return [];
597
702
  }
598
703
  }
704
+ function updateFor(updater, rebasePending) {
705
+ switch (updater) {
706
+ case "dependabot":
707
+ return rebasePending ? "none" : "dependabot-rebase";
708
+ case "github":
709
+ return "update-branch";
710
+ }
711
+ }
712
+ function planUpdate(facts, policy, state, autoMerge) {
713
+ if (policy.autoUpdate !== true || state !== "approved" || autoMerge === "merge") {
714
+ return "none";
715
+ }
716
+ if (!facts.labels.includes(UPDATE_REQUIRED_LABEL)) {
717
+ return "none";
718
+ }
719
+ return updateFor(facts.updater, facts.rebasePending);
720
+ }
599
721
  function planReconcile(facts, policy) {
600
722
  const state = computeState(facts, policy);
601
723
  if (state === "closed") {
602
- return { state, addLabels: [], removeLabels: [], autoMerge: "none" };
724
+ return { state, addLabels: [], removeLabels: [], autoMerge: "none", update: "none" };
603
725
  }
604
726
  const arming = policy.armAutoMerge === true;
605
727
  const shouldArm = arming && state === "approved";
@@ -624,20 +746,39 @@ function planReconcile(facts, policy) {
624
746
  state,
625
747
  addLabels: [...desired].filter((name) => !present.has(name)),
626
748
  removeLabels: owned.filter((name) => present.has(name) && !desired.has(name)),
627
- autoMerge
749
+ autoMerge,
750
+ update: planUpdate(facts, policy, state, autoMerge)
628
751
  };
629
752
  }
630
- function withoutArming(plan) {
631
- if (plan.autoMerge !== "arm" && plan.autoMerge !== "merge") {
632
- return plan;
633
- }
753
+ function withoutReleaseActions(plan) {
754
+ const arming = plan.autoMerge === "arm" || plan.autoMerge === "merge";
634
755
  return {
635
756
  ...plan,
636
- addLabels: plan.addLabels.filter((name) => name !== AUTO_MERGE_LABEL),
637
- autoMerge: "none"
757
+ addLabels: arming ? plan.addLabels.filter((name) => name !== AUTO_MERGE_LABEL) : plan.addLabels,
758
+ autoMerge: arming ? "none" : plan.autoMerge,
759
+ update: "none"
638
760
  };
639
761
  }
640
762
 
763
+ // src/github/dependabot-rebase.ts
764
+ var DEPENDABOT_REBASE_COMMAND = "@dependabot rebase";
765
+ var DEPENDABOT_REBASING_NOTICE = "Dependabot is rebasing this PR";
766
+ function rebaseRequestMarker(headSha) {
767
+ return `<!-- pr-lifecycle:dependabot-rebase head=${headSha} -->`;
768
+ }
769
+ function buildRebaseRequestBody(headSha) {
770
+ return `${DEPENDABOT_REBASE_COMMAND}
771
+
772
+ ${rebaseRequestMarker(headSha)}`;
773
+ }
774
+ function isRebasePending(prBody, comments, headSha) {
775
+ if (prBody.includes(DEPENDABOT_REBASING_NOTICE)) {
776
+ return true;
777
+ }
778
+ const marker = rebaseRequestMarker(headSha);
779
+ return comments.some((comment) => comment.includes(marker));
780
+ }
781
+
641
782
  // src/github/label-roster.ts
642
783
  var OWNED_LABEL_DEFINITIONS = {
643
784
  approved: {
@@ -734,6 +875,25 @@ async function executePlan(client, target, plan, release = client) {
734
875
  if (plan.autoMerge === "merge") {
735
876
  await release.mergePullRequest(target.nodeId, target.headSha);
736
877
  }
878
+ switch (plan.update) {
879
+ case "update-branch":
880
+ await updateBranch(release, target);
881
+ break;
882
+ case "dependabot-rebase":
883
+ await release.createIssueComment(target.pr, buildRebaseRequestBody(target.headSha));
884
+ break;
885
+ case "none":
886
+ break;
887
+ }
888
+ }
889
+ async function updateBranch(release, target) {
890
+ try {
891
+ await release.updateBranch(target.pr, target.headSha);
892
+ } catch (error) {
893
+ if (!isApiStatus(error, 422)) {
894
+ throw error;
895
+ }
896
+ }
737
897
  }
738
898
 
739
899
  // src/lineage/git.ts
@@ -858,7 +1018,7 @@ var MAX_CHAIN_STEPS = 20;
858
1018
  function reviewedAncestors(input) {
859
1019
  return new Set(
860
1020
  input.reviews.filter((review) => review.commitSha !== input.headSha).filter(
861
- (review) => review.login === COPILOT_REVIEWER_LOGIN || review.state === "APPROVED" || review.state === "CHANGES_REQUESTED" || review.body.includes("skill-meta")
1021
+ (review) => review.state === "APPROVED" || review.state === "CHANGES_REQUESTED" || review.body.includes("skill-meta")
862
1022
  ).map((review) => review.commitSha)
863
1023
  );
864
1024
  }
@@ -917,6 +1077,28 @@ async function gatherLineage(client, git, input) {
917
1077
  }
918
1078
  }
919
1079
 
1080
+ // src/github/bot-facts.ts
1081
+ async function listAllChangedFiles(client, pr, pull) {
1082
+ const files = await client.listPullRequestFiles(pr);
1083
+ return files.length === pull.changedFileCount ? files : void 0;
1084
+ }
1085
+ async function gatherBotEligibility(client, pr, pull) {
1086
+ const sameRepo = !pull.isCrossRepository;
1087
+ const isDependabot = pull.authorLogin === DEPENDABOT_LOGIN;
1088
+ const isReleaseBranch = !isDependabot && pull.headRef.startsWith(RELEASE_PLEASE_BRANCH_PREFIX);
1089
+ const [commits, changedFiles] = await Promise.all([
1090
+ sameRepo && isDependabot ? client.listCommits(pr) : [],
1091
+ sameRepo && isReleaseBranch ? listAllChangedFiles(client, pr, pull) : void 0
1092
+ ]);
1093
+ return classifyBotPr({
1094
+ authorLogin: pull.authorLogin ?? "ghost",
1095
+ headRef: pull.headRef,
1096
+ isCrossRepository: pull.isCrossRepository,
1097
+ commits,
1098
+ changedFiles
1099
+ });
1100
+ }
1101
+
920
1102
  // src/ci.ts
921
1103
  var DEFAULT_HOLD_CHECKS = ["pr-policy"];
922
1104
  var DEFAULT_IGNORED_CHECKS = ["merge-safety"];
@@ -1010,16 +1192,6 @@ async function gatherCiFacts(client, pull, policy) {
1010
1192
  }
1011
1193
 
1012
1194
  // src/github/gather.ts
1013
- async function gatherBotEligibility(client, pr, pull) {
1014
- const needsCommits = pull.authorLogin === DEPENDABOT_LOGIN && !pull.isCrossRepository;
1015
- return classifyBotPr({
1016
- authorLogin: pull.authorLogin ?? "ghost",
1017
- headRef: pull.headRef,
1018
- isCrossRepository: pull.isCrossRepository,
1019
- labels: pull.labels,
1020
- commits: needsCommits ? await client.listCommits(pr) : []
1021
- });
1022
- }
1023
1195
  function isRepoPermission(value) {
1024
1196
  return REPO_PERMISSIONS.some((permission) => permission === value);
1025
1197
  }
@@ -1084,19 +1256,35 @@ async function gatherLineageFor(client, pull, reviews, options) {
1084
1256
  return { cleanAncestors: [], stoppedBecause: `verification failed: ${message}` };
1085
1257
  }
1086
1258
  }
1259
+ async function gatherRebasePending(client, pr, pull, updater, policy) {
1260
+ if (updater !== "dependabot") {
1261
+ return false;
1262
+ }
1263
+ if (pull.body.includes(DEPENDABOT_REBASING_NOTICE)) {
1264
+ return true;
1265
+ }
1266
+ const couldUpdate = policy.autoUpdate === true && pull.state === "open" && pull.labels.includes(UPDATE_REQUIRED_LABEL);
1267
+ if (!couldUpdate) {
1268
+ return false;
1269
+ }
1270
+ return isRebasePending(pull.body, await client.listIssueComments(pr), pull.headSha);
1271
+ }
1087
1272
  var IMMEDIATE_MERGE_STATES = /* @__PURE__ */ new Set(["clean", "has_hooks", "unstable"]);
1088
1273
  async function gatherFacts(client, pr, policy = {}, options = {}) {
1089
1274
  const [pull, reviews] = await Promise.all([client.getPullRequest(pr), client.listReviews(pr)]);
1090
- const [permissions, botEligibility, ci, lineage] = await Promise.all([
1275
+ const updater = pull.authorLogin === DEPENDABOT_LOGIN ? "dependabot" : "github";
1276
+ const [permissions, botEligibility, ci, lineage, rebasePending] = await Promise.all([
1091
1277
  lookupPermissions(client, reviews),
1092
1278
  gatherBotEligibility(client, pr, pull),
1093
1279
  gatherCiFacts(client, pull, policy),
1094
- gatherLineageFor(client, pull, reviews, options)
1280
+ gatherLineageFor(client, pull, reviews, options),
1281
+ gatherRebasePending(client, pr, pull, updater, policy)
1095
1282
  ]);
1096
1283
  return {
1097
1284
  nodeId: pull.nodeId,
1098
1285
  botEligibility,
1099
1286
  lineage,
1287
+ createdAt: pull.createdAt,
1100
1288
  facts: {
1101
1289
  status: pull.merged ? "merged" : pull.state,
1102
1290
  isDraft: pull.draft,
@@ -1110,28 +1298,75 @@ async function gatherFacts(client, pr, policy = {}, options = {}) {
1110
1298
  ciStatus: ci.ciStatus,
1111
1299
  baseCiFailing: ci.baseCiFailing,
1112
1300
  cleanAncestors: lineage?.cleanAncestors ?? [],
1113
- reviews: reviews.map((review) => toReviewFact(review, permissions))
1301
+ updater,
1302
+ rebasePending,
1303
+ reviews: reviews.map((review) => toReviewFact(review, permissions)),
1304
+ pendingBotReviewers: pull.requestedBotReviewers
1114
1305
  }
1115
1306
  };
1116
1307
  }
1117
1308
 
1309
+ // src/github/settle.ts
1310
+ var DEFAULT_SETTLE_MS = 3e4;
1311
+ function realSleep(ms) {
1312
+ return new Promise((resolve) => {
1313
+ setTimeout(resolve, ms);
1314
+ });
1315
+ }
1316
+ async function settleDelay(client, pr, gathered, policy, settleMs, now) {
1317
+ const { facts } = gathered;
1318
+ if (policy.skipCopilotReview === true || computeState(facts, policy) !== "review-requested") {
1319
+ return 0;
1320
+ }
1321
+ if (facts.labels.includes("review requested")) {
1322
+ return 0;
1323
+ }
1324
+ const readyAt = await client.getLastReadyForReviewAt(pr);
1325
+ const reviewableSince = Math.max(
1326
+ Date.parse(gathered.createdAt),
1327
+ readyAt === void 0 ? 0 : Date.parse(readyAt)
1328
+ );
1329
+ const elapsed = now - reviewableSince;
1330
+ if (!Number.isFinite(elapsed)) {
1331
+ return settleMs;
1332
+ }
1333
+ return Math.min(settleMs, Math.max(0, settleMs - elapsed));
1334
+ }
1335
+ function resolveSettle(options = {}) {
1336
+ return {
1337
+ settleMs: options.settleMs ?? DEFAULT_SETTLE_MS,
1338
+ now: options.now ?? Date.now,
1339
+ sleep: options.sleep ?? realSleep
1340
+ };
1341
+ }
1342
+
1118
1343
  // src/github/reconcile.ts
1119
1344
  var REFUSE_RELEASE = {
1345
+ createIssueComment: () => Promise.reject(new Error("rebase requests need a release token")),
1120
1346
  enableAutoMerge: () => Promise.reject(new Error("arming needs a release token")),
1121
- mergePullRequest: () => Promise.reject(new Error("merging needs a release token"))
1347
+ mergePullRequest: () => Promise.reject(new Error("merging needs a release token")),
1348
+ updateBranch: () => Promise.reject(new Error("updating needs a release token"))
1122
1349
  };
1123
1350
  async function reconcilePullRequest(client, pr, policy, options = {}) {
1124
- const { facts, nodeId, botEligibility, lineage } = await gatherFacts(client, pr, policy, options);
1351
+ const settle = resolveSettle(options.settle);
1352
+ let gathered = await gatherFacts(client, pr, policy, options);
1353
+ const settledMs = await settleDelay(client, pr, gathered, policy, settle.settleMs, settle.now());
1354
+ if (settledMs > 0) {
1355
+ await settle.sleep(settledMs);
1356
+ gathered = await gatherFacts(client, pr, policy, options);
1357
+ }
1358
+ const { facts, nodeId, botEligibility, lineage } = gathered;
1125
1359
  const planned = planReconcile(facts, policy);
1126
1360
  const unavailable = options.release === "unavailable";
1127
- const plan = unavailable ? withoutArming(planned) : planned;
1361
+ const plan = unavailable ? withoutReleaseActions(planned) : planned;
1128
1362
  if (options.dryRun !== true) {
1129
1363
  const release = options.release === "unavailable" ? REFUSE_RELEASE : options.release ?? client;
1130
1364
  await executePlan(client, { pr, nodeId, headSha: facts.headSha }, plan, release);
1131
1365
  }
1132
1366
  const wanted = planned.autoMerge;
1133
1367
  const skippedAutoMerge = unavailable && (wanted === "arm" || wanted === "merge") ? wanted : void 0;
1134
- return { plan, botEligibility, lineage, skippedAutoMerge };
1368
+ const skippedUpdate = unavailable && planned.update !== "none" ? planned.update : void 0;
1369
+ return { plan, botEligibility, lineage, skippedAutoMerge, skippedUpdate, settledMs };
1135
1370
  }
1136
1371
 
1137
1372
  // src/github/token-advisory.ts
@@ -1168,8 +1403,10 @@ reconcile options:
1168
1403
  --repo <owner/repo> Repository (required)
1169
1404
  --pr <number> Pull request number (required)
1170
1405
  --arm-auto-merge Arm/disarm native auto-merge from the state
1406
+ --auto-update Update approved PRs merge-safety flags 'update required'
1407
+ (Dependabot PRs are asked to rebase, never updated)
1171
1408
  --trusted-authors <a,b> Only these logins (with write access) may cast verdicts
1172
- --skip-copilot-review Don't wait for a Copilot review
1409
+ --skip-copilot-review Don't wait for requested bot reviewers (deprecated)
1173
1410
  --hold-checks <a,b> Required checks whose pending is a hold (default: pr-policy)
1174
1411
  --ignore-checks <a,b> Required checks CI never counts (default: merge-safety)
1175
1412
  --dry-run Compute the plan without writing anything
@@ -1179,8 +1416,8 @@ reconcile options:
1179
1416
  Environment:
1180
1417
  GITHUB_TOKEN Token for reads and writes (required)
1181
1418
  PR_LIFECYCLE_TOKEN
1182
- Real-actor token for arming and merging; without it,
1183
- --arm-auto-merge keeps labels but skips arm/merge
1419
+ Real-actor token for arming, merging, and updating;
1420
+ without it, labels are kept but those are skipped
1184
1421
  GITHUB_API_URL API base URL (GitHub Enterprise Server)`;
1185
1422
  async function runCli(argv, io, deps) {
1186
1423
  const args = parseArgs(argv);
@@ -1216,8 +1453,8 @@ async function runCli(argv, io, deps) {
1216
1453
  const result2 = await reconcilePullRequest(client, args.pr, args.policy, {
1217
1454
  dryRun: args.dryRun,
1218
1455
  lineage: { git: deps.git ?? createGitRunner(), token },
1219
- // Only arming and merging use the release token (never reads). Without one
1220
- // they are skipped, not done with GITHUB_TOKEN; see docs/cli.md.
1456
+ // Only arming, merging, and updating use the release token (never reads).
1457
+ // Without one they are skipped, not done with GITHUB_TOKEN; see docs/cli.md.
1221
1458
  release: hasReleaseToken ? clientFor(releaseToken) : "unavailable"
1222
1459
  });
1223
1460
  if (result2.skippedAutoMerge !== void 0) {
@@ -1228,6 +1465,11 @@ async function runCli(argv, io, deps) {
1228
1465
  await adviseMissingToken(client, args.pr, result2.skippedAutoMerge, io);
1229
1466
  }
1230
1467
  }
1468
+ if (result2.skippedUpdate !== void 0) {
1469
+ io.stderr(
1470
+ `warning: ${args.owner}/${args.repo}#${args.pr} needs an update, but ${result2.skippedUpdate} was skipped: PR_LIFECYCLE_TOKEN is not set`
1471
+ );
1472
+ }
1231
1473
  if (args.json) {
1232
1474
  io.stdout(JSON.stringify(toReconcileJson(target, result2)));
1233
1475
  } else {
@@ -1250,9 +1492,10 @@ async function adviseMissingToken(client, pr, skipped, io) {
1250
1492
  }
1251
1493
 
1252
1494
  export {
1495
+ isVersionOnlyPatch,
1496
+ classifyReleaseDiff,
1253
1497
  DEPENDABOT_LOGIN,
1254
1498
  RELEASE_PLEASE_BRANCH_PREFIX,
1255
- RELEASE_PLEASE_PENDING_LABEL,
1256
1499
  DEPENDABOT_UPDATE_TYPES,
1257
1500
  parseDependabotUpdateType,
1258
1501
  classifyBotPr,
@@ -1266,18 +1509,24 @@ export {
1266
1509
  ACTOR_TYPES,
1267
1510
  REVIEW_STATES,
1268
1511
  PR_STATUSES,
1512
+ BRANCH_UPDATERS,
1269
1513
  createHttpClient,
1270
1514
  VERDICTS,
1271
1515
  parseVerdict,
1272
1516
  isTrustedAuthor,
1273
1517
  currentVerdict,
1274
1518
  LIFECYCLE_STATES,
1275
- COPILOT_REVIEWER_LOGIN,
1276
1519
  computeState,
1277
1520
  LIFECYCLE_LABELS,
1278
1521
  AUTO_MERGE_LABEL,
1522
+ UPDATE_REQUIRED_LABEL,
1279
1523
  planReconcile,
1280
- withoutArming,
1524
+ withoutReleaseActions,
1525
+ DEPENDABOT_REBASE_COMMAND,
1526
+ DEPENDABOT_REBASING_NOTICE,
1527
+ rebaseRequestMarker,
1528
+ buildRebaseRequestBody,
1529
+ isRebasePending,
1281
1530
  OWNED_LABEL_DEFINITIONS,
1282
1531
  executePlan,
1283
1532
  createGitRunner,
@@ -1286,6 +1535,7 @@ export {
1286
1535
  MAX_CHAIN_STEPS,
1287
1536
  gatherLineage,
1288
1537
  gatherFacts,
1538
+ DEFAULT_SETTLE_MS,
1289
1539
  reconcilePullRequest,
1290
1540
  TOKEN_ADVISORY_MARKER,
1291
1541
  RELEASE_TOKEN_DOCS_URL,
package/dist/index.d.ts CHANGED
@@ -1,14 +1,53 @@
1
+ /**
2
+ * Is a release-please PR's diff a pure release: only the files release-please
3
+ * writes, with `package.json` changed in its `version` line and nowhere else?
4
+ * The branch name alone can't say (any write user can push a `release-please--`
5
+ * branch with arbitrary code), and release-please commits as whoever owns its
6
+ * token, so its commit authors can't either. See docs/bot-eligibility.md.
7
+ *
8
+ * FAIL-SAFE: a file outside the release set, a status other than added or
9
+ * modified, a missing patch, or any doubt about a `package.json` line means the
10
+ * diff is not a release.
11
+ */
12
+ /** One changed file, as the PR files API reports it. */
13
+ interface ChangedFile {
14
+ /** Repository-relative path. */
15
+ filename: string;
16
+ /** `added`, `modified`, `removed`, `renamed`, `copied`, `changed`, or `unchanged`. */
17
+ status: string;
18
+ /** The unified-diff hunk text; undefined when GitHub omits it (e.g. a large diff). */
19
+ patch: string | undefined;
20
+ }
21
+ type ReleaseDiffVerdict = {
22
+ release: true;
23
+ } | {
24
+ release: false;
25
+ reason: string;
26
+ };
27
+ /**
28
+ * Whether a `package.json` patch changes exactly one line, its `version`: one
29
+ * removed and one added line, both a bare `"version"` member. Context lines,
30
+ * hunk headers, and the no-newline marker are ignored.
31
+ */
32
+ declare function isVersionOnlyPatch(patch: string): boolean;
33
+ /**
34
+ * The verdict over a PR's changed files. `undefined` means the list couldn't be
35
+ * read in full (truncated or not fetched), which is never a release.
36
+ */
37
+ declare function classifyReleaseDiff(files: readonly ChangedFile[] | undefined): ReleaseDiffVerdict;
38
+
1
39
  /**
2
40
  * Bot-PR eligibility: is this a bot PR trusted enough to count as `approved`
3
41
  * without a review (state priority 7 in docs/reconciler-design.md)? Ported from
4
- * @rmartz/bot-automerge's classifier, with one deliberate tightening: the head
5
- * branch must live in the base repository. See docs/bot-eligibility.md.
42
+ * @rmartz/bot-automerge's classifier, with deliberate tightenings: the head
43
+ * branch must live in the base repository, and a release-please PR is recognized
44
+ * by its branch and a release-only diff, never by a label. See
45
+ * docs/bot-eligibility.md.
6
46
  *
7
47
  * FAIL-SAFE: every uncertain or unrecognized case is not eligible.
8
48
  */
9
49
  declare const DEPENDABOT_LOGIN = "dependabot[bot]";
10
50
  declare const RELEASE_PLEASE_BRANCH_PREFIX = "release-please--";
11
- declare const RELEASE_PLEASE_PENDING_LABEL = "autorelease: pending";
12
51
  /** Dependabot's semver update types, lowest to highest risk. */
13
52
  declare const DEPENDABOT_UPDATE_TYPES: readonly ["version-update:semver-patch", "version-update:semver-minor", "version-update:semver-major"];
14
53
  type DependabotUpdateType = (typeof DEPENDABOT_UPDATE_TYPES)[number];
@@ -23,9 +62,13 @@ interface BotPrFacts {
23
62
  headRef: string;
24
63
  /** True when the head branch lives in a fork, not the base repository. */
25
64
  isCrossRepository: boolean;
26
- labels: readonly string[];
27
65
  /** The PR's commits; only consulted for Dependabot PRs. */
28
66
  commits: readonly BotPrCommit[];
67
+ /**
68
+ * The PR's changed files; only consulted for release-please PRs. Undefined when
69
+ * not fetched or not read in full, which is never eligible.
70
+ */
71
+ changedFiles: readonly ChangedFile[] | undefined;
29
72
  }
30
73
  interface BotEligibility {
31
74
  eligible: boolean;
@@ -73,6 +116,12 @@ type ActorType = (typeof ACTOR_TYPES)[number];
73
116
  declare const REVIEW_STATES: readonly ["APPROVED", "CHANGES_REQUESTED", "COMMENTED", "DISMISSED", "PENDING"];
74
117
  type ReviewState = (typeof REVIEW_STATES)[number];
75
118
  declare const PR_STATUSES: readonly ["closed", "merged", "open"];
119
+ /**
120
+ * Who brings a PR's branch up to date with its base. Dependabot owns its branches
121
+ * and rebases them itself when asked; anything else uses GitHub's update-branch.
122
+ */
123
+ declare const BRANCH_UPDATERS: readonly ["dependabot", "github"];
124
+ type BranchUpdater = (typeof BRANCH_UPDATERS)[number];
76
125
  type PullRequestStatus = (typeof PR_STATUSES)[number];
77
126
  interface ReviewAuthor {
78
127
  login: string;
@@ -120,7 +169,21 @@ interface PullRequestFacts {
120
169
  * docs/reconciler-design.md §Approval carry-over). Verified at the edge.
121
170
  */
122
171
  cleanAncestors: readonly string[];
172
+ /** Who updates the branch (see `BranchUpdater`). */
173
+ updater: BranchUpdater;
174
+ /**
175
+ * For a Dependabot PR: a rebase is running (the PR body says so) or was already
176
+ * requested for this head, so asking again would only add noise. A request is
177
+ * made at most once per head, whether Dependabot then rebases or replies with an
178
+ * error. Always `false` for other PRs.
179
+ */
180
+ rebasePending: boolean;
123
181
  reviews: readonly ReviewFact[];
182
+ /**
183
+ * Logins of the bots (e.g. Copilot) whose review is requested and not yet
184
+ * submitted. Users and teams are left out: the lifecycle never waits on people.
185
+ */
186
+ pendingBotReviewers: readonly string[];
124
187
  }
125
188
  interface ReconcilePolicy {
126
189
  /**
@@ -132,8 +195,16 @@ interface ReconcilePolicy {
132
195
  /** Arm/disarm native auto-merge from the lifecycle state. Off by default. */
133
196
  armAutoMerge?: boolean;
134
197
  /**
135
- * Don't wait for a Copilot review before `review-requested`, for repos without
136
- * Copilot code review (where the wait would never end). Off by default.
198
+ * Bring an approved PR up to date when merge-safety labels it `update required`:
199
+ * `update-branch` for most PRs, a `@dependabot rebase` request for Dependabot's.
200
+ * Off by default.
201
+ */
202
+ autoUpdate?: boolean;
203
+ /**
204
+ * Don't wait for requested bot reviewers (e.g. Copilot) before
205
+ * `review-requested`. Rarely needed: a bot that won't review is never requested.
206
+ * Off by default. Deprecated: the wait now ends on its own; kept so existing
207
+ * callers keep working.
137
208
  */
138
209
  skipCopilotReview?: boolean;
139
210
  /**
@@ -159,6 +230,8 @@ interface PullRequestData {
159
230
  merged: boolean;
160
231
  draft: boolean;
161
232
  title: string;
233
+ /** The PR description (empty when none); Dependabot reports a running rebase here. */
234
+ body: string;
162
235
  headSha: string;
163
236
  labels: string[];
164
237
  autoMergeEnabled: boolean;
@@ -175,6 +248,12 @@ interface PullRequestData {
175
248
  mergeable: boolean | undefined;
176
249
  /** GitHub's merge state (`clean`, `blocked`, `behind`, …); undefined while computing. */
177
250
  mergeState: string | undefined;
251
+ /** How many files the PR changes (`changed_files`), to tell a full file list from a truncated one. */
252
+ changedFileCount: number;
253
+ /** ISO-8601 time the PR was opened. */
254
+ createdAt: string;
255
+ /** Logins of requested reviewers that are bots (users and teams left out). */
256
+ requestedBotReviewers: string[];
178
257
  }
179
258
  /** A check-run on a commit (the latest run per name). */
180
259
  interface CheckRunData {
@@ -231,6 +310,8 @@ interface GitHubClient {
231
310
  getPullRequest(pr: number): Promise<PullRequestData>;
232
311
  listReviews(pr: number): Promise<ReviewData[]>;
233
312
  listCommits(pr: number): Promise<CommitData[]>;
313
+ /** The PR's changed files. GitHub lists at most 3,000, so compare with `changedFileCount`. */
314
+ listPullRequestFiles(pr: number): Promise<ChangedFile[]>;
234
315
  /** The contexts the branch's rulesets require (empty when none are configured). */
235
316
  getRequiredStatusChecks(branch: string): Promise<string[]>;
236
317
  getBranchHeadSha(branch: string): Promise<string>;
@@ -246,6 +327,8 @@ interface GitHubClient {
246
327
  addLabels(pr: number, names: readonly string[]): Promise<void>;
247
328
  /** Throws a GitHubApiError with status 404 when the label is not on the PR. */
248
329
  removeLabel(pr: number, name: string): Promise<void>;
330
+ /** When the PR was last marked ready for review (ISO-8601), if ever. */
331
+ getLastReadyForReviewAt(pr: number): Promise<string | undefined>;
249
332
  /** Bodies of the PR's conversation comments, oldest first. */
250
333
  listIssueComments(pr: number): Promise<string[]>;
251
334
  createIssueComment(pr: number, body: string): Promise<void>;
@@ -257,13 +340,19 @@ interface GitHubClient {
257
340
  /** Squash-merge now, bound to `expectedHeadOid` like `enableAutoMerge`. */
258
341
  mergePullRequest(pullRequestNodeId: string, expectedHeadOid: string): Promise<void>;
259
342
  disableAutoMerge(pullRequestNodeId: string): Promise<void>;
343
+ /**
344
+ * Merge the base into the PR branch, bound to `expectedHeadSha`. Throws a
345
+ * GitHubApiError with status 422 when the head moved or there is nothing to merge.
346
+ */
347
+ updateBranch(pr: number, expectedHeadSha: string): Promise<void>;
260
348
  }
261
349
  /**
262
- * The writes that must come from a real actor, so the merge they cause re-triggers
263
- * `on: push` workflows (a `GITHUB_TOKEN` merge triggers nothing). Kept to these two
264
- * calls so the release token is never used for anything else, reads included.
350
+ * The writes that must come from a real actor: a merge or branch update made with
351
+ * `GITHUB_TOKEN` triggers no workflows (so no release, and no CI on the new head),
352
+ * and Dependabot only takes commands from people. Kept to these calls so the
353
+ * release token is never used for anything else, reads included.
265
354
  */
266
- type ReleaseActions = Pick<GitHubClient, 'enableAutoMerge' | 'mergePullRequest'>;
355
+ type ReleaseActions = Pick<GitHubClient, 'createIssueComment' | 'enableAutoMerge' | 'mergePullRequest' | 'updateBranch'>;
267
356
  declare class GitHubApiError extends Error {
268
357
  readonly status: number;
269
358
  constructor(status: number, message: string);
@@ -271,10 +360,9 @@ declare class GitHubApiError extends Error {
271
360
  declare function isApiStatus(error: unknown, status: number): boolean;
272
361
 
273
362
  /**
274
- * The real GitHubClient: REST for reads and labels, GraphQL for the auto-merge
275
- * mutations (REST has no endpoint for them). Deliberately thin — it maps requests
276
- * and responses and throws GitHubApiError on failure; every decision lives in
277
- * gather.ts / execute.ts. `fetch` is injectable so tests use a fake transport.
363
+ * The HTTP transport under the real GitHubClient: authenticated REST requests,
364
+ * pagination, and GraphQL, each throwing GitHubApiError on failure. Split out of
365
+ * http-client.ts so the client modules only map requests and responses.
278
366
  */
279
367
  interface HttpClientOptions {
280
368
  token: string;
@@ -284,6 +372,7 @@ interface HttpClientOptions {
284
372
  apiUrl?: string;
285
373
  fetch?: typeof fetch;
286
374
  }
375
+
287
376
  declare function createHttpClient(options: HttpClientOptions): GitHubClient;
288
377
 
289
378
  /**
@@ -324,7 +413,7 @@ interface CliDeps {
324
413
  /** Runs git for approval carry-over; tests inject one. Defaults to the real binary. */
325
414
  git?: GitRunner;
326
415
  }
327
- 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 --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 and merging; without it,\n --arm-auto-merge keeps labels but skips arm/merge\n GITHUB_API_URL API base URL (GitHub Enterprise Server)";
416
+ 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)";
328
417
  declare function runCli(argv: readonly string[], io: CliIo, deps: CliDeps): Promise<number>;
329
418
 
330
419
  /**
@@ -355,9 +444,8 @@ declare function parseArgs(argv: readonly string[]): ParsedArgs;
355
444
  * Lifecycle state, computed from facts in a fixed priority order. See
356
445
  * docs/reconciler-design.md §State.
357
446
  */
358
- 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"];
447
+ 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"];
359
448
  type LifecycleState = (typeof LIFECYCLE_STATES)[number];
360
- declare const COPILOT_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
361
449
  declare function computeState(facts: PullRequestFacts, policy: ReconcilePolicy): LifecycleState;
362
450
 
363
451
  /**
@@ -369,24 +457,33 @@ declare function computeState(facts: PullRequestFacts, policy: ReconcilePolicy):
369
457
  declare const LIFECYCLE_LABELS: readonly ["approved", "changes requested", "escalation needed", "fix required", "ci failing", "review requested"];
370
458
  type LifecycleLabel = (typeof LIFECYCLE_LABELS)[number];
371
459
  declare const AUTO_MERGE_LABEL = "auto-merge enabled";
460
+ /** merge-safety's label for a PR whose base moved in a way that matters. Read, never written. */
461
+ declare const UPDATE_REQUIRED_LABEL = "update required";
372
462
  /**
373
463
  * `merge` is arming's twin for a PR GitHub would already merge: auto-merge can't
374
464
  * be armed then, so it is merged directly (see `immediatelyMergeable`).
375
465
  */
376
466
  type AutoMergeAction = 'arm' | 'disarm' | 'merge' | 'none';
467
+ /**
468
+ * How an approved PR flagged `update required` is brought up to date. A Dependabot
469
+ * PR is never updated directly — a foreign commit on its branch permanently breaks
470
+ * its own rebasing — so it is only ever asked to rebase itself.
471
+ */
472
+ type UpdateAction = 'dependabot-rebase' | 'none' | 'update-branch';
377
473
  interface ReconcilePlan {
378
474
  state: LifecycleState;
379
475
  addLabels: string[];
380
476
  removeLabels: string[];
381
477
  autoMerge: AutoMergeAction;
478
+ update: UpdateAction;
382
479
  }
383
480
  declare function planReconcile(facts: PullRequestFacts, policy: ReconcilePolicy): ReconcilePlan;
384
481
  /**
385
- * The plan with arming and merging taken out, for when no real-actor token is
386
- * available to perform them (docs/cli.md §Release token). Disarming stays: it is
387
- * safety, and any token may do it. Pure, like the planner.
482
+ * The plan with arming, merging, and updating taken out, for when no real-actor
483
+ * token is available to perform them (docs/cli.md §Release token). Disarming
484
+ * stays: it is safety, and any token may do it. Pure, like the planner.
388
485
  */
389
- declare function withoutArming(plan: ReconcilePlan): ReconcilePlan;
486
+ declare function withoutReleaseActions(plan: ReconcilePlan): ReconcilePlan;
390
487
 
391
488
  interface MergeStep {
392
489
  /** The merge base of the two parents. */
@@ -451,6 +548,8 @@ interface GatheredPullRequest {
451
548
  botEligibility: BotEligibility;
452
549
  /** How far approval carry-over verified, for reporting (absent when off). */
453
550
  lineage: Lineage | undefined;
551
+ /** When the PR was opened (ISO-8601), for the settle wait (settle.ts). */
552
+ createdAt: string;
454
553
  }
455
554
  interface GatherOptions {
456
555
  /**
@@ -465,13 +564,34 @@ interface GatherOptions {
465
564
  }
466
565
  declare function gatherFacts(client: GitHubClient, pr: number, policy?: ReconcilePolicy, options?: GatherOptions): Promise<GatheredPullRequest>;
467
566
 
567
+ /**
568
+ * The settle wait: a PR that just became reviewable isn't moved to
569
+ * `review-requested` until bot reviewers have had time to be requested. Copilot's
570
+ * auto-review request lands a few seconds after the PR opens (observed: up to 5s),
571
+ * so a run that reads the PR sooner would see no request and skip the wait. The
572
+ * wait happens inside the run, never as "stay put and wait for the next event":
573
+ * when no bot is coming, no further event may ever fire. See
574
+ * docs/reconciler-design.md §Waiting for bot reviewers.
575
+ */
576
+ /** Long enough to cover the observed request delay several times over. */
577
+ declare const DEFAULT_SETTLE_MS = 30000;
578
+ interface SettleOptions {
579
+ /** How long after a PR becomes reviewable to wait for bot requests. */
580
+ settleMs?: number;
581
+ /** Clock and timer, injectable for tests. */
582
+ now?: () => number;
583
+ sleep?: (ms: number) => Promise<void>;
584
+ }
585
+
468
586
  interface ReconcileOptions extends GatherOptions {
587
+ /** The settle wait for a PR that just became reviewable (settle.ts). */
588
+ settle?: SettleOptions;
469
589
  /** Compute and return the plan without writing anything. */
470
590
  dryRun?: boolean;
471
591
  /**
472
- * Who arms and merges. Defaults to `client`, right when its token is a real
473
- * actor. `'unavailable'` (no real-actor token configured) skips arming and
474
- * merging rather than doing them with a token whose merge triggers no workflows.
592
+ * Who arms, merges, and updates. Defaults to `client`, right when its token is a
593
+ * real actor. `'unavailable'` (no real-actor token configured) skips them rather
594
+ * than doing them with a token whose pushes and merges trigger no workflows.
475
595
  */
476
596
  release?: ReleaseActions | 'unavailable';
477
597
  }
@@ -483,10 +603,14 @@ interface ReconcileResult {
483
603
  lineage: Lineage | undefined;
484
604
  /** The arm or merge the plan wanted but skipped because `release` was unavailable. */
485
605
  skippedAutoMerge: 'arm' | 'merge' | undefined;
606
+ /** The branch update the plan wanted but skipped because `release` was unavailable. */
607
+ skippedUpdate: Exclude<UpdateAction, 'none'> | undefined;
608
+ /** How long the run waited for bot review requests before gathering again (ms). */
609
+ settledMs: number;
486
610
  }
487
611
  /**
488
- * One full reconcile pass for a PR: gather its facts, plan, and (unless dry-run)
489
- * execute. Returns the plan, the bot-eligibility verdict, and the carry-over
612
+ * One full reconcile pass for a PR: gather its facts (waiting to settle first if it
613
+ * just became reviewable), plan, and (unless dry-run) execute. Returns the plan, the bot-eligibility verdict, and the carry-over
490
614
  * result so callers can report what changed and why.
491
615
  */
492
616
  declare function reconcilePullRequest(client: GitHubClient, pr: number, policy: ReconcilePolicy, options?: ReconcileOptions): Promise<ReconcileResult>;
@@ -530,16 +654,45 @@ interface ReconcileJson {
530
654
  action: 'arm' | 'merge';
531
655
  reason: 'token-missing';
532
656
  } | null;
657
+ /** The branch update performed: `none`, `update-branch`, or `dependabot-rebase`. */
658
+ update: string;
659
+ /** A branch update the plan wanted but skipped; `null` when nothing was skipped. */
660
+ updateSkipped: {
661
+ action: 'dependabot-rebase' | 'update-branch';
662
+ reason: 'token-missing';
663
+ } | null;
533
664
  }
534
665
  declare function toReconcileJson(target: ReconcileTarget, result: ReconcileResult): ReconcileJson;
535
666
  /** One human-readable line, e.g. `rmartz/x#7 → approved: +approved, auto-merge arm`. */
536
667
  declare function formatSummary(target: ReconcileTarget, result: ReconcileResult): string;
537
668
 
669
+ /**
670
+ * Asking Dependabot to rebase its own PR — the only way a Dependabot PR is ever
671
+ * brought up to date. The request is made at most once per head: after asking,
672
+ * the reconciler waits for Dependabot to either rebase (a new head, which may be
673
+ * asked again) or reply with an error (the head is unchanged, so it stays quiet
674
+ * and leaves the PR to a person). See docs/github-edge-layer.md §Auto-update.
675
+ */
676
+ declare const DEPENDABOT_REBASE_COMMAND = "@dependabot rebase";
677
+ /** Dependabot puts this in the PR body while a rebase runs, and removes it after. */
678
+ declare const DEPENDABOT_REBASING_NOTICE = "Dependabot is rebasing this PR";
679
+ /** Hidden marker recording which head a request was made for. */
680
+ declare function rebaseRequestMarker(headSha: string): string;
681
+ /** The command on its own first line, as Dependabot parses it, then the marker. */
682
+ declare function buildRebaseRequestBody(headSha: string): string;
683
+ /**
684
+ * Whether asking again would only add noise: a rebase is already running, or one
685
+ * was already requested for this head. A forged marker can only suppress a
686
+ * request, never cause one, so the comment author doesn't matter.
687
+ */
688
+ declare function isRebasePending(prBody: string, comments: readonly string[], headSha: string): boolean;
689
+
538
690
  /**
539
691
  * Executes a reconcile plan against GitHub. Writes happen in a fail-safe order —
540
- * disarm, remove labels, add labels, arm — so a run that dies midway never leaves
541
- * auto-merge armed on a PR that is not approved. Arming and merging go through
542
- * `release` (a real-actor token) and are bound to the planned head. See
692
+ * disarm, remove labels, add labels, arm, update — so a run that dies midway never
693
+ * leaves auto-merge armed on a PR that is not approved, and an update (which moves
694
+ * the head) comes last. Arming, merging, and updating go through `release` (a
695
+ * real-actor token) and are bound to the planned head. See
543
696
  * docs/github-edge-layer.md.
544
697
  */
545
698
  interface PlanTarget {
@@ -591,6 +744,7 @@ interface ParsedVerdict {
591
744
  declare function parseVerdict(review: ReviewFact): ParsedVerdict | undefined;
592
745
  /** Whether an author may cast a counting verdict under the policy. */
593
746
  declare function isTrustedAuthor(author: ReviewAuthor, policy: ReconcilePolicy): boolean;
747
+ /** The latest verdict that counts for the PR's current head, if any. */
594
748
  declare function currentVerdict(facts: PullRequestFacts, policy: ReconcilePolicy): Verdict | undefined;
595
749
 
596
- export { ACTOR_TYPES, AUTO_MERGE_LABEL, type ActorType, type AutoMergeAction, type BotEligibility, type BotPrCommit, type BotPrFacts, type BotPrType, COPILOT_REVIEWER_LOGIN, type CliDeps, type CliIo, type CollaboratorPermission, type CommitData, DEPENDABOT_LOGIN, 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_PLEASE_PENDING_LABEL, 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 RepoPermission, type RepoSource, type ReviewAuthor, type ReviewData, type ReviewFact, type ReviewState, SCHEMA_VERSION, TOKEN_ADVISORY_MARKER, USAGE, VERDICTS, type Verdict, buildTokenAdvisoryBody, classifyBotPr, computeState, createGitRunner, createHttpClient, createMergeVerifier, currentVerdict, executePlan, formatSummary, gatherFacts, gatherLineage, isApiStatus, isTrustedAuthor, parseArgs, parseDependabotUpdateType, parseGitVersion, parseVerdict, planReconcile, postTokenAdvisory, reconcilePullRequest, runCli, toReconcileJson, withoutArming };
750
+ 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_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, isRebasePending, isTrustedAuthor, isVersionOnlyPatch, parseArgs, parseDependabotUpdateType, parseGitVersion, parseVerdict, planReconcile, postTokenAdvisory, rebaseRequestMarker, reconcilePullRequest, runCli, toReconcileJson, withoutReleaseActions };
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  ACTOR_TYPES,
3
3
  AUTO_MERGE_LABEL,
4
- COPILOT_REVIEWER_LOGIN,
4
+ BRANCH_UPDATERS,
5
+ DEFAULT_SETTLE_MS,
5
6
  DEPENDABOT_LOGIN,
7
+ DEPENDABOT_REBASE_COMMAND,
8
+ DEPENDABOT_REBASING_NOTICE,
6
9
  DEPENDABOT_UPDATE_TYPES,
7
10
  GitHubApiError,
8
11
  LIFECYCLE_LABELS,
@@ -11,16 +14,18 @@ import {
11
14
  OWNED_LABEL_DEFINITIONS,
12
15
  PR_STATUSES,
13
16
  RELEASE_PLEASE_BRANCH_PREFIX,
14
- RELEASE_PLEASE_PENDING_LABEL,
15
17
  RELEASE_TOKEN_DOCS_URL,
16
18
  REPO_PERMISSIONS,
17
19
  REVIEW_STATES,
18
20
  SCHEMA_VERSION,
19
21
  TOKEN_ADVISORY_MARKER,
22
+ UPDATE_REQUIRED_LABEL,
20
23
  USAGE,
21
24
  VERDICTS,
25
+ buildRebaseRequestBody,
22
26
  buildTokenAdvisoryBody,
23
27
  classifyBotPr,
28
+ classifyReleaseDiff,
24
29
  computeState,
25
30
  createGitRunner,
26
31
  createHttpClient,
@@ -31,23 +36,29 @@ import {
31
36
  gatherFacts,
32
37
  gatherLineage,
33
38
  isApiStatus,
39
+ isRebasePending,
34
40
  isTrustedAuthor,
41
+ isVersionOnlyPatch,
35
42
  parseArgs,
36
43
  parseDependabotUpdateType,
37
44
  parseGitVersion,
38
45
  parseVerdict,
39
46
  planReconcile,
40
47
  postTokenAdvisory,
48
+ rebaseRequestMarker,
41
49
  reconcilePullRequest,
42
50
  runCli,
43
51
  toReconcileJson,
44
- withoutArming
45
- } from "./chunk-RQEBRPZU.js";
52
+ withoutReleaseActions
53
+ } from "./chunk-ENU2AAWK.js";
46
54
  export {
47
55
  ACTOR_TYPES,
48
56
  AUTO_MERGE_LABEL,
49
- COPILOT_REVIEWER_LOGIN,
57
+ BRANCH_UPDATERS,
58
+ DEFAULT_SETTLE_MS,
50
59
  DEPENDABOT_LOGIN,
60
+ DEPENDABOT_REBASE_COMMAND,
61
+ DEPENDABOT_REBASING_NOTICE,
51
62
  DEPENDABOT_UPDATE_TYPES,
52
63
  GitHubApiError,
53
64
  LIFECYCLE_LABELS,
@@ -56,16 +67,18 @@ export {
56
67
  OWNED_LABEL_DEFINITIONS,
57
68
  PR_STATUSES,
58
69
  RELEASE_PLEASE_BRANCH_PREFIX,
59
- RELEASE_PLEASE_PENDING_LABEL,
60
70
  RELEASE_TOKEN_DOCS_URL,
61
71
  REPO_PERMISSIONS,
62
72
  REVIEW_STATES,
63
73
  SCHEMA_VERSION,
64
74
  TOKEN_ADVISORY_MARKER,
75
+ UPDATE_REQUIRED_LABEL,
65
76
  USAGE,
66
77
  VERDICTS,
78
+ buildRebaseRequestBody,
67
79
  buildTokenAdvisoryBody,
68
80
  classifyBotPr,
81
+ classifyReleaseDiff,
69
82
  computeState,
70
83
  createGitRunner,
71
84
  createHttpClient,
@@ -76,15 +89,18 @@ export {
76
89
  gatherFacts,
77
90
  gatherLineage,
78
91
  isApiStatus,
92
+ isRebasePending,
79
93
  isTrustedAuthor,
94
+ isVersionOnlyPatch,
80
95
  parseArgs,
81
96
  parseDependabotUpdateType,
82
97
  parseGitVersion,
83
98
  parseVerdict,
84
99
  planReconcile,
85
100
  postTokenAdvisory,
101
+ rebaseRequestMarker,
86
102
  reconcilePullRequest,
87
103
  runCli,
88
104
  toReconcileJson,
89
- withoutArming
105
+ withoutReleaseActions
90
106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmartz/pr-lifecycle",
3
- "version": "5.0.0",
3
+ "version": "8.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",