@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.0

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 (83) hide show
  1. package/README.md +31 -0
  2. package/actions/macos-credential-island/README.md +8 -0
  3. package/bin/buildchain.mjs +1 -1
  4. package/contracts/auditable-demo-media-profiles-v1.json +61 -0
  5. package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
  6. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
  8. package/dist/site/buildchain-contract.json +107 -26
  9. package/dist/site/buildchain-site.json +121 -49
  10. package/dist/site/capability-registry.json +3 -3
  11. package/dist/site/cli-registry.json +9 -3
  12. package/dist/site/controller-registry.json +27 -3
  13. package/dist/site/kfd-claims.json +101 -15
  14. package/dist/site/kfd-upstream-aggregate.json +1 -1
  15. package/dist/site/manual-registry.json +10 -9
  16. package/dist/site/node-api-registry.json +540 -31
  17. package/dist/site/page-registry.json +105 -29
  18. package/dist/site/public-surface-audit.json +184 -11
  19. package/dist/site/publication-authority-registry.json +62 -1
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/site-manifest.json +13 -13
  22. package/dist/site/workflow-registry.json +118 -4
  23. package/docs/MAP.md +4 -1
  24. package/docs/auditable-demo.md +90 -7
  25. package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
  26. package/docs/cli-reference.md +35 -2
  27. package/docs/dev-alpha-candidate-patrol.md +14 -5
  28. package/docs/dev-qualification-patrol.md +108 -0
  29. package/docs/node-api-reference.md +123 -72
  30. package/docs/publish-transaction.md +7 -5
  31. package/docs/release-flow.md +4 -0
  32. package/docs/release-governance.md +39 -0
  33. package/docs/release-propagation.md +93 -0
  34. package/docs/reusable-build-surface.md +53 -12
  35. package/docs/stable-candidate-patrol.md +5 -2
  36. package/package.json +1 -1
  37. package/packages/core/buildchain-contract.js +36 -1
  38. package/packages/core/buildchain-publication-authority.js +3 -0
  39. package/packages/core/public-surface-audit.js +4 -35
  40. package/packages/core/release-propagation-agent-entry.js +185 -0
  41. package/packages/core/release-propagation-pickup.js +387 -0
  42. package/packages/core/release-propagation-push.js +293 -0
  43. package/packages/core/release-propagation-release.js +61 -18
  44. package/packages/core/release-propagation-stage-evidence.js +36 -0
  45. package/packages/core/release-propagation-work.js +8 -4
  46. package/packages/core/release-propagation.js +27 -2
  47. package/packages/core/workflow-call-contract.js +308 -0
  48. package/packages/core/workflow-yaml-contract.js +272 -0
  49. package/scripts/artifact-signing-controller-core.mjs +525 -0
  50. package/scripts/artifact-signing-controller.mjs +302 -0
  51. package/scripts/artifact-signing-delegation.mjs +51 -3
  52. package/scripts/auditable-demo-platform.mjs +564 -0
  53. package/scripts/auditable-demo-renditions.mjs +7 -2
  54. package/scripts/auditable-demo-transport-smoke.mjs +176 -0
  55. package/scripts/auditable-demo.mjs +27 -21
  56. package/scripts/aws-macos-jit-controller-core.mjs +389 -0
  57. package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
  58. package/scripts/aws-macos-jit-controller.mjs +558 -0
  59. package/scripts/aws-macos-jit-job-controller.mjs +401 -0
  60. package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
  61. package/scripts/aws-windows-jit-campaign.mjs +202 -0
  62. package/scripts/aws-windows-jit-controller-core.mjs +20 -2
  63. package/scripts/aws-windows-jit-controller.mjs +184 -109
  64. package/scripts/aws-windows-jit-core.mjs +21 -3
  65. package/scripts/aws-windows-jit.mjs +2 -0
  66. package/scripts/build-standalone-binary.mjs +6 -0
  67. package/scripts/buildchain-cli-help.mjs +2 -1
  68. package/scripts/capture-package-release-propagation.mjs +24 -1
  69. package/scripts/check-inventory.mjs +6 -0
  70. package/scripts/compiler-cache-evidence.mjs +90 -7
  71. package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
  72. package/scripts/dev-pr-auto-merge.mjs +4 -4
  73. package/scripts/dev-qualification-patrol.mjs +594 -0
  74. package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
  75. package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
  76. package/scripts/generate-site-bundle.mjs +5 -0
  77. package/scripts/inspect-artifact-signing-requests.mjs +12 -0
  78. package/scripts/locked-source-checkout.mjs +17 -3
  79. package/scripts/release-propagation.mjs +150 -2
  80. package/scripts/run-lifecycle-core.mjs +25 -0
  81. package/scripts/site-capability-metadata.mjs +1 -1
  82. package/scripts/stable-candidate-patrol.mjs +30 -4
  83. package/scripts/workflow-call-contract.mjs +133 -0
@@ -10,9 +10,9 @@ export const WINDOWS_EC2_JIT = Object.freeze({
10
10
  instanceType: "c7i.4xlarge",
11
11
  pricePerHourUsd: 1.45,
12
12
  maximumInstanceLifetimeMinutes: 180,
13
- maxConcurrentInstances: 2,
14
- maxAcceptedInstances: 6,
15
- budgetLimitUsd: 40,
13
+ maxConcurrentInstances: 1,
14
+ maxAcceptedInstances: 5,
15
+ budgetLimitUsd: 80,
16
16
  minimumSmokeJobs: 1,
17
17
  minimumFullJobs: 3,
18
18
  maximumCleanupLatencySeconds: 900,
@@ -75,6 +75,14 @@ export function windowsJitRunnerLabels(value) {
75
75
  return ["self-hosted", "Windows", "X64", windowsJitRunnerLabel(value)];
76
76
  }
77
77
 
78
+ export function windowsJitCampaignId(value) {
79
+ const campaign = String(value || "").trim();
80
+ if (!/^win-[a-z0-9][a-z0-9-]{2,15}$/.test(campaign)) {
81
+ throw new Error("campaignId must be a bounded Windows campaign id");
82
+ }
83
+ return campaign;
84
+ }
85
+
78
86
  export function renderWindowsJitBootstrap(template, values = {}) {
79
87
  const runnerLabel = windowsJitRunnerLabel(values.runnerLabel);
80
88
  const jitParameterName = String(values.jitParameterName || "").trim();
@@ -86,6 +94,7 @@ export function renderWindowsJitBootstrap(template, values = {}) {
86
94
  }
87
95
  const bounded = {
88
96
  REGION: String(values.region || WINDOWS_EC2_JIT.region),
97
+ CAMPAIGN_ID: String(values.campaignId || ""),
89
98
  JIT_PARAMETER_NAME: jitParameterName,
90
99
  EVIDENCE_BUCKET: String(values.evidenceBucket || ""),
91
100
  RUNNER_LABEL: runnerLabel,
@@ -99,6 +108,7 @@ export function renderWindowsJitBootstrap(template, values = {}) {
99
108
  };
100
109
  const patterns = {
101
110
  REGION: /^us-[a-z]+-\d$/,
111
+ CAMPAIGN_ID: /^win-[a-z0-9][a-z0-9-]{2,43}$/,
102
112
  EVIDENCE_BUCKET: /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/,
103
113
  GITHUB_RUN_ID: /^\d+$/,
104
114
  GITHUB_RUN_ATTEMPT: /^\d+$/,
@@ -184,6 +194,7 @@ export function windowsEc2JitPlan(overrides = {}) {
184
194
 
185
195
  export function createWindowsJitEvidence({
186
196
  repository,
197
+ campaignId,
187
198
  sourceSha,
188
199
  sourceRef,
189
200
  githubRunId,
@@ -235,6 +246,7 @@ export function createWindowsJitEvidence({
235
246
  phase: WINDOWS_EC2_JIT.phase,
236
247
  provider: "aws-ec2",
237
248
  repository,
249
+ campaign: { id: windowsJitCampaignId(campaignId) },
238
250
  source: {
239
251
  sha: exactSha(sourceSha, "sourceSha"),
240
252
  ref: String(sourceRef || "").trim(),
@@ -286,6 +298,7 @@ export function createWindowsJitEvidence({
286
298
  }
287
299
 
288
300
  export function verifyWindowsEc2JitQualification({
301
+ campaignId,
289
302
  jobs = [],
290
303
  cancellationCleanup = {},
291
304
  timeoutCleanup = {},
@@ -299,6 +312,7 @@ export function verifyWindowsEc2JitQualification({
299
312
  } = {}) {
300
313
  const plan = windowsEc2JitPlan();
301
314
  const issues = [];
315
+ const campaign = windowsJitCampaignId(campaignId);
302
316
  const accepted = jobs.filter(
303
317
  (job) =>
304
318
  job?.trusted === true &&
@@ -308,6 +322,9 @@ export function verifyWindowsEc2JitQualification({
308
322
  );
309
323
  const smokeJobs = accepted.filter((job) => job.kind === "smoke");
310
324
  const fullJobs = accepted.filter((job) => job.kind === "full");
325
+ if (accepted.some((job) => job.campaignId !== campaign)) {
326
+ issues.push("accepted-jobs-not-bound-to-campaign");
327
+ }
311
328
  if (smokeJobs.length < plan.config.minimumSmokeJobs) {
312
329
  issues.push("runner-profile-smoke-missing");
313
330
  }
@@ -349,6 +366,7 @@ export function verifyWindowsEc2JitQualification({
349
366
  contract: AWS_WINDOWS_JIT_CONTRACT,
350
367
  kind: "phase-verification",
351
368
  phase: plan.config.phase,
369
+ campaign: { id: campaign },
352
370
  status: issues.length ? "failed" : "passed",
353
371
  qualifying: issues.length === 0,
354
372
  metrics: {
@@ -35,6 +35,7 @@ export function main() {
35
35
  if (mode === "evidence") {
36
36
  const result = createWindowsJitEvidence({
37
37
  repository: process.env.GITHUB_REPOSITORY,
38
+ campaignId: process.env.AWS_EC2_CAMPAIGN_ID,
38
39
  sourceSha:
39
40
  process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || process.env.GITHUB_SHA,
40
41
  sourceRef:
@@ -77,6 +78,7 @@ export function main() {
77
78
  fs.readFileSync(path.resolve(template), "utf8"),
78
79
  {
79
80
  region: arg("region", "us-east-1"),
81
+ campaignId: arg("campaign-id"),
80
82
  jitParameterName: arg("jit-parameter"),
81
83
  evidenceBucket: arg("evidence-bucket"),
82
84
  runnerLabel: arg("runner-label"),
@@ -346,13 +346,19 @@ export function buildStandaloneBinary({
346
346
  phase: "archive",
347
347
  });
348
348
  }
349
+ const binarySha256 = crypto.createHash("sha256").update(fs.readFileSync(binaryPath)).digest("hex");
349
350
  const manifest = {
350
351
  schemaVersion: 1,
351
352
  contract: "kungfu-buildchain-standalone-binary",
352
353
  name,
353
354
  version,
354
355
  platform: triple,
356
+ platformId: process.platform === "linux" && process.arch === "x64" ? "linux-x64" : triple,
355
357
  binary: relativePath(cwd, binaryPath),
358
+ sha256: binarySha256,
359
+ executableFiles: [{ path: path.basename(binaryPath), sha256: binarySha256 }],
360
+ sourceSha: sourceSha(cwd),
361
+ runtimeDependencies: [],
356
362
  archive: relativePath(cwd, archivePath),
357
363
  node: process.version,
358
364
  observability: {
@@ -208,7 +208,7 @@ export const BUILDCHAIN_USAGE = `Usage:
208
208
  [--execute] [--json]
209
209
  buildchain paper status [--cwd <dir>] [--json]
210
210
  buildchain paper resume [--cwd <dir>] [--buildchain-ref <ref>] [--execute] [--json]
211
- buildchain release-propagation <plan|write-lock> ...
211
+ buildchain release-propagation <plan|write-lock|work|entry|pickup> ...
212
212
  buildchain badges readme [--cwd <dir>] [--readme <path>] [--check] [--write] [--json]
213
213
  buildchain badges bundle [--cwd <dir>] [--readme <path>] [--claims <csv>] [--check] [--write] [--json]
214
214
  buildchain homebrew update-formula --package <name> --release-passport <file-or-url> [--write] [--json]
@@ -246,6 +246,7 @@ Examples:
246
246
  buildchain paper preflight --json
247
247
  buildchain paper status --json
248
248
  buildchain release-propagation plan --graph graph.json --upstream-release release.json --json
249
+ buildchain release-propagation pickup plan --config manual-upstreams.json --source-id buildchain --channel release --current-version 3.0.3 --json
249
250
  buildchain kfd status --json
250
251
  buildchain kfd schema list --json
251
252
  buildchain kfd 1 witness --json
@@ -49,7 +49,30 @@ function assertSourcePath(value) {
49
49
  return sourcePath;
50
50
  }
51
51
 
52
- function readConfigAtSource(sourceSha, configPath, cwd) {
52
+ function hasCommit(sourceSha, cwd) {
53
+ try {
54
+ execFileSync("git", ["cat-file", "-e", `${sourceSha}^{commit}`], {
55
+ cwd,
56
+ stdio: "ignore",
57
+ });
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ export function readConfigAtSource(sourceSha, configPath, cwd) {
65
+ if (!hasCommit(sourceSha, cwd)) {
66
+ try {
67
+ execFileSync("git", ["fetch", "--no-tags", "--depth=1", "origin", sourceSha], {
68
+ cwd,
69
+ stdio: ["ignore", "pipe", "pipe"],
70
+ });
71
+ } catch (error) {
72
+ const detail = String(error.stderr || error.message || "unknown git fetch failure").trim();
73
+ throw new Error(`exact release source ${sourceSha} is unavailable from origin: ${detail}`);
74
+ }
75
+ }
53
76
  const bytes = execFileSync("git", ["show", `${sourceSha}:${configPath}`], {
54
77
  cwd,
55
78
  encoding: "utf8",
@@ -73,6 +73,7 @@ const requiredPaths = [
73
73
  "docs/github-artifact-attestation.md",
74
74
  "docs/shifu-gate-profiles.md",
75
75
  "docs/auditable-demo.md",
76
+ "contracts/auditable-demo-scenario-v1.schema.json",
76
77
  "contracts/auditable-demo-media-profiles-v1.json",
77
78
  "contracts/evidence/auditable-demo-web-delivery-v1.json",
78
79
  "contracts/evidence/auditable-demo-responsive-web-delivery-v1.json",
@@ -103,6 +104,9 @@ const requiredPaths = [
103
104
  "scripts/seal-macos-credential-input.mjs",
104
105
  "scripts/shifu-gate-profile.mjs",
105
106
  "scripts/auditable-demo.mjs",
107
+ "scripts/auditable-demo-platform.mjs",
108
+ "scripts/auditable-demo-transport-smoke.mjs",
109
+ "scripts/auditable-demo-capture.py",
106
110
  "scripts/resolve-artifact-coordinates.mjs",
107
111
  "scripts/artifact-relay-s3.mjs",
108
112
  "scripts/anchored-version-material.mjs",
@@ -154,6 +158,8 @@ const requiredPaths = [
154
158
  ".github/workflows/.build.yml",
155
159
  ".github/workflows/.gate-profile.yml",
156
160
  ".github/workflows/.auditable-demo.yml",
161
+ ".github/workflows/.declarative-auditable-demo.yml",
162
+ ".github/workflows/auditable-demo.yml",
157
163
  ".github/workflows/build.yml",
158
164
  ".github/workflows/build-surface-fixture.yml",
159
165
  ".github/workflows/candidate-lab.yml",
@@ -7,6 +7,12 @@ import { fileURLToPath } from "node:url";
7
7
  export const BUILDCHAIN_COMPILER_CACHE_PREPARATION_CONTRACT =
8
8
  "kungfu-buildchain-compiler-cache-preparation";
9
9
 
10
+ const SCCACHE_COMPILER_BINDINGS = Object.freeze({
11
+ RUSTC_WRAPPER: "sccache",
12
+ CMAKE_C_COMPILER_LAUNCHER: "sccache",
13
+ CMAKE_CXX_COMPILER_LAUNCHER: "sccache",
14
+ });
15
+
10
16
  const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
11
17
 
12
18
  function stableValue(value) {
@@ -76,6 +82,33 @@ function appendGithubEnv(filePath, values) {
76
82
  );
77
83
  }
78
84
 
85
+ function sumCounters(value) {
86
+ if (typeof value === "number" && Number.isFinite(value)) return value;
87
+ if (!value || typeof value !== "object") return 0;
88
+ return Object.values(value).reduce((total, entry) => total + sumCounters(entry), 0);
89
+ }
90
+
91
+ function readSccacheStats({ cwd, env, runCommand }) {
92
+ const result = runCommand(
93
+ "sccache",
94
+ ["--show-stats", "--stats-format", "json"],
95
+ { cwd, env },
96
+ );
97
+ assertCommandSucceeded(result, "sccache current-run stats probe");
98
+ let payload;
99
+ try {
100
+ payload = JSON.parse(cleanText(result.stdout));
101
+ } catch {
102
+ throw new Error("sccache current-run stats probe returned invalid JSON");
103
+ }
104
+ const stats = payload?.stats || payload;
105
+ return {
106
+ compileRequests: sumCounters(stats?.compile_requests),
107
+ cacheHits: sumCounters(stats?.cache_hits),
108
+ cacheMisses: sumCounters(stats?.cache_misses),
109
+ };
110
+ }
111
+
79
112
  function readToolEvidence({ cwd, env, expectedRoot }) {
80
113
  const configured = cleanText(
81
114
  env.BUILDCHAIN_COMPILER_CACHE_TOOL_EVIDENCE_PATH,
@@ -178,6 +211,7 @@ export function prepareCompilerCacheEvidence({
178
211
  action: {
179
212
  statsReset: true,
180
213
  command: ["sccache", "--zero-stats"],
214
+ compilerBindings: SCCACHE_COMPILER_BINDINGS,
181
215
  },
182
216
  bindings,
183
217
  };
@@ -191,25 +225,74 @@ export function prepareCompilerCacheEvidence({
191
225
  BUILDCHAIN_COMPILER_CACHE_ACTIVE_PROVIDER: provider,
192
226
  BUILDCHAIN_COMPILER_CACHE_PREPARATION_PATH: outputPath,
193
227
  BUILDCHAIN_COMPILER_CACHE_PREPARATION_ROOT: receipt.root,
228
+ ...SCCACHE_COMPILER_BINDINGS,
194
229
  });
195
230
  return receipt;
196
231
  }
197
232
 
233
+ export function verifyCompilerCacheActivity({
234
+ cwd = process.cwd(),
235
+ env = process.env,
236
+ runCommand = defaultRunCommand,
237
+ } = {}) {
238
+ const provider = cleanText(
239
+ env.BUILDCHAIN_COMPILER_CACHE_ACTIVE_PROVIDER ||
240
+ env.BUILDCHAIN_COMPILER_CACHE_PROVIDER ||
241
+ "none",
242
+ ).toLowerCase();
243
+ const required = cleanText(env.BUILDCHAIN_COMPILER_CACHE_REQUIRED).toLowerCase() === "true";
244
+ if (provider !== "sccache") {
245
+ if (required) {
246
+ throw new Error(`required compiler cache provider must be sccache, received ${provider || "empty"}`);
247
+ }
248
+ return undefined;
249
+ }
250
+
251
+ const stats = readSccacheStats({ cwd, env, runCommand });
252
+ const cacheableRequests = stats.cacheHits + stats.cacheMisses;
253
+ if (required && stats.compileRequests < 1) {
254
+ throw new Error(
255
+ "required sccache observed zero compile requests; compiler launcher bindings were not effective",
256
+ );
257
+ }
258
+ if (required && cacheableRequests < 1) {
259
+ throw new Error(
260
+ "required sccache observed no cacheable compiler requests",
261
+ );
262
+ }
263
+ return {
264
+ ...stats,
265
+ cacheableRequests,
266
+ };
267
+ }
268
+
198
269
  function main() {
199
270
  const command = process.argv[2] || "";
200
- if (command !== "prepare") {
201
- throw new Error("usage: node scripts/compiler-cache-evidence.mjs prepare");
271
+ if (!new Set(["prepare", "verify"]).has(command)) {
272
+ throw new Error("usage: node scripts/compiler-cache-evidence.mjs <prepare|verify>");
202
273
  }
203
- const receipt = prepareCompilerCacheEvidence();
204
- if (receipt) {
205
- console.log(`compiler_cache_provider=${receipt.provider}`);
206
- console.log(`compiler_cache_preparation_root=${receipt.root}`);
274
+ if (command === "prepare") {
275
+ const receipt = prepareCompilerCacheEvidence();
276
+ if (receipt) {
277
+ console.log(`compiler_cache_provider=${receipt.provider}`);
278
+ console.log(`compiler_cache_preparation_root=${receipt.root}`);
279
+ } else {
280
+ console.log("compiler_cache_provider=none");
281
+ }
207
282
  } else {
208
- console.log("compiler_cache_provider=none");
283
+ const activity = verifyCompilerCacheActivity();
284
+ if (activity) {
285
+ console.log(`compiler_cache_compile_requests=${activity.compileRequests}`);
286
+ console.log(`compiler_cache_hits=${activity.cacheHits}`);
287
+ console.log(`compiler_cache_misses=${activity.cacheMisses}`);
288
+ } else {
289
+ console.log("compiler_cache_activity=not-applicable");
290
+ }
209
291
  }
210
292
  }
211
293
 
212
294
  const isMain = process.argv[1] &&
295
+ path.basename(process.argv[1]) === "compiler-cache-evidence.mjs" &&
213
296
  path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
214
297
  if (isMain) {
215
298
  try {
@@ -24,6 +24,16 @@ function bool(value, fallback = false) {
24
24
  return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
25
25
  }
26
26
 
27
+ function autoMergeMethod(value, fallback = "merge") {
28
+ const normalized = text(value || fallback).toLowerCase();
29
+ if (!["merge", "squash", "rebase"].includes(normalized)) {
30
+ throw new Error(
31
+ `mergeMethod must be merge, squash, or rebase, got ${value || "<empty>"}`,
32
+ );
33
+ }
34
+ return normalized;
35
+ }
36
+
27
37
  function repository(value) {
28
38
  const normalized = text(value);
29
39
  if (!/^[^/\s]+\/[^/\s]+$/.test(normalized))
@@ -169,6 +179,8 @@ export function managedCandidateFromPullRequest(pullRequest, targetBranch) {
169
179
  }
170
180
  return {
171
181
  number: Number(pullRequest.number),
182
+ nodeId: text(pullRequest.node_id),
183
+ autoMerge: pullRequest.auto_merge || null,
172
184
  url: text(pullRequest.html_url),
173
185
  body,
174
186
  sourceSha,
@@ -191,6 +203,8 @@ export function managedCandidateFromPullRequest(pullRequest, targetBranch) {
191
203
  );
192
204
  return {
193
205
  number: Number(pullRequest.number),
206
+ nodeId: text(pullRequest.node_id),
207
+ autoMerge: pullRequest.auto_merge || null,
194
208
  url: text(pullRequest.html_url),
195
209
  body,
196
210
  sourceSha,
@@ -282,6 +296,14 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
282
296
  process.env.BUILDCHAIN_CHANNEL_PATROL_SETTLEMENT_AUTHORIZED,
283
297
  createPullRequest,
284
298
  ),
299
+ autoMerge: bool(
300
+ options.autoMerge ?? process.env.BUILDCHAIN_CHANNEL_PATROL_AUTO_MERGE,
301
+ false,
302
+ ),
303
+ mergeMethod: autoMergeMethod(
304
+ options.mergeMethod ??
305
+ process.env.BUILDCHAIN_CHANNEL_PATROL_MERGE_METHOD,
306
+ ),
285
307
  dryRun: bool(
286
308
  options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN,
287
309
  true,
@@ -541,12 +563,50 @@ function pullRequestBody({
541
563
  `- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
542
564
  ),
543
565
  "",
544
- "The source-lock branch must continue to point at the exact source SHA. This patrol never merges the PR, publishes a package, creates a tag, or creates a release.",
566
+ "The source-lock branch must continue to point at the exact source SHA. This patrol never directly merges the PR, publishes a package, creates a tag, or creates a release. Repository policy may arm GitHub auto-merge while every protected branch gate remains authoritative.",
545
567
  "",
546
568
  candidateStateMarker(state),
547
569
  ].join("\n");
548
570
  }
549
571
 
572
+ async function armCandidateAutoMerge(
573
+ options,
574
+ controllerState,
575
+ pullRequest,
576
+ client,
577
+ ) {
578
+ if (
579
+ !options.autoMerge ||
580
+ !options.settlementAuthorized ||
581
+ options.dryRun ||
582
+ controllerState !== "active" ||
583
+ !pullRequest
584
+ ) {
585
+ return {
586
+ requested: options.autoMerge,
587
+ enabled: false,
588
+ mergeMethod: options.mergeMethod,
589
+ };
590
+ }
591
+ if (!pullRequest.auto_merge) {
592
+ await client.enableAutoMerge(pullRequest, options.mergeMethod);
593
+ }
594
+ return {
595
+ requested: true,
596
+ enabled: true,
597
+ mergeMethod: options.mergeMethod,
598
+ };
599
+ }
600
+
601
+ function activePullRequest(candidate) {
602
+ return {
603
+ number: candidate.number,
604
+ node_id: candidate.nodeId,
605
+ auto_merge: candidate.autoMerge,
606
+ html_url: candidate.url,
607
+ };
608
+ }
609
+
550
610
  export async function runDevAlphaCandidatePatrol(
551
611
  optionsInput = {},
552
612
  clientInput,
@@ -793,10 +853,7 @@ export async function runDevAlphaCandidatePatrol(
793
853
  : "retain-next-candidate"
794
854
  : "reconcile-active-candidate";
795
855
  }
796
- pullRequest = {
797
- number: activeCandidate.number,
798
- html_url: activeCandidate.url,
799
- };
856
+ pullRequest = activePullRequest(activeCandidate);
800
857
  } else if (decision.eligible) {
801
858
  await client.ensureImmutableBranch(decision.sourceLockRef, sourceSha);
802
859
  state = candidateStateBody({
@@ -870,11 +927,13 @@ export async function runDevAlphaCandidatePatrol(
870
927
  }
871
928
  }
872
929
  }
930
+ const autoMerge = await armCandidateAutoMerge(options, controllerState, pullRequest, client);
873
931
  return {
874
932
  schema: "kungfu-buildchain-dev-alpha-candidate-patrol/v1",
875
933
  dryRun: options.dryRun,
876
934
  createPullRequest: options.createPullRequest,
877
935
  settlementAuthorized: options.settlementAuthorized,
936
+ autoMerge,
878
937
  decision,
879
938
  controller: {
880
939
  schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
@@ -1031,6 +1090,22 @@ export function createGitHubChannelCandidateClient({
1031
1090
  body: { head, base, title, body },
1032
1091
  });
1033
1092
  },
1093
+ async enableAutoMerge(pullRequest, mergeMethod) {
1094
+ if (!text(pullRequest.node_id)) {
1095
+ throw new Error("candidate pull request has no GraphQL node id");
1096
+ }
1097
+ const query = `mutation($id:ID!,$mergeMethod:PullRequestMergeMethod!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:$mergeMethod}){pullRequest{url}}}`;
1098
+ return api("/graphql", {
1099
+ method: "POST",
1100
+ body: {
1101
+ query,
1102
+ variables: {
1103
+ id: pullRequest.node_id,
1104
+ mergeMethod: autoMergeMethod(mergeMethod).toUpperCase(),
1105
+ },
1106
+ },
1107
+ });
1108
+ },
1034
1109
  async updatePullRequestBody(
1035
1110
  number,
1036
1111
  body,
@@ -152,11 +152,11 @@ function summarizeChecks({ statuses = [], checkRuns = [] } = {}, requiredChecks
152
152
  };
153
153
  }
154
154
 
155
- function mergeableAccepted(pr) {
155
+ function mergeableAccepted(pr, landingMode = "direct") {
156
156
  if (pr.mergeable === false) return false;
157
157
  const state = String(pr.mergeable_state || pr.mergeStateStatus || "").toLowerCase();
158
158
  if (!state) return pr.mergeable === true;
159
- return ["clean", "has_hooks", "unstable", "unknown"].includes(state);
159
+ return ["clean", "has_hooks", "unstable", "unknown", ...(landingMode === "queue" && pr.mergeable === true ? ["blocked"] : [])].includes(state);
160
160
  }
161
161
 
162
162
  function skip(reason, details = {}) {
@@ -189,7 +189,7 @@ export async function evaluatePullRequest(pr, options, client) {
189
189
  observedBaseRef: detailed.base.ref,
190
190
  });
191
191
  }
192
- if (!mergeableAccepted(detailed)) {
192
+ if (!mergeableAccepted(detailed, options.landingMode)) {
193
193
  return skip("not-mergeable", {
194
194
  mergeable: detailed.mergeable,
195
195
  mergeableState: detailed.mergeable_state || detailed.mergeStateStatus || "",
@@ -629,7 +629,7 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
629
629
  } else if (observedHeadSha !== expectedHeadSha) {
630
630
  admissionDecision = "rejected";
631
631
  admissionReason = "head-sha-drift";
632
- } else if (!mergeableAccepted(observedPullRequest)) {
632
+ } else if (!mergeableAccepted(observedPullRequest, landingMode)) {
633
633
  admissionDecision = "rejected";
634
634
  admissionReason = "not-mergeable-on-admission-recheck";
635
635
  }