@kungfu-tech/buildchain 3.0.8 → 3.0.9-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.
- package/actions/promote-buildchain-ref/README.md +8 -7
- package/bin/buildchain.mjs +34 -33
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/dist/site/agent-index.json +0 -1
- package/dist/site/artifact-schemas.json +0 -2
- package/dist/site/badge-endpoint-registry.json +9 -9
- package/dist/site/badges/v1/kfd-8/aligned.json +1 -1
- package/dist/site/badges/v1/kfd-8/declared.json +1 -1
- package/dist/site/badges/v1/kfd-8/downgraded.json +1 -1
- package/dist/site/badges/v1/kfd-8/draft.json +1 -1
- package/dist/site/badges/v1/kfd-8/failed.json +1 -1
- package/dist/site/badges/v1/kfd-8/missing.json +1 -1
- package/dist/site/badges/v1/kfd-8/passed.json +1 -1
- package/dist/site/badges/v1/kfd-8/planned.json +1 -1
- package/dist/site/buildchain-contract.json +27 -22
- package/dist/site/buildchain-site.json +38 -28
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/cli-registry.json +8 -7
- package/dist/site/controller-registry.json +6 -2
- package/dist/site/kfd-claims.json +34 -7
- package/dist/site/kfd-upstream-aggregate.json +10 -10
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +754 -228
- package/dist/site/page-registry.json +28 -18
- package/dist/site/public-surface-audit.json +19 -9
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-passport-check-manifest.json +33 -5
- package/dist/site/release-provenance.json +2 -1
- package/dist/site/schemas/release-passport-v1.schema.json +3 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +19 -9
- package/docs/aws-us-elastic-runner-burst-plane.md +15 -3
- package/docs/cli-reference.md +6 -6
- package/docs/cli.md +2 -3
- package/docs/dev-alpha-candidate-patrol.md +45 -20
- package/docs/kfd-support.md +7 -7
- package/docs/node-api-reference.md +179 -140
- package/docs/release-passport.md +18 -12
- package/docs/release-train.md +8 -0
- package/package.json +4 -3
- package/packages/core/artifact-verification-envelope.js +114 -0
- package/packages/core/dev-alpha-active-release-train.js +460 -0
- package/packages/core/dev-alpha-candidate-selection.js +181 -0
- package/packages/core/index.js +4 -5
- package/packages/core/kfd-adopter-manifest.js +390 -0
- package/packages/core/kfd-product-gates.js +0 -300
- package/packages/core/release-passport-contract.js +219 -6
- package/packages/core/release-passport.js +139 -242
- package/scripts/aws-macos-jit-controller-core.mjs +76 -0
- package/scripts/aws-macos-jit-controller.mjs +29 -0
- package/scripts/aws-macos-jit-source-rebind.mjs +503 -0
- package/scripts/buildchain-cli-help.mjs +4 -2
- package/scripts/check-inventory.mjs +2 -2
- package/scripts/dev-alpha-candidate-patrol.mjs +196 -201
- package/scripts/generate-site-bundle.mjs +1 -8
- package/scripts/site-capability-metadata.mjs +3 -1
- package/dist/site/schemas/kfd-support-projection-v1.schema.json +0 -106
package/docs/release-passport.md
CHANGED
|
@@ -399,18 +399,24 @@ instead of trusting docs or release notes.
|
|
|
399
399
|
|
|
400
400
|
### KFD support projection
|
|
401
401
|
|
|
402
|
-
Pass `--kfd-
|
|
403
|
-
`--kfd-product-gate-json` arguments for KFD-4, KFD-5, and
|
|
404
|
-
|
|
405
|
-
`@kungfu-tech/kfd`
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
402
|
+
Pass the standard full-cut declaration with `--kfd-adopter-manifest-json`
|
|
403
|
+
together with three `--kfd-product-gate-json` arguments for KFD-4, KFD-5, and
|
|
404
|
+
KFD-7. The collector invokes the verifier from the exact installed
|
|
405
|
+
`@kungfu-tech/kfd` package, binds the package artifact, registry and verifier
|
|
406
|
+
roots, the exact Buildchain source, the manifest/report/bundle witness roots,
|
|
407
|
+
and the existing product-gate roots. It emits `kfd-adopter-manifest.json`,
|
|
408
|
+
`kfd-adopter-manifest-gate.json`, and a legacy `kfd-support.json` projection.
|
|
409
|
+
The release passport and `artifact-evidence.json` carry the same rooted
|
|
410
|
+
`kfdAdopter` binding.
|
|
411
|
+
|
|
412
|
+
The adopter manifest is the sole declaration authority. The old
|
|
413
|
+
`--kfd-support-matrix-json` input is retained only as a compatibility check: if
|
|
414
|
+
supplied with the manifest, it must exactly equal the generated legacy
|
|
415
|
+
projection. Release verification fails closed when any sibling, package root,
|
|
416
|
+
source root, witness root, gate root, passport binding, or artifact-evidence
|
|
417
|
+
binding differs. A passed binding is release evidence only; product
|
|
418
|
+
qualification, activation, certification, and support ownership remain outside
|
|
419
|
+
Buildchain.
|
|
414
420
|
|
|
415
421
|
### Floating Buildchain contract lock
|
|
416
422
|
|
package/docs/release-train.md
CHANGED
|
@@ -74,6 +74,14 @@ an invalid transition fails closed.
|
|
|
74
74
|
changing `trainRoot`, `cutRoot`, candidate identity, generation, or state. The
|
|
75
75
|
same exact observation is idempotent.
|
|
76
76
|
|
|
77
|
+
Buildchain v3 Candidate Patrol resolves the open managed candidate's persisted
|
|
78
|
+
train before it considers a new qualified development head. It reads back the
|
|
79
|
+
candidate ref and tree, Alpha base, and exact Buildchain runtime. Matching
|
|
80
|
+
coordinates resume the frozen candidate; a newer development head becomes a
|
|
81
|
+
single rooted observation. Candidate, tree, base, runtime, or route drift emits
|
|
82
|
+
a rooted hold and stops settlement. Only a validated train already carrying an
|
|
83
|
+
enumerated `superseded` transition is reported as superseded.
|
|
84
|
+
|
|
77
85
|
## Readback and legacy state
|
|
78
86
|
|
|
79
87
|
`validateReleaseCut()` checks the standalone immutable cut and its canonical
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9-alpha.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"./issue-reporting": "./packages/core/issue-reporting.js",
|
|
34
34
|
"./kfd": "./packages/core/kfd.js",
|
|
35
35
|
"./kfd-product-gates": "./packages/core/kfd-product-gates.js",
|
|
36
|
+
"./kfd-adopter-manifest": "./packages/core/kfd-adopter-manifest.js",
|
|
37
|
+
"./kfd-adopter-release-binding": "./packages/core/artifact-verification-envelope.js",
|
|
36
38
|
"./kfd-agent-hub": "./packages/core/kfd-agent-hub.js",
|
|
37
39
|
"./buildchain-layout": "./packages/core/buildchain-layout.js",
|
|
38
40
|
"./release-line-bootstrap": "./packages/core/release-line-bootstrap.js",
|
|
@@ -87,7 +89,6 @@
|
|
|
87
89
|
"./site/schemas/release-passport-v1.schema.json": "./dist/site/schemas/release-passport-v1.schema.json",
|
|
88
90
|
"./site/schemas/kfd-agent-hub-adoption.schema.json": "./dist/site/schemas/kfd-agent-hub-adoption.schema.json",
|
|
89
91
|
"./site/schemas/kfd-product-gate-input-v1.schema.json": "./dist/site/schemas/kfd-product-gate-input-v1.schema.json",
|
|
90
|
-
"./site/schemas/kfd-support-projection-v1.schema.json": "./dist/site/schemas/kfd-support-projection-v1.schema.json",
|
|
91
92
|
"./site/buildchain-contract.json": "./dist/site/buildchain-contract.json",
|
|
92
93
|
"./site/product-mechanism.json": "./dist/site/product-mechanism.json",
|
|
93
94
|
"./site/release-provenance.json": "./dist/site/release-provenance.json",
|
|
@@ -139,7 +140,7 @@
|
|
|
139
140
|
"typescript": "^5.9.0"
|
|
140
141
|
},
|
|
141
142
|
"dependencies": {
|
|
142
|
-
"@kungfu-tech/kfd": "1.0.0-alpha.
|
|
143
|
+
"@kungfu-tech/kfd": "1.0.0-alpha.60",
|
|
143
144
|
"ajv": "8.20.0",
|
|
144
145
|
"smol-toml": "1.7.0"
|
|
145
146
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import kfdPackageJson from "@kungfu-tech/kfd/package.json" with { type: "json" };
|
|
3
|
+
import kfdSelfAdopterManifest from "@kungfu-tech/kfd/adopter-conformance/adopters/kfd/manifest.json" with { type: "json" };
|
|
4
|
+
import {
|
|
5
|
+
validateKfdAdopterManifestGate,
|
|
6
|
+
validateKfdLegacySupportMatrixProjection,
|
|
7
|
+
} from "./kfd-adopter-manifest.js";
|
|
8
|
+
import { kfdProductGateDigest } from "./kfd-product-gates.js";
|
|
2
9
|
|
|
3
10
|
export const ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT =
|
|
4
11
|
"kungfu-buildchain-artifact-verification-envelope";
|
|
@@ -6,6 +13,8 @@ export const ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT =
|
|
|
6
13
|
"kungfu-buildchain-artifact-verification-envelope-check";
|
|
7
14
|
export const KFX_ADMISSION_INPUTS_CONTRACT =
|
|
8
15
|
"kungfu-buildchain-kfx-admission-inputs";
|
|
16
|
+
export const KFD_ADOPTER_RELEASE_BINDING_CONTRACT =
|
|
17
|
+
"kungfu-buildchain-kfd-adopter-release-binding";
|
|
9
18
|
|
|
10
19
|
const ARTIFACT_VERIFICATION_CONTRACT =
|
|
11
20
|
"kungfu-buildchain-artifact-verification";
|
|
@@ -384,3 +393,108 @@ export function projectArtifactVerificationEnvelopeToKfx({
|
|
|
384
393
|
kfdAssessment: jsonClone(envelope.kfdAssessment),
|
|
385
394
|
};
|
|
386
395
|
}
|
|
396
|
+
|
|
397
|
+
export function installedKfdPackageArtifactRoot() {
|
|
398
|
+
const cut = kfdSelfAdopterManifest?.kfdCut?.package;
|
|
399
|
+
if (cut?.name !== kfdPackageJson.name || cut?.version !== kfdPackageJson.version
|
|
400
|
+
|| !SHA256_PATTERN.test(String(cut?.artifactRoot || ""))) {
|
|
401
|
+
throw new Error("installed KFD package does not expose a matching rooted self-adopter package cut");
|
|
402
|
+
}
|
|
403
|
+
return cut.artifactRoot;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function kfdBindingBody({ manifest, manifestGate, legacyProjection, paths }) {
|
|
407
|
+
return {
|
|
408
|
+
schemaVersion: 1,
|
|
409
|
+
contract: KFD_ADOPTER_RELEASE_BINDING_CONTRACT,
|
|
410
|
+
status: "passed",
|
|
411
|
+
qualifying: false,
|
|
412
|
+
selfCertified: false,
|
|
413
|
+
authority: {
|
|
414
|
+
manifestPath: paths.manifest,
|
|
415
|
+
manifestContract: manifest.contract,
|
|
416
|
+
manifestRoot: manifestGate.authority.manifestRoot,
|
|
417
|
+
gatePath: paths.gate,
|
|
418
|
+
gateRoot: manifestGate.gateRoot,
|
|
419
|
+
},
|
|
420
|
+
source: jsonClone(manifestGate.source),
|
|
421
|
+
standardPackage: jsonClone(manifestGate.standardPackage),
|
|
422
|
+
witness: {
|
|
423
|
+
decisionRoot: manifestGate.decisionWitness.root,
|
|
424
|
+
verificationReportRoot: manifestGate.manifestVerificationReportRoot,
|
|
425
|
+
bundleRoot: manifestGate.manifestBundleRoot,
|
|
426
|
+
},
|
|
427
|
+
legacyProjection: {
|
|
428
|
+
path: paths.legacy,
|
|
429
|
+
contract: legacyProjection.contract,
|
|
430
|
+
root: kfdProductGateDigest(legacyProjection),
|
|
431
|
+
authorityRoot: legacyProjection.authority.root,
|
|
432
|
+
gateRoot: legacyProjection.authority.gateRoot,
|
|
433
|
+
},
|
|
434
|
+
nonClaims: [
|
|
435
|
+
"This binding proves exact release-envelope consumption, not KFD certification or release authorization.",
|
|
436
|
+
"The legacy support matrix remains a projection of the standard adopter manifest authority.",
|
|
437
|
+
],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function createKfdAdopterReleaseBinding({
|
|
442
|
+
manifest,
|
|
443
|
+
manifestGate,
|
|
444
|
+
legacyProjection,
|
|
445
|
+
manifestPath = "kfd-adopter-manifest.json",
|
|
446
|
+
gatePath = "kfd-adopter-manifest-gate.json",
|
|
447
|
+
legacyProjectionPath = "kfd-support.json",
|
|
448
|
+
expectedSourceSha = "",
|
|
449
|
+
} = {}) {
|
|
450
|
+
const gate = validateKfdAdopterManifestGate(manifestGate, {
|
|
451
|
+
expectedSourceSha,
|
|
452
|
+
checkedAt: manifestGate?.checkedAt,
|
|
453
|
+
});
|
|
454
|
+
const legacy = validateKfdLegacySupportMatrixProjection(legacyProjection, { manifest, manifestGate });
|
|
455
|
+
if (!gate.valid || !legacy.valid) {
|
|
456
|
+
throw new Error(`KFD adopter release binding inputs are invalid: ${JSON.stringify([...gate.issues, ...legacy.issues])}`);
|
|
457
|
+
}
|
|
458
|
+
const body = kfdBindingBody({
|
|
459
|
+
manifest,
|
|
460
|
+
manifestGate,
|
|
461
|
+
legacyProjection,
|
|
462
|
+
paths: { manifest: manifestPath, gate: gatePath, legacy: legacyProjectionPath },
|
|
463
|
+
});
|
|
464
|
+
return { ...body, bindingRoot: kfdProductGateDigest(body) };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function validateKfdAdopterReleaseBinding(binding, {
|
|
468
|
+
manifest,
|
|
469
|
+
manifestGate,
|
|
470
|
+
legacyProjection,
|
|
471
|
+
expectedSourceSha = "",
|
|
472
|
+
} = {}) {
|
|
473
|
+
const issues = [];
|
|
474
|
+
if (!binding || binding.schemaVersion !== 1 || binding.contract !== KFD_ADOPTER_RELEASE_BINDING_CONTRACT) {
|
|
475
|
+
return { valid: false, issues: [{ code: "adopter-release-binding-contract", path: "", message: `binding must use ${KFD_ADOPTER_RELEASE_BINDING_CONTRACT} v1` }] };
|
|
476
|
+
}
|
|
477
|
+
const copy = jsonClone(binding);
|
|
478
|
+
const bindingRoot = copy.bindingRoot;
|
|
479
|
+
delete copy.bindingRoot;
|
|
480
|
+
if (bindingRoot !== kfdProductGateDigest(copy)) {
|
|
481
|
+
issues.push({ code: "adopter-release-binding-root", path: "bindingRoot", message: "release binding root does not match its content" });
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
const expected = createKfdAdopterReleaseBinding({
|
|
485
|
+
manifest,
|
|
486
|
+
manifestGate,
|
|
487
|
+
legacyProjection,
|
|
488
|
+
manifestPath: binding.authority?.manifestPath,
|
|
489
|
+
gatePath: binding.authority?.gatePath,
|
|
490
|
+
legacyProjectionPath: binding.legacyProjection?.path,
|
|
491
|
+
expectedSourceSha,
|
|
492
|
+
});
|
|
493
|
+
if (stableJson(binding) !== stableJson(expected)) {
|
|
494
|
+
issues.push({ code: "adopter-release-binding-drift", path: "", message: "release binding differs from the exact manifest, gate, or legacy projection closure" });
|
|
495
|
+
}
|
|
496
|
+
} catch (error) {
|
|
497
|
+
issues.push({ code: "adopter-release-binding-authority", path: "", message: error.message });
|
|
498
|
+
}
|
|
499
|
+
return { valid: issues.length === 0, issues };
|
|
500
|
+
}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
createReleaseTrain,
|
|
6
|
+
observeReleaseTrain,
|
|
7
|
+
validateReleaseTrain,
|
|
8
|
+
} from "./release-train.js";
|
|
9
|
+
|
|
10
|
+
export const RELEASE_TRAIN_HOLD_SCHEMA =
|
|
11
|
+
"kungfu-buildchain-release-train-hold/v1";
|
|
12
|
+
|
|
13
|
+
function canonical(value) {
|
|
14
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
15
|
+
if (value && typeof value === "object") {
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(value)
|
|
18
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
19
|
+
.map(([key, item]) => [key, canonical(item)]),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function evidenceRoot(value) {
|
|
26
|
+
return `sha256:${crypto
|
|
27
|
+
.createHash("sha256")
|
|
28
|
+
.update(JSON.stringify(canonical(value)))
|
|
29
|
+
.digest("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sameHold(left, right) {
|
|
33
|
+
return (
|
|
34
|
+
left?.schema === RELEASE_TRAIN_HOLD_SCHEMA &&
|
|
35
|
+
right?.schema === RELEASE_TRAIN_HOLD_SCHEMA &&
|
|
36
|
+
left.trainRoot === right.trainRoot &&
|
|
37
|
+
left.code === right.code &&
|
|
38
|
+
left.observedDevSha === right.observedDevSha &&
|
|
39
|
+
left.observedCandidateSha === right.observedCandidateSha &&
|
|
40
|
+
left.observedCandidateTreeSha === right.observedCandidateTreeSha &&
|
|
41
|
+
left.observedAlphaBaseSha === right.observedAlphaBaseSha &&
|
|
42
|
+
left.observedRuntimeSha === right.observedRuntimeSha
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function releaseTrainHold({
|
|
47
|
+
train,
|
|
48
|
+
code,
|
|
49
|
+
reason,
|
|
50
|
+
observedDevSha,
|
|
51
|
+
observedCandidateSha,
|
|
52
|
+
observedCandidateTreeSha,
|
|
53
|
+
observedAlphaBaseSha,
|
|
54
|
+
observedRuntimeSha,
|
|
55
|
+
recordedAt,
|
|
56
|
+
priorHold,
|
|
57
|
+
}) {
|
|
58
|
+
const body = {
|
|
59
|
+
schema: RELEASE_TRAIN_HOLD_SCHEMA,
|
|
60
|
+
trainRoot: train.trainRoot,
|
|
61
|
+
cutRoot: train.releaseCut.cutRoot,
|
|
62
|
+
generation: train.releaseCut.generation,
|
|
63
|
+
candidateSha: train.releaseCut.candidateSha,
|
|
64
|
+
candidateTreeSha: train.releaseCut.candidateTreeSha,
|
|
65
|
+
code,
|
|
66
|
+
reason,
|
|
67
|
+
observedDevSha,
|
|
68
|
+
observedCandidateSha,
|
|
69
|
+
observedCandidateTreeSha,
|
|
70
|
+
observedAlphaBaseSha,
|
|
71
|
+
observedRuntimeSha,
|
|
72
|
+
recordedAt,
|
|
73
|
+
};
|
|
74
|
+
if (sameHold(priorHold, body)) return priorHold;
|
|
75
|
+
return { ...body, holdRoot: evidenceRoot(body) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function observeDevOnce(train, observedDevSha, observedAt) {
|
|
79
|
+
if (
|
|
80
|
+
train.observations.some(
|
|
81
|
+
(observation) => observation.observedDevSha === observedDevSha,
|
|
82
|
+
)
|
|
83
|
+
) {
|
|
84
|
+
return train;
|
|
85
|
+
}
|
|
86
|
+
return observeReleaseTrain(train, { observedDevSha, observedAt });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function resolveManagedCandidate({
|
|
90
|
+
client,
|
|
91
|
+
targetBranch,
|
|
92
|
+
repository,
|
|
93
|
+
sourceBranch,
|
|
94
|
+
parseCandidate,
|
|
95
|
+
}) {
|
|
96
|
+
const openPullRequests = await client.listOpenPullRequests(targetBranch);
|
|
97
|
+
const managed = openPullRequests
|
|
98
|
+
.map((pullRequest) => parseCandidate(pullRequest, targetBranch))
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
for (const candidate of managed) {
|
|
101
|
+
if (
|
|
102
|
+
candidate.state &&
|
|
103
|
+
(candidate.state.repository !== repository ||
|
|
104
|
+
candidate.state.sourceBranch !== sourceBranch)
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`candidate PR #${candidate.number} state does not bind ${repository} ${sourceBranch}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (managed.length > 1) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`multiple open Buildchain candidate PRs target ${targetBranch}: ${managed
|
|
114
|
+
.map((candidate) => `#${candidate.number}`)
|
|
115
|
+
.join(", ")}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return managed[0] || null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function cutInitialReleaseTrain({
|
|
122
|
+
options,
|
|
123
|
+
client,
|
|
124
|
+
decision,
|
|
125
|
+
candidate,
|
|
126
|
+
sourceSha,
|
|
127
|
+
targetSha,
|
|
128
|
+
observedSourceHeadSha,
|
|
129
|
+
}) {
|
|
130
|
+
if (!candidate) return null;
|
|
131
|
+
if (!options.buildchainRuntimeSha) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"buildchainRuntimeSha is required to cut an active Release Train",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (!client.resolveCommitTreeSha) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"active Release Train creation requires candidate tree readback",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const candidateTreeSha = await client.resolveCommitTreeSha(sourceSha);
|
|
142
|
+
let train = createReleaseTrain({
|
|
143
|
+
repository: options.repository,
|
|
144
|
+
sourceBranch: options.sourceBranch,
|
|
145
|
+
targetBranch: options.targetBranch,
|
|
146
|
+
originDevSha: sourceSha,
|
|
147
|
+
candidateSha: sourceSha,
|
|
148
|
+
candidateTreeSha,
|
|
149
|
+
alphaBaseSha: targetSha,
|
|
150
|
+
buildchainRuntimeSha: options.buildchainRuntimeSha,
|
|
151
|
+
generation: 1,
|
|
152
|
+
authorityRoots: [decision.decisionRoot, candidate.qualificationRoot].sort(),
|
|
153
|
+
createdAt: options.cutCreatedAt || options.now,
|
|
154
|
+
});
|
|
155
|
+
train = observeDevOnce(train, observedSourceHeadSha, options.now);
|
|
156
|
+
if (
|
|
157
|
+
options.expectedCutRoot &&
|
|
158
|
+
train.releaseCut.cutRoot !== options.expectedCutRoot
|
|
159
|
+
) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Release Cut changed between observation and settlement: expected ${options.expectedCutRoot}, observed ${train.releaseCut.cutRoot}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return train;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function reconcileActiveReleaseTrain({
|
|
168
|
+
releaseTrain,
|
|
169
|
+
repository,
|
|
170
|
+
sourceBranch,
|
|
171
|
+
targetBranch,
|
|
172
|
+
activeCandidateSha,
|
|
173
|
+
sourceLockSha,
|
|
174
|
+
candidateTreeSha,
|
|
175
|
+
alphaBaseSha,
|
|
176
|
+
buildchainRuntimeSha,
|
|
177
|
+
observedDevSha,
|
|
178
|
+
observedAt,
|
|
179
|
+
priorHold = null,
|
|
180
|
+
}) {
|
|
181
|
+
const train = observeDevOnce(
|
|
182
|
+
validateReleaseTrain(releaseTrain),
|
|
183
|
+
observedDevSha,
|
|
184
|
+
observedAt,
|
|
185
|
+
);
|
|
186
|
+
if (train.state.status === "superseded") {
|
|
187
|
+
return { status: "superseded", train, hold: null };
|
|
188
|
+
}
|
|
189
|
+
const cut = train.releaseCut;
|
|
190
|
+
const checks = [
|
|
191
|
+
[
|
|
192
|
+
cut.repository !== repository ||
|
|
193
|
+
cut.sourceBranch !== sourceBranch ||
|
|
194
|
+
cut.targetBranch !== targetBranch,
|
|
195
|
+
"invalid-authority",
|
|
196
|
+
"the active Release Cut route no longer matches the controller route",
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
activeCandidateSha !== cut.candidateSha ||
|
|
200
|
+
sourceLockSha !== cut.candidateSha,
|
|
201
|
+
"candidate-ref-drift",
|
|
202
|
+
"the managed candidate PR or source-lock ref drifted from the frozen candidate",
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
candidateTreeSha !== cut.candidateTreeSha,
|
|
206
|
+
"candidate-tree-drift",
|
|
207
|
+
"the frozen candidate tree readback does not match the Release Cut",
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
alphaBaseSha !== cut.alphaBaseSha,
|
|
211
|
+
"alpha-base-drift",
|
|
212
|
+
"the Alpha base moved after the Release Cut and requires explicit supersession or repair",
|
|
213
|
+
],
|
|
214
|
+
[
|
|
215
|
+
buildchainRuntimeSha !== cut.buildchainRuntimeSha,
|
|
216
|
+
"runtime-drift",
|
|
217
|
+
"the Buildchain runtime moved after the Release Cut and requires explicit supersession or repair",
|
|
218
|
+
],
|
|
219
|
+
];
|
|
220
|
+
const failure = checks.find(([failed]) => failed);
|
|
221
|
+
if (failure) {
|
|
222
|
+
const [, code, reason] = failure;
|
|
223
|
+
return {
|
|
224
|
+
status: "held",
|
|
225
|
+
train,
|
|
226
|
+
hold: releaseTrainHold({
|
|
227
|
+
train,
|
|
228
|
+
code,
|
|
229
|
+
reason,
|
|
230
|
+
observedDevSha,
|
|
231
|
+
observedCandidateSha: sourceLockSha,
|
|
232
|
+
observedCandidateTreeSha: candidateTreeSha,
|
|
233
|
+
observedAlphaBaseSha: alphaBaseSha,
|
|
234
|
+
observedRuntimeSha: buildchainRuntimeSha,
|
|
235
|
+
recordedAt: observedAt,
|
|
236
|
+
priorHold,
|
|
237
|
+
}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
status: "active",
|
|
242
|
+
train,
|
|
243
|
+
hold: null,
|
|
244
|
+
drift: {
|
|
245
|
+
observedDevSha,
|
|
246
|
+
originDevSha: cut.originDevSha,
|
|
247
|
+
moved: observedDevSha !== cut.originDevSha,
|
|
248
|
+
observationRoot:
|
|
249
|
+
train.observations.find(
|
|
250
|
+
(observation) => observation.observedDevSha === observedDevSha,
|
|
251
|
+
)?.observationRoot || null,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function activeTrainDecision({
|
|
257
|
+
options,
|
|
258
|
+
candidate,
|
|
259
|
+
reconciliation,
|
|
260
|
+
observedSourceHeadSha,
|
|
261
|
+
targetSha,
|
|
262
|
+
}) {
|
|
263
|
+
const cut = reconciliation.train.releaseCut;
|
|
264
|
+
const eligible = reconciliation.status === "active";
|
|
265
|
+
const body = {
|
|
266
|
+
schema: "kungfu-buildchain-channel-candidate-decision/v1",
|
|
267
|
+
eligible,
|
|
268
|
+
reason: eligible
|
|
269
|
+
? "active-release-train"
|
|
270
|
+
: reconciliation.status === "superseded"
|
|
271
|
+
? "active-release-train-superseded"
|
|
272
|
+
: "active-release-train-held",
|
|
273
|
+
repository: options.repository,
|
|
274
|
+
source: { branch: options.sourceBranch, sha: cut.candidateSha },
|
|
275
|
+
target: { branch: options.targetBranch, sha: targetSha },
|
|
276
|
+
comparison: { status: "frozen-release-cut", aheadBy: 0 },
|
|
277
|
+
selection: {
|
|
278
|
+
mode: "active-release-train",
|
|
279
|
+
observedSourceHeadSha,
|
|
280
|
+
skippedNewerCommitCount: 0,
|
|
281
|
+
trainRoot: reconciliation.train.trainRoot,
|
|
282
|
+
cutRoot: cut.cutRoot,
|
|
283
|
+
generation: cut.generation,
|
|
284
|
+
candidateTreeSha: cut.candidateTreeSha,
|
|
285
|
+
},
|
|
286
|
+
workflowEvidence: [],
|
|
287
|
+
requiredWorkflowPaths: [],
|
|
288
|
+
sourceLockRef: candidate.sourceLockRef,
|
|
289
|
+
...(reconciliation.hold
|
|
290
|
+
? {
|
|
291
|
+
blockReason: reconciliation.hold.reason,
|
|
292
|
+
holdRoot: reconciliation.hold.holdRoot,
|
|
293
|
+
}
|
|
294
|
+
: {}),
|
|
295
|
+
decidedAt: options.now,
|
|
296
|
+
};
|
|
297
|
+
return { ...body, decisionRoot: evidenceRoot(body) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function candidateIdentity(candidate) {
|
|
301
|
+
return {
|
|
302
|
+
sourceSha: candidate.sourceSha,
|
|
303
|
+
sourceLockRef: candidate.sourceLockRef,
|
|
304
|
+
decisionRoot: candidate.decisionRoot || null,
|
|
305
|
+
pullRequestNumber: candidate.number,
|
|
306
|
+
pullRequestUrl: candidate.url,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function semanticState(value) {
|
|
311
|
+
if (!value) return null;
|
|
312
|
+
const normalized = { ...value };
|
|
313
|
+
delete normalized.observedAt;
|
|
314
|
+
delete normalized.observationDecisionRoot;
|
|
315
|
+
delete normalized.stateRoot;
|
|
316
|
+
delete normalized.priorStateRoot;
|
|
317
|
+
return normalized;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function runActiveReleaseTrain({
|
|
321
|
+
options,
|
|
322
|
+
client,
|
|
323
|
+
activeCandidate,
|
|
324
|
+
adapters,
|
|
325
|
+
}) {
|
|
326
|
+
const [observedSourceHeadSha, targetSha, sourceLockSha] = await Promise.all([
|
|
327
|
+
client.resolveBranch(options.sourceBranch),
|
|
328
|
+
client.resolveBranch(options.targetBranch),
|
|
329
|
+
client.resolveBranch(activeCandidate.sourceLockRef),
|
|
330
|
+
]);
|
|
331
|
+
if (!client.resolveCommitTreeSha) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
"active Release Train validation requires candidate tree readback",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const candidateTreeSha = await client.resolveCommitTreeSha(
|
|
337
|
+
activeCandidate.sourceSha,
|
|
338
|
+
);
|
|
339
|
+
const reconciliation = reconcileActiveReleaseTrain({
|
|
340
|
+
releaseTrain: activeCandidate.releaseTrain,
|
|
341
|
+
repository: options.repository,
|
|
342
|
+
sourceBranch: options.sourceBranch,
|
|
343
|
+
targetBranch: options.targetBranch,
|
|
344
|
+
activeCandidateSha: activeCandidate.sourceSha,
|
|
345
|
+
sourceLockSha,
|
|
346
|
+
candidateTreeSha,
|
|
347
|
+
alphaBaseSha: targetSha,
|
|
348
|
+
buildchainRuntimeSha: options.buildchainRuntimeSha,
|
|
349
|
+
observedDevSha: observedSourceHeadSha,
|
|
350
|
+
observedAt: options.now,
|
|
351
|
+
priorHold: activeCandidate.hold,
|
|
352
|
+
});
|
|
353
|
+
if (
|
|
354
|
+
options.expectedCutRoot &&
|
|
355
|
+
reconciliation.train.releaseCut.cutRoot !== options.expectedCutRoot
|
|
356
|
+
) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`active Release Cut changed between observation and settlement: expected ${options.expectedCutRoot}, observed ${reconciliation.train.releaseCut.cutRoot}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const decision = activeTrainDecision({
|
|
362
|
+
options,
|
|
363
|
+
candidate: activeCandidate,
|
|
364
|
+
reconciliation,
|
|
365
|
+
observedSourceHeadSha,
|
|
366
|
+
targetSha,
|
|
367
|
+
});
|
|
368
|
+
if (
|
|
369
|
+
options.expectedSelectedSha &&
|
|
370
|
+
decision.source.sha !== options.expectedSelectedSha
|
|
371
|
+
) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`selected source changed between observation and settlement: expected ${options.expectedSelectedSha}, observed ${decision.source.sha}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const priorStateRoot = adapters.persistedStateRoot(activeCandidate);
|
|
377
|
+
if (
|
|
378
|
+
options.expectedPriorStateRoot &&
|
|
379
|
+
options.expectedPriorStateRoot !== priorStateRoot
|
|
380
|
+
) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`candidate controller compare-and-swap failed: expected prior state root ${options.expectedPriorStateRoot}, observed ${priorStateRoot}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const desiredState = adapters.candidateStateBody({
|
|
386
|
+
options,
|
|
387
|
+
targetSha,
|
|
388
|
+
decision,
|
|
389
|
+
activeCandidate: candidateIdentity(activeCandidate),
|
|
390
|
+
nextCandidate: null,
|
|
391
|
+
supersededCandidate: null,
|
|
392
|
+
priorStateRoot,
|
|
393
|
+
generation: reconciliation.train.releaseCut.generation,
|
|
394
|
+
tombstones: activeCandidate.state?.tombstones || [],
|
|
395
|
+
releaseTrain: reconciliation.train,
|
|
396
|
+
hold: reconciliation.hold,
|
|
397
|
+
});
|
|
398
|
+
const transitionNeeded =
|
|
399
|
+
!activeCandidate.state ||
|
|
400
|
+
JSON.stringify(canonical(semanticState(activeCandidate.state))) !==
|
|
401
|
+
JSON.stringify(canonical(semanticState(desiredState)));
|
|
402
|
+
const state = transitionNeeded ? desiredState : activeCandidate.state;
|
|
403
|
+
let settlementAction = "none";
|
|
404
|
+
if (transitionNeeded && options.settlementAuthorized && !options.dryRun) {
|
|
405
|
+
const nextBody = adapters.replaceCandidateStateMarker(
|
|
406
|
+
activeCandidate.body,
|
|
407
|
+
state,
|
|
408
|
+
);
|
|
409
|
+
await client.updatePullRequestBody(
|
|
410
|
+
activeCandidate.number,
|
|
411
|
+
nextBody,
|
|
412
|
+
priorStateRoot,
|
|
413
|
+
);
|
|
414
|
+
settlementAction =
|
|
415
|
+
reconciliation.status === "held"
|
|
416
|
+
? "hold-active-release-train"
|
|
417
|
+
: reconciliation.status === "superseded"
|
|
418
|
+
? "record-superseded-release-train"
|
|
419
|
+
: "observe-active-release-train";
|
|
420
|
+
}
|
|
421
|
+
const pullRequest = adapters.activePullRequest(activeCandidate);
|
|
422
|
+
const autoMerge = await adapters.armCandidateAutoMerge(
|
|
423
|
+
options,
|
|
424
|
+
reconciliation.status,
|
|
425
|
+
pullRequest,
|
|
426
|
+
client,
|
|
427
|
+
);
|
|
428
|
+
return {
|
|
429
|
+
schema: "kungfu-buildchain-dev-alpha-candidate-patrol/v1",
|
|
430
|
+
dryRun: options.dryRun,
|
|
431
|
+
createPullRequest: options.createPullRequest,
|
|
432
|
+
settlementAuthorized: options.settlementAuthorized,
|
|
433
|
+
autoMerge,
|
|
434
|
+
decision,
|
|
435
|
+
releaseTrain: reconciliation.train,
|
|
436
|
+
drift: reconciliation.drift || null,
|
|
437
|
+
hold: reconciliation.hold,
|
|
438
|
+
controller: {
|
|
439
|
+
schema: adapters.stateSchema,
|
|
440
|
+
state: reconciliation.status,
|
|
441
|
+
activeCandidate,
|
|
442
|
+
nextCandidate: null,
|
|
443
|
+
supersededCandidate: null,
|
|
444
|
+
settlementAction,
|
|
445
|
+
stateRoot: state.stateRoot,
|
|
446
|
+
priorStateRoot,
|
|
447
|
+
generation: reconciliation.train.releaseCut.generation,
|
|
448
|
+
trainRoot: reconciliation.train.trainRoot,
|
|
449
|
+
cutRoot: reconciliation.train.releaseCut.cutRoot,
|
|
450
|
+
candidateTreeSha: reconciliation.train.releaseCut.candidateTreeSha,
|
|
451
|
+
buildchainRuntimeSha:
|
|
452
|
+
reconciliation.train.releaseCut.buildchainRuntimeSha,
|
|
453
|
+
cutCreatedAt: reconciliation.train.releaseCut.createdAt,
|
|
454
|
+
observedAt: options.now,
|
|
455
|
+
holdRoot: reconciliation.hold?.holdRoot || null,
|
|
456
|
+
tombstones: state.tombstones || [],
|
|
457
|
+
},
|
|
458
|
+
pullRequest,
|
|
459
|
+
};
|
|
460
|
+
}
|