@kungfu-tech/buildchain 3.0.6-alpha.1 → 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 +158 -42
- 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 +139 -19
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +9 -9
- package/dist/site/node-api-registry.json +1161 -180
- package/dist/site/page-registry.json +145 -29
- package/dist/site/public-surface-audit.json +385 -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 +150 -13
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +58 -11
- package/docs/aws-us-elastic-runner-burst-plane.md +23 -19
- 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 +52 -0
- 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 +2 -1
- 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/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 +488 -46
- package/scripts/dev-pr-delivery-warrant.mjs +209 -0
- 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,15 +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"]);
|
|
15
|
+
const VALID_WARRANT_MODES = new Set(["off", "required"]);
|
|
11
16
|
const STATIC_SKIP_REASONS = new Set(["draft", "fork-or-cross-repository-head", "head-prefix-not-allowed", "missing-ready-label", "blocked-label"]);
|
|
12
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}$/;
|
|
13
21
|
|
|
14
22
|
function splitList(value, fallback = []) {
|
|
15
23
|
if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
@@ -33,6 +41,11 @@ function intOption(value, fallback) {
|
|
|
33
41
|
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
function positiveIntOption(value, fallback = 0) {
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
function choiceOption(value, valid, fallback, field) {
|
|
37
50
|
const normalized = String(value || fallback).trim().toLowerCase();
|
|
38
51
|
if (!valid.has(normalized)) {
|
|
@@ -52,7 +65,7 @@ function normalizeOptions(options = {}) {
|
|
|
52
65
|
return {
|
|
53
66
|
repository: normalizeRepo(options.repository || process.env.GITHUB_REPOSITORY),
|
|
54
67
|
targetBranch: String(options.targetBranch || "").replace(/^refs\/heads\//, ""),
|
|
55
|
-
readyLabel: String(options.readyLabel
|
|
68
|
+
readyLabel: String(options.readyLabel || DEFAULT_READY_LABEL).trim(),
|
|
56
69
|
blockLabels: splitList(options.blockLabels, DEFAULT_BLOCK_LABELS).map((label) => label.toLowerCase()),
|
|
57
70
|
allowedHeadPrefixes: splitList(options.allowedHeadPrefixes, DEFAULT_ALLOWED_HEAD_PREFIXES),
|
|
58
71
|
requiredChecks: splitList(options.requiredChecks, DEFAULT_REQUIRED_CHECKS),
|
|
@@ -66,9 +79,34 @@ function normalizeOptions(options = {}) {
|
|
|
66
79
|
pollMergeableAttempts: intOption(options.pollMergeableAttempts, 3),
|
|
67
80
|
pollMergeableDelayMs: intOption(options.pollMergeableDelayMs, 1000),
|
|
68
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),
|
|
69
90
|
};
|
|
70
91
|
}
|
|
71
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
|
+
|
|
72
110
|
function labelsOf(pr) {
|
|
73
111
|
return (pr.labels || []).map((label) => String(label.name || label).toLowerCase());
|
|
74
112
|
}
|
|
@@ -147,10 +185,32 @@ function summarizeChecks({ statuses = [], checkRuns = [] } = {}, requiredChecks
|
|
|
147
185
|
};
|
|
148
186
|
}
|
|
149
187
|
|
|
150
|
-
function mergeableAccepted(pr, landingMode = "direct") {
|
|
188
|
+
function mergeableAccepted(pr, landingMode = "direct", projectCutQualified = false) {
|
|
151
189
|
if (pr.mergeable === false) return false;
|
|
152
190
|
const state = String(pr.mergeable_state || pr.mergeStateStatus || "").toLowerCase();
|
|
153
|
-
return state ? ["clean", "has_hooks", "unstable", "unknown", ...(landingMode === "queue" && pr.mergeable === true ? ["blocked"] : [])].includes(state) : pr.mergeable === true;
|
|
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 };
|
|
154
214
|
}
|
|
155
215
|
async function setQueueAdmissionStatus(client, repository, sha, context, state) {
|
|
156
216
|
if (!context) return null;
|
|
@@ -188,7 +248,12 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
188
248
|
observedBaseRef: detailed.base.ref,
|
|
189
249
|
});
|
|
190
250
|
}
|
|
191
|
-
|
|
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)) {
|
|
192
257
|
return skip("not-mergeable", {
|
|
193
258
|
mergeable: detailed.mergeable,
|
|
194
259
|
mergeableState: detailed.mergeable_state || detailed.mergeStateStatus || "",
|
|
@@ -212,6 +277,7 @@ export async function evaluatePullRequest(pr, options, client) {
|
|
|
212
277
|
reason: options.dryRun ? "dry-run" : "eligible",
|
|
213
278
|
checks: checkSummary,
|
|
214
279
|
approval,
|
|
280
|
+
projectCut,
|
|
215
281
|
pullRequestId: detailed.node_id || pr.node_id || "",
|
|
216
282
|
observedHeadSha,
|
|
217
283
|
};
|
|
@@ -418,6 +484,124 @@ export class GitHubClient {
|
|
|
418
484
|
headSha: entry.headCommit?.oid || "",
|
|
419
485
|
};
|
|
420
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
|
+
}
|
|
421
605
|
}
|
|
422
606
|
|
|
423
607
|
function queuePredecessor(queueState, fallback = {}) {
|
|
@@ -450,6 +634,7 @@ function admissionReceipt({
|
|
|
450
634
|
checks,
|
|
451
635
|
approval,
|
|
452
636
|
predecessor,
|
|
637
|
+
projectCut,
|
|
453
638
|
} = {}) {
|
|
454
639
|
return {
|
|
455
640
|
schemaVersion: 1,
|
|
@@ -467,6 +652,7 @@ function admissionReceipt({
|
|
|
467
652
|
decision,
|
|
468
653
|
reason,
|
|
469
654
|
predecessor: predecessor || null,
|
|
655
|
+
projectCut: projectCut || null,
|
|
470
656
|
finalSafetyBoundary: "github-merge-group",
|
|
471
657
|
};
|
|
472
658
|
}
|
|
@@ -505,6 +691,228 @@ function blockRemainingPullRequests(result, pullRequests, startIndex, options, e
|
|
|
505
691
|
}
|
|
506
692
|
}
|
|
507
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
|
+
|
|
508
916
|
export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
509
917
|
const options = normalizeOptions(optionsInput);
|
|
510
918
|
if (!options.targetBranch) throw new Error("target branch is required");
|
|
@@ -513,7 +921,6 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
513
921
|
repository: options.repository,
|
|
514
922
|
apiUrl: optionsInput.apiUrl || process.env.GITHUB_API_URL || "https://api.github.com",
|
|
515
923
|
});
|
|
516
|
-
|
|
517
924
|
const [pullRequests, initialBaseSha, initialQueueState] = await Promise.all([
|
|
518
925
|
client.listPullRequests(options.targetBranch),
|
|
519
926
|
client.getBranchSha(options.targetBranch).catch(() => ""),
|
|
@@ -539,7 +946,6 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
539
946
|
finalBaseSha: initialBaseSha,
|
|
540
947
|
mergeQueue: initialQueueState,
|
|
541
948
|
};
|
|
542
|
-
|
|
543
949
|
if (landingMode === "queue" && !initialQueueState.enabled) {
|
|
544
950
|
blockRemainingPullRequests(result, orderedPullRequests, 0, options, initialBaseSha, null);
|
|
545
951
|
for (const entry of result.evaluated) {
|
|
@@ -547,12 +953,12 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
547
953
|
entry.admissionReceipt.reason = "merge-queue-not-enabled";
|
|
548
954
|
entry.admissionReceipt.decision = "rejected";
|
|
549
955
|
}
|
|
550
|
-
return result;
|
|
956
|
+
return finalizePatrolResult(result);
|
|
551
957
|
}
|
|
552
958
|
|
|
553
959
|
if (landingMode === "queue" && initialQueueState.entries.length > 0) {
|
|
554
960
|
blockRemainingPullRequests(result, orderedPullRequests, 0, options, initialBaseSha, queuePredecessor(initialQueueState));
|
|
555
|
-
return result;
|
|
961
|
+
return finalizePatrolResult(result);
|
|
556
962
|
}
|
|
557
963
|
|
|
558
964
|
for (let index = 0; index < orderedPullRequests.length; index += 1) {
|
|
@@ -564,7 +970,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
564
970
|
continue;
|
|
565
971
|
}
|
|
566
972
|
|
|
567
|
-
const decision = await evaluatePullRequest(pr, options, client);
|
|
973
|
+
const decision = await evaluatePullRequest(pr, { ...options, landingMode }, client);
|
|
568
974
|
const entry = evaluatedEntry(pr, decision);
|
|
569
975
|
result.evaluated.push(entry);
|
|
570
976
|
|
|
@@ -594,6 +1000,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
594
1000
|
reason: decision.reason,
|
|
595
1001
|
checks: decision.checks,
|
|
596
1002
|
approval: decision.approval,
|
|
1003
|
+
projectCut: decision.projectCut,
|
|
597
1004
|
});
|
|
598
1005
|
result.skipped.push(entry);
|
|
599
1006
|
blockRemainingPullRequests(result, orderedPullRequests, index + 1, options, initialBaseSha, queuePredecessor(null, {
|
|
@@ -626,7 +1033,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
626
1033
|
} else if (observedHeadSha !== expectedHeadSha) {
|
|
627
1034
|
admissionDecision = "rejected";
|
|
628
1035
|
admissionReason = "head-sha-drift";
|
|
629
|
-
} else if (!mergeableAccepted(observedPullRequest, landingMode)) {
|
|
1036
|
+
} else if (!mergeableAccepted(observedPullRequest, landingMode, decision.projectCut?.ok === true)) {
|
|
630
1037
|
admissionDecision = "rejected";
|
|
631
1038
|
admissionReason = "not-mergeable-on-admission-recheck";
|
|
632
1039
|
}
|
|
@@ -646,6 +1053,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
646
1053
|
checks: decision.checks,
|
|
647
1054
|
approval: decision.approval,
|
|
648
1055
|
predecessor,
|
|
1056
|
+
projectCut: decision.projectCut,
|
|
649
1057
|
});
|
|
650
1058
|
|
|
651
1059
|
if (admissionDecision === "planned") {
|
|
@@ -671,16 +1079,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
671
1079
|
result.actions.push(entry);
|
|
672
1080
|
result.enqueued.push(entry);
|
|
673
1081
|
} catch (error) {
|
|
674
|
-
|
|
675
|
-
entry.action = "skip";
|
|
676
|
-
entry.reason = "enqueue-rejected";
|
|
677
|
-
entry.enqueueError = {
|
|
678
|
-
status: error.status || null,
|
|
679
|
-
message: error.message || "GitHub rejected merge queue admission",
|
|
680
|
-
};
|
|
681
|
-
entry.admissionReceipt.decision = "rejected";
|
|
682
|
-
entry.admissionReceipt.reason = entry.reason;
|
|
683
|
-
result.skipped.push(entry);
|
|
1082
|
+
await reconcileEnqueueError({ client, options, pr, expectedHeadSha, entry, result, error });
|
|
684
1083
|
}
|
|
685
1084
|
}
|
|
686
1085
|
} else {
|
|
@@ -699,7 +1098,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
|
|
|
699
1098
|
}
|
|
700
1099
|
|
|
701
1100
|
result.finalBaseSha = await client.getBranchSha(options.targetBranch).catch(() => "");
|
|
702
|
-
return result;
|
|
1101
|
+
return finalizePatrolResult(result);
|
|
703
1102
|
}
|
|
704
1103
|
|
|
705
1104
|
export function renderMarkdownSummary(result) {
|
|
@@ -735,38 +1134,81 @@ export function writeGitHubOutputs(outputs, outputFile = process.env.GITHUB_OUTP
|
|
|
735
1134
|
fs.appendFileSync(outputFile, `${lines.join("\n")}\n`);
|
|
736
1135
|
}
|
|
737
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
|
+
|
|
738
1181
|
async function main() {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
maxMerges: process.env.BUILDCHAIN_DEV_PR_MAX_MERGES,
|
|
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]}`) {
|