@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 +4 -3
- package/dist/bin/pr-lifecycle.js +1 -1
- package/dist/{chunk-RQEBRPZU.js → chunk-ENU2AAWK.js} +360 -110
- package/dist/index.d.ts +184 -30
- package/dist/index.js +23 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
An **event-driven pull-request lifecycle reconciler** for GitHub Actions. On every
|
|
4
4
|
relevant GitHub event it recomputes a PR's lifecycle state from the PR's current
|
|
5
|
-
facts — draft status,
|
|
5
|
+
facts — draft status, pending bot review requests, the latest verdict from a
|
|
6
6
|
trusted author and the head SHA it reviewed — converges the PR's labels to match,
|
|
7
7
|
and **arms GitHub-native auto-merge when the PR reaches `approved`**. The
|
|
8
8
|
consumer's branch ruleset (required checks) then decides when the merge lands.
|
|
@@ -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
|
|
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
|
|
package/dist/bin/pr-lifecycle.js
CHANGED
|
@@ -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)
|
|
76
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
383
|
-
const
|
|
384
|
-
return
|
|
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
|
|
558
|
-
return "review
|
|
665
|
+
if (facts.pendingBotReviewers.length > 0 && policy.skipCopilotReview !== true) {
|
|
666
|
+
return "awaiting-bot-review";
|
|
559
667
|
}
|
|
560
|
-
|
|
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
|
|
631
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
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
|
|
1183
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
5
|
-
* branch must live in the base repository
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
|
263
|
-
* `
|
|
264
|
-
*
|
|
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
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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
|
|
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-
|
|
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
|
|
386
|
-
* available to perform them (docs/cli.md §Release token). Disarming
|
|
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
|
|
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
|
|
473
|
-
* actor. `'unavailable'` (no real-actor token configured) skips
|
|
474
|
-
*
|
|
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
|
|
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
|
|
541
|
-
* auto-merge armed on a PR that is not approved
|
|
542
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
} from "./chunk-
|
|
52
|
+
withoutReleaseActions
|
|
53
|
+
} from "./chunk-ENU2AAWK.js";
|
|
46
54
|
export {
|
|
47
55
|
ACTOR_TYPES,
|
|
48
56
|
AUTO_MERGE_LABEL,
|
|
49
|
-
|
|
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
|
-
|
|
105
|
+
withoutReleaseActions
|
|
90
106
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmartz/pr-lifecycle",
|
|
3
|
-
"version": "
|
|
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",
|