@kungfu-tech/buildchain 2.14.1-alpha.1 → 2.14.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 +5 -8
- 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 +6 -11
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +4 -9
- package/docs/release-governance.md +3 -16
- package/package.json +1 -1
- package/scripts/buildchain-patrol.mjs +1 -3
- package/scripts/dev-pr-auto-merge.mjs +20 -361
|
@@ -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:55:24.253Z",
|
|
5
|
+
"publishedAt": "2026-07-17T00:55:24.253Z",
|
|
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": "d962181c3023e587488a5276b10307007cc5a13f",
|
|
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
|
|
35
|
+
"version": "2.14.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:55:24.253Z",
|
|
5
|
+
"publishedAt": "2026-07-17T00:55:24.253Z",
|
|
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": "d962181c3023e587488a5276b10307007cc5a13f",
|
|
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
|
|
40
|
+
"version": "2.14.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:5cf4bf4332119531f6aaa1c123d7f64e6af4a61611c4e4a33d40bd9fb46448cf"
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
243
|
"id": "release-flow",
|
|
@@ -879,7 +879,6 @@
|
|
|
879
879
|
"cadence",
|
|
880
880
|
"capabilities",
|
|
881
881
|
"dry-run",
|
|
882
|
-
"landing-mode",
|
|
883
882
|
"max-actions",
|
|
884
883
|
"merge-method",
|
|
885
884
|
"mode",
|
|
@@ -889,7 +888,7 @@
|
|
|
889
888
|
"same-repository-only",
|
|
890
889
|
"target-branch"
|
|
891
890
|
],
|
|
892
|
-
"inputCount":
|
|
891
|
+
"inputCount": 15,
|
|
893
892
|
"secrets": [
|
|
894
893
|
"github-token"
|
|
895
894
|
],
|
|
@@ -1002,7 +1001,6 @@
|
|
|
1002
1001
|
"buildchain-ref",
|
|
1003
1002
|
"buildchain-repository",
|
|
1004
1003
|
"dry-run",
|
|
1005
|
-
"landing-mode",
|
|
1006
1004
|
"max-merges",
|
|
1007
1005
|
"merge-method",
|
|
1008
1006
|
"ready-label",
|
|
@@ -1011,20 +1009,18 @@
|
|
|
1011
1009
|
"same-repository-only",
|
|
1012
1010
|
"target-branch"
|
|
1013
1011
|
],
|
|
1014
|
-
"inputCount":
|
|
1012
|
+
"inputCount": 12,
|
|
1015
1013
|
"secrets": [
|
|
1016
1014
|
"github-token"
|
|
1017
1015
|
],
|
|
1018
1016
|
"secretCount": 1,
|
|
1019
1017
|
"outputs": [
|
|
1020
|
-
"action-count",
|
|
1021
|
-
"enqueued-count",
|
|
1022
1018
|
"evaluated-count",
|
|
1023
1019
|
"final-base-sha",
|
|
1024
1020
|
"merged-count",
|
|
1025
1021
|
"skipped-count"
|
|
1026
1022
|
],
|
|
1027
|
-
"outputCount":
|
|
1023
|
+
"outputCount": 4,
|
|
1028
1024
|
"surface": "dev-governance",
|
|
1029
1025
|
"capabilityGroup": "governance-versioning",
|
|
1030
1026
|
"status": "active"
|
|
@@ -1166,7 +1162,6 @@
|
|
|
1166
1162
|
"buildchain-ref",
|
|
1167
1163
|
"buildchain-repository",
|
|
1168
1164
|
"dry-run",
|
|
1169
|
-
"landing-mode",
|
|
1170
1165
|
"max-actions",
|
|
1171
1166
|
"merge-method",
|
|
1172
1167
|
"ready-label",
|
|
@@ -1175,7 +1170,7 @@
|
|
|
1175
1170
|
"same-repository-only",
|
|
1176
1171
|
"target-branch"
|
|
1177
1172
|
],
|
|
1178
|
-
"inputCount":
|
|
1173
|
+
"inputCount": 12,
|
|
1179
1174
|
"secrets": [
|
|
1180
1175
|
"github-token"
|
|
1181
1176
|
],
|
|
@@ -435,21 +435,9 @@ 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
|
-
|
|
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.
|
|
438
|
+
After each merge, the next PR is re-evaluated before it can move the protected
|
|
439
|
+
dev branch. This prevents one merge from silently making the next candidate
|
|
440
|
+
stale or conflicting.
|
|
453
441
|
|
|
454
442
|
The canonical consumer required check context is `check / check`, matching the
|
|
455
443
|
reusable workflow call plus its `check` job. Buildchain's own `Verify` workflow
|
|
@@ -509,7 +497,6 @@ jobs:
|
|
|
509
497
|
ready-label: ready
|
|
510
498
|
block-labels: blocked,do-not-merge
|
|
511
499
|
max-merges: 1
|
|
512
|
-
landing-mode: auto
|
|
513
500
|
dry-run: ${{ inputs.dry-run || false }}
|
|
514
501
|
```
|
|
515
502
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.14.1
|
|
3
|
+
"version": "2.14.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,7 +91,6 @@ 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(),
|
|
95
94
|
dryRun: boolOption(options.dryRun ?? process.env.BUILDCHAIN_PATROL_DRY_RUN, true),
|
|
96
95
|
outputPath: String(options.outputPath || process.env.BUILDCHAIN_PATROL_OUTPUT_PATH || DEFAULT_OUTPUT_PATH),
|
|
97
96
|
};
|
|
@@ -168,7 +167,6 @@ export async function runBuildchainPatrol(optionsInput = {}, clientInput) {
|
|
|
168
167
|
sameRepositoryOnly: options.sameRepositoryOnly,
|
|
169
168
|
maxMerges: options.maxActions,
|
|
170
169
|
mergeMethod: options.mergeMethod,
|
|
171
|
-
landingMode: options.landingMode,
|
|
172
170
|
dryRun: options.dryRun,
|
|
173
171
|
outputPath: path.join(path.dirname(options.outputPath), "dev-pr-auto-merge.json"),
|
|
174
172
|
},
|
|
@@ -180,7 +178,7 @@ export async function runBuildchainPatrol(optionsInput = {}, clientInput) {
|
|
|
180
178
|
result: mergeResult,
|
|
181
179
|
});
|
|
182
180
|
result.summary.evaluatedCount += mergeResult.evaluated.length;
|
|
183
|
-
result.summary.actionCount += mergeResult.
|
|
181
|
+
result.summary.actionCount += mergeResult.merged.length;
|
|
184
182
|
result.summary.skippedCount += mergeResult.skipped.length;
|
|
185
183
|
}
|
|
186
184
|
|
|
@@ -7,15 +7,6 @@ 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";
|
|
19
10
|
|
|
20
11
|
function splitList(value, fallback = []) {
|
|
21
12
|
if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
@@ -39,14 +30,6 @@ function intOption(value, fallback) {
|
|
|
39
30
|
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
|
|
40
31
|
}
|
|
41
32
|
|
|
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
|
-
|
|
50
33
|
function normalizeRepo(value) {
|
|
51
34
|
const text = String(value || "").trim();
|
|
52
35
|
const match = text.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
@@ -66,7 +49,6 @@ function normalizeOptions(options = {}) {
|
|
|
66
49
|
sameRepositoryOnly: boolOption(options.sameRepositoryOnly, true),
|
|
67
50
|
maxMerges: intOption(options.maxMerges, 1),
|
|
68
51
|
mergeMethod: String(options.mergeMethod || "merge").trim(),
|
|
69
|
-
landingMode: choiceOption(options.landingMode, VALID_LANDING_MODES, "auto", "landing mode"),
|
|
70
52
|
dryRun: boolOption(options.dryRun, true),
|
|
71
53
|
pollMergeableAttempts: intOption(options.pollMergeableAttempts, 3),
|
|
72
54
|
pollMergeableDelayMs: intOption(options.pollMergeableDelayMs, 1000),
|
|
@@ -183,12 +165,6 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
183
165
|
attempts: options.pollMergeableAttempts,
|
|
184
166
|
delayMs: options.pollMergeableDelayMs,
|
|
185
167
|
});
|
|
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
|
-
}
|
|
192
168
|
if (!mergeableAccepted(detailed)) {
|
|
193
169
|
return skip("not-mergeable", {
|
|
194
170
|
mergeable: detailed.mergeable,
|
|
@@ -196,25 +172,19 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
196
172
|
});
|
|
197
173
|
}
|
|
198
174
|
|
|
199
|
-
const approval = { required: options.requireApproval, passed: true };
|
|
200
175
|
if (options.requireApproval) {
|
|
201
176
|
const reviews = await client.listReviews(pr.number);
|
|
202
|
-
|
|
203
|
-
if (!approval.passed) return skip("missing-approval", { approval });
|
|
177
|
+
if (!hasRequiredApproval(reviews)) return skip("missing-approval");
|
|
204
178
|
}
|
|
205
179
|
|
|
206
|
-
const
|
|
207
|
-
const checks = await client.listCommitChecks(observedHeadSha);
|
|
180
|
+
const checks = await client.listCommitChecks(pr.head?.sha);
|
|
208
181
|
const checkSummary = summarizeChecks(checks, options.requiredChecks);
|
|
209
|
-
if (!checkSummary.passed) return skip("required-checks-not-passing", { checks: checkSummary
|
|
182
|
+
if (!checkSummary.passed) return skip("required-checks-not-passing", { checks: checkSummary });
|
|
210
183
|
|
|
211
184
|
return {
|
|
212
185
|
action: options.dryRun ? "would-merge" : "merge",
|
|
213
186
|
reason: options.dryRun ? "dry-run" : "eligible",
|
|
214
187
|
checks: checkSummary,
|
|
215
|
-
approval,
|
|
216
|
-
pullRequestId: detailed.node_id || pr.node_id || "",
|
|
217
|
-
observedHeadSha,
|
|
218
188
|
};
|
|
219
189
|
}
|
|
220
190
|
|
|
@@ -332,178 +302,6 @@ export class GitHubClient {
|
|
|
332
302
|
const { data } = await this.request("GET", `/repos/${this.repository.owner}/${this.repository.repo}/git/ref/${ref}`);
|
|
333
303
|
return data.object?.sha || "";
|
|
334
304
|
}
|
|
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
|
-
}
|
|
507
305
|
}
|
|
508
306
|
|
|
509
307
|
export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
@@ -515,51 +313,21 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
515
313
|
apiUrl: optionsInput.apiUrl || process.env.GITHUB_API_URL || "https://api.github.com",
|
|
516
314
|
});
|
|
517
315
|
|
|
518
|
-
const
|
|
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;
|
|
316
|
+
const pullRequests = await client.listPullRequests(options.targetBranch);
|
|
527
317
|
const result = {
|
|
528
318
|
schemaVersion: 1,
|
|
529
319
|
contract: "kungfu-buildchain-dev-pr-auto-merge",
|
|
530
320
|
repository: options.repository.fullName,
|
|
531
321
|
targetBranch: options.targetBranch,
|
|
532
|
-
requestedLandingMode: options.landingMode,
|
|
533
|
-
landingMode,
|
|
534
322
|
dryRun: options.dryRun,
|
|
535
323
|
maxMerges: options.maxMerges,
|
|
536
324
|
evaluated: [],
|
|
537
|
-
actions: [],
|
|
538
325
|
merged: [],
|
|
539
|
-
enqueued: [],
|
|
540
326
|
skipped: [],
|
|
541
|
-
|
|
542
|
-
finalBaseSha: initialBaseSha,
|
|
543
|
-
mergeQueue: initialQueueState,
|
|
327
|
+
finalBaseSha: "",
|
|
544
328
|
};
|
|
545
329
|
|
|
546
|
-
|
|
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];
|
|
330
|
+
for (const pr of pullRequests) {
|
|
563
331
|
if (result.merged.length >= options.maxMerges) {
|
|
564
332
|
const entry = { number: pr.number, title: pr.title || "", action: "skip", reason: "max-merges-reached" };
|
|
565
333
|
result.evaluated.push(entry);
|
|
@@ -568,134 +336,29 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
568
336
|
}
|
|
569
337
|
|
|
570
338
|
const decision = await evaluatePullRequest(pr, options, client);
|
|
571
|
-
const entry =
|
|
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
|
+
};
|
|
572
348
|
result.evaluated.push(entry);
|
|
573
349
|
|
|
574
|
-
if (
|
|
350
|
+
if (decision.action === "merge") {
|
|
575
351
|
const mergeResult = await client.mergePullRequest(pr.number, {
|
|
576
352
|
method: options.mergeMethod,
|
|
577
353
|
sha: pr.head?.sha,
|
|
578
354
|
});
|
|
579
355
|
const mergedEntry = { ...entry, mergeSha: mergeResult.sha || "" };
|
|
580
356
|
result.merged.push(mergedEntry);
|
|
581
|
-
result.actions.push(mergedEntry);
|
|
582
357
|
result.evaluated[result.evaluated.length - 1] = mergedEntry;
|
|
583
|
-
} else if (
|
|
358
|
+
} else if (decision.action === "would-merge") {
|
|
584
359
|
result.merged.push(entry);
|
|
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
|
-
});
|
|
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
360
|
} else {
|
|
609
|
-
|
|
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;
|
|
361
|
+
result.skipped.push(entry);
|
|
699
362
|
}
|
|
700
363
|
}
|
|
701
364
|
|
|
@@ -709,10 +372,9 @@ export function renderMarkdownSummary(result) {
|
|
|
709
372
|
"",
|
|
710
373
|
`Repository: \`${result.repository}\``,
|
|
711
374
|
`Target branch: \`${result.targetBranch}\``,
|
|
712
|
-
`
|
|
713
|
-
`Execution: \`${result.dryRun ? "dry-run" : "apply"}\``,
|
|
375
|
+
`Mode: \`${result.dryRun ? "dry-run" : "merge"}\``,
|
|
714
376
|
`Evaluated PRs: ${result.evaluated.length}`,
|
|
715
|
-
`
|
|
377
|
+
`Eligible ${result.dryRun ? "dry-run" : "merged"} PRs: ${result.merged.length}`,
|
|
716
378
|
"",
|
|
717
379
|
"| PR | Action | Reason | Head |",
|
|
718
380
|
"| --- | --- | --- | --- |",
|
|
@@ -748,7 +410,6 @@ async function main() {
|
|
|
748
410
|
sameRepositoryOnly: process.env.BUILDCHAIN_DEV_PR_SAME_REPOSITORY_ONLY,
|
|
749
411
|
maxMerges: process.env.BUILDCHAIN_DEV_PR_MAX_MERGES,
|
|
750
412
|
mergeMethod: process.env.BUILDCHAIN_DEV_PR_MERGE_METHOD,
|
|
751
|
-
landingMode: process.env.BUILDCHAIN_DEV_PR_LANDING_MODE,
|
|
752
413
|
dryRun: process.env.BUILDCHAIN_DEV_PR_DRY_RUN,
|
|
753
414
|
outputPath: process.env.BUILDCHAIN_DEV_PR_OUTPUT_PATH,
|
|
754
415
|
});
|
|
@@ -761,8 +422,6 @@ async function main() {
|
|
|
761
422
|
writeGitHubOutputs({
|
|
762
423
|
"evaluated-count": result.evaluated.length,
|
|
763
424
|
"merged-count": result.merged.length,
|
|
764
|
-
"enqueued-count": result.enqueued.length,
|
|
765
|
-
"action-count": result.actions.length,
|
|
766
425
|
"skipped-count": result.skipped.length,
|
|
767
426
|
"final-base-sha": result.finalBaseSha,
|
|
768
427
|
"result-path": options.outputPath,
|