@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.
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/dist/site/buildchain-contract.json +24 -24
- package/dist/site/buildchain-site.json +91 -30
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +6 -2
- package/dist/site/kfd-claims.json +122 -11
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +8 -7
- package/dist/site/node-api-registry.json +683 -105
- package/dist/site/page-registry.json +80 -19
- package/dist/site/public-surface-audit.json +98 -7
- package/dist/site/publication-authority-registry.json +81 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +119 -2
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +2 -2
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +277 -212
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-provider-plane.md +1 -1
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +4 -1
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-publication-authority.js +4 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +3 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/publication-rehearsal-runtime.js +13 -1
- package/packages/core/release-passport.js +130 -20
- package/scripts/assemble-self-publication-admission.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/check-inventory.mjs +3 -1
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-site-bundle.mjs +23 -4
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +6 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +2 -0
- package/scripts/web-surface-core.mjs +8 -2
- 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
|
-
?
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
-
|
|
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@
|
|
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
|
|
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
|
-
|
|
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
|
|
1072
|
-
|
|
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) {
|