@kungfu-tech/buildchain 2.14.1-alpha.0 → 2.14.1-alpha.1
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/dist/site/buildchain-contract.json +3 -3
- package/dist/site/buildchain-site.json +7 -7
- package/dist/site/kfd-claims.json +8 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +11 -6
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +9 -4
- package/docs/release-governance.md +16 -3
- package/package.json +1 -1
- package/scripts/buildchain-patrol.mjs +3 -1
- package/scripts/dev-pr-auto-merge.mjs +361 -20
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-publication-release-registry",
|
|
4
|
-
"generatedAt": "2026-07-17T00:
|
|
5
|
-
"publishedAt": "2026-07-17T00:
|
|
4
|
+
"generatedAt": "2026-07-17T00:51:57.848Z",
|
|
5
|
+
"publishedAt": "2026-07-17T00:51:57.848Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "5073d72855cd9136112a1866398f072ac3a065bb",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"package": {
|
|
34
34
|
"name": "@kungfu-tech/buildchain",
|
|
35
|
-
"version": "2.14.1-alpha.
|
|
35
|
+
"version": "2.14.1-alpha.1",
|
|
36
36
|
"versionSource": "package.json#version"
|
|
37
37
|
},
|
|
38
38
|
"sourceKind": "package-site-bundle",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-manifest",
|
|
4
|
-
"generatedAt": "2026-07-17T00:
|
|
5
|
-
"publishedAt": "2026-07-17T00:
|
|
4
|
+
"generatedAt": "2026-07-17T00:51:57.848Z",
|
|
5
|
+
"publishedAt": "2026-07-17T00:51:57.848Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "5073d72855cd9136112a1866398f072ac3a065bb",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "2.14.1-alpha.
|
|
40
|
+
"version": "2.14.1-alpha.1",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"entrypoint": "buildchain-site.json",
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
"path": "docs/release-governance.md",
|
|
238
238
|
"plane": "why",
|
|
239
239
|
"exists": true,
|
|
240
|
-
"digest": "sha256:
|
|
240
|
+
"digest": "sha256:5e0e6ef6720b51bb156da54390850688ef9a538908afb22484011364102e48eb"
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
243
|
"id": "release-flow",
|
|
@@ -879,6 +879,7 @@
|
|
|
879
879
|
"cadence",
|
|
880
880
|
"capabilities",
|
|
881
881
|
"dry-run",
|
|
882
|
+
"landing-mode",
|
|
882
883
|
"max-actions",
|
|
883
884
|
"merge-method",
|
|
884
885
|
"mode",
|
|
@@ -888,7 +889,7 @@
|
|
|
888
889
|
"same-repository-only",
|
|
889
890
|
"target-branch"
|
|
890
891
|
],
|
|
891
|
-
"inputCount":
|
|
892
|
+
"inputCount": 16,
|
|
892
893
|
"secrets": [
|
|
893
894
|
"github-token"
|
|
894
895
|
],
|
|
@@ -1001,6 +1002,7 @@
|
|
|
1001
1002
|
"buildchain-ref",
|
|
1002
1003
|
"buildchain-repository",
|
|
1003
1004
|
"dry-run",
|
|
1005
|
+
"landing-mode",
|
|
1004
1006
|
"max-merges",
|
|
1005
1007
|
"merge-method",
|
|
1006
1008
|
"ready-label",
|
|
@@ -1009,18 +1011,20 @@
|
|
|
1009
1011
|
"same-repository-only",
|
|
1010
1012
|
"target-branch"
|
|
1011
1013
|
],
|
|
1012
|
-
"inputCount":
|
|
1014
|
+
"inputCount": 13,
|
|
1013
1015
|
"secrets": [
|
|
1014
1016
|
"github-token"
|
|
1015
1017
|
],
|
|
1016
1018
|
"secretCount": 1,
|
|
1017
1019
|
"outputs": [
|
|
1020
|
+
"action-count",
|
|
1021
|
+
"enqueued-count",
|
|
1018
1022
|
"evaluated-count",
|
|
1019
1023
|
"final-base-sha",
|
|
1020
1024
|
"merged-count",
|
|
1021
1025
|
"skipped-count"
|
|
1022
1026
|
],
|
|
1023
|
-
"outputCount":
|
|
1027
|
+
"outputCount": 6,
|
|
1024
1028
|
"surface": "dev-governance",
|
|
1025
1029
|
"capabilityGroup": "governance-versioning",
|
|
1026
1030
|
"status": "active"
|
|
@@ -1162,6 +1166,7 @@
|
|
|
1162
1166
|
"buildchain-ref",
|
|
1163
1167
|
"buildchain-repository",
|
|
1164
1168
|
"dry-run",
|
|
1169
|
+
"landing-mode",
|
|
1165
1170
|
"max-actions",
|
|
1166
1171
|
"merge-method",
|
|
1167
1172
|
"ready-label",
|
|
@@ -1170,7 +1175,7 @@
|
|
|
1170
1175
|
"same-repository-only",
|
|
1171
1176
|
"target-branch"
|
|
1172
1177
|
],
|
|
1173
|
-
"inputCount":
|
|
1178
|
+
"inputCount": 13,
|
|
1174
1179
|
"secrets": [
|
|
1175
1180
|
"github-token"
|
|
1176
1181
|
],
|
|
@@ -435,9 +435,21 @@ The workflow defaults are conservative. A PR is skipped unless it targets the
|
|
|
435
435
|
configured dev line, is not a draft, has the ready label, has no block label,
|
|
436
436
|
comes from the same repository, uses an allowed work-branch prefix, has a
|
|
437
437
|
current approval, is mergeable, and has the configured required checks passing.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
`landing-mode: auto` reads the target branch's native merge-queue state. When a
|
|
439
|
+
queue exists, Buildchain never calls the direct merge endpoint: it admits at
|
|
440
|
+
most one PR against the observed target-branch SHA and immutable PR head, then
|
|
441
|
+
calls GraphQL `enqueuePullRequest` with `expectedHeadOid`. GitHub's
|
|
442
|
+
`merge_group` checks remain the final authority for the projected merge.
|
|
443
|
+
|
|
444
|
+
The admission receipt records the expected and observed base/head SHAs, policy
|
|
445
|
+
checks, decision, reason, and active predecessor. Buildchain re-reads the base,
|
|
446
|
+
head, mergeability, and native queue immediately before enqueueing. Base or
|
|
447
|
+
head drift fails closed, an active queue entry blocks admission, and a rejected
|
|
448
|
+
ready predecessor leaves its PR open while later PRs receive
|
|
449
|
+
`blocked-by-predecessor`. Workflow concurrency serializes Buildchain-owned
|
|
450
|
+
admission runs; GitHub still owns the atomic queue and protected-ref update.
|
|
451
|
+
Repositories may explicitly select `landing-mode: direct` only when the target
|
|
452
|
+
branch has no native queue. Queue presence always disables the direct path.
|
|
441
453
|
|
|
442
454
|
The canonical consumer required check context is `check / check`, matching the
|
|
443
455
|
reusable workflow call plus its `check` job. Buildchain's own `Verify` workflow
|
|
@@ -497,6 +509,7 @@ jobs:
|
|
|
497
509
|
ready-label: ready
|
|
498
510
|
block-labels: blocked,do-not-merge
|
|
499
511
|
max-merges: 1
|
|
512
|
+
landing-mode: auto
|
|
500
513
|
dry-run: ${{ inputs.dry-run || false }}
|
|
501
514
|
```
|
|
502
515
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.14.1-alpha.
|
|
3
|
+
"version": "2.14.1-alpha.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -91,6 +91,7 @@ export function normalizePatrolOptions(options = {}) {
|
|
|
91
91
|
sameRepositoryOnly: options.sameRepositoryOnly ?? process.env.BUILDCHAIN_PATROL_SAME_REPOSITORY_ONLY,
|
|
92
92
|
maxActions: intOption(options.maxActions ?? process.env.BUILDCHAIN_PATROL_MAX_ACTIONS, 1),
|
|
93
93
|
mergeMethod: String(options.mergeMethod || process.env.BUILDCHAIN_PATROL_MERGE_METHOD || "merge").trim(),
|
|
94
|
+
landingMode: String(options.landingMode || process.env.BUILDCHAIN_PATROL_LANDING_MODE || "auto").trim(),
|
|
94
95
|
dryRun: boolOption(options.dryRun ?? process.env.BUILDCHAIN_PATROL_DRY_RUN, true),
|
|
95
96
|
outputPath: String(options.outputPath || process.env.BUILDCHAIN_PATROL_OUTPUT_PATH || DEFAULT_OUTPUT_PATH),
|
|
96
97
|
};
|
|
@@ -167,6 +168,7 @@ export async function runBuildchainPatrol(optionsInput = {}, clientInput) {
|
|
|
167
168
|
sameRepositoryOnly: options.sameRepositoryOnly,
|
|
168
169
|
maxMerges: options.maxActions,
|
|
169
170
|
mergeMethod: options.mergeMethod,
|
|
171
|
+
landingMode: options.landingMode,
|
|
170
172
|
dryRun: options.dryRun,
|
|
171
173
|
outputPath: path.join(path.dirname(options.outputPath), "dev-pr-auto-merge.json"),
|
|
172
174
|
},
|
|
@@ -178,7 +180,7 @@ export async function runBuildchainPatrol(optionsInput = {}, clientInput) {
|
|
|
178
180
|
result: mergeResult,
|
|
179
181
|
});
|
|
180
182
|
result.summary.evaluatedCount += mergeResult.evaluated.length;
|
|
181
|
-
result.summary.actionCount += mergeResult.
|
|
183
|
+
result.summary.actionCount += mergeResult.actions.length;
|
|
182
184
|
result.summary.skippedCount += mergeResult.skipped.length;
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -7,6 +7,15 @@ const DEFAULT_ALLOWED_HEAD_PREFIXES = ["feature/", "fix/", "chore/", "docs/", "c
|
|
|
7
7
|
const DEFAULT_REQUIRED_CHECKS = ["check"];
|
|
8
8
|
const SUCCESS_STATES = new Set(["success"]);
|
|
9
9
|
const SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
10
|
+
const VALID_LANDING_MODES = new Set(["auto", "direct", "queue"]);
|
|
11
|
+
const STATIC_SKIP_REASONS = new Set([
|
|
12
|
+
"draft",
|
|
13
|
+
"fork-or-cross-repository-head",
|
|
14
|
+
"head-prefix-not-allowed",
|
|
15
|
+
"missing-ready-label",
|
|
16
|
+
"blocked-label",
|
|
17
|
+
]);
|
|
18
|
+
const ADMISSION_CONTRACT = "kungfu-buildchain-dev-merge-queue-admission";
|
|
10
19
|
|
|
11
20
|
function splitList(value, fallback = []) {
|
|
12
21
|
if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
@@ -30,6 +39,14 @@ function intOption(value, fallback) {
|
|
|
30
39
|
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
function choiceOption(value, valid, fallback, field) {
|
|
43
|
+
const normalized = String(value || fallback).trim().toLowerCase();
|
|
44
|
+
if (!valid.has(normalized)) {
|
|
45
|
+
throw new Error(`${field} must be one of ${[...valid].join(", ")}, got: ${value || "<empty>"}`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
function normalizeRepo(value) {
|
|
34
51
|
const text = String(value || "").trim();
|
|
35
52
|
const match = text.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
@@ -49,6 +66,7 @@ function normalizeOptions(options = {}) {
|
|
|
49
66
|
sameRepositoryOnly: boolOption(options.sameRepositoryOnly, true),
|
|
50
67
|
maxMerges: intOption(options.maxMerges, 1),
|
|
51
68
|
mergeMethod: String(options.mergeMethod || "merge").trim(),
|
|
69
|
+
landingMode: choiceOption(options.landingMode, VALID_LANDING_MODES, "auto", "landing mode"),
|
|
52
70
|
dryRun: boolOption(options.dryRun, true),
|
|
53
71
|
pollMergeableAttempts: intOption(options.pollMergeableAttempts, 3),
|
|
54
72
|
pollMergeableDelayMs: intOption(options.pollMergeableDelayMs, 1000),
|
|
@@ -165,6 +183,12 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
165
183
|
attempts: options.pollMergeableAttempts,
|
|
166
184
|
delayMs: options.pollMergeableDelayMs,
|
|
167
185
|
});
|
|
186
|
+
if (detailed.base?.ref && detailed.base.ref !== options.targetBranch) {
|
|
187
|
+
return skip("base-branch-drift", {
|
|
188
|
+
expectedBaseRef: options.targetBranch,
|
|
189
|
+
observedBaseRef: detailed.base.ref,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
168
192
|
if (!mergeableAccepted(detailed)) {
|
|
169
193
|
return skip("not-mergeable", {
|
|
170
194
|
mergeable: detailed.mergeable,
|
|
@@ -172,19 +196,25 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
172
196
|
});
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
const approval = { required: options.requireApproval, passed: true };
|
|
175
200
|
if (options.requireApproval) {
|
|
176
201
|
const reviews = await client.listReviews(pr.number);
|
|
177
|
-
|
|
202
|
+
approval.passed = hasRequiredApproval(reviews);
|
|
203
|
+
if (!approval.passed) return skip("missing-approval", { approval });
|
|
178
204
|
}
|
|
179
205
|
|
|
180
|
-
const
|
|
206
|
+
const observedHeadSha = detailed.head?.sha || pr.head?.sha || "";
|
|
207
|
+
const checks = await client.listCommitChecks(observedHeadSha);
|
|
181
208
|
const checkSummary = summarizeChecks(checks, options.requiredChecks);
|
|
182
|
-
if (!checkSummary.passed) return skip("required-checks-not-passing", { checks: checkSummary });
|
|
209
|
+
if (!checkSummary.passed) return skip("required-checks-not-passing", { checks: checkSummary, approval });
|
|
183
210
|
|
|
184
211
|
return {
|
|
185
212
|
action: options.dryRun ? "would-merge" : "merge",
|
|
186
213
|
reason: options.dryRun ? "dry-run" : "eligible",
|
|
187
214
|
checks: checkSummary,
|
|
215
|
+
approval,
|
|
216
|
+
pullRequestId: detailed.node_id || pr.node_id || "",
|
|
217
|
+
observedHeadSha,
|
|
188
218
|
};
|
|
189
219
|
}
|
|
190
220
|
|
|
@@ -302,6 +332,178 @@ export class GitHubClient {
|
|
|
302
332
|
const { data } = await this.request("GET", `/repos/${this.repository.owner}/${this.repository.repo}/git/ref/${ref}`);
|
|
303
333
|
return data.object?.sha || "";
|
|
304
334
|
}
|
|
335
|
+
|
|
336
|
+
async graphql(query, variables = {}) {
|
|
337
|
+
const { data } = await this.request("POST", "/graphql", {
|
|
338
|
+
body: { query, variables },
|
|
339
|
+
});
|
|
340
|
+
if (Array.isArray(data?.errors) && data.errors.length > 0) {
|
|
341
|
+
const error = new Error(data.errors.map((entry) => entry.message).filter(Boolean).join("; ") || "GitHub GraphQL request failed");
|
|
342
|
+
error.data = data;
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
return data?.data || {};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async getMergeQueueState(branch) {
|
|
349
|
+
const data = await this.graphql(
|
|
350
|
+
`query BuildchainMergeQueueState($owner: String!, $repo: String!, $branch: String!) {
|
|
351
|
+
repository(owner: $owner, name: $repo) {
|
|
352
|
+
mergeQueue(branch: $branch) {
|
|
353
|
+
id
|
|
354
|
+
entries(first: 100) {
|
|
355
|
+
nodes {
|
|
356
|
+
id
|
|
357
|
+
position
|
|
358
|
+
state
|
|
359
|
+
baseCommit { oid }
|
|
360
|
+
headCommit { oid }
|
|
361
|
+
pullRequest { number headRefOid }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}`,
|
|
367
|
+
{
|
|
368
|
+
owner: this.repository.owner,
|
|
369
|
+
repo: this.repository.repo,
|
|
370
|
+
branch,
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
const queue = data.repository?.mergeQueue || null;
|
|
374
|
+
return {
|
|
375
|
+
enabled: Boolean(queue),
|
|
376
|
+
id: queue?.id || "",
|
|
377
|
+
entries: (queue?.entries?.nodes || []).map((entry) => ({
|
|
378
|
+
id: entry.id || "",
|
|
379
|
+
position: entry.position,
|
|
380
|
+
state: entry.state || "",
|
|
381
|
+
pullRequestNumber: entry.pullRequest?.number || null,
|
|
382
|
+
pullRequestHeadSha: entry.pullRequest?.headRefOid || "",
|
|
383
|
+
baseSha: entry.baseCommit?.oid || "",
|
|
384
|
+
headSha: entry.headCommit?.oid || "",
|
|
385
|
+
})),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async enqueuePullRequest({ pullRequestId, expectedHeadOid }) {
|
|
390
|
+
const data = await this.graphql(
|
|
391
|
+
`mutation BuildchainEnqueuePullRequest($input: EnqueuePullRequestInput!) {
|
|
392
|
+
enqueuePullRequest(input: $input) {
|
|
393
|
+
mergeQueueEntry {
|
|
394
|
+
id
|
|
395
|
+
position
|
|
396
|
+
state
|
|
397
|
+
baseCommit { oid }
|
|
398
|
+
headCommit { oid }
|
|
399
|
+
pullRequest { number headRefOid }
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}`,
|
|
403
|
+
{
|
|
404
|
+
input: {
|
|
405
|
+
pullRequestId,
|
|
406
|
+
expectedHeadOid,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
const entry = data.enqueuePullRequest?.mergeQueueEntry;
|
|
411
|
+
if (!entry?.id) throw new Error("GitHub did not return a merge queue entry");
|
|
412
|
+
return {
|
|
413
|
+
id: entry.id,
|
|
414
|
+
position: entry.position,
|
|
415
|
+
state: entry.state || "",
|
|
416
|
+
pullRequestNumber: entry.pullRequest?.number || null,
|
|
417
|
+
pullRequestHeadSha: entry.pullRequest?.headRefOid || "",
|
|
418
|
+
baseSha: entry.baseCommit?.oid || "",
|
|
419
|
+
headSha: entry.headCommit?.oid || "",
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function queuePredecessor(queueState, fallback = {}) {
|
|
425
|
+
const entry = queueState?.entries?.[0];
|
|
426
|
+
if (entry) {
|
|
427
|
+
return {
|
|
428
|
+
queueEntryId: entry.id,
|
|
429
|
+
pullRequestNumber: entry.pullRequestNumber,
|
|
430
|
+
headSha: entry.pullRequestHeadSha || entry.headSha || "",
|
|
431
|
+
state: entry.state || "",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
queueEntryId: fallback.queueEntryId || "",
|
|
436
|
+
pullRequestNumber: fallback.pullRequestNumber || null,
|
|
437
|
+
headSha: fallback.headSha || "",
|
|
438
|
+
state: fallback.state || "",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function admissionReceipt({
|
|
443
|
+
options,
|
|
444
|
+
pr,
|
|
445
|
+
expectedBaseSha,
|
|
446
|
+
observedBaseSha,
|
|
447
|
+
expectedHeadSha,
|
|
448
|
+
observedHeadSha,
|
|
449
|
+
decision,
|
|
450
|
+
reason,
|
|
451
|
+
checks,
|
|
452
|
+
approval,
|
|
453
|
+
predecessor,
|
|
454
|
+
} = {}) {
|
|
455
|
+
return {
|
|
456
|
+
schemaVersion: 1,
|
|
457
|
+
contract: ADMISSION_CONTRACT,
|
|
458
|
+
repository: options.repository.fullName,
|
|
459
|
+
targetBranch: options.targetBranch,
|
|
460
|
+
pullRequestNumber: pr.number,
|
|
461
|
+
expectedBaseSha: expectedBaseSha || "",
|
|
462
|
+
observedBaseSha: observedBaseSha || "",
|
|
463
|
+
expectedHeadSha: expectedHeadSha || "",
|
|
464
|
+
observedHeadSha: observedHeadSha || "",
|
|
465
|
+
approvalRequired: options.requireApproval,
|
|
466
|
+
approval: approval || { required: options.requireApproval, passed: false },
|
|
467
|
+
checks: checks || { required: options.requiredChecks, entries: [], passed: false },
|
|
468
|
+
decision,
|
|
469
|
+
reason,
|
|
470
|
+
predecessor: predecessor || null,
|
|
471
|
+
finalSafetyBoundary: "github-merge-group",
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function evaluatedEntry(pr, decision) {
|
|
476
|
+
return {
|
|
477
|
+
number: pr.number,
|
|
478
|
+
title: pr.title || "",
|
|
479
|
+
headRef: pr.head?.ref || "",
|
|
480
|
+
headSha: pr.head?.sha || "",
|
|
481
|
+
action: decision.action,
|
|
482
|
+
reason: decision.reason,
|
|
483
|
+
checks: decision.checks,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function blockRemainingPullRequests(result, pullRequests, startIndex, options, expectedBaseSha, predecessor) {
|
|
488
|
+
for (const pr of pullRequests.slice(startIndex)) {
|
|
489
|
+
const entry = evaluatedEntry(pr, {
|
|
490
|
+
action: "skip",
|
|
491
|
+
reason: "blocked-by-predecessor",
|
|
492
|
+
});
|
|
493
|
+
entry.admissionReceipt = admissionReceipt({
|
|
494
|
+
options,
|
|
495
|
+
pr,
|
|
496
|
+
expectedBaseSha,
|
|
497
|
+
observedBaseSha: expectedBaseSha,
|
|
498
|
+
expectedHeadSha: pr.head?.sha || "",
|
|
499
|
+
observedHeadSha: pr.head?.sha || "",
|
|
500
|
+
decision: "blocked",
|
|
501
|
+
reason: "blocked-by-predecessor",
|
|
502
|
+
predecessor,
|
|
503
|
+
});
|
|
504
|
+
result.evaluated.push(entry);
|
|
505
|
+
result.skipped.push(entry);
|
|
506
|
+
}
|
|
305
507
|
}
|
|
306
508
|
|
|
307
509
|
export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
@@ -313,21 +515,51 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
313
515
|
apiUrl: optionsInput.apiUrl || process.env.GITHUB_API_URL || "https://api.github.com",
|
|
314
516
|
});
|
|
315
517
|
|
|
316
|
-
const pullRequests = await
|
|
518
|
+
const [pullRequests, initialBaseSha, initialQueueState] = await Promise.all([
|
|
519
|
+
client.listPullRequests(options.targetBranch),
|
|
520
|
+
client.getBranchSha(options.targetBranch).catch(() => ""),
|
|
521
|
+
client.getMergeQueueState(options.targetBranch),
|
|
522
|
+
]);
|
|
523
|
+
const landingMode = initialQueueState.enabled ? "queue" : options.landingMode === "queue" ? "queue" : "direct";
|
|
524
|
+
const orderedPullRequests = landingMode === "queue"
|
|
525
|
+
? [...pullRequests].sort((left, right) => Number(left.number) - Number(right.number))
|
|
526
|
+
: pullRequests;
|
|
317
527
|
const result = {
|
|
318
528
|
schemaVersion: 1,
|
|
319
529
|
contract: "kungfu-buildchain-dev-pr-auto-merge",
|
|
320
530
|
repository: options.repository.fullName,
|
|
321
531
|
targetBranch: options.targetBranch,
|
|
532
|
+
requestedLandingMode: options.landingMode,
|
|
533
|
+
landingMode,
|
|
322
534
|
dryRun: options.dryRun,
|
|
323
535
|
maxMerges: options.maxMerges,
|
|
324
536
|
evaluated: [],
|
|
537
|
+
actions: [],
|
|
325
538
|
merged: [],
|
|
539
|
+
enqueued: [],
|
|
326
540
|
skipped: [],
|
|
327
|
-
|
|
541
|
+
initialBaseSha,
|
|
542
|
+
finalBaseSha: initialBaseSha,
|
|
543
|
+
mergeQueue: initialQueueState,
|
|
328
544
|
};
|
|
329
545
|
|
|
330
|
-
|
|
546
|
+
if (landingMode === "queue" && !initialQueueState.enabled) {
|
|
547
|
+
blockRemainingPullRequests(result, orderedPullRequests, 0, options, initialBaseSha, null);
|
|
548
|
+
for (const entry of result.evaluated) {
|
|
549
|
+
entry.reason = "merge-queue-not-enabled";
|
|
550
|
+
entry.admissionReceipt.reason = "merge-queue-not-enabled";
|
|
551
|
+
entry.admissionReceipt.decision = "rejected";
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (landingMode === "queue" && initialQueueState.entries.length > 0) {
|
|
557
|
+
blockRemainingPullRequests(result, orderedPullRequests, 0, options, initialBaseSha, queuePredecessor(initialQueueState));
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (let index = 0; index < orderedPullRequests.length; index += 1) {
|
|
562
|
+
const pr = orderedPullRequests[index];
|
|
331
563
|
if (result.merged.length >= options.maxMerges) {
|
|
332
564
|
const entry = { number: pr.number, title: pr.title || "", action: "skip", reason: "max-merges-reached" };
|
|
333
565
|
result.evaluated.push(entry);
|
|
@@ -336,29 +568,134 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
336
568
|
}
|
|
337
569
|
|
|
338
570
|
const decision = await evaluatePullRequest(pr, options, client);
|
|
339
|
-
const entry =
|
|
340
|
-
number: pr.number,
|
|
341
|
-
title: pr.title || "",
|
|
342
|
-
headRef: pr.head?.ref || "",
|
|
343
|
-
headSha: pr.head?.sha || "",
|
|
344
|
-
action: decision.action,
|
|
345
|
-
reason: decision.reason,
|
|
346
|
-
checks: decision.checks,
|
|
347
|
-
};
|
|
571
|
+
const entry = evaluatedEntry(pr, decision);
|
|
348
572
|
result.evaluated.push(entry);
|
|
349
573
|
|
|
350
|
-
if (decision.action === "merge") {
|
|
574
|
+
if (landingMode === "direct" && decision.action === "merge") {
|
|
351
575
|
const mergeResult = await client.mergePullRequest(pr.number, {
|
|
352
576
|
method: options.mergeMethod,
|
|
353
577
|
sha: pr.head?.sha,
|
|
354
578
|
});
|
|
355
579
|
const mergedEntry = { ...entry, mergeSha: mergeResult.sha || "" };
|
|
356
580
|
result.merged.push(mergedEntry);
|
|
581
|
+
result.actions.push(mergedEntry);
|
|
357
582
|
result.evaluated[result.evaluated.length - 1] = mergedEntry;
|
|
358
|
-
} else if (decision.action === "would-merge") {
|
|
583
|
+
} else if (landingMode === "direct" && decision.action === "would-merge") {
|
|
359
584
|
result.merged.push(entry);
|
|
360
|
-
|
|
585
|
+
result.actions.push(entry);
|
|
586
|
+
} else if (landingMode === "direct" || STATIC_SKIP_REASONS.has(decision.reason)) {
|
|
587
|
+
result.skipped.push(entry);
|
|
588
|
+
} else if (decision.action === "skip") {
|
|
589
|
+
entry.admissionReceipt = admissionReceipt({
|
|
590
|
+
options,
|
|
591
|
+
pr,
|
|
592
|
+
expectedBaseSha: initialBaseSha,
|
|
593
|
+
observedBaseSha: initialBaseSha,
|
|
594
|
+
expectedHeadSha: decision.observedHeadSha || pr.head?.sha || "",
|
|
595
|
+
observedHeadSha: decision.observedHeadSha || pr.head?.sha || "",
|
|
596
|
+
decision: "rejected",
|
|
597
|
+
reason: decision.reason,
|
|
598
|
+
checks: decision.checks,
|
|
599
|
+
approval: decision.approval,
|
|
600
|
+
});
|
|
361
601
|
result.skipped.push(entry);
|
|
602
|
+
blockRemainingPullRequests(result, orderedPullRequests, index + 1, options, initialBaseSha, queuePredecessor(null, {
|
|
603
|
+
pullRequestNumber: pr.number,
|
|
604
|
+
headSha: decision.observedHeadSha || pr.head?.sha || "",
|
|
605
|
+
state: "ADMISSION_REJECTED",
|
|
606
|
+
}));
|
|
607
|
+
break;
|
|
608
|
+
} else {
|
|
609
|
+
const expectedHeadSha = decision.observedHeadSha || pr.head?.sha || "";
|
|
610
|
+
const [observedPullRequest, observedBaseSha, observedQueueState] = await Promise.all([
|
|
611
|
+
client.getPullRequest(pr.number, {
|
|
612
|
+
attempts: options.pollMergeableAttempts,
|
|
613
|
+
delayMs: options.pollMergeableDelayMs,
|
|
614
|
+
}),
|
|
615
|
+
client.getBranchSha(options.targetBranch),
|
|
616
|
+
client.getMergeQueueState(options.targetBranch),
|
|
617
|
+
]);
|
|
618
|
+
const observedHeadSha = observedPullRequest.head?.sha || "";
|
|
619
|
+
let admissionDecision = options.dryRun ? "planned" : "accepted";
|
|
620
|
+
let admissionReason = options.dryRun ? "dry-run" : "eligible";
|
|
621
|
+
let predecessor = null;
|
|
622
|
+
if (observedQueueState.entries.length > 0) {
|
|
623
|
+
admissionDecision = "blocked";
|
|
624
|
+
admissionReason = "blocked-by-predecessor";
|
|
625
|
+
predecessor = queuePredecessor(observedQueueState);
|
|
626
|
+
} else if (observedBaseSha !== initialBaseSha) {
|
|
627
|
+
admissionDecision = "rejected";
|
|
628
|
+
admissionReason = "base-sha-drift";
|
|
629
|
+
} else if (observedHeadSha !== expectedHeadSha) {
|
|
630
|
+
admissionDecision = "rejected";
|
|
631
|
+
admissionReason = "head-sha-drift";
|
|
632
|
+
} else if (!mergeableAccepted(observedPullRequest)) {
|
|
633
|
+
admissionDecision = "rejected";
|
|
634
|
+
admissionReason = "not-mergeable-on-admission-recheck";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
entry.action = admissionDecision === "planned" ? "would-enqueue" : admissionDecision === "accepted" ? "enqueue" : "skip";
|
|
638
|
+
entry.reason = admissionReason;
|
|
639
|
+
entry.headSha = expectedHeadSha;
|
|
640
|
+
entry.admissionReceipt = admissionReceipt({
|
|
641
|
+
options,
|
|
642
|
+
pr,
|
|
643
|
+
expectedBaseSha: initialBaseSha,
|
|
644
|
+
observedBaseSha,
|
|
645
|
+
expectedHeadSha,
|
|
646
|
+
observedHeadSha,
|
|
647
|
+
decision: admissionDecision,
|
|
648
|
+
reason: admissionReason,
|
|
649
|
+
checks: decision.checks,
|
|
650
|
+
approval: decision.approval,
|
|
651
|
+
predecessor,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
if (admissionDecision === "planned") {
|
|
655
|
+
result.actions.push(entry);
|
|
656
|
+
} else if (admissionDecision === "accepted") {
|
|
657
|
+
if (!decision.pullRequestId) {
|
|
658
|
+
entry.action = "skip";
|
|
659
|
+
entry.reason = "missing-pull-request-node-id";
|
|
660
|
+
entry.admissionReceipt.decision = "rejected";
|
|
661
|
+
entry.admissionReceipt.reason = entry.reason;
|
|
662
|
+
result.skipped.push(entry);
|
|
663
|
+
} else {
|
|
664
|
+
try {
|
|
665
|
+
const queueEntry = await client.enqueuePullRequest({
|
|
666
|
+
pullRequestId: decision.pullRequestId,
|
|
667
|
+
expectedHeadOid: expectedHeadSha,
|
|
668
|
+
});
|
|
669
|
+
entry.action = "enqueued";
|
|
670
|
+
entry.reason = "enqueued-with-expected-head";
|
|
671
|
+
entry.queueEntry = queueEntry;
|
|
672
|
+
entry.admissionReceipt.reason = entry.reason;
|
|
673
|
+
result.actions.push(entry);
|
|
674
|
+
result.enqueued.push(entry);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
entry.action = "skip";
|
|
677
|
+
entry.reason = "enqueue-rejected";
|
|
678
|
+
entry.enqueueError = {
|
|
679
|
+
status: error.status || null,
|
|
680
|
+
message: error.message || "GitHub rejected merge queue admission",
|
|
681
|
+
};
|
|
682
|
+
entry.admissionReceipt.decision = "rejected";
|
|
683
|
+
entry.admissionReceipt.reason = entry.reason;
|
|
684
|
+
result.skipped.push(entry);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
result.skipped.push(entry);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const activePredecessor = queuePredecessor(null, {
|
|
692
|
+
queueEntryId: entry.queueEntry?.id || predecessor?.queueEntryId || "",
|
|
693
|
+
pullRequestNumber: pr.number,
|
|
694
|
+
headSha: expectedHeadSha,
|
|
695
|
+
state: entry.queueEntry?.state || (admissionDecision === "planned" ? "ADMISSION_PLANNED" : "ADMISSION_REJECTED"),
|
|
696
|
+
});
|
|
697
|
+
blockRemainingPullRequests(result, orderedPullRequests, index + 1, options, observedBaseSha || initialBaseSha, activePredecessor);
|
|
698
|
+
break;
|
|
362
699
|
}
|
|
363
700
|
}
|
|
364
701
|
|
|
@@ -372,9 +709,10 @@ export function renderMarkdownSummary(result) {
|
|
|
372
709
|
"",
|
|
373
710
|
`Repository: \`${result.repository}\``,
|
|
374
711
|
`Target branch: \`${result.targetBranch}\``,
|
|
375
|
-
`
|
|
712
|
+
`Landing mode: \`${result.landingMode}\``,
|
|
713
|
+
`Execution: \`${result.dryRun ? "dry-run" : "apply"}\``,
|
|
376
714
|
`Evaluated PRs: ${result.evaluated.length}`,
|
|
377
|
-
`
|
|
715
|
+
`Actions ${result.dryRun ? "planned" : "taken"}: ${result.actions.length}`,
|
|
378
716
|
"",
|
|
379
717
|
"| PR | Action | Reason | Head |",
|
|
380
718
|
"| --- | --- | --- | --- |",
|
|
@@ -410,6 +748,7 @@ async function main() {
|
|
|
410
748
|
sameRepositoryOnly: process.env.BUILDCHAIN_DEV_PR_SAME_REPOSITORY_ONLY,
|
|
411
749
|
maxMerges: process.env.BUILDCHAIN_DEV_PR_MAX_MERGES,
|
|
412
750
|
mergeMethod: process.env.BUILDCHAIN_DEV_PR_MERGE_METHOD,
|
|
751
|
+
landingMode: process.env.BUILDCHAIN_DEV_PR_LANDING_MODE,
|
|
413
752
|
dryRun: process.env.BUILDCHAIN_DEV_PR_DRY_RUN,
|
|
414
753
|
outputPath: process.env.BUILDCHAIN_DEV_PR_OUTPUT_PATH,
|
|
415
754
|
});
|
|
@@ -422,6 +761,8 @@ async function main() {
|
|
|
422
761
|
writeGitHubOutputs({
|
|
423
762
|
"evaluated-count": result.evaluated.length,
|
|
424
763
|
"merged-count": result.merged.length,
|
|
764
|
+
"enqueued-count": result.enqueued.length,
|
|
765
|
+
"action-count": result.actions.length,
|
|
425
766
|
"skipped-count": result.skipped.length,
|
|
426
767
|
"final-base-sha": result.finalBaseSha,
|
|
427
768
|
"result-path": options.outputPath,
|