@rmartz/pr-lifecycle 5.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/bin/pr-lifecycle.d.ts +1 -0
- package/dist/bin/pr-lifecycle.js +14 -0
- package/dist/chunk-RQEBRPZU.js +1296 -0
- package/dist/index.d.ts +596 -0
- package/dist/index.js +90 -0
- package/package.json +84 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot-PR eligibility: is this a bot PR trusted enough to count as `approved`
|
|
3
|
+
* 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.
|
|
6
|
+
*
|
|
7
|
+
* FAIL-SAFE: every uncertain or unrecognized case is not eligible.
|
|
8
|
+
*/
|
|
9
|
+
declare const DEPENDABOT_LOGIN = "dependabot[bot]";
|
|
10
|
+
declare const RELEASE_PLEASE_BRANCH_PREFIX = "release-please--";
|
|
11
|
+
declare const RELEASE_PLEASE_PENDING_LABEL = "autorelease: pending";
|
|
12
|
+
/** Dependabot's semver update types, lowest to highest risk. */
|
|
13
|
+
declare const DEPENDABOT_UPDATE_TYPES: readonly ["version-update:semver-patch", "version-update:semver-minor", "version-update:semver-major"];
|
|
14
|
+
type DependabotUpdateType = (typeof DEPENDABOT_UPDATE_TYPES)[number];
|
|
15
|
+
type BotPrType = 'dependabot' | 'release-please';
|
|
16
|
+
interface BotPrCommit {
|
|
17
|
+
/** The commit author's GitHub login; undefined when not linked to an account. */
|
|
18
|
+
authorLogin: string | undefined;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
interface BotPrFacts {
|
|
22
|
+
authorLogin: string;
|
|
23
|
+
headRef: string;
|
|
24
|
+
/** True when the head branch lives in a fork, not the base repository. */
|
|
25
|
+
isCrossRepository: boolean;
|
|
26
|
+
labels: readonly string[];
|
|
27
|
+
/** The PR's commits; only consulted for Dependabot PRs. */
|
|
28
|
+
commits: readonly BotPrCommit[];
|
|
29
|
+
}
|
|
30
|
+
interface BotEligibility {
|
|
31
|
+
eligible: boolean;
|
|
32
|
+
/** One-line justification, positive or negative, for dry-run output and logs. */
|
|
33
|
+
reason: string;
|
|
34
|
+
prType: BotPrType | undefined;
|
|
35
|
+
updateType: DependabotUpdateType | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The highest update type in a Dependabot commit's `updated-dependencies`
|
|
39
|
+
* metadata block, or undefined when the block is missing or any entry's type is
|
|
40
|
+
* unrecognized (fail-safe). A grouped update lists several dependencies; the
|
|
41
|
+
* riskiest one decides. Scans line by line — commit messages are untrusted
|
|
42
|
+
* input, so no backtracking regex runs over the whole message.
|
|
43
|
+
*/
|
|
44
|
+
declare function parseDependabotUpdateType(message: string): DependabotUpdateType | undefined;
|
|
45
|
+
declare function classifyBotPr(facts: BotPrFacts): BotEligibility;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The CI gate: is the PR's required CI passing, failing, or still running?
|
|
49
|
+
* Pure — the edge layer gathers the raw check results; this decides. See
|
|
50
|
+
* docs/reconciler-design.md §CI gate.
|
|
51
|
+
*
|
|
52
|
+
* "CI" is the base branch's required checks, with two kinds of exception:
|
|
53
|
+
* - **hold checks** (e.g. `pr-policy`) report *pending* while they wait on a human
|
|
54
|
+
* act, which is a hold, not a running build — so their pending is ignored, but
|
|
55
|
+
* their failure (a fixable problem) still counts;
|
|
56
|
+
* - **ignored checks** (e.g. `merge-safety`, whose failures mean "update needed"
|
|
57
|
+
* or "base is red", handled by other states) never count at all.
|
|
58
|
+
*/
|
|
59
|
+
declare const CI_STATUSES: readonly ["failing", "passing", "pending"];
|
|
60
|
+
type CiStatus = (typeof CI_STATUSES)[number];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The facts the reconciler core computes a PR's lifecycle state from. The edge
|
|
64
|
+
* layer gathers these from GitHub; the core never performs I/O. See
|
|
65
|
+
* docs/reconciler-design.md §Facts.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
declare const REPO_PERMISSIONS: readonly ["admin", "maintain", "none", "read", "triage", "write"];
|
|
69
|
+
type RepoPermission = (typeof REPO_PERMISSIONS)[number];
|
|
70
|
+
declare const ACTOR_TYPES: readonly ["Bot", "User"];
|
|
71
|
+
type ActorType = (typeof ACTOR_TYPES)[number];
|
|
72
|
+
/** GitHub's native review states, as returned by the REST reviews API. */
|
|
73
|
+
declare const REVIEW_STATES: readonly ["APPROVED", "CHANGES_REQUESTED", "COMMENTED", "DISMISSED", "PENDING"];
|
|
74
|
+
type ReviewState = (typeof REVIEW_STATES)[number];
|
|
75
|
+
declare const PR_STATUSES: readonly ["closed", "merged", "open"];
|
|
76
|
+
type PullRequestStatus = (typeof PR_STATUSES)[number];
|
|
77
|
+
interface ReviewAuthor {
|
|
78
|
+
login: string;
|
|
79
|
+
type: ActorType;
|
|
80
|
+
/** The author's permission on the repository, fetched at the edge. */
|
|
81
|
+
permission: RepoPermission;
|
|
82
|
+
}
|
|
83
|
+
interface ReviewFact {
|
|
84
|
+
id: number;
|
|
85
|
+
author: ReviewAuthor;
|
|
86
|
+
/** The commit GitHub bound the review to (REST `commit_id`). */
|
|
87
|
+
commitSha: string;
|
|
88
|
+
state: ReviewState;
|
|
89
|
+
body: string;
|
|
90
|
+
/** ISO-8601 submission time. */
|
|
91
|
+
submittedAt: string;
|
|
92
|
+
}
|
|
93
|
+
interface PullRequestFacts {
|
|
94
|
+
status: PullRequestStatus;
|
|
95
|
+
isDraft: boolean;
|
|
96
|
+
title: string;
|
|
97
|
+
headSha: string;
|
|
98
|
+
labels: readonly string[];
|
|
99
|
+
autoMergeEnabled: boolean;
|
|
100
|
+
/** Result of the bot-PR eligibility predicate (Dependabot patch/minor, release-please). */
|
|
101
|
+
botEligible: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Whether the PR merges cleanly into its base: `false` is a merge conflict, and
|
|
104
|
+
* `undefined` means GitHub hasn't computed it yet (never treated as a conflict).
|
|
105
|
+
*/
|
|
106
|
+
mergeable: boolean | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Whether GitHub would merge the PR right now, with nothing left to wait on
|
|
109
|
+
* (merge state `clean`, `has_hooks`, or `unstable`). Auto-merge can't be armed
|
|
110
|
+
* then, so an approved PR is merged directly instead, as `gh pr merge --auto` does.
|
|
111
|
+
*/
|
|
112
|
+
immediatelyMergeable: boolean;
|
|
113
|
+
/** The CI gate over the head's required checks (see src/ci.ts). */
|
|
114
|
+
ciStatus: CiStatus;
|
|
115
|
+
/** Whether the same required checks are failing on the base branch head. */
|
|
116
|
+
baseCiFailing: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Earlier commits whose verdicts carry over to the head: every commit reached
|
|
119
|
+
* from the head through a chain of verified clean base merges (see
|
|
120
|
+
* docs/reconciler-design.md §Approval carry-over). Verified at the edge.
|
|
121
|
+
*/
|
|
122
|
+
cleanAncestors: readonly string[];
|
|
123
|
+
reviews: readonly ReviewFact[];
|
|
124
|
+
}
|
|
125
|
+
interface ReconcilePolicy {
|
|
126
|
+
/**
|
|
127
|
+
* When set, only these logins (case-insensitive) may author a counting verdict,
|
|
128
|
+
* in addition to the write-permission requirement. It narrows trust and never
|
|
129
|
+
* widens it.
|
|
130
|
+
*/
|
|
131
|
+
trustedAuthors?: readonly string[];
|
|
132
|
+
/** Arm/disarm native auto-merge from the lifecycle state. Off by default. */
|
|
133
|
+
armAutoMerge?: boolean;
|
|
134
|
+
/**
|
|
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.
|
|
137
|
+
*/
|
|
138
|
+
skipCopilotReview?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Required checks whose *pending* is a hold on a human act, not a running build;
|
|
141
|
+
* their failures still count. Defaults to `DEFAULT_HOLD_CHECKS` (src/ci.ts).
|
|
142
|
+
*/
|
|
143
|
+
holdChecks?: readonly string[];
|
|
144
|
+
/** Required checks the CI gate never counts. Defaults to `DEFAULT_IGNORED_CHECKS`. */
|
|
145
|
+
ignoredChecks?: readonly string[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The narrow GitHub surface the edge layer needs, shaped around the domain rather
|
|
150
|
+
* than raw REST payloads. Decisions (permission mapping, which errors are
|
|
151
|
+
* tolerable, write order) live in gather.ts / execute.ts and are tested against
|
|
152
|
+
* a fake of this interface; the real implementation (http-client.ts) only maps
|
|
153
|
+
* requests and responses.
|
|
154
|
+
*/
|
|
155
|
+
interface PullRequestData {
|
|
156
|
+
/** GraphQL node id, needed for the auto-merge mutations. */
|
|
157
|
+
nodeId: string;
|
|
158
|
+
state: 'closed' | 'open';
|
|
159
|
+
merged: boolean;
|
|
160
|
+
draft: boolean;
|
|
161
|
+
title: string;
|
|
162
|
+
headSha: string;
|
|
163
|
+
labels: string[];
|
|
164
|
+
autoMergeEnabled: boolean;
|
|
165
|
+
/** The PR author's login; undefined when the account was deleted. */
|
|
166
|
+
authorLogin: string | undefined;
|
|
167
|
+
headRef: string;
|
|
168
|
+
/** The base branch name, whose rulesets define the required checks. */
|
|
169
|
+
baseRef: string;
|
|
170
|
+
/** Clone URL of the base repository, used to fetch commits for carry-over checks. */
|
|
171
|
+
cloneUrl: string;
|
|
172
|
+
/** True when the head branch lives in a fork (or a deleted repo), not the base. */
|
|
173
|
+
isCrossRepository: boolean;
|
|
174
|
+
/** `false` is a merge conflict; `undefined` means GitHub is still computing it. */
|
|
175
|
+
mergeable: boolean | undefined;
|
|
176
|
+
/** GitHub's merge state (`clean`, `blocked`, `behind`, …); undefined while computing. */
|
|
177
|
+
mergeState: string | undefined;
|
|
178
|
+
}
|
|
179
|
+
/** A check-run on a commit (the latest run per name). */
|
|
180
|
+
interface CheckRunData {
|
|
181
|
+
name: string;
|
|
182
|
+
status: string;
|
|
183
|
+
conclusion: string | null;
|
|
184
|
+
completedAt: string | null;
|
|
185
|
+
}
|
|
186
|
+
/** A commit status (the latest per context) — the channel some CI uses instead of checks. */
|
|
187
|
+
interface CommitStatusData {
|
|
188
|
+
context: string;
|
|
189
|
+
state: string;
|
|
190
|
+
}
|
|
191
|
+
/** A git commit's structure: its tree and parents (for carry-over verification). */
|
|
192
|
+
interface CommitObject {
|
|
193
|
+
treeSha: string;
|
|
194
|
+
parents: string[];
|
|
195
|
+
}
|
|
196
|
+
/** The compare API's answer: how `head` relates to `base`, and their merge base. */
|
|
197
|
+
interface CommitComparison {
|
|
198
|
+
/** `ahead` / `identical` mean `head` contains `base`. */
|
|
199
|
+
status: string;
|
|
200
|
+
mergeBaseSha: string;
|
|
201
|
+
}
|
|
202
|
+
interface CommitData {
|
|
203
|
+
/** The commit author's GitHub login; undefined when not linked to an account. */
|
|
204
|
+
authorLogin: string | undefined;
|
|
205
|
+
message: string;
|
|
206
|
+
}
|
|
207
|
+
interface ReviewData {
|
|
208
|
+
id: number;
|
|
209
|
+
/** Undefined when the author's account was deleted (a "ghost" review). */
|
|
210
|
+
login: string | undefined;
|
|
211
|
+
type: ActorType;
|
|
212
|
+
commitSha: string;
|
|
213
|
+
state: ReviewState;
|
|
214
|
+
body: string;
|
|
215
|
+
/** Undefined for a pending (unsubmitted) review. */
|
|
216
|
+
submittedAt: string | undefined;
|
|
217
|
+
}
|
|
218
|
+
/** The collaborator-permission API's answer: the legacy level and the role. */
|
|
219
|
+
interface CollaboratorPermission {
|
|
220
|
+
/** Legacy level: admin, write, read, or none (maintain → write, triage → read). */
|
|
221
|
+
permission: string;
|
|
222
|
+
/** Fine-grained role: admin, maintain, write, triage, read, or a custom role. */
|
|
223
|
+
roleName: string;
|
|
224
|
+
}
|
|
225
|
+
interface LabelDefinition {
|
|
226
|
+
name: string;
|
|
227
|
+
color: string;
|
|
228
|
+
description: string;
|
|
229
|
+
}
|
|
230
|
+
interface GitHubClient {
|
|
231
|
+
getPullRequest(pr: number): Promise<PullRequestData>;
|
|
232
|
+
listReviews(pr: number): Promise<ReviewData[]>;
|
|
233
|
+
listCommits(pr: number): Promise<CommitData[]>;
|
|
234
|
+
/** The contexts the branch's rulesets require (empty when none are configured). */
|
|
235
|
+
getRequiredStatusChecks(branch: string): Promise<string[]>;
|
|
236
|
+
getBranchHeadSha(branch: string): Promise<string>;
|
|
237
|
+
listCheckRuns(sha: string): Promise<CheckRunData[]>;
|
|
238
|
+
listCommitStatuses(sha: string): Promise<CommitStatusData[]>;
|
|
239
|
+
getCommit(sha: string): Promise<CommitObject>;
|
|
240
|
+
compareCommits(base: string, head: string): Promise<CommitComparison>;
|
|
241
|
+
/** Throws a GitHubApiError with status 404 when the user is not a collaborator. */
|
|
242
|
+
getCollaboratorPermission(login: string): Promise<CollaboratorPermission>;
|
|
243
|
+
listRepoLabels(): Promise<string[]>;
|
|
244
|
+
/** Throws a GitHubApiError with status 422 when the label already exists. */
|
|
245
|
+
createLabel(label: LabelDefinition): Promise<void>;
|
|
246
|
+
addLabels(pr: number, names: readonly string[]): Promise<void>;
|
|
247
|
+
/** Throws a GitHubApiError with status 404 when the label is not on the PR. */
|
|
248
|
+
removeLabel(pr: number, name: string): Promise<void>;
|
|
249
|
+
/** Bodies of the PR's conversation comments, oldest first. */
|
|
250
|
+
listIssueComments(pr: number): Promise<string[]>;
|
|
251
|
+
createIssueComment(pr: number, body: string): Promise<void>;
|
|
252
|
+
/**
|
|
253
|
+
* Arm squash auto-merge, bound to `expectedHeadOid`: GitHub rejects it if the
|
|
254
|
+
* head has moved. Throws "…clean status" when the PR is already mergeable.
|
|
255
|
+
*/
|
|
256
|
+
enableAutoMerge(pullRequestNodeId: string, expectedHeadOid: string): Promise<void>;
|
|
257
|
+
/** Squash-merge now, bound to `expectedHeadOid` like `enableAutoMerge`. */
|
|
258
|
+
mergePullRequest(pullRequestNodeId: string, expectedHeadOid: string): Promise<void>;
|
|
259
|
+
disableAutoMerge(pullRequestNodeId: string): Promise<void>;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
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.
|
|
265
|
+
*/
|
|
266
|
+
type ReleaseActions = Pick<GitHubClient, 'enableAutoMerge' | 'mergePullRequest'>;
|
|
267
|
+
declare class GitHubApiError extends Error {
|
|
268
|
+
readonly status: number;
|
|
269
|
+
constructor(status: number, message: string);
|
|
270
|
+
}
|
|
271
|
+
declare function isApiStatus(error: unknown, status: number): boolean;
|
|
272
|
+
|
|
273
|
+
/**
|
|
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.
|
|
278
|
+
*/
|
|
279
|
+
interface HttpClientOptions {
|
|
280
|
+
token: string;
|
|
281
|
+
owner: string;
|
|
282
|
+
repo: string;
|
|
283
|
+
/** Defaults to GitHub.com; pass GITHUB_API_URL for GHES. */
|
|
284
|
+
apiUrl?: string;
|
|
285
|
+
fetch?: typeof fetch;
|
|
286
|
+
}
|
|
287
|
+
declare function createHttpClient(options: HttpClientOptions): GitHubClient;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* A bounded `git` subprocess runner. A non-zero exit is data (returned, not
|
|
291
|
+
* thrown), because `git merge-tree` reports conflicts that way; a timeout, a
|
|
292
|
+
* missing binary, or an oversized output throws. Injected so lineage code can be
|
|
293
|
+
* tested against real git in fixture repositories.
|
|
294
|
+
*/
|
|
295
|
+
interface GitResult {
|
|
296
|
+
code: number;
|
|
297
|
+
stdout: string;
|
|
298
|
+
stderr: string;
|
|
299
|
+
}
|
|
300
|
+
interface GitRunner {
|
|
301
|
+
run(args: readonly string[], env?: Readonly<Record<string, string>>): Promise<GitResult>;
|
|
302
|
+
}
|
|
303
|
+
declare function createGitRunner(): GitRunner;
|
|
304
|
+
/** Parse `git version 2.54.0 (…)` into [major, minor]; undefined when unrecognized. */
|
|
305
|
+
declare function parseGitVersion(output: string): [number, number] | undefined;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Command-line entry for `ai-pr-lifecycle`. Kept free of `process` so it is fully
|
|
309
|
+
* testable: the bin shim (src/bin/pr-lifecycle.ts) supplies argv, the environment,
|
|
310
|
+
* and the output streams, and turns the returned code into the exit status.
|
|
311
|
+
*
|
|
312
|
+
* Exit codes (a contract with rmartz/pr-lifecycle-action; see docs/cli.md):
|
|
313
|
+
* 0 reconciled (including "nothing to do" and a skipped closed PR), 1 a GitHub or
|
|
314
|
+
* other runtime failure, 2 a usage or configuration error.
|
|
315
|
+
*/
|
|
316
|
+
interface CliIo {
|
|
317
|
+
stdout: (line: string) => void;
|
|
318
|
+
stderr: (line: string) => void;
|
|
319
|
+
}
|
|
320
|
+
interface CliDeps {
|
|
321
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
322
|
+
/** Builds the GitHub client; tests inject a fake. */
|
|
323
|
+
createClient?: (options: HttpClientOptions) => GitHubClient;
|
|
324
|
+
/** Runs git for approval carry-over; tests inject one. Defaults to the real binary. */
|
|
325
|
+
git?: GitRunner;
|
|
326
|
+
}
|
|
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)";
|
|
328
|
+
declare function runCli(argv: readonly string[], io: CliIo, deps: CliDeps): Promise<number>;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Argument parsing for `ai-pr-lifecycle`. Pure: argv in, a parsed command or a
|
|
332
|
+
* usage error out, so every flag is tested without a process. The contract is
|
|
333
|
+
* recorded on #6 and in docs/cli.md.
|
|
334
|
+
*/
|
|
335
|
+
interface ReconcileArgs {
|
|
336
|
+
command: 'reconcile';
|
|
337
|
+
owner: string;
|
|
338
|
+
repo: string;
|
|
339
|
+
pr: number;
|
|
340
|
+
policy: ReconcilePolicy;
|
|
341
|
+
dryRun: boolean;
|
|
342
|
+
json: boolean;
|
|
343
|
+
/** Comment on the PR when arming is skipped for want of a release token. */
|
|
344
|
+
tokenAdvisory: boolean;
|
|
345
|
+
}
|
|
346
|
+
type ParsedArgs = ReconcileArgs | {
|
|
347
|
+
command: 'help';
|
|
348
|
+
} | {
|
|
349
|
+
command: 'error';
|
|
350
|
+
message: string;
|
|
351
|
+
};
|
|
352
|
+
declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Lifecycle state, computed from facts in a fixed priority order. See
|
|
356
|
+
* docs/reconciler-design.md §State.
|
|
357
|
+
*/
|
|
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"];
|
|
359
|
+
type LifecycleState = (typeof LIFECYCLE_STATES)[number];
|
|
360
|
+
declare const COPILOT_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
|
|
361
|
+
declare function computeState(facts: PullRequestFacts, policy: ReconcilePolicy): LifecycleState;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The reconcile plan: the minimal label and auto-merge changes that converge a PR
|
|
365
|
+
* to its computed lifecycle state. Labels are output only — the core owns the
|
|
366
|
+
* lifecycle labels (plus `auto-merge enabled` in arming mode) and never touches
|
|
367
|
+
* any other label. See docs/reconciler-design.md §Plan.
|
|
368
|
+
*/
|
|
369
|
+
declare const LIFECYCLE_LABELS: readonly ["approved", "changes requested", "escalation needed", "fix required", "ci failing", "review requested"];
|
|
370
|
+
type LifecycleLabel = (typeof LIFECYCLE_LABELS)[number];
|
|
371
|
+
declare const AUTO_MERGE_LABEL = "auto-merge enabled";
|
|
372
|
+
/**
|
|
373
|
+
* `merge` is arming's twin for a PR GitHub would already merge: auto-merge can't
|
|
374
|
+
* be armed then, so it is merged directly (see `immediatelyMergeable`).
|
|
375
|
+
*/
|
|
376
|
+
type AutoMergeAction = 'arm' | 'disarm' | 'merge' | 'none';
|
|
377
|
+
interface ReconcilePlan {
|
|
378
|
+
state: LifecycleState;
|
|
379
|
+
addLabels: string[];
|
|
380
|
+
removeLabels: string[];
|
|
381
|
+
autoMerge: AutoMergeAction;
|
|
382
|
+
}
|
|
383
|
+
declare function planReconcile(facts: PullRequestFacts, policy: ReconcilePolicy): ReconcilePlan;
|
|
384
|
+
/**
|
|
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.
|
|
388
|
+
*/
|
|
389
|
+
declare function withoutArming(plan: ReconcilePlan): ReconcilePlan;
|
|
390
|
+
|
|
391
|
+
interface MergeStep {
|
|
392
|
+
/** The merge base of the two parents. */
|
|
393
|
+
mergeBase: string;
|
|
394
|
+
first: string;
|
|
395
|
+
second: string;
|
|
396
|
+
/** The tree the merge commit actually has. */
|
|
397
|
+
tree: string;
|
|
398
|
+
}
|
|
399
|
+
interface RepoSource {
|
|
400
|
+
/** Clone URL of the repository (https, or file:// in tests). */
|
|
401
|
+
url: string;
|
|
402
|
+
/** Token for an https URL; sent as a header, never in argv or the URL. */
|
|
403
|
+
token?: string;
|
|
404
|
+
}
|
|
405
|
+
interface MergeVerifier {
|
|
406
|
+
/** True only when the step is a verified clean merge; false on any doubt. */
|
|
407
|
+
verify(step: MergeStep): Promise<boolean>;
|
|
408
|
+
dispose(): Promise<void>;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* A verifier bound to one repository and one scratch directory. Returns
|
|
412
|
+
* undefined when git is too old to verify at all (the caller then carries
|
|
413
|
+
* nothing over).
|
|
414
|
+
*/
|
|
415
|
+
declare function createMergeVerifier(git: GitRunner, source: RepoSource): Promise<MergeVerifier | undefined>;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Finds the head's clean ancestors: commits reached by walking back from the head
|
|
419
|
+
* through a chain of **verified clean base merges**, so reviews bound to them
|
|
420
|
+
* still describe the change being merged (docs/reconciler-design.md §Approval
|
|
421
|
+
* carry-over). Fails closed: any doubt or error ends the chain, and the cost is
|
|
422
|
+
* an extra review, never a false approval.
|
|
423
|
+
*/
|
|
424
|
+
/** A longer chain of base merges than this is unusual enough to review again. */
|
|
425
|
+
declare const MAX_CHAIN_STEPS = 20;
|
|
426
|
+
type LineageClient = Pick<GitHubClient, 'compareCommits' | 'getCommit'>;
|
|
427
|
+
interface LineageInput {
|
|
428
|
+
headSha: string;
|
|
429
|
+
/** The base branch's current head: every merged-in base commit must be on it. */
|
|
430
|
+
baseHeadSha: string;
|
|
431
|
+
source: RepoSource;
|
|
432
|
+
reviews: readonly ReviewData[];
|
|
433
|
+
}
|
|
434
|
+
interface Lineage {
|
|
435
|
+
cleanAncestors: string[];
|
|
436
|
+
/** Why the walk stopped, for reporting. */
|
|
437
|
+
stoppedBecause: string;
|
|
438
|
+
}
|
|
439
|
+
declare function gatherLineage(client: LineageClient, git: GitRunner, input: LineageInput): Promise<Lineage>;
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Gathers a PR's facts from GitHub for the pure core. Everything is read before
|
|
443
|
+
* anything is written (one gather → plan → execute pass per run), so a run never
|
|
444
|
+
* reacts to its own writes. See docs/github-edge-layer.md.
|
|
445
|
+
*/
|
|
446
|
+
interface GatheredPullRequest {
|
|
447
|
+
facts: PullRequestFacts;
|
|
448
|
+
/** GraphQL node id, for the auto-merge mutations. */
|
|
449
|
+
nodeId: string;
|
|
450
|
+
/** The bot-eligibility verdict behind `facts.botEligible`, with its reason. */
|
|
451
|
+
botEligibility: BotEligibility;
|
|
452
|
+
/** How far approval carry-over verified, for reporting (absent when off). */
|
|
453
|
+
lineage: Lineage | undefined;
|
|
454
|
+
}
|
|
455
|
+
interface GatherOptions {
|
|
456
|
+
/**
|
|
457
|
+
* Enables approval carry-over across clean base updates, which needs git and a
|
|
458
|
+
* token that can fetch the repository. Omitted, carry-over is off (fail closed:
|
|
459
|
+
* earlier-commit verdicts simply don't count).
|
|
460
|
+
*/
|
|
461
|
+
lineage?: {
|
|
462
|
+
git: GitRunner;
|
|
463
|
+
token?: string;
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
declare function gatherFacts(client: GitHubClient, pr: number, policy?: ReconcilePolicy, options?: GatherOptions): Promise<GatheredPullRequest>;
|
|
467
|
+
|
|
468
|
+
interface ReconcileOptions extends GatherOptions {
|
|
469
|
+
/** Compute and return the plan without writing anything. */
|
|
470
|
+
dryRun?: boolean;
|
|
471
|
+
/**
|
|
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.
|
|
475
|
+
*/
|
|
476
|
+
release?: ReleaseActions | 'unavailable';
|
|
477
|
+
}
|
|
478
|
+
interface ReconcileResult {
|
|
479
|
+
plan: ReconcilePlan;
|
|
480
|
+
/** Why the PR was (or wasn't) treated as a trusted bot PR, for reporting. */
|
|
481
|
+
botEligibility: BotEligibility;
|
|
482
|
+
/** How far approval carry-over verified (absent when carry-over is off). */
|
|
483
|
+
lineage: Lineage | undefined;
|
|
484
|
+
/** The arm or merge the plan wanted but skipped because `release` was unavailable. */
|
|
485
|
+
skippedAutoMerge: 'arm' | 'merge' | undefined;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
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
|
|
490
|
+
* result so callers can report what changed and why.
|
|
491
|
+
*/
|
|
492
|
+
declare function reconcilePullRequest(client: GitHubClient, pr: number, policy: ReconcilePolicy, options?: ReconcileOptions): Promise<ReconcileResult>;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* CLI output. `--json` is a versioned contract consumed by rmartz/pr-lifecycle-action
|
|
496
|
+
* (as step outputs): exactly one object on stdout, every field always present
|
|
497
|
+
* (absent values are `null`, never omitted). Renaming, removing, or retyping a field
|
|
498
|
+
* is a breaking change that bumps SCHEMA_VERSION; adding a field is not. Pinned by
|
|
499
|
+
* test/cli.test.ts and documented in docs/cli.md.
|
|
500
|
+
*/
|
|
501
|
+
declare const SCHEMA_VERSION = 1;
|
|
502
|
+
interface ReconcileTarget {
|
|
503
|
+
owner: string;
|
|
504
|
+
repo: string;
|
|
505
|
+
pr: number;
|
|
506
|
+
dryRun: boolean;
|
|
507
|
+
}
|
|
508
|
+
interface ReconcileJson {
|
|
509
|
+
schemaVersion: typeof SCHEMA_VERSION;
|
|
510
|
+
repo: string;
|
|
511
|
+
pr: number;
|
|
512
|
+
dryRun: boolean;
|
|
513
|
+
state: string;
|
|
514
|
+
addLabels: string[];
|
|
515
|
+
removeLabels: string[];
|
|
516
|
+
autoMerge: string;
|
|
517
|
+
botEligibility: {
|
|
518
|
+
eligible: boolean;
|
|
519
|
+
reason: string;
|
|
520
|
+
prType: string | null;
|
|
521
|
+
updateType: string | null;
|
|
522
|
+
};
|
|
523
|
+
/** Approval carry-over across clean base updates; `null` when it didn't run. */
|
|
524
|
+
carryOver: {
|
|
525
|
+
cleanAncestors: string[];
|
|
526
|
+
stoppedBecause: string;
|
|
527
|
+
} | null;
|
|
528
|
+
/** An arm or merge the plan wanted but skipped; `null` when nothing was skipped. */
|
|
529
|
+
autoMergeSkipped: {
|
|
530
|
+
action: 'arm' | 'merge';
|
|
531
|
+
reason: 'token-missing';
|
|
532
|
+
} | null;
|
|
533
|
+
}
|
|
534
|
+
declare function toReconcileJson(target: ReconcileTarget, result: ReconcileResult): ReconcileJson;
|
|
535
|
+
/** One human-readable line, e.g. `rmartz/x#7 → approved: +approved, auto-merge arm`. */
|
|
536
|
+
declare function formatSummary(target: ReconcileTarget, result: ReconcileResult): string;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* 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
|
|
543
|
+
* docs/github-edge-layer.md.
|
|
544
|
+
*/
|
|
545
|
+
interface PlanTarget {
|
|
546
|
+
pr: number;
|
|
547
|
+
nodeId: string;
|
|
548
|
+
/** The head the plan was computed for; a merge or arm is rejected if it moved. */
|
|
549
|
+
headSha: string;
|
|
550
|
+
}
|
|
551
|
+
declare function executePlan(client: GitHubClient, target: PlanTarget, plan: ReconcilePlan, release?: ReleaseActions): Promise<void>;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Canonical definitions for the labels the reconciler owns, mirroring the fleet
|
|
555
|
+
* roster (rmartz/dotfiles claude/scripts/labels.yml). Used only to create a label
|
|
556
|
+
* a consumer repo is missing; an existing label's color and description are never
|
|
557
|
+
* changed.
|
|
558
|
+
*/
|
|
559
|
+
declare const OWNED_LABEL_DEFINITIONS: Record<LifecycleLabel | typeof AUTO_MERGE_LABEL, LabelDefinition>;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The advisory comment posted when a PR is ready to arm or merge but no release
|
|
563
|
+
* token is configured (modelled on storybook-ci's missing-PAT advisory). It is the
|
|
564
|
+
* one signal a human will actually see — a warning in the job log goes unread —
|
|
565
|
+
* and it is posted at most once per PR, found again by its marker.
|
|
566
|
+
*/
|
|
567
|
+
declare const TOKEN_ADVISORY_MARKER = "<!-- pr-lifecycle:token-missing -->";
|
|
568
|
+
declare const RELEASE_TOKEN_DOCS_URL = "https://github.com/rmartz/pr-lifecycle/blob/main/docs/cli.md#release-token";
|
|
569
|
+
/** Pure, so it is unit-tested. */
|
|
570
|
+
declare function buildTokenAdvisoryBody(skipped: 'arm' | 'merge'): string;
|
|
571
|
+
/**
|
|
572
|
+
* Post the advisory unless the PR already has one. Returns whether a comment was
|
|
573
|
+
* posted, so callers can report it.
|
|
574
|
+
*/
|
|
575
|
+
declare function postTokenAdvisory(client: GitHubClient, pr: number, skipped: 'arm' | 'merge'): Promise<boolean>;
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Verdict parsing and trust. A review is a verdict when it carries a /review
|
|
579
|
+
* `skill-meta` marker with a verdict outcome, or (with no marker) a native
|
|
580
|
+
* APPROVED / CHANGES_REQUESTED state. It *counts* only from a trusted author and
|
|
581
|
+
* only when bound to the current head. See docs/reconciler-design.md §Verdicts.
|
|
582
|
+
*/
|
|
583
|
+
declare const VERDICTS: readonly ["approved", "changes-requested", "escalation-needed"];
|
|
584
|
+
type Verdict = (typeof VERDICTS)[number];
|
|
585
|
+
interface ParsedVerdict {
|
|
586
|
+
verdict: Verdict;
|
|
587
|
+
/** The head SHA the marker says was reviewed, when the marker names one. */
|
|
588
|
+
markerHead: string | undefined;
|
|
589
|
+
}
|
|
590
|
+
/** The verdict a review expresses, or undefined when it is not a verdict. */
|
|
591
|
+
declare function parseVerdict(review: ReviewFact): ParsedVerdict | undefined;
|
|
592
|
+
/** Whether an author may cast a counting verdict under the policy. */
|
|
593
|
+
declare function isTrustedAuthor(author: ReviewAuthor, policy: ReconcilePolicy): boolean;
|
|
594
|
+
declare function currentVerdict(facts: PullRequestFacts, policy: ReconcilePolicy): Verdict | undefined;
|
|
595
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTOR_TYPES,
|
|
3
|
+
AUTO_MERGE_LABEL,
|
|
4
|
+
COPILOT_REVIEWER_LOGIN,
|
|
5
|
+
DEPENDABOT_LOGIN,
|
|
6
|
+
DEPENDABOT_UPDATE_TYPES,
|
|
7
|
+
GitHubApiError,
|
|
8
|
+
LIFECYCLE_LABELS,
|
|
9
|
+
LIFECYCLE_STATES,
|
|
10
|
+
MAX_CHAIN_STEPS,
|
|
11
|
+
OWNED_LABEL_DEFINITIONS,
|
|
12
|
+
PR_STATUSES,
|
|
13
|
+
RELEASE_PLEASE_BRANCH_PREFIX,
|
|
14
|
+
RELEASE_PLEASE_PENDING_LABEL,
|
|
15
|
+
RELEASE_TOKEN_DOCS_URL,
|
|
16
|
+
REPO_PERMISSIONS,
|
|
17
|
+
REVIEW_STATES,
|
|
18
|
+
SCHEMA_VERSION,
|
|
19
|
+
TOKEN_ADVISORY_MARKER,
|
|
20
|
+
USAGE,
|
|
21
|
+
VERDICTS,
|
|
22
|
+
buildTokenAdvisoryBody,
|
|
23
|
+
classifyBotPr,
|
|
24
|
+
computeState,
|
|
25
|
+
createGitRunner,
|
|
26
|
+
createHttpClient,
|
|
27
|
+
createMergeVerifier,
|
|
28
|
+
currentVerdict,
|
|
29
|
+
executePlan,
|
|
30
|
+
formatSummary,
|
|
31
|
+
gatherFacts,
|
|
32
|
+
gatherLineage,
|
|
33
|
+
isApiStatus,
|
|
34
|
+
isTrustedAuthor,
|
|
35
|
+
parseArgs,
|
|
36
|
+
parseDependabotUpdateType,
|
|
37
|
+
parseGitVersion,
|
|
38
|
+
parseVerdict,
|
|
39
|
+
planReconcile,
|
|
40
|
+
postTokenAdvisory,
|
|
41
|
+
reconcilePullRequest,
|
|
42
|
+
runCli,
|
|
43
|
+
toReconcileJson,
|
|
44
|
+
withoutArming
|
|
45
|
+
} from "./chunk-RQEBRPZU.js";
|
|
46
|
+
export {
|
|
47
|
+
ACTOR_TYPES,
|
|
48
|
+
AUTO_MERGE_LABEL,
|
|
49
|
+
COPILOT_REVIEWER_LOGIN,
|
|
50
|
+
DEPENDABOT_LOGIN,
|
|
51
|
+
DEPENDABOT_UPDATE_TYPES,
|
|
52
|
+
GitHubApiError,
|
|
53
|
+
LIFECYCLE_LABELS,
|
|
54
|
+
LIFECYCLE_STATES,
|
|
55
|
+
MAX_CHAIN_STEPS,
|
|
56
|
+
OWNED_LABEL_DEFINITIONS,
|
|
57
|
+
PR_STATUSES,
|
|
58
|
+
RELEASE_PLEASE_BRANCH_PREFIX,
|
|
59
|
+
RELEASE_PLEASE_PENDING_LABEL,
|
|
60
|
+
RELEASE_TOKEN_DOCS_URL,
|
|
61
|
+
REPO_PERMISSIONS,
|
|
62
|
+
REVIEW_STATES,
|
|
63
|
+
SCHEMA_VERSION,
|
|
64
|
+
TOKEN_ADVISORY_MARKER,
|
|
65
|
+
USAGE,
|
|
66
|
+
VERDICTS,
|
|
67
|
+
buildTokenAdvisoryBody,
|
|
68
|
+
classifyBotPr,
|
|
69
|
+
computeState,
|
|
70
|
+
createGitRunner,
|
|
71
|
+
createHttpClient,
|
|
72
|
+
createMergeVerifier,
|
|
73
|
+
currentVerdict,
|
|
74
|
+
executePlan,
|
|
75
|
+
formatSummary,
|
|
76
|
+
gatherFacts,
|
|
77
|
+
gatherLineage,
|
|
78
|
+
isApiStatus,
|
|
79
|
+
isTrustedAuthor,
|
|
80
|
+
parseArgs,
|
|
81
|
+
parseDependabotUpdateType,
|
|
82
|
+
parseGitVersion,
|
|
83
|
+
parseVerdict,
|
|
84
|
+
planReconcile,
|
|
85
|
+
postTokenAdvisory,
|
|
86
|
+
reconcilePullRequest,
|
|
87
|
+
runCli,
|
|
88
|
+
toReconcileJson,
|
|
89
|
+
withoutArming
|
|
90
|
+
};
|