@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.
Files changed (72) hide show
  1. package/README.md +4 -4
  2. package/actions/promote-buildchain-ref/README.md +8 -0
  3. package/bin/buildchain.mjs +13 -1
  4. package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
  5. package/dist/site/buildchain-contract.json +47 -27
  6. package/dist/site/buildchain-site.json +165 -44
  7. package/dist/site/capability-registry.json +3 -3
  8. package/dist/site/cli-registry.json +40 -4
  9. package/dist/site/controller-registry.json +20 -4
  10. package/dist/site/kfd-claims.json +140 -19
  11. package/dist/site/kfd-upstream-aggregate.json +9 -9
  12. package/dist/site/manual-registry.json +9 -9
  13. package/dist/site/node-api-registry.json +1161 -180
  14. package/dist/site/page-registry.json +152 -31
  15. package/dist/site/public-surface-audit.json +386 -19
  16. package/dist/site/publication-authority-registry.json +61 -1
  17. package/dist/site/publication-registry.json +4 -4
  18. package/dist/site/release-provenance.json +1 -0
  19. package/dist/site/site-manifest.json +13 -13
  20. package/dist/site/workflow-registry.json +151 -13
  21. package/docs/MAP.md +2 -0
  22. package/docs/auditable-demo.md +58 -11
  23. package/docs/aws-us-elastic-runner-burst-plane.md +114 -80
  24. package/docs/cli-reference.md +154 -0
  25. package/docs/dev-alpha-candidate-patrol.md +13 -5
  26. package/docs/dev-delivery-warrant.md +158 -0
  27. package/docs/node-api-reference.md +54 -15
  28. package/docs/publication-authority.md +11 -0
  29. package/docs/release-candidate.md +19 -2
  30. package/docs/release-governance.md +60 -1
  31. package/docs/reusable-build-surface.md +11 -1
  32. package/docs/shifu-gate-profiles.md +12 -1
  33. package/docs/versioning.md +2 -0
  34. package/package.json +4 -2
  35. package/packages/core/buildchain-publication-authority.js +3 -1
  36. package/packages/core/channel-candidate.js +2 -21
  37. package/packages/core/channel-promotion-baseline.js +199 -0
  38. package/packages/core/dev-delivery-candidate-identity.js +94 -0
  39. package/packages/core/dev-delivery-common.js +73 -0
  40. package/packages/core/dev-delivery-proof.js +252 -0
  41. package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
  42. package/packages/core/dev-delivery-warrant-settlement.js +73 -0
  43. package/packages/core/dev-delivery-warrant.js +591 -0
  44. package/scripts/auditable-demo-bundle-verification.mjs +148 -0
  45. package/scripts/auditable-demo-platform.mjs +86 -50
  46. package/scripts/auditable-demo-presentation.mjs +83 -0
  47. package/scripts/auditable-demo-renditions.mjs +264 -0
  48. package/scripts/auditable-demo.mjs +24 -30
  49. package/scripts/aws-windows-jit-campaign-core.mjs +7 -8
  50. package/scripts/aws-windows-jit-controller.mjs +1 -0
  51. package/scripts/aws-windows-jit-core.mjs +1 -1
  52. package/scripts/build-contract-core.mjs +58 -3
  53. package/scripts/buildchain-cli-help.mjs +8 -0
  54. package/scripts/buildchain-patrol.mjs +9 -0
  55. package/scripts/check-inventory.mjs +1 -0
  56. package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
  57. package/scripts/dev-delivery-proof.mjs +193 -0
  58. package/scripts/dev-delivery-warrant.mjs +426 -0
  59. package/scripts/dev-pr-auto-merge.mjs +497 -55
  60. package/scripts/dev-pr-delivery-warrant.mjs +209 -0
  61. package/scripts/dispatch-artifact-signing-authority.mjs +2 -4
  62. package/scripts/gate-profile-core.mjs +24 -0
  63. package/scripts/generate-site-bundle.mjs +2 -2
  64. package/scripts/git-fetch-process-tree.mjs +142 -0
  65. package/scripts/lifecycle-substage-evidence.mjs +274 -0
  66. package/scripts/locked-source-checkout.mjs +6 -3
  67. package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
  68. package/scripts/resolve-build-contract.mjs +7 -0
  69. package/scripts/route-offline-runners.mjs +1 -0
  70. package/scripts/run-lifecycle-core.mjs +9 -9
  71. package/scripts/shifu-gate-profile.mjs +10 -16
  72. 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 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
- ]);
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 ?? "ready").trim(),
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
- if (!state) return pr.mergeable === true;
159
- return ["clean", "has_hooks", "unstable", "unknown", ...(landingMode === "queue" && pr.mergeable === true ? ["blocked"] : [])].includes(state);
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
- if (!mergeableAccepted(detailed, options.landingMode)) {
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.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);
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 options = normalizeOptions({
741
- repository: process.env.BUILDCHAIN_DEV_PR_REPOSITORY || process.env.GITHUB_REPOSITORY,
742
- targetBranch: process.env.BUILDCHAIN_DEV_PR_TARGET_BRANCH || process.env.GITHUB_REF_NAME,
743
- readyLabel: process.env.BUILDCHAIN_DEV_PR_READY_LABEL,
744
- blockLabels: process.env.BUILDCHAIN_DEV_PR_BLOCK_LABELS,
745
- allowedHeadPrefixes: process.env.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES,
746
- requiredChecks: process.env.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS,
747
- requireApproval: process.env.BUILDCHAIN_DEV_PR_REQUIRE_APPROVAL,
748
- sameRepositoryOnly: process.env.BUILDCHAIN_DEV_PR_SAME_REPOSITORY_ONLY,
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]}`) {