@kungfu-tech/buildchain 3.0.6-alpha.0 → 3.0.6-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/actions/promote-buildchain-ref/README.md +8 -0
- package/bin/buildchain.mjs +13 -1
- package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
- package/dist/site/buildchain-contract.json +47 -27
- package/dist/site/buildchain-site.json +165 -44
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +40 -4
- package/dist/site/controller-registry.json +20 -4
- package/dist/site/kfd-claims.json +140 -19
- package/dist/site/kfd-upstream-aggregate.json +9 -9
- package/dist/site/manual-registry.json +9 -9
- package/dist/site/node-api-registry.json +1161 -180
- package/dist/site/page-registry.json +152 -31
- package/dist/site/public-surface-audit.json +386 -19
- package/dist/site/publication-authority-registry.json +61 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +151 -13
- package/docs/MAP.md +2 -0
- package/docs/auditable-demo.md +58 -11
- package/docs/aws-us-elastic-runner-burst-plane.md +114 -80
- package/docs/cli-reference.md +154 -0
- package/docs/dev-alpha-candidate-patrol.md +13 -5
- package/docs/dev-delivery-warrant.md +158 -0
- package/docs/node-api-reference.md +54 -15
- package/docs/publication-authority.md +11 -0
- package/docs/release-candidate.md +19 -2
- package/docs/release-governance.md +60 -1
- package/docs/reusable-build-surface.md +11 -1
- package/docs/shifu-gate-profiles.md +12 -1
- package/docs/versioning.md +2 -0
- package/package.json +4 -2
- package/packages/core/buildchain-publication-authority.js +3 -1
- package/packages/core/channel-candidate.js +2 -21
- package/packages/core/channel-promotion-baseline.js +199 -0
- package/packages/core/dev-delivery-candidate-identity.js +94 -0
- package/packages/core/dev-delivery-common.js +73 -0
- package/packages/core/dev-delivery-proof.js +252 -0
- package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
- package/packages/core/dev-delivery-warrant-settlement.js +73 -0
- package/packages/core/dev-delivery-warrant.js +591 -0
- package/scripts/auditable-demo-bundle-verification.mjs +148 -0
- package/scripts/auditable-demo-platform.mjs +86 -50
- package/scripts/auditable-demo-presentation.mjs +83 -0
- package/scripts/auditable-demo-renditions.mjs +264 -0
- package/scripts/auditable-demo.mjs +24 -30
- package/scripts/aws-windows-jit-campaign-core.mjs +7 -8
- package/scripts/aws-windows-jit-controller.mjs +1 -0
- package/scripts/aws-windows-jit-core.mjs +1 -1
- package/scripts/build-contract-core.mjs +58 -3
- package/scripts/buildchain-cli-help.mjs +8 -0
- package/scripts/buildchain-patrol.mjs +9 -0
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
- package/scripts/dev-delivery-proof.mjs +193 -0
- package/scripts/dev-delivery-warrant.mjs +426 -0
- package/scripts/dev-pr-auto-merge.mjs +497 -55
- package/scripts/dev-pr-delivery-warrant.mjs +209 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +2 -4
- package/scripts/gate-profile-core.mjs +24 -0
- package/scripts/generate-site-bundle.mjs +2 -2
- package/scripts/git-fetch-process-tree.mjs +142 -0
- package/scripts/lifecycle-substage-evidence.mjs +274 -0
- package/scripts/locked-source-checkout.mjs +6 -3
- package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
- package/scripts/resolve-build-contract.mjs +7 -0
- package/scripts/route-offline-runners.mjs +1 -0
- package/scripts/run-lifecycle-core.mjs +9 -9
- package/scripts/shifu-gate-profile.mjs +10 -16
- package/scripts/site-capability-metadata.mjs +14 -0
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
2
4
|
import fs from "node:fs";
|
|
3
5
|
import path from "node:path";
|
|
4
|
-
|
|
6
|
+
import { verifyProjectCutReplayProof } from "../packages/core/dev-delivery-warrant.js";
|
|
7
|
+
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission } from "./dev-pr-delivery-warrant.mjs";
|
|
5
8
|
const DEFAULT_BLOCK_LABELS = ["blocked", "do-not-merge", "work-in-progress"];
|
|
6
9
|
const DEFAULT_ALLOWED_HEAD_PREFIXES = ["feature/", "fix/", "chore/", "docs/", "ci/", "refactor/"];
|
|
7
10
|
const DEFAULT_REQUIRED_CHECKS = ["check"];
|
|
11
|
+
const DEFAULT_READY_LABEL = "ready";
|
|
8
12
|
const SUCCESS_STATES = new Set(["success"]);
|
|
9
13
|
const SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
10
14
|
const VALID_LANDING_MODES = new Set(["auto", "direct", "queue"]);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
"fork-or-cross-repository-head",
|
|
14
|
-
"head-prefix-not-allowed",
|
|
15
|
-
"missing-ready-label",
|
|
16
|
-
"blocked-label",
|
|
17
|
-
]);
|
|
15
|
+
const VALID_WARRANT_MODES = new Set(["off", "required"]);
|
|
16
|
+
const STATIC_SKIP_REASONS = new Set(["draft", "fork-or-cross-repository-head", "head-prefix-not-allowed", "missing-ready-label", "blocked-label"]);
|
|
18
17
|
const ADMISSION_CONTRACT = "kungfu-buildchain-dev-merge-queue-admission";
|
|
18
|
+
const AGENT_ADMISSION_RESULT_SCHEMA = "kungfu.buildchain.dev-pr-admission-result/v1";
|
|
19
|
+
const AGENT_ADMISSION_MARKER = "buildchain-dev-pr-admission:v1";
|
|
20
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
19
21
|
|
|
20
22
|
function splitList(value, fallback = []) {
|
|
21
23
|
if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
@@ -39,6 +41,11 @@ function intOption(value, fallback) {
|
|
|
39
41
|
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
function positiveIntOption(value, fallback = 0) {
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
function choiceOption(value, valid, fallback, field) {
|
|
43
50
|
const normalized = String(value || fallback).trim().toLowerCase();
|
|
44
51
|
if (!valid.has(normalized)) {
|
|
@@ -58,10 +65,11 @@ function normalizeOptions(options = {}) {
|
|
|
58
65
|
return {
|
|
59
66
|
repository: normalizeRepo(options.repository || process.env.GITHUB_REPOSITORY),
|
|
60
67
|
targetBranch: String(options.targetBranch || "").replace(/^refs\/heads\//, ""),
|
|
61
|
-
readyLabel: String(options.readyLabel
|
|
68
|
+
readyLabel: String(options.readyLabel || DEFAULT_READY_LABEL).trim(),
|
|
62
69
|
blockLabels: splitList(options.blockLabels, DEFAULT_BLOCK_LABELS).map((label) => label.toLowerCase()),
|
|
63
70
|
allowedHeadPrefixes: splitList(options.allowedHeadPrefixes, DEFAULT_ALLOWED_HEAD_PREFIXES),
|
|
64
71
|
requiredChecks: splitList(options.requiredChecks, DEFAULT_REQUIRED_CHECKS),
|
|
72
|
+
queueAdmissionContext: String(options.queueAdmissionContext || "").trim(),
|
|
65
73
|
requireApproval: boolOption(options.requireApproval, true),
|
|
66
74
|
sameRepositoryOnly: boolOption(options.sameRepositoryOnly, true),
|
|
67
75
|
maxMerges: intOption(options.maxMerges, 1),
|
|
@@ -71,9 +79,34 @@ function normalizeOptions(options = {}) {
|
|
|
71
79
|
pollMergeableAttempts: intOption(options.pollMergeableAttempts, 3),
|
|
72
80
|
pollMergeableDelayMs: intOption(options.pollMergeableDelayMs, 1000),
|
|
73
81
|
outputPath: String(options.outputPath || ".buildchain/dev-pr-auto-merge/result.json"),
|
|
82
|
+
targetPullRequestNumber: positiveIntOption(options.targetPullRequestNumber, 0),
|
|
83
|
+
expectedHeadSha: String(options.expectedHeadSha || "").trim().toLowerCase(),
|
|
84
|
+
diagnosticContext: String(options.diagnosticContext || "Buildchain delivery intent").trim(),
|
|
85
|
+
warrantMode: choiceOption(options.warrantMode, VALID_WARRANT_MODES, "off", "delivery Warrant mode"),
|
|
86
|
+
warrantResultPath: String(options.warrantResultPath || "").trim(),
|
|
87
|
+
projectCutProofPath: String(options.projectCutProofPath || "").trim(),
|
|
88
|
+
sourcePatchRoot: String(options.sourcePatchRoot || "").trim().toLowerCase(),
|
|
89
|
+
qualificationOnly: boolOption(options.qualificationOnly, false),
|
|
74
90
|
};
|
|
75
91
|
}
|
|
76
92
|
|
|
93
|
+
function stableValue(value) {
|
|
94
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
95
|
+
if (value && typeof value === "object") {
|
|
96
|
+
return Object.fromEntries(
|
|
97
|
+
Object.entries(value)
|
|
98
|
+
.filter(([, entry]) => entry !== undefined)
|
|
99
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
100
|
+
.map(([key, entry]) => [key, stableValue(entry)]),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function contentRoot(value) {
|
|
107
|
+
return `sha256:${crypto.createHash("sha256").update(`${JSON.stringify(stableValue(value))}\n`).digest("hex")}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
function labelsOf(pr) {
|
|
78
111
|
return (pr.labels || []).map((label) => String(label.name || label).toLowerCase());
|
|
79
112
|
}
|
|
@@ -152,11 +185,37 @@ function summarizeChecks({ statuses = [], checkRuns = [] } = {}, requiredChecks
|
|
|
152
185
|
};
|
|
153
186
|
}
|
|
154
187
|
|
|
155
|
-
function mergeableAccepted(pr, landingMode = "direct") {
|
|
188
|
+
function mergeableAccepted(pr, landingMode = "direct", projectCutQualified = false) {
|
|
156
189
|
if (pr.mergeable === false) return false;
|
|
157
190
|
const state = String(pr.mergeable_state || pr.mergeStateStatus || "").toLowerCase();
|
|
158
|
-
|
|
159
|
-
|
|
191
|
+
return state ? ["clean", "has_hooks", "unstable", "unknown", ...(landingMode === "queue" && pr.mergeable === true ? ["blocked", ...(projectCutQualified ? ["behind"] : [])] : [])].includes(state) : pr.mergeable === true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function projectCutQualification(pr, options, client) {
|
|
195
|
+
if (!options.projectCutProofPath) return { ok: false, reason: "project-cut-proof-required" };
|
|
196
|
+
let proof;
|
|
197
|
+
try {
|
|
198
|
+
proof = JSON.parse(fs.readFileSync(options.projectCutProofPath, "utf8"));
|
|
199
|
+
} catch {
|
|
200
|
+
return { ok: false, reason: "project-cut-proof-invalid" };
|
|
201
|
+
}
|
|
202
|
+
const currentBase = await client.getBranchSha(options.targetBranch).catch(() => "");
|
|
203
|
+
const verification = verifyProjectCutReplayProof(proof, {
|
|
204
|
+
repository: options.repository.fullName,
|
|
205
|
+
protectedBase: options.targetBranch,
|
|
206
|
+
pullRequestNumber: Number(pr.number),
|
|
207
|
+
sourceHead: String(pr.head?.sha || "").toLowerCase(),
|
|
208
|
+
...(options.sourcePatchRoot ? { sourcePatchRoot: options.sourcePatchRoot } : {}),
|
|
209
|
+
currentBase,
|
|
210
|
+
});
|
|
211
|
+
return verification.ok
|
|
212
|
+
? { ok: true, reason: verification.reason, proofRoot: verification.proofRoot, currentBase }
|
|
213
|
+
: { ok: false, reason: `project-cut-${verification.reason}`, currentBase };
|
|
214
|
+
}
|
|
215
|
+
async function setQueueAdmissionStatus(client, repository, sha, context, state) {
|
|
216
|
+
if (!context) return null;
|
|
217
|
+
await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, { body: { state, context, description: state === "success" ? "Buildchain admitted this exact PR head to the merge queue" : "Buildchain rejected merge queue admission for this exact PR head" } });
|
|
218
|
+
return { context, state, sha };
|
|
160
219
|
}
|
|
161
220
|
|
|
162
221
|
function skip(reason, details = {}) {
|
|
@@ -189,7 +248,12 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
189
248
|
observedBaseRef: detailed.base.ref,
|
|
190
249
|
});
|
|
191
250
|
}
|
|
192
|
-
|
|
251
|
+
const mergeableState = String(detailed.mergeable_state || detailed.mergeStateStatus || "").toLowerCase();
|
|
252
|
+
const projectCut = mergeableState === "behind" && options.landingMode === "queue"
|
|
253
|
+
? await projectCutQualification(detailed, options, client)
|
|
254
|
+
: null;
|
|
255
|
+
if (projectCut && !projectCut.ok) return skip(projectCut.reason, { projectCut });
|
|
256
|
+
if (!mergeableAccepted(detailed, options.landingMode, projectCut?.ok === true)) {
|
|
193
257
|
return skip("not-mergeable", {
|
|
194
258
|
mergeable: detailed.mergeable,
|
|
195
259
|
mergeableState: detailed.mergeable_state || detailed.mergeStateStatus || "",
|
|
@@ -213,6 +277,7 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
213
277
|
reason: options.dryRun ? "dry-run" : "eligible",
|
|
214
278
|
checks: checkSummary,
|
|
215
279
|
approval,
|
|
280
|
+
projectCut,
|
|
216
281
|
pullRequestId: detailed.node_id || pr.node_id || "",
|
|
217
282
|
observedHeadSha,
|
|
218
283
|
};
|
|
@@ -419,6 +484,124 @@ export class GitHubClient {
|
|
|
419
484
|
headSha: entry.headCommit?.oid || "",
|
|
420
485
|
};
|
|
421
486
|
}
|
|
487
|
+
|
|
488
|
+
async addLabels(number, labels) {
|
|
489
|
+
const { data } = await this.request(
|
|
490
|
+
"POST",
|
|
491
|
+
`/repos/${this.repository.owner}/${this.repository.repo}/issues/${number}/labels`,
|
|
492
|
+
{ body: { labels } },
|
|
493
|
+
);
|
|
494
|
+
return data;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async listIssueComments(number) {
|
|
498
|
+
return this.paginate(`/repos/${this.repository.owner}/${this.repository.repo}/issues/${number}/comments?per_page=100`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async createIssueComment(number, body) {
|
|
502
|
+
const { data } = await this.request(
|
|
503
|
+
"POST",
|
|
504
|
+
`/repos/${this.repository.owner}/${this.repository.repo}/issues/${number}/comments`,
|
|
505
|
+
{ body: { body } },
|
|
506
|
+
);
|
|
507
|
+
return data;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async updateIssueComment(commentId, body) {
|
|
511
|
+
const { data } = await this.request(
|
|
512
|
+
"PATCH",
|
|
513
|
+
`/repos/${this.repository.owner}/${this.repository.repo}/issues/comments/${commentId}`,
|
|
514
|
+
{ body: { body } },
|
|
515
|
+
);
|
|
516
|
+
return data;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async setCommitStatus(sha, { state, context, description, targetUrl = "" }) {
|
|
520
|
+
const { data } = await this.request(
|
|
521
|
+
"POST",
|
|
522
|
+
`/repos/${this.repository.owner}/${this.repository.repo}/statuses/${sha}`,
|
|
523
|
+
{ body: { state, context, description, target_url: targetUrl } },
|
|
524
|
+
);
|
|
525
|
+
return data;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function ghJson(args, { input } = {}) {
|
|
530
|
+
const ghEnvironment = { ...process.env };
|
|
531
|
+
delete ghEnvironment.GITHUB_TOKEN;
|
|
532
|
+
delete ghEnvironment.GH_TOKEN;
|
|
533
|
+
const result = spawnSync("gh", args, { encoding: "utf8", input, env: ghEnvironment });
|
|
534
|
+
if (result.error) throw result.error;
|
|
535
|
+
if (result.status !== 0) {
|
|
536
|
+
const error = new Error((result.stderr || result.stdout || "gh command failed").trim());
|
|
537
|
+
error.status = result.status;
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
return result.stdout.trim() ? JSON.parse(result.stdout) : null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export class GhCliClient extends GitHubClient {
|
|
544
|
+
constructor({ repository } = {}) {
|
|
545
|
+
super({ token: "gh-cli", repository, fetchImpl: async () => { throw new Error("unexpected fetch"); } });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async request(method, requestPath, { body } = {}) {
|
|
549
|
+
const endpoint = requestPath.replace(/^https:\/\/api\.github\.com/, "");
|
|
550
|
+
const args = ["api", "--method", method, endpoint];
|
|
551
|
+
if (body !== undefined) args.push("--input", "-");
|
|
552
|
+
return {
|
|
553
|
+
data: ghJson(args, { input: body === undefined ? undefined : `${JSON.stringify(body)}\n` }),
|
|
554
|
+
response: { headers: { get: () => "" } },
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async paginate(requestPath) {
|
|
559
|
+
return ghJson(["api", "--paginate", requestPath, "--slurp"]).flatMap((page) => page);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async getMergeQueueState(branch) {
|
|
563
|
+
const query = `query($owner:String!,$repo:String!,$branch:String!){repository(owner:$owner,name:$repo){mergeQueue(branch:$branch){id entries(first:100){nodes{id position state baseCommit{oid} headCommit{oid} pullRequest{number headRefOid}}}}}}`;
|
|
564
|
+
const data = ghJson([
|
|
565
|
+
"api", "graphql", "-f", `query=${query}`,
|
|
566
|
+
"-f", `owner=${this.repository.owner}`,
|
|
567
|
+
"-f", `repo=${this.repository.repo}`,
|
|
568
|
+
"-f", `branch=${branch}`,
|
|
569
|
+
]).data;
|
|
570
|
+
const queue = data?.repository?.mergeQueue || null;
|
|
571
|
+
return {
|
|
572
|
+
enabled: Boolean(queue),
|
|
573
|
+
id: queue?.id || "",
|
|
574
|
+
entries: (queue?.entries?.nodes || []).map((entry) => ({
|
|
575
|
+
id: entry.id || "",
|
|
576
|
+
position: entry.position,
|
|
577
|
+
state: entry.state || "",
|
|
578
|
+
pullRequestNumber: entry.pullRequest?.number || null,
|
|
579
|
+
pullRequestHeadSha: entry.pullRequest?.headRefOid || "",
|
|
580
|
+
baseSha: entry.baseCommit?.oid || "",
|
|
581
|
+
headSha: entry.headCommit?.oid || "",
|
|
582
|
+
})),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async enqueuePullRequest({ pullRequestId, expectedHeadOid }) {
|
|
587
|
+
const query = `mutation($id:ID!,$head:GitObjectID!){enqueuePullRequest(input:{pullRequestId:$id,expectedHeadOid:$head}){mergeQueueEntry{id position state baseCommit{oid} headCommit{oid} pullRequest{number headRefOid}}}}`;
|
|
588
|
+
const data = ghJson([
|
|
589
|
+
"api", "graphql", "-f", `query=${query}`,
|
|
590
|
+
"-f", `id=${pullRequestId}`,
|
|
591
|
+
"-f", `head=${expectedHeadOid}`,
|
|
592
|
+
]).data;
|
|
593
|
+
const entry = data?.enqueuePullRequest?.mergeQueueEntry;
|
|
594
|
+
if (!entry?.id) throw new Error("GitHub did not return a merge queue entry");
|
|
595
|
+
return {
|
|
596
|
+
id: entry.id,
|
|
597
|
+
position: entry.position,
|
|
598
|
+
state: entry.state || "",
|
|
599
|
+
pullRequestNumber: entry.pullRequest?.number || null,
|
|
600
|
+
pullRequestHeadSha: entry.pullRequest?.headRefOid || "",
|
|
601
|
+
baseSha: entry.baseCommit?.oid || "",
|
|
602
|
+
headSha: entry.headCommit?.oid || "",
|
|
603
|
+
};
|
|
604
|
+
}
|
|
422
605
|
}
|
|
423
606
|
|
|
424
607
|
function queuePredecessor(queueState, fallback = {}) {
|
|
@@ -451,6 +634,7 @@ function admissionReceipt({
|
|
|
451
634
|
checks,
|
|
452
635
|
approval,
|
|
453
636
|
predecessor,
|
|
637
|
+
projectCut,
|
|
454
638
|
} = {}) {
|
|
455
639
|
return {
|
|
456
640
|
schemaVersion: 1,
|
|
@@ -468,6 +652,7 @@ function admissionReceipt({
|
|
|
468
652
|
decision,
|
|
469
653
|
reason,
|
|
470
654
|
predecessor: predecessor || null,
|
|
655
|
+
projectCut: projectCut || null,
|
|
471
656
|
finalSafetyBoundary: "github-merge-group",
|
|
472
657
|
};
|
|
473
658
|
}
|
|
@@ -506,6 +691,228 @@ function blockRemainingPullRequests(result, pullRequests, startIndex, options, e
|
|
|
506
691
|
}
|
|
507
692
|
}
|
|
508
693
|
|
|
694
|
+
function admissionStateFor(entry = {}) {
|
|
695
|
+
if (["enqueued", "merged"].includes(entry.action)) return "queued";
|
|
696
|
+
if (["would-enqueue", "would-merge", "merge"].includes(entry.action)) return "ready";
|
|
697
|
+
if (entry.reason === "missing-approval") return "waiting-approval";
|
|
698
|
+
if (entry.reason === "required-checks-not-passing") return "waiting-checks";
|
|
699
|
+
if (entry.reason === "blocked-by-predecessor") return "waiting-queue";
|
|
700
|
+
if (["head-sha-drift", "base-branch-drift", "base-sha-drift"].includes(entry.reason)) return "stale";
|
|
701
|
+
if (["blocked-label", "fork-or-cross-repository-head"].includes(entry.reason)) return "blocked";
|
|
702
|
+
return "rejected";
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function nextAdmissionAction({ options, state, reason, observedHeadSha = "" }) {
|
|
706
|
+
const command = [
|
|
707
|
+
"buildchain dev pr-admit",
|
|
708
|
+
`--repository ${options.repository.fullName}`,
|
|
709
|
+
`--branch ${options.targetBranch}`,
|
|
710
|
+
`--pull-request ${options.targetPullRequestNumber}`,
|
|
711
|
+
`--expected-head ${observedHeadSha || options.expectedHeadSha}`,
|
|
712
|
+
"--execute",
|
|
713
|
+
].join(" ");
|
|
714
|
+
if (state === "queued") return `Monitor PR #${options.targetPullRequestNumber} and its native merge-group checks.`;
|
|
715
|
+
if (state === "waiting-approval") return `Obtain an independent approval, then rerun: ${command}`;
|
|
716
|
+
if (state === "waiting-checks") return `Wait for or repair required checks, then rerun: ${command}`;
|
|
717
|
+
if (state === "waiting-queue") return `Wait for the active queue predecessor to finish, then rerun: ${command}`;
|
|
718
|
+
if (state === "stale") return `Re-read the PR head and rerun with that exact SHA: ${command}`;
|
|
719
|
+
if (reason === "missing-ready-label") return `Declare the exact delivery intent by running: ${command}`;
|
|
720
|
+
if (state === "ready") return options.dryRun ? `Apply the reviewed plan: ${command}` : "Continue with native merge-group qualification.";
|
|
721
|
+
return `Inspect reason ${reason || "unknown"}, repair it, and rerun the exact-head command.`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function diagnosticState(state) {
|
|
725
|
+
if (["ready", "queued"].includes(state)) return "success";
|
|
726
|
+
if (["waiting-approval", "waiting-checks", "waiting-queue"].includes(state)) return "pending";
|
|
727
|
+
return "failure";
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function renderAdmissionComment(receipt, receiptRoot) {
|
|
731
|
+
const marker = `<!-- ${AGENT_ADMISSION_MARKER} pr=${receipt.pullRequestNumber} head=${receipt.expectedHeadSha} -->`;
|
|
732
|
+
return [
|
|
733
|
+
marker,
|
|
734
|
+
"## Buildchain dev PR admission",
|
|
735
|
+
"",
|
|
736
|
+
`- State: \`${receipt.state}\``,
|
|
737
|
+
`- Reason: \`${receipt.reason}\``,
|
|
738
|
+
`- Exact head: \`${receipt.expectedHeadSha}\``,
|
|
739
|
+
`- Readiness: \`${receipt.readiness.observed ? "present" : "missing"}\` (\`${receipt.readiness.label || "policy-equivalent"}\`)`,
|
|
740
|
+
`- Receipt root: \`${receiptRoot}\``,
|
|
741
|
+
`- Next action: ${receipt.nextAction}`,
|
|
742
|
+
"",
|
|
743
|
+
"GitHub auto-merge state is observed evidence only; it is not Buildchain admission authority.",
|
|
744
|
+
].join("\n");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function publishAdmissionDiagnostic(client, options, receipt, receiptRoot) {
|
|
748
|
+
const marker = `<!-- ${AGENT_ADMISSION_MARKER} pr=${receipt.pullRequestNumber} head=${receipt.expectedHeadSha} -->`;
|
|
749
|
+
const body = renderAdmissionComment(receipt, receiptRoot);
|
|
750
|
+
const comments = await client.listIssueComments(receipt.pullRequestNumber);
|
|
751
|
+
const existing = comments.find((comment) => String(comment.body || "").includes(marker));
|
|
752
|
+
const comment = existing
|
|
753
|
+
? await client.updateIssueComment(existing.id, body)
|
|
754
|
+
: await client.createIssueComment(receipt.pullRequestNumber, body);
|
|
755
|
+
let status = null;
|
|
756
|
+
if (receipt.expectedHeadSha === receipt.observedHeadSha) {
|
|
757
|
+
status = await client.setCommitStatus(receipt.expectedHeadSha, {
|
|
758
|
+
state: diagnosticState(receipt.state),
|
|
759
|
+
context: options.diagnosticContext,
|
|
760
|
+
description: `${receipt.state}: ${receipt.reason}`.slice(0, 140),
|
|
761
|
+
targetUrl: receipt.pullRequestUrl,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return { commentId: comment?.id || null, commentUrl: comment?.html_url || "", statusPublished: Boolean(status) };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function createAdmissionReceipt({ options, pr = {}, state, reason, readiness, decision = {}, queue = null, warrant = null }) {
|
|
768
|
+
return createDevPrAdmissionReceipt({ options, pr, state, reason, readiness, decision, queue, warrant, labels: labelsOf(pr), nextAction: nextAdmissionAction });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function targetedFailure({ options, pr, state, reason, readiness, decision, queue }) {
|
|
772
|
+
const receipt = createAdmissionReceipt({ options, pr, state, reason, readiness, decision, queue });
|
|
773
|
+
return {
|
|
774
|
+
schema: AGENT_ADMISSION_RESULT_SCHEMA,
|
|
775
|
+
ok: false,
|
|
776
|
+
mode: options.dryRun ? "plan" : "execute",
|
|
777
|
+
outcome: "targeted-admission-failed",
|
|
778
|
+
receipt,
|
|
779
|
+
receiptRoot: contentRoot(receipt),
|
|
780
|
+
diagnostic: null,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
785
|
+
const options = normalizeOptions(optionsInput);
|
|
786
|
+
if (!options.targetBranch) throw new Error("target branch is required");
|
|
787
|
+
if (!options.targetPullRequestNumber) throw new Error("pull request number is required");
|
|
788
|
+
if (!SHA_PATTERN.test(options.expectedHeadSha)) throw new Error("expected head must be an exact 40-character lowercase Git SHA");
|
|
789
|
+
const client = clientInput || (optionsInput.useGhCli || !process.env.GITHUB_TOKEN
|
|
790
|
+
? new GhCliClient({ repository: options.repository })
|
|
791
|
+
: new GitHubClient({
|
|
792
|
+
token: optionsInput.token || process.env.GITHUB_TOKEN,
|
|
793
|
+
repository: options.repository,
|
|
794
|
+
apiUrl: optionsInput.apiUrl || process.env.GITHUB_API_URL || "https://api.github.com",
|
|
795
|
+
}));
|
|
796
|
+
|
|
797
|
+
let pr;
|
|
798
|
+
try {
|
|
799
|
+
pr = await client.getPullRequest(options.targetPullRequestNumber, {
|
|
800
|
+
attempts: options.pollMergeableAttempts,
|
|
801
|
+
delayMs: options.pollMergeableDelayMs,
|
|
802
|
+
});
|
|
803
|
+
} catch (error) {
|
|
804
|
+
return targetedFailure({ options, pr: {}, state: "missing", reason: "pull-request-not-found" });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const reject = async (state, reason, readiness = { observed: hasReadyLabel(pr, options.readyLabel), established: false }) => {
|
|
808
|
+
const result = targetedFailure({ options, pr, state, reason, readiness });
|
|
809
|
+
if (!options.dryRun) result.diagnostic = await publishAdmissionDiagnostic(client, options, result.receipt, result.receiptRoot);
|
|
810
|
+
return result;
|
|
811
|
+
};
|
|
812
|
+
if (String(pr.state || "open").toLowerCase() !== "open") return reject("rejected", "pull-request-not-open");
|
|
813
|
+
if (pr.base?.ref !== options.targetBranch) return reject("stale", "base-branch-drift");
|
|
814
|
+
if (String(pr.head?.sha || "").toLowerCase() !== options.expectedHeadSha) return reject("stale", "head-sha-drift");
|
|
815
|
+
if (!sameRepositoryAllowed(pr, options.repository, options.sameRepositoryOnly)) return reject("blocked", "fork-or-cross-repository-head");
|
|
816
|
+
if (hasBlockedLabel(pr, options.blockLabels)) return reject("blocked", "blocked-label");
|
|
817
|
+
if (pr.draft) return reject("blocked", "draft");
|
|
818
|
+
if (!headPrefixAllowed(pr, options.allowedHeadPrefixes)) return reject("blocked", "head-prefix-not-allowed");
|
|
819
|
+
|
|
820
|
+
let readiness = { observed: hasReadyLabel(pr, options.readyLabel), established: false };
|
|
821
|
+
if (!readiness.observed) {
|
|
822
|
+
if (options.dryRun) return reject("rejected", "missing-ready-label", readiness);
|
|
823
|
+
await client.addLabels(pr.number, [options.readyLabel]);
|
|
824
|
+
const readback = await client.getPullRequest(pr.number, {
|
|
825
|
+
attempts: options.pollMergeableAttempts,
|
|
826
|
+
delayMs: options.pollMergeableDelayMs,
|
|
827
|
+
});
|
|
828
|
+
if (String(readback.head?.sha || "").toLowerCase() !== options.expectedHeadSha) {
|
|
829
|
+
pr = readback;
|
|
830
|
+
return reject("stale", "head-sha-drift-after-readiness-write", readiness);
|
|
831
|
+
}
|
|
832
|
+
if (readback.base?.ref !== options.targetBranch) {
|
|
833
|
+
pr = readback;
|
|
834
|
+
return reject("stale", "base-branch-drift-after-readiness-write", readiness);
|
|
835
|
+
}
|
|
836
|
+
pr = readback;
|
|
837
|
+
readiness = { observed: hasReadyLabel(pr, options.readyLabel), established: true };
|
|
838
|
+
if (!readiness.observed) return reject("rejected", "readiness-readback-failed", readiness);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (options.qualificationOnly) {
|
|
842
|
+
return runSourceQualification({
|
|
843
|
+
options, pullRequest: pr, readiness, client, reject,
|
|
844
|
+
evaluate: evaluatePullRequest,
|
|
845
|
+
admissionState: admissionStateFor,
|
|
846
|
+
createReceipt: createAdmissionReceipt,
|
|
847
|
+
root: contentRoot,
|
|
848
|
+
publishDiagnostic: publishAdmissionDiagnostic,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const initialQueue = await client.getMergeQueueState(options.targetBranch);
|
|
853
|
+
let warrant = null;
|
|
854
|
+
try {
|
|
855
|
+
warrant = readDeliveryWarrantResult(options, pr);
|
|
856
|
+
} catch (error) {
|
|
857
|
+
return reject("blocked", error.code || "invalid-delivery-warrant");
|
|
858
|
+
}
|
|
859
|
+
const matchingEntry = initialQueue.entries.find((entry) =>
|
|
860
|
+
entry.pullRequestNumber === pr.number && entry.pullRequestHeadSha === options.expectedHeadSha);
|
|
861
|
+
const existing = await admitExistingQueueEntry({
|
|
862
|
+
options, pullRequest: pr, readiness, client, entry: matchingEntry, warrant,
|
|
863
|
+
createReceipt: createAdmissionReceipt,
|
|
864
|
+
root: contentRoot,
|
|
865
|
+
publishDiagnostic: publishAdmissionDiagnostic,
|
|
866
|
+
});
|
|
867
|
+
if (existing) return existing;
|
|
868
|
+
|
|
869
|
+
return runTargetedQueueAdmission({
|
|
870
|
+
options, pullRequest: pr, readiness, client, warrant,
|
|
871
|
+
runController: runDevPrAutoMerge,
|
|
872
|
+
admissionState: admissionStateFor,
|
|
873
|
+
createReceipt: createAdmissionReceipt,
|
|
874
|
+
root: contentRoot,
|
|
875
|
+
publishDiagnostic: publishAdmissionDiagnostic,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function finalizePatrolResult(result) {
|
|
880
|
+
result.runKind = "cadence-patrol";
|
|
881
|
+
result.outcome = result.evaluated.length === 0
|
|
882
|
+
? "no-op-no-candidates"
|
|
883
|
+
: result.actions.length === 0
|
|
884
|
+
? "no-op-all-skipped"
|
|
885
|
+
: "actions-present";
|
|
886
|
+
result.qualification = false;
|
|
887
|
+
result.noOp = result.actions.length === 0;
|
|
888
|
+
return result;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function reconcileEnqueueError({ client, options, pr, expectedHeadSha, entry, result, error }) {
|
|
892
|
+
const queueReadback = await client.getMergeQueueState(options.targetBranch).catch(() => null);
|
|
893
|
+
const exactEntry = queueReadback?.entries?.find((candidate) =>
|
|
894
|
+
candidate.pullRequestNumber === pr.number && candidate.pullRequestHeadSha === expectedHeadSha);
|
|
895
|
+
if (exactEntry) {
|
|
896
|
+
entry.action = "enqueued";
|
|
897
|
+
entry.reason = "already-enqueued-exact-head";
|
|
898
|
+
entry.queueEntry = exactEntry;
|
|
899
|
+
entry.admissionReceipt.reason = entry.reason;
|
|
900
|
+
result.actions.push(entry);
|
|
901
|
+
result.enqueued.push(entry);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "failure");
|
|
905
|
+
entry.action = "skip";
|
|
906
|
+
entry.reason = "enqueue-rejected";
|
|
907
|
+
entry.enqueueError = {
|
|
908
|
+
status: error.status || null,
|
|
909
|
+
message: error.message || "GitHub rejected merge queue admission",
|
|
910
|
+
};
|
|
911
|
+
entry.admissionReceipt.decision = "rejected";
|
|
912
|
+
entry.admissionReceipt.reason = entry.reason;
|
|
913
|
+
result.skipped.push(entry);
|
|
914
|
+
}
|
|
915
|
+
|
|
509
916
|
export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
510
917
|
const options = normalizeOptions(optionsInput);
|
|
511
918
|
if (!options.targetBranch) throw new Error("target branch is required");
|
|
@@ -514,16 +921,13 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
514
921
|
repository: options.repository,
|
|
515
922
|
apiUrl: optionsInput.apiUrl || process.env.GITHUB_API_URL || "https://api.github.com",
|
|
516
923
|
});
|
|
517
|
-
|
|
518
924
|
const [pullRequests, initialBaseSha, initialQueueState] = await Promise.all([
|
|
519
925
|
client.listPullRequests(options.targetBranch),
|
|
520
926
|
client.getBranchSha(options.targetBranch).catch(() => ""),
|
|
521
927
|
client.getMergeQueueState(options.targetBranch),
|
|
522
928
|
]);
|
|
523
929
|
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;
|
|
930
|
+
const orderedPullRequests = landingMode === "queue" ? [...pullRequests].sort((left, right) => Number(left.number) - Number(right.number)) : pullRequests;
|
|
527
931
|
const result = {
|
|
528
932
|
schemaVersion: 1,
|
|
529
933
|
contract: "kungfu-buildchain-dev-pr-auto-merge",
|
|
@@ -542,7 +946,6 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
542
946
|
finalBaseSha: initialBaseSha,
|
|
543
947
|
mergeQueue: initialQueueState,
|
|
544
948
|
};
|
|
545
|
-
|
|
546
949
|
if (landingMode === "queue" && !initialQueueState.enabled) {
|
|
547
950
|
blockRemainingPullRequests(result, orderedPullRequests, 0, options, initialBaseSha, null);
|
|
548
951
|
for (const entry of result.evaluated) {
|
|
@@ -550,12 +953,12 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
550
953
|
entry.admissionReceipt.reason = "merge-queue-not-enabled";
|
|
551
954
|
entry.admissionReceipt.decision = "rejected";
|
|
552
955
|
}
|
|
553
|
-
return result;
|
|
956
|
+
return finalizePatrolResult(result);
|
|
554
957
|
}
|
|
555
958
|
|
|
556
959
|
if (landingMode === "queue" && initialQueueState.entries.length > 0) {
|
|
557
960
|
blockRemainingPullRequests(result, orderedPullRequests, 0, options, initialBaseSha, queuePredecessor(initialQueueState));
|
|
558
|
-
return result;
|
|
961
|
+
return finalizePatrolResult(result);
|
|
559
962
|
}
|
|
560
963
|
|
|
561
964
|
for (let index = 0; index < orderedPullRequests.length; index += 1) {
|
|
@@ -567,7 +970,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
567
970
|
continue;
|
|
568
971
|
}
|
|
569
972
|
|
|
570
|
-
const decision = await evaluatePullRequest(pr, options, client);
|
|
973
|
+
const decision = await evaluatePullRequest(pr, { ...options, landingMode }, client);
|
|
571
974
|
const entry = evaluatedEntry(pr, decision);
|
|
572
975
|
result.evaluated.push(entry);
|
|
573
976
|
|
|
@@ -597,6 +1000,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
597
1000
|
reason: decision.reason,
|
|
598
1001
|
checks: decision.checks,
|
|
599
1002
|
approval: decision.approval,
|
|
1003
|
+
projectCut: decision.projectCut,
|
|
600
1004
|
});
|
|
601
1005
|
result.skipped.push(entry);
|
|
602
1006
|
blockRemainingPullRequests(result, orderedPullRequests, index + 1, options, initialBaseSha, queuePredecessor(null, {
|
|
@@ -629,7 +1033,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
629
1033
|
} else if (observedHeadSha !== expectedHeadSha) {
|
|
630
1034
|
admissionDecision = "rejected";
|
|
631
1035
|
admissionReason = "head-sha-drift";
|
|
632
|
-
} else if (!mergeableAccepted(observedPullRequest, landingMode)) {
|
|
1036
|
+
} else if (!mergeableAccepted(observedPullRequest, landingMode, decision.projectCut?.ok === true)) {
|
|
633
1037
|
admissionDecision = "rejected";
|
|
634
1038
|
admissionReason = "not-mergeable-on-admission-recheck";
|
|
635
1039
|
}
|
|
@@ -649,6 +1053,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
649
1053
|
checks: decision.checks,
|
|
650
1054
|
approval: decision.approval,
|
|
651
1055
|
predecessor,
|
|
1056
|
+
projectCut: decision.projectCut,
|
|
652
1057
|
});
|
|
653
1058
|
|
|
654
1059
|
if (admissionDecision === "planned") {
|
|
@@ -662,6 +1067,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
662
1067
|
result.skipped.push(entry);
|
|
663
1068
|
} else {
|
|
664
1069
|
try {
|
|
1070
|
+
entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "success");
|
|
665
1071
|
const queueEntry = await client.enqueuePullRequest({
|
|
666
1072
|
pullRequestId: decision.pullRequestId,
|
|
667
1073
|
expectedHeadOid: expectedHeadSha,
|
|
@@ -673,15 +1079,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
673
1079
|
result.actions.push(entry);
|
|
674
1080
|
result.enqueued.push(entry);
|
|
675
1081
|
} catch (error) {
|
|
676
|
-
entry
|
|
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);
|
|
1082
|
+
await reconcileEnqueueError({ client, options, pr, expectedHeadSha, entry, result, error });
|
|
685
1083
|
}
|
|
686
1084
|
}
|
|
687
1085
|
} else {
|
|
@@ -700,7 +1098,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
700
1098
|
}
|
|
701
1099
|
|
|
702
1100
|
result.finalBaseSha = await client.getBranchSha(options.targetBranch).catch(() => "");
|
|
703
|
-
return result;
|
|
1101
|
+
return finalizePatrolResult(result);
|
|
704
1102
|
}
|
|
705
1103
|
|
|
706
1104
|
export function renderMarkdownSummary(result) {
|
|
@@ -736,37 +1134,81 @@ export function writeGitHubOutputs(outputs, outputFile = process.env.GITHUB_OUTP
|
|
|
736
1134
|
fs.appendFileSync(outputFile, `${lines.join("\n")}\n`);
|
|
737
1135
|
}
|
|
738
1136
|
|
|
1137
|
+
function cliValue(args, name, fallback = "") {
|
|
1138
|
+
const index = args.indexOf(`--${name}`);
|
|
1139
|
+
return index === -1 ? fallback : args[index + 1] || "";
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function cliFlag(args, name) {
|
|
1143
|
+
return args.includes(`--${name}`);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
export function cliOptions(args = [], environment = process.env) {
|
|
1147
|
+
const targetPullRequestNumber = cliValue(args, "pull-request", environment.BUILDCHAIN_DEV_PR_EXPECTED_PR_NUMBER);
|
|
1148
|
+
return {
|
|
1149
|
+
repository: cliValue(args, "repository", environment.BUILDCHAIN_DEV_PR_REPOSITORY || environment.GITHUB_REPOSITORY),
|
|
1150
|
+
targetBranch: cliValue(args, "branch", environment.BUILDCHAIN_DEV_PR_TARGET_BRANCH || environment.GITHUB_REF_NAME),
|
|
1151
|
+
targetPullRequestNumber,
|
|
1152
|
+
expectedHeadSha: cliValue(args, "expected-head", environment.BUILDCHAIN_DEV_PR_EXPECTED_HEAD_SHA),
|
|
1153
|
+
readyLabel: cliValue(args, "ready-label", environment.BUILDCHAIN_DEV_PR_READY_LABEL || DEFAULT_READY_LABEL),
|
|
1154
|
+
blockLabels: cliValue(args, "block-labels", environment.BUILDCHAIN_DEV_PR_BLOCK_LABELS),
|
|
1155
|
+
allowedHeadPrefixes: cliValue(args, "allowed-head-prefixes", environment.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES),
|
|
1156
|
+
requiredChecks: cliValue(args, "required-checks", environment.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS),
|
|
1157
|
+
queueAdmissionContext: cliValue(args, "queue-admission-context", environment.BUILDCHAIN_DEV_PR_QUEUE_ADMISSION_CONTEXT),
|
|
1158
|
+
diagnosticContext: cliValue(args, "diagnostic-context", environment.BUILDCHAIN_DEV_PR_DIAGNOSTIC_CONTEXT),
|
|
1159
|
+
warrantMode: cliValue(args, "warrant-mode", environment.BUILDCHAIN_DEV_PR_WARRANT_MODE),
|
|
1160
|
+
warrantResultPath: cliValue(args, "warrant-result", environment.BUILDCHAIN_DEV_PR_WARRANT_RESULT_PATH),
|
|
1161
|
+
projectCutProofPath: cliValue(args, "project-cut-proof", environment.BUILDCHAIN_DEV_PR_PROJECT_CUT_PROOF_PATH),
|
|
1162
|
+
sourcePatchRoot: cliValue(args, "source-patch-root", environment.BUILDCHAIN_DEV_PR_SOURCE_PATCH_ROOT),
|
|
1163
|
+
qualificationOnly: cliFlag(args, "qualification-only"),
|
|
1164
|
+
requireApproval: environment.BUILDCHAIN_DEV_PR_REQUIRE_APPROVAL,
|
|
1165
|
+
sameRepositoryOnly: environment.BUILDCHAIN_DEV_PR_SAME_REPOSITORY_ONLY,
|
|
1166
|
+
maxMerges: environment.BUILDCHAIN_DEV_PR_MAX_MERGES,
|
|
1167
|
+
mergeMethod: environment.BUILDCHAIN_DEV_PR_MERGE_METHOD,
|
|
1168
|
+
landingMode: cliValue(args, "landing-mode", environment.BUILDCHAIN_DEV_PR_LANDING_MODE),
|
|
1169
|
+
dryRun: cliFlag(args, "execute") ? false : environment.BUILDCHAIN_DEV_PR_DRY_RUN,
|
|
1170
|
+
outputPath: cliValue(
|
|
1171
|
+
args,
|
|
1172
|
+
"output",
|
|
1173
|
+
environment.BUILDCHAIN_DEV_PR_OUTPUT_PATH || (targetPullRequestNumber
|
|
1174
|
+
? ".buildchain/dev-pr-admission/result.json"
|
|
1175
|
+
: ".buildchain/dev-pr-auto-merge/result.json"),
|
|
1176
|
+
),
|
|
1177
|
+
useGhCli: cliFlag(args, "gh-cli"),
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
739
1181
|
async function main() {
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
mergeMethod: process.env.BUILDCHAIN_DEV_PR_MERGE_METHOD,
|
|
751
|
-
landingMode: process.env.BUILDCHAIN_DEV_PR_LANDING_MODE,
|
|
752
|
-
dryRun: process.env.BUILDCHAIN_DEV_PR_DRY_RUN,
|
|
753
|
-
outputPath: process.env.BUILDCHAIN_DEV_PR_OUTPUT_PATH,
|
|
754
|
-
});
|
|
755
|
-
const result = await runDevPrAutoMerge(options);
|
|
1182
|
+
const args = process.argv.slice(2);
|
|
1183
|
+
if (cliFlag(args, "help")) {
|
|
1184
|
+
process.stdout.write("Usage:\n buildchain dev pr-admit --repository owner/repo --branch dev/vN/vN.M --pull-request N --expected-head SHA [--qualification-only] [--landing-mode auto|direct|queue] [--warrant-mode off|required] [--warrant-result FILE] [--execute] [--output FILE] [--json]\n");
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const options = normalizeOptions(cliOptions(args));
|
|
1188
|
+
const targeted = options.targetPullRequestNumber > 0 || Boolean(options.expectedHeadSha);
|
|
1189
|
+
const result = targeted
|
|
1190
|
+
? await runDevPrAdmission({ ...options, useGhCli: cliFlag(args, "gh-cli") })
|
|
1191
|
+
: await runDevPrAutoMerge(options);
|
|
756
1192
|
fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
|
|
757
1193
|
fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
758
|
-
const summary = renderMarkdownSummary(result);
|
|
1194
|
+
const summary = targeted ? `${renderAdmissionComment(result.receipt, result.receiptRoot)}\n` : renderMarkdownSummary(result);
|
|
759
1195
|
if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
|
|
1196
|
+
else if (cliFlag(args, "json")) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
760
1197
|
else process.stdout.write(summary);
|
|
761
1198
|
writeGitHubOutputs({
|
|
762
|
-
"evaluated-count": result.evaluated.length,
|
|
763
|
-
"merged-count": result.merged.length,
|
|
764
|
-
"enqueued-count": result.enqueued.length,
|
|
765
|
-
"action-count": result.actions.length,
|
|
766
|
-
"skipped-count": result.skipped.length,
|
|
767
|
-
"final-base-sha": result.finalBaseSha,
|
|
1199
|
+
"evaluated-count": targeted ? 1 : result.evaluated.length,
|
|
1200
|
+
"merged-count": targeted ? Number(result.receipt.state === "merged") : result.merged.length,
|
|
1201
|
+
"enqueued-count": targeted ? Number(result.receipt.state === "queued") : result.enqueued.length,
|
|
1202
|
+
"action-count": targeted ? Number(result.ok) : result.actions.length,
|
|
1203
|
+
"skipped-count": targeted ? Number(!result.ok) : result.skipped.length,
|
|
1204
|
+
"final-base-sha": targeted ? "" : result.finalBaseSha,
|
|
1205
|
+
"targeted": targeted,
|
|
1206
|
+
"targeted-ok": targeted ? result.ok : "",
|
|
1207
|
+
"admission-state": targeted ? result.receipt.state : "",
|
|
1208
|
+
"receipt-root": targeted ? result.receiptRoot : "",
|
|
768
1209
|
"result-path": options.outputPath,
|
|
769
1210
|
});
|
|
1211
|
+
if (targeted && !result.ok) process.exitCode = 1;
|
|
770
1212
|
}
|
|
771
1213
|
|
|
772
1214
|
if (import.meta.url === `file://${process.argv[1]}`) {
|