@kungfu-tech/buildchain 3.0.7-alpha.0 → 3.0.7

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 (67) hide show
  1. package/actions/promote-buildchain-ref/README.md +10 -0
  2. package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
  3. package/contracts/engineering-housekeeper-v1.schema.json +143 -0
  4. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
  5. package/dist/site/buildchain-contract.json +24 -24
  6. package/dist/site/buildchain-site.json +91 -30
  7. package/dist/site/capability-registry.json +3 -3
  8. package/dist/site/controller-registry.json +6 -2
  9. package/dist/site/kfd-claims.json +122 -11
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +8 -7
  12. package/dist/site/node-api-registry.json +683 -105
  13. package/dist/site/page-registry.json +80 -19
  14. package/dist/site/public-surface-audit.json +98 -7
  15. package/dist/site/publication-authority-registry.json +81 -1
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +2 -0
  18. package/dist/site/site-manifest.json +11 -11
  19. package/dist/site/workflow-registry.json +119 -2
  20. package/docs/MAP.md +1 -0
  21. package/docs/auditable-demo.md +2 -2
  22. package/docs/dev-delivery-warrant.md +49 -4
  23. package/docs/engineering-housekeeper.md +138 -0
  24. package/docs/lifecycle-protocol.md +4 -2
  25. package/docs/node-api-reference.md +277 -212
  26. package/docs/release-governance.md +17 -2
  27. package/docs/release-tail-provider-plane.md +1 -1
  28. package/docs/reusable-build-surface.md +11 -0
  29. package/package.json +4 -1
  30. package/packages/core/artifact-signing.js +61 -0
  31. package/packages/core/buildchain-config.js +66 -6
  32. package/packages/core/buildchain-publication-authority.js +4 -0
  33. package/packages/core/controller-evidence.js +2 -1
  34. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  35. package/packages/core/dev-delivery-warrant-shadow.js +502 -0
  36. package/packages/core/dev-delivery-warrant.js +15 -6
  37. package/packages/core/diagnostics.js +8 -3
  38. package/packages/core/engineering-housekeeper-github-client.js +222 -0
  39. package/packages/core/engineering-housekeeper-github.js +501 -0
  40. package/packages/core/engineering-housekeeper.js +259 -0
  41. package/packages/core/index.js +3 -0
  42. package/packages/core/kfd-gate.js +45 -15
  43. package/packages/core/publication-rehearsal-runtime.js +13 -1
  44. package/packages/core/release-passport.js +130 -20
  45. package/scripts/assemble-self-publication-admission.mjs +1 -1
  46. package/scripts/audit-publication-control-plane.mjs +1 -1
  47. package/scripts/auditable-demo-bundle-verification.mjs +2 -3
  48. package/scripts/auditable-demo-platform.mjs +2 -2
  49. package/scripts/auditable-demo-renditions.mjs +1 -1
  50. package/scripts/auditable-demo.mjs +2 -2
  51. package/scripts/build-contract-core.mjs +8 -3
  52. package/scripts/build-standalone-binary.mjs +14 -3
  53. package/scripts/check-inventory.mjs +3 -1
  54. package/scripts/dev-delivery-warrant.mjs +31 -4
  55. package/scripts/dev-pr-auto-merge.mjs +30 -4
  56. package/scripts/dev-pr-delivery-warrant.mjs +50 -0
  57. package/scripts/engineering-housekeeper-workflow.mjs +394 -0
  58. package/scripts/generate-site-bundle.mjs +23 -4
  59. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  60. package/scripts/materialize-self-release-candidate-version.mjs +6 -0
  61. package/scripts/publication-commit-evidence.mjs +69 -23
  62. package/scripts/release-candidate-resolver.mjs +16 -10
  63. package/scripts/resume-from-candidate-run.mjs +123 -9
  64. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  65. package/scripts/site-capability-metadata.mjs +2 -0
  66. package/scripts/web-surface-core.mjs +8 -2
  67. package/scripts/workflow-call-contract.mjs +1 -1
@@ -4,11 +4,10 @@
4
4
  import crypto from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
- import { readRendererManifest } from "./auditable-demo-renditions.mjs";
7
+ import { MAX_LONG_FORM_RENDERER_MANIFEST_BYTES, readRendererManifest } from "./auditable-demo-renditions.mjs";
8
8
 
9
9
  const DIGEST = /^sha256:[0-9a-f]{64}$/u;
10
10
  const MAX_METADATA_MEMBER_BYTES = 8 * 1024 * 1024;
11
- const MAX_LONG_FORM_MANIFEST_BYTES = 32 * 1024 * 1024;
12
11
  const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
13
12
  const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
14
13
  const DIGEST_BUFFER_BYTES = 64 * 1024;
@@ -113,7 +112,7 @@ export function verifyBundleChecksums(root, label, options = {}) {
113
112
  declared.add(match[2]);
114
113
  const metadataMember = match[2].endsWith(".json") || match[2].endsWith(".sha256");
115
114
  const maximum = options.allowLongFormRendererManifest && match[2] === "manifest.json"
116
- ? MAX_LONG_FORM_MANIFEST_BYTES
115
+ ? MAX_LONG_FORM_RENDERER_MANIFEST_BYTES
117
116
  : metadataMember
118
117
  ? MAX_METADATA_MEMBER_BYTES
119
118
  : (options.maximumMemberBytes || MAX_METADATA_MEMBER_BYTES);
@@ -23,10 +23,10 @@ const NON_AUTHORITIES = [
23
23
  ];
24
24
  const RENDITIONS = [
25
25
  { id: "1080p", role: "primary", columns: 150, rows: 36, width: 1920, height: 1080 },
26
- { id: "720p", role: "responsive", columns: 100, rows: 28, width: 1280, height: 720 },
26
+ { id: "720p", role: "responsive", columns: 150, rows: 28, width: 1280, height: 720 },
27
27
  ];
28
28
  const STANDARD_MAX_SECONDS = 60;
29
- const LONG_FORM_MAX_SECONDS = 180;
29
+ const LONG_FORM_MAX_SECONDS = 360;
30
30
  const PRESENTATION_FRAMED = "presentation-framed";
31
31
  const TERMINAL_FILL = "terminal-fill";
32
32
  const MAX_EXECUTABLE_FILES = 32;
@@ -17,7 +17,7 @@ const RENDITION_SET_NON_AUTHORITIES = [
17
17
  ...TERMINAL_CAPTURE_NON_AUTHORITIES,
18
18
  ];
19
19
  const MAX_BUNDLE_MEMBER_BYTES = 8 * 1024 * 1024;
20
- const MAX_LONG_FORM_RENDERER_MANIFEST_BYTES = 32 * 1024 * 1024;
20
+ export const MAX_LONG_FORM_RENDERER_MANIFEST_BYTES = 64 * 1024 * 1024;
21
21
  const PRESENTATION_FRAMED = "presentation-framed";
22
22
  const TERMINAL_FILL = "terminal-fill";
23
23
  const GEOMETRY_TOLERANCE = 0.001;
@@ -7,7 +7,7 @@ import os from "node:os";
7
7
  import path from "node:path";
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { fileURLToPath } from "node:url";
10
- import { readRendererManifest, validateRendererCompositionInputs, validateRenditionSet, validateTerminalCapture } from "./auditable-demo-renditions.mjs";
10
+ import { MAX_LONG_FORM_RENDERER_MANIFEST_BYTES, readRendererManifest, validateRendererCompositionInputs, validateRenditionSet, validateTerminalCapture } from "./auditable-demo-renditions.mjs";
11
11
 
12
12
  const UTF8 = new TextDecoder("utf-8", { fatal: true });
13
13
  const IMAGE_PATTERN = /^[a-z0-9][a-z0-9./_-]*@sha256:[0-9a-f]{64}$/;
@@ -141,7 +141,7 @@ function verifyChecksums(root, checksumName = "checksums.sha256", options = {})
141
141
  invariant(!declared.has(member), `duplicate checksum member: ${member}`);
142
142
  declared.add(member);
143
143
  const maximumBytes = options.allowLongFormRendererManifest && member === "manifest.json"
144
- ? 32 * 1024 * 1024
144
+ ? MAX_LONG_FORM_RENDERER_MANIFEST_BYTES
145
145
  : MAX_BUNDLE_MEMBER_BYTES;
146
146
  invariant(
147
147
  sha256(readRegular(target, member, maximumBytes)).slice(7) === match[1],
@@ -1209,9 +1209,14 @@ export function writeGitHubOutputs(outputs) {
1209
1209
  }
1210
1210
  return;
1211
1211
  }
1212
- const lines = Object.entries(outputs).map(
1213
- ([key, value]) => `${key}=${value}`,
1214
- );
1212
+ const lines = Object.entries(outputs).map(([key, value]) => {
1213
+ const text = String(value);
1214
+ if (!/[\r\n]/.test(text)) return `${key}=${text}`;
1215
+ const valueLines = new Set(text.split(/\r?\n/));
1216
+ let delimiter = "BUILDCHAIN_OUTPUT";
1217
+ while (valueLines.has(delimiter)) delimiter += "_";
1218
+ return `${key}<<${delimiter}\n${text}\n${delimiter}`;
1219
+ });
1215
1220
  fs.appendFileSync(outputPath, `${lines.join("\n")}\n`);
1216
1221
  }
1217
1222
 
@@ -2,7 +2,6 @@
2
2
  import { spawnSync } from "node:child_process";
3
3
  import crypto from "node:crypto";
4
4
  import fs from "node:fs";
5
- import os from "node:os";
6
5
  import path from "node:path";
7
6
  import { pathToFileURL } from "node:url";
8
7
  import { createBuildchainLogger } from "../packages/core/logging.js";
@@ -224,16 +223,16 @@ function postjectArgs(binaryPath, blobPath) {
224
223
  return args;
225
224
  }
226
225
 
227
- export function buildStandaloneBinary({
226
+ function buildStandaloneBinaryInTemp({
228
227
  cwd = process.cwd(),
229
228
  outputDir = "dist/binary",
230
229
  name = "buildchain",
231
230
  version = "",
232
231
  packageManagerInstall = false,
233
232
  logPath = undefined,
233
+ tempDir,
234
234
  } = {}) {
235
235
  const resolvedOutputDir = path.resolve(cwd, outputDir);
236
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-sea-"));
237
236
  const triple = platformTriple();
238
237
  const archiveBase = `${name}-${triple}`;
239
238
  const logger = createBuildchainLogger({
@@ -384,6 +383,18 @@ export function buildStandaloneBinary({
384
383
  return manifest;
385
384
  }
386
385
 
386
+ export function buildStandaloneBinary(options = {}) {
387
+ const cwd = path.resolve(options.cwd || process.cwd());
388
+ const tempRoot = path.join(cwd, ".buildchain", "tmp");
389
+ fs.mkdirSync(tempRoot, { recursive: true });
390
+ const tempDir = fs.mkdtempSync(path.join(tempRoot, "sea-"));
391
+ try {
392
+ return buildStandaloneBinaryInTemp({ ...options, cwd, tempDir });
393
+ } finally {
394
+ fs.rmSync(tempDir, { recursive: true, force: true });
395
+ }
396
+ }
397
+
387
398
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
388
399
  try {
389
400
  const result = buildStandaloneBinary({
@@ -992,7 +992,7 @@ for (const forbiddenSnippet of [
992
992
  for (const requiredSnippet of [
993
993
  "id-token: write",
994
994
  "actions: write",
995
- "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@9a0cdf8d84aacf8c7daaac82efa43d1b34696a03\n",
995
+ "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@769b221bad7a6b9104afad4c2628d9dca396ab0f\n",
996
996
  "github.event.workflow_run.event == 'push'",
997
997
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
998
998
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
@@ -1038,9 +1038,11 @@ for (const requiredSnippet of [
1038
1038
  "github-release-notes: ${{ inputs.github-release-notes }}",
1039
1039
  "publication-commit-command:",
1040
1040
  "BUILDCHAIN_PUBLICATION_COMMIT_SIGNING_KEY:",
1041
+ "BUILDCHAIN_PUBLICATION_COMMIT_CANDIDATE_SOURCE_SHA: ${{ steps.rc.outputs.release-candidate-source-sha || needs.preflight.outputs.requested-sha }}",
1041
1042
  "KUNGFU_GOVERNANCE_AUDITOR_APP_PRIVATE_KEY:",
1042
1043
  "Commit consumer publication authority last",
1043
1044
  "node .buildchain/runtime/scripts/publication-commit-evidence.mjs",
1045
+ '--candidate-source-sha "${BUILDCHAIN_PUBLICATION_COMMIT_CANDIDATE_SOURCE_SHA}"',
1044
1046
  "require-publish-source-lock: \"true\"",
1045
1047
  "publish-source-ref: ${{ steps.publish-gate.outputs.ref }}",
1046
1048
  "publish-source-sha: ${{ steps.publish-gate.outputs.sha }}",
@@ -2,6 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { cancelQueuedDevDeliveryCandidate, closeDevDeliveryWarrant, createDevDeliveryQueue, heartbeatDevDeliveryWarrant, observeDevDeliveryQueue, recoverExpiredDevDeliveryWarrant, selectDevDeliveryWarrant, settleDevDeliveryTerminalEvent, submitDevDeliveryCandidate } from "../packages/core/dev-delivery-warrant.js";
5
+ import { planDevDeliveryWarrantShadow, qualifyDevDeliveryWarrantShadow } from "../packages/core/dev-delivery-warrant-shadow.js";
5
6
 
6
7
  const STATE_PATH = "queue.json";
7
8
  const STATE_REF_PREFIX = "buildchain/dev-delivery-warrant/";
@@ -189,6 +190,7 @@ function transitionFor(command, queue, options) {
189
190
  closureRoot: exactRoot(options.closureRoot, "closureRoot"),
190
191
  dependencyRoot: exactRoot(options.dependencyRoot, "dependencyRoot"),
191
192
  toolchainRoot: exactRoot(options.toolchainRoot, "toolchainRoot"),
193
+ sourceWorkflowRunId: options.sourceWorkflowRunId ? positiveInteger(options.sourceWorkflowRunId, "sourceWorkflowRunId") : 0,
192
194
  deliveryClass: options.deliveryClass,
193
195
  priority: options.priority || "ordinary",
194
196
  },
@@ -345,6 +347,23 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
345
347
  };
346
348
  }
347
349
 
350
+ export function runDevDeliveryShadowCommand(options = {}) {
351
+ if (bool(options.execute, false)) {
352
+ throw new Error("shadow qualification is effect-disabled and rejects --execute");
353
+ }
354
+ if (!options.inputPath) throw new Error("shadow qualification requires --input FILE");
355
+ const input = JSON.parse(fs.readFileSync(options.inputPath, "utf8"));
356
+ if (options.command === "shadow-plan") {
357
+ return planDevDeliveryWarrantShadow(input, {
358
+ maxConcurrency: positiveInteger(options.maxConcurrency, "maxConcurrency", 2),
359
+ });
360
+ }
361
+ if (options.command === "shadow-qualify") {
362
+ return qualifyDevDeliveryWarrantShadow(input);
363
+ }
364
+ throw new Error(`unsupported shadow command ${options.command || "<empty>"}`);
365
+ }
366
+
348
367
  function flag(args, name, fallback = "") {
349
368
  const index = args.indexOf(`--${name}`);
350
369
  return index === -1 ? fallback : args[index + 1] || "";
@@ -386,6 +405,8 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
386
405
  evidenceRoot: flag(rest, "evidence-root", environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT),
387
406
  reason: flag(rest, "reason", environment.BUILDCHAIN_DEV_DELIVERY_REASON),
388
407
  now: flag(rest, "now", environment.BUILDCHAIN_DEV_DELIVERY_NOW),
408
+ inputPath: flag(rest, "input", environment.BUILDCHAIN_DEV_DELIVERY_SHADOW_INPUT),
409
+ maxConcurrency: flag(rest, "max-concurrency", environment.BUILDCHAIN_DEV_DELIVERY_SHADOW_MAX_CONCURRENCY || "2"),
389
410
  outputPath: flag(rest, "output", environment.BUILDCHAIN_DEV_DELIVERY_OUTPUT || ".buildchain/dev-delivery/result.json"),
390
411
  execute: hasFlag(rest, "execute"),
391
412
  json: hasFlag(rest, "json"),
@@ -393,7 +414,7 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
393
414
  }
394
415
 
395
416
  function usage() {
396
- return "Usage:\n buildchain dev warrant <submit|select|heartbeat|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n";
417
+ return "Usage:\n buildchain dev warrant <submit|select|heartbeat|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n buildchain dev warrant <shadow-plan|shadow-qualify> --input FILE [--max-concurrency 1|2] [--output FILE] [--json]\n";
397
418
  }
398
419
 
399
420
  async function main() {
@@ -403,16 +424,22 @@ async function main() {
403
424
  return;
404
425
  }
405
426
  const options = devDeliveryCliOptions(args);
406
- if (!["submit", "select", "heartbeat", "recover", "close", "settle", "cancel-queued", "observe"].includes(options.command)) {
427
+ if (!["submit", "select", "heartbeat", "recover", "close", "settle", "cancel-queued", "observe", "shadow-plan", "shadow-qualify"].includes(options.command)) {
407
428
  throw new Error(usage().trim());
408
429
  }
409
- const result = await runDevDeliveryCommand(options);
430
+ const shadow = options.command.startsWith("shadow-");
431
+ const result = shadow ? runDevDeliveryShadowCommand(options) : await runDevDeliveryCommand(options);
410
432
  fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
411
433
  fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
412
434
  if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
413
435
  else {
414
436
  process.stdout.write(`Buildchain dev delivery ${options.command}: ${result.receipt?.reason || result.mode}\n`);
415
- process.stdout.write(`State root: ${result.after?.stateRoot || result.observation.stateRoot}\n`);
437
+ if (shadow) {
438
+ process.stdout.write(`Decision: ${result.decision}\n`);
439
+ process.stdout.write(`Evidence root: ${result.planRoot || result.qualificationRoot}\n`);
440
+ } else {
441
+ process.stdout.write(`State root: ${result.after?.stateRoot || result.observation.stateRoot}\n`);
442
+ }
416
443
  if (result.receiptRoot) process.stdout.write(`Receipt root: ${result.receiptRoot}\n`);
417
444
  process.stdout.write(`Result: ${options.outputPath}\n`);
418
445
  }
@@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { verifyProjectCutReplayProof } from "../packages/core/dev-delivery-warrant.js";
7
- import { admitExistingQueueEntry, createDevPrAdmissionReceipt, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission } from "./dev-pr-delivery-warrant.mjs";
7
+ import { admitExistingQueueEntry, createDevPrAdmissionReceipt, enqueueAfterStatusPropagation, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission, verifyCurrentDeliveryWarrant } from "./dev-pr-delivery-warrant.mjs";
8
8
  const DEFAULT_BLOCK_LABELS = ["blocked", "do-not-merge", "work-in-progress"];
9
9
  const DEFAULT_ALLOWED_HEAD_PREFIXES = ["feature/", "fix/", "chore/", "docs/", "ci/", "refactor/"];
10
10
  const DEFAULT_REQUIRED_CHECKS = ["check"];
@@ -70,6 +70,7 @@ function normalizeOptions(options = {}) {
70
70
  allowedHeadPrefixes: splitList(options.allowedHeadPrefixes, DEFAULT_ALLOWED_HEAD_PREFIXES),
71
71
  requiredChecks: splitList(options.requiredChecks, DEFAULT_REQUIRED_CHECKS),
72
72
  queueAdmissionContext: String(options.queueAdmissionContext || "").trim(),
73
+ activeLeaseContext: String(options.activeLeaseContext || "").trim(),
73
74
  requireApproval: boolOption(options.requireApproval, true),
74
75
  sameRepositoryOnly: boolOption(options.sameRepositoryOnly, true),
75
76
  maxMerges: intOption(options.maxMerges, 1),
@@ -218,6 +219,20 @@ async function setQueueAdmissionStatus(client, repository, sha, context, state)
218
219
  return { context, state, sha };
219
220
  }
220
221
 
222
+ async function setActiveLeaseStatus(client, repository, sha, context, state) {
223
+ if (!context) return null;
224
+ await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, {
225
+ body: {
226
+ state,
227
+ context,
228
+ description: state === "pending"
229
+ ? "Buildchain reactivated this exact lease for merge-group qualification"
230
+ : "Buildchain released this exact lease after queue admission failed",
231
+ },
232
+ });
233
+ return { context, state, sha };
234
+ }
235
+
221
236
  function skip(reason, details = {}) {
222
237
  return { action: "skip", reason, ...details };
223
238
  }
@@ -853,11 +868,15 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
853
868
  let warrant = null;
854
869
  try {
855
870
  warrant = readDeliveryWarrantResult(options, pr);
871
+ await verifyCurrentDeliveryWarrant(client, options, pr, warrant);
856
872
  } catch (error) {
857
873
  return reject("blocked", error.code || "invalid-delivery-warrant");
858
874
  }
859
875
  const matchingEntry = initialQueue.entries.find((entry) =>
860
876
  entry.pullRequestNumber === pr.number && entry.pullRequestHeadSha === options.expectedHeadSha);
877
+ if (matchingEntry && !options.dryRun) {
878
+ await setActiveLeaseStatus(client, options.repository, options.expectedHeadSha, options.activeLeaseContext, "pending");
879
+ }
861
880
  const existing = await admitExistingQueueEntry({
862
881
  options, pullRequest: pr, readiness, client, entry: matchingEntry, warrant,
863
882
  createReceipt: createAdmissionReceipt,
@@ -893,6 +912,7 @@ async function reconcileEnqueueError({ client, options, pr, expectedHeadSha, ent
893
912
  const exactEntry = queueReadback?.entries?.find((candidate) =>
894
913
  candidate.pullRequestNumber === pr.number && candidate.pullRequestHeadSha === expectedHeadSha);
895
914
  if (exactEntry) {
915
+ entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "pending");
896
916
  entry.action = "enqueued";
897
917
  entry.reason = "already-enqueued-exact-head";
898
918
  entry.queueEntry = exactEntry;
@@ -902,6 +922,7 @@ async function reconcileEnqueueError({ client, options, pr, expectedHeadSha, ent
902
922
  return;
903
923
  }
904
924
  entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "failure");
925
+ entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "failure");
905
926
  entry.action = "skip";
906
927
  entry.reason = "enqueue-rejected";
907
928
  entry.enqueueError = {
@@ -1067,10 +1088,14 @@ export async function runDevPrAutoMerge(optionsInput = {}, clientInput) {
1067
1088
  result.skipped.push(entry);
1068
1089
  } else {
1069
1090
  try {
1091
+ entry.activeLeaseStatus = await setActiveLeaseStatus(client, options.repository, expectedHeadSha, options.activeLeaseContext, "pending");
1070
1092
  entry.queueAdmissionStatus = await setQueueAdmissionStatus(client, options.repository, expectedHeadSha, options.queueAdmissionContext, "success");
1071
- const queueEntry = await client.enqueuePullRequest({
1072
- pullRequestId: decision.pullRequestId,
1073
- expectedHeadOid: expectedHeadSha,
1093
+ const queueEntry = await enqueueAfterStatusPropagation({
1094
+ enqueue: (input) => client.enqueuePullRequest(input),
1095
+ input: { pullRequestId: decision.pullRequestId, expectedHeadOid: expectedHeadSha },
1096
+ attempts: options.pollMergeableAttempts,
1097
+ delayMs: options.pollMergeableDelayMs,
1098
+ sleep: delay,
1074
1099
  });
1075
1100
  entry.action = "enqueued";
1076
1101
  entry.reason = "enqueued-with-expected-head";
@@ -1155,6 +1180,7 @@ export function cliOptions(args = [], environment = process.env) {
1155
1180
  allowedHeadPrefixes: cliValue(args, "allowed-head-prefixes", environment.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES),
1156
1181
  requiredChecks: cliValue(args, "required-checks", environment.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS),
1157
1182
  queueAdmissionContext: cliValue(args, "queue-admission-context", environment.BUILDCHAIN_DEV_PR_QUEUE_ADMISSION_CONTEXT),
1183
+ activeLeaseContext: cliValue(args, "active-lease-context", environment.BUILDCHAIN_DEV_PR_ACTIVE_LEASE_CONTEXT),
1158
1184
  diagnosticContext: cliValue(args, "diagnostic-context", environment.BUILDCHAIN_DEV_PR_DIAGNOSTIC_CONTEXT),
1159
1185
  warrantMode: cliValue(args, "warrant-mode", environment.BUILDCHAIN_DEV_PR_WARRANT_MODE),
1160
1186
  warrantResultPath: cliValue(args, "warrant-result", environment.BUILDCHAIN_DEV_PR_WARRANT_RESULT_PATH),
@@ -116,6 +116,56 @@ export function readDeliveryWarrantResult(options, pullRequest) {
116
116
  };
117
117
  }
118
118
 
119
+ export async function readCurrentDeliveryQueueState(client, repository, targetBranch) {
120
+ if (typeof client.getDevDeliveryQueueState === "function") {
121
+ return client.getDevDeliveryQueueState(targetBranch);
122
+ }
123
+ const stateRef = `buildchain/dev-delivery-warrant/${targetBranch.replaceAll("/", "-")}`;
124
+ const query = new URLSearchParams({ ref: stateRef });
125
+ const { data } = await client.request(
126
+ "GET",
127
+ `/repos/${repository.owner}/${repository.repo}/contents/queue.json?${query}`,
128
+ );
129
+ if (data?.type !== "file" || data?.encoding !== "base64" || !data?.content) {
130
+ mismatch("delivery-warrant-current-readback-invalid");
131
+ }
132
+ return JSON.parse(Buffer.from(String(data.content).replace(/\s+/g, ""), "base64").toString("utf8"));
133
+ }
134
+
135
+ export async function verifyCurrentDeliveryWarrant(client, options, pullRequest, warrant) {
136
+ if (!warrant) return;
137
+ let queue;
138
+ try {
139
+ queue = await readCurrentDeliveryQueueState(client, options.repository, options.targetBranch);
140
+ } catch {
141
+ mismatch("delivery-warrant-current-readback-failed");
142
+ }
143
+ const active = queue?.activeWarrant;
144
+ const candidate = queue?.candidates?.find((entry) => entry.candidateId === active?.candidateId);
145
+ requireMatch(active?.candidateId === warrant.candidateId, "delivery-warrant-no-longer-active");
146
+ requireMatch(active?.fencingToken === warrant.fencingToken, "delivery-warrant-current-fencing-mismatch");
147
+ requireMatch(Number(active?.generation) === Number(warrant.generation), "delivery-warrant-current-generation-mismatch");
148
+ requireMatch(Number(active?.pullRequestNumber) === Number(pullRequest.number), "delivery-warrant-current-pr-mismatch");
149
+ requireMatch(String(active?.sourceHead || "").toLowerCase() === options.expectedHeadSha, "delivery-warrant-current-head-mismatch");
150
+ requireMatch(["selected", "proving", "waiting", "blocked"].includes(candidate?.status), "delivery-warrant-current-candidate-not-selected");
151
+ requireMatch(candidate?.sourceHead === active.sourceHead, "delivery-warrant-current-candidate-head-mismatch");
152
+ }
153
+
154
+ export async function enqueueAfterStatusPropagation({ enqueue, input, attempts, delayMs, sleep }) {
155
+ let lastError;
156
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
157
+ try {
158
+ return await enqueue(input);
159
+ } catch (error) {
160
+ lastError = error;
161
+ const propagation = /required status(?:es| check).*failing|failing required status|cannot change this locked branch/i.test(String(error?.message || ""));
162
+ if (!propagation || attempt === attempts) throw error;
163
+ await sleep(delayMs);
164
+ }
165
+ }
166
+ throw lastError;
167
+ }
168
+
119
169
  export async function runSourceQualification({ options, pullRequest, readiness, client, evaluate, admissionState, createReceipt, root, publishDiagnostic, reject }) {
120
170
  const decision = await evaluate(pullRequest, { ...options, landingMode: options.landingMode, dryRun: true }, client);
121
171
  if (decision.observedHeadSha && String(decision.observedHeadSha).toLowerCase() !== options.expectedHeadSha) {