@kungfu-tech/buildchain 2.12.0 → 2.12.1-alpha.10
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/bin/buildchain.mjs +18 -0
- package/dist/site/buildchain-contract.json +8 -8
- package/dist/site/buildchain-site.json +92 -26
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +12 -0
- package/dist/site/kfd-claims.json +182 -14
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +8 -7
- package/dist/site/node-api-registry.json +20 -7
- package/dist/site/page-registry.json +81 -15
- package/dist/site/public-surface-audit.json +95 -10
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +68 -3
- package/docs/MAP.md +3 -0
- package/docs/binary-distribution.md +20 -1
- package/docs/cli.md +17 -0
- package/docs/kfd-support.md +44 -1
- package/docs/lifecycle-protocol.md +12 -0
- package/docs/release-governance.md +23 -0
- package/docs/reusable-build-surface.md +69 -0
- package/docs/stable-candidate-patrol.md +188 -0
- package/package.json +3 -1
- package/packages/core/buildchain-config.js +58 -0
- package/packages/core/buildchain-kfd-claims.js +51 -2
- package/packages/core/buildchain-layout.js +46 -0
- package/packages/core/index.js +19 -0
- package/packages/core/kfd3-surface-register.js +59 -2
- package/packages/core/stable-candidate-ledger.js +276 -0
- package/scripts/build-standalone-binary.mjs +2 -1
- package/scripts/check-inventory.mjs +4 -1
- package/scripts/generate-site-bundle.mjs +9 -1
- package/scripts/init-repo.mjs +1 -1
- package/scripts/locked-source-checkout.mjs +52 -12
- package/scripts/npm-publish-dry-run.mjs +1 -1
- package/scripts/publication-artifact.mjs +1 -1
- package/scripts/publication-package.mjs +1 -1
- package/scripts/release-propagation.mjs +1 -1
- package/scripts/stable-candidate-patrol.mjs +448 -0
- package/scripts/stable-candidate-policy.mjs +25 -0
- package/scripts/stable-candidate-qualification.mjs +239 -0
|
@@ -67,6 +67,63 @@ function normalizeKinds(kinds = []) {
|
|
|
67
67
|
return [...new Set(selected.map(normalizeKind))].sort();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export function normalizeKfd3DistributionDeclaration(distribution, { surfaceId = "surface" } = {}) {
|
|
71
|
+
if (!distribution || typeof distribution !== "object" || Array.isArray(distribution)) {
|
|
72
|
+
throw new Error(`KFD-3 surface ${surfaceId} distribution must be an object`);
|
|
73
|
+
}
|
|
74
|
+
const registrar = String(distribution.registrar || "").trim();
|
|
75
|
+
if (!registrar) {
|
|
76
|
+
throw new Error(`KFD-3 surface ${surfaceId} distribution.registrar is required`);
|
|
77
|
+
}
|
|
78
|
+
const tasks = Array.isArray(distribution.tasks)
|
|
79
|
+
? [...new Set(distribution.tasks.map((entry) => String(entry || "").trim()).filter(Boolean))]
|
|
80
|
+
: [];
|
|
81
|
+
if (tasks.length === 0) {
|
|
82
|
+
throw new Error(`KFD-3 surface ${surfaceId} distribution.tasks must contain at least one task`);
|
|
83
|
+
}
|
|
84
|
+
const sourceArtifacts = Array.isArray(distribution.artifacts) ? distribution.artifacts : [];
|
|
85
|
+
if (sourceArtifacts.length === 0) {
|
|
86
|
+
throw new Error(`KFD-3 surface ${surfaceId} distribution.artifacts must contain at least one artifact`);
|
|
87
|
+
}
|
|
88
|
+
const artifacts = sourceArtifacts.map((artifact, index) => {
|
|
89
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
|
|
90
|
+
throw new Error(`KFD-3 surface ${surfaceId} distribution.artifacts[${index}] must be an object`);
|
|
91
|
+
}
|
|
92
|
+
const normalized = {
|
|
93
|
+
...artifact,
|
|
94
|
+
kind: String(artifact.kind || "").trim(),
|
|
95
|
+
platform: String(artifact.platform || "").trim(),
|
|
96
|
+
pathGlob: String(artifact.pathGlob || "").trim(),
|
|
97
|
+
};
|
|
98
|
+
for (const field of ["kind", "platform", "pathGlob"]) {
|
|
99
|
+
if (!normalized[field]) {
|
|
100
|
+
throw new Error(`KFD-3 surface ${surfaceId} distribution.artifacts[${index}].${field} is required`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (artifact.sha256 !== undefined) {
|
|
104
|
+
normalized.sha256 = String(artifact.sha256 || "").trim().toLowerCase();
|
|
105
|
+
if (!/^[a-f0-9]{64}$/.test(normalized.sha256)) {
|
|
106
|
+
throw new Error(`KFD-3 surface ${surfaceId} distribution.artifacts[${index}].sha256 must be 64 lowercase hex characters`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return normalized;
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
...distribution,
|
|
113
|
+
registrar,
|
|
114
|
+
tasks,
|
|
115
|
+
artifacts,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeRegistrySurface(entry) {
|
|
120
|
+
if (!entry?.distribution) return entry;
|
|
121
|
+
return {
|
|
122
|
+
...entry,
|
|
123
|
+
distribution: normalizeKfd3DistributionDeclaration(entry.distribution, { surfaceId: entry.id || "surface" }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
70
127
|
function stableId(value) {
|
|
71
128
|
return String(value || "")
|
|
72
129
|
.toLowerCase()
|
|
@@ -366,7 +423,7 @@ export function readKfd3SurfaceRegistry({ cwd = process.cwd(), registryPath = ""
|
|
|
366
423
|
contract: registry.contract || KFD3_SURFACE_REGISTRY_CONTRACT,
|
|
367
424
|
product: registry.product || {},
|
|
368
425
|
registryPath: registry.registryPath || effectiveRegistryPath,
|
|
369
|
-
surfaces: Array.isArray(registry.surfaces) ? registry.surfaces : [],
|
|
426
|
+
surfaces: Array.isArray(registry.surfaces) ? registry.surfaces.map(normalizeRegistrySurface) : [],
|
|
370
427
|
policy: registry.policy || {},
|
|
371
428
|
};
|
|
372
429
|
}
|
|
@@ -379,7 +436,7 @@ export function writeKfd3SurfaceRegistry({ cwd = process.cwd(), registryPath = "
|
|
|
379
436
|
product: registry.product || {},
|
|
380
437
|
registryPath: effectiveRegistryPath,
|
|
381
438
|
surfaces: uniqueSurfaces(registry.surfaces || []).map((entry) => ({
|
|
382
|
-
...entry,
|
|
439
|
+
...normalizeRegistrySurface(entry),
|
|
383
440
|
state: entry.state || "declared",
|
|
384
441
|
declaration: entry.declaration || {
|
|
385
442
|
owner: "product",
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
export const STABLE_CANDIDATE_LEDGER_CONTRACT = "kungfu-buildchain-stable-candidate-ledger";
|
|
2
|
+
export const STABLE_CANDIDATE_STATES = Object.freeze([
|
|
3
|
+
"soaking",
|
|
4
|
+
"qualified",
|
|
5
|
+
"revoked",
|
|
6
|
+
"promoted",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
function text(value = "") {
|
|
10
|
+
return String(value ?? "").trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function iso(value, label) {
|
|
14
|
+
const normalized = text(value);
|
|
15
|
+
const milliseconds = Date.parse(normalized);
|
|
16
|
+
if (!normalized || !Number.isFinite(milliseconds)) {
|
|
17
|
+
throw new Error(`${label} must be an ISO-8601 timestamp`);
|
|
18
|
+
}
|
|
19
|
+
return new Date(milliseconds).toISOString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function exactAlpha(version) {
|
|
23
|
+
const normalized = text(version).replace(/^v/, "");
|
|
24
|
+
const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)-alpha\.(\d+)$/);
|
|
25
|
+
if (!match) {
|
|
26
|
+
throw new Error(`candidate version must be an exact alpha, got ${version || "<empty>"}`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
version: normalized,
|
|
30
|
+
stableVersion: `${match[1]}.${match[2]}.${match[3]}`,
|
|
31
|
+
order: match.slice(1).map(Number),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sha(value, label = "candidate sha") {
|
|
36
|
+
const normalized = text(value).toLowerCase();
|
|
37
|
+
if (!/^[0-9a-f]{40}$/.test(normalized)) {
|
|
38
|
+
throw new Error(`${label} must be a 40-character commit SHA`);
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clone(value) {
|
|
44
|
+
return JSON.parse(JSON.stringify(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compareCandidates(left, right) {
|
|
48
|
+
const a = exactAlpha(left.version).order;
|
|
49
|
+
const b = exactAlpha(right.version).order;
|
|
50
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
51
|
+
if (a[index] !== b[index]) return a[index] - b[index];
|
|
52
|
+
}
|
|
53
|
+
return left.publishedAt.localeCompare(right.publishedAt);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createStableCandidateLedger({ repository, targetBranch, now = new Date().toISOString() } = {}) {
|
|
57
|
+
const normalizedRepository = text(repository);
|
|
58
|
+
const normalizedTargetBranch = text(targetBranch).replace(/^refs\/heads\//, "");
|
|
59
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(normalizedRepository)) {
|
|
60
|
+
throw new Error(`repository must be owner/repo, got ${repository || "<empty>"}`);
|
|
61
|
+
}
|
|
62
|
+
if (!/^release\/v\d+\/v\d+\.\d+$/.test(normalizedTargetBranch)) {
|
|
63
|
+
throw new Error(`targetBranch must be release/vN/vN.M, got ${targetBranch || "<empty>"}`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
contract: STABLE_CANDIDATE_LEDGER_CONTRACT,
|
|
68
|
+
repository: normalizedRepository,
|
|
69
|
+
targetBranch: normalizedTargetBranch,
|
|
70
|
+
hold: { enabled: false, reason: "", updatedAt: iso(now, "now") },
|
|
71
|
+
candidates: [],
|
|
72
|
+
updatedAt: iso(now, "now"),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function normalizeStableCandidateLedger(input, expected = {}) {
|
|
77
|
+
const ledger = clone(input || {});
|
|
78
|
+
if (ledger.contract !== STABLE_CANDIDATE_LEDGER_CONTRACT || Number(ledger.schemaVersion) !== 1) {
|
|
79
|
+
throw new Error(`stable candidate ledger must use ${STABLE_CANDIDATE_LEDGER_CONTRACT} schemaVersion 1`);
|
|
80
|
+
}
|
|
81
|
+
if (expected.repository && ledger.repository !== expected.repository) {
|
|
82
|
+
throw new Error(`stable candidate ledger repository mismatch: ${ledger.repository} != ${expected.repository}`);
|
|
83
|
+
}
|
|
84
|
+
if (expected.targetBranch && ledger.targetBranch !== expected.targetBranch) {
|
|
85
|
+
throw new Error(`stable candidate ledger targetBranch mismatch: ${ledger.targetBranch} != ${expected.targetBranch}`);
|
|
86
|
+
}
|
|
87
|
+
ledger.hold ||= { enabled: false, reason: "", updatedAt: ledger.updatedAt };
|
|
88
|
+
ledger.candidates = (ledger.candidates || []).map((candidate) => {
|
|
89
|
+
const parsed = exactAlpha(candidate.version);
|
|
90
|
+
const state = text(candidate.state);
|
|
91
|
+
if (!STABLE_CANDIDATE_STATES.includes(state)) {
|
|
92
|
+
throw new Error(`unsupported candidate state ${state || "<empty>"}`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...candidate,
|
|
96
|
+
version: parsed.version,
|
|
97
|
+
stableVersion: parsed.stableVersion,
|
|
98
|
+
sha: sha(candidate.sha),
|
|
99
|
+
publishedAt: iso(candidate.publishedAt, `candidate ${parsed.version} publishedAt`),
|
|
100
|
+
state,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
return ledger;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function registerStableCandidate(ledgerInput, candidateInput, { now = new Date().toISOString() } = {}) {
|
|
107
|
+
const ledger = normalizeStableCandidateLedger(ledgerInput);
|
|
108
|
+
const parsed = exactAlpha(candidateInput.version);
|
|
109
|
+
const candidateSha = sha(candidateInput.sha);
|
|
110
|
+
const existing = ledger.candidates.find((candidate) => candidate.version === parsed.version);
|
|
111
|
+
if (existing) {
|
|
112
|
+
if (existing.sha !== candidateSha) {
|
|
113
|
+
throw new Error(`candidate ${parsed.version} is already bound to ${existing.sha}, not ${candidateSha}`);
|
|
114
|
+
}
|
|
115
|
+
return ledger;
|
|
116
|
+
}
|
|
117
|
+
ledger.candidates.push({
|
|
118
|
+
version: parsed.version,
|
|
119
|
+
stableVersion: parsed.stableVersion,
|
|
120
|
+
sha: candidateSha,
|
|
121
|
+
tag: `v${parsed.version}`,
|
|
122
|
+
publishedAt: iso(candidateInput.publishedAt, `candidate ${parsed.version} publishedAt`),
|
|
123
|
+
state: "soaking",
|
|
124
|
+
qualification: {
|
|
125
|
+
ok: false,
|
|
126
|
+
observedAt: "",
|
|
127
|
+
soakStartedAt: "",
|
|
128
|
+
soakElapsedSeconds: 0,
|
|
129
|
+
requiredSeconds: 0,
|
|
130
|
+
checks: [],
|
|
131
|
+
},
|
|
132
|
+
decision: { reason: "registered", updatedAt: iso(now, "now"), actor: text(candidateInput.actor) },
|
|
133
|
+
});
|
|
134
|
+
ledger.candidates.sort(compareCandidates);
|
|
135
|
+
ledger.updatedAt = iso(now, "now");
|
|
136
|
+
return ledger;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function qualifyStableCandidate(
|
|
140
|
+
ledgerInput,
|
|
141
|
+
observation,
|
|
142
|
+
{ minimumSoakSeconds = 3600, now = new Date().toISOString() } = {},
|
|
143
|
+
) {
|
|
144
|
+
const ledger = normalizeStableCandidateLedger(ledgerInput);
|
|
145
|
+
const version = exactAlpha(observation.version).version;
|
|
146
|
+
const candidate = ledger.candidates.find((entry) => entry.version === version);
|
|
147
|
+
if (!candidate) throw new Error(`candidate ${version} is not registered`);
|
|
148
|
+
if (candidate.sha !== sha(observation.sha)) {
|
|
149
|
+
throw new Error(`candidate ${version} observation SHA does not match ledger`);
|
|
150
|
+
}
|
|
151
|
+
if (["revoked", "promoted"].includes(candidate.state)) return ledger;
|
|
152
|
+
|
|
153
|
+
const checks = Array.isArray(observation.checks) ? observation.checks.map((check) => ({
|
|
154
|
+
id: text(check.id),
|
|
155
|
+
status: text(check.status),
|
|
156
|
+
completedAt: check.completedAt ? iso(check.completedAt, `check ${check.id} completedAt`) : "",
|
|
157
|
+
evidenceUrl: text(check.evidenceUrl),
|
|
158
|
+
})) : [];
|
|
159
|
+
const passed = checks.length > 0 && checks.every((check) => check.id && check.status === "pass" && check.completedAt);
|
|
160
|
+
const latestCheck = passed ? Math.max(...checks.map((check) => Date.parse(check.completedAt))) : NaN;
|
|
161
|
+
const soakStartedAt = passed
|
|
162
|
+
? new Date(Math.max(Date.parse(candidate.publishedAt), latestCheck)).toISOString()
|
|
163
|
+
: "";
|
|
164
|
+
const elapsedSeconds = passed
|
|
165
|
+
? Math.max(0, Math.floor((Date.parse(iso(now, "now")) - Date.parse(soakStartedAt)) / 1000))
|
|
166
|
+
: 0;
|
|
167
|
+
candidate.qualification = {
|
|
168
|
+
ok: passed && elapsedSeconds >= Number(minimumSoakSeconds),
|
|
169
|
+
observedAt: iso(now, "now"),
|
|
170
|
+
soakStartedAt,
|
|
171
|
+
soakElapsedSeconds: elapsedSeconds,
|
|
172
|
+
requiredSeconds: Number(minimumSoakSeconds),
|
|
173
|
+
checks,
|
|
174
|
+
};
|
|
175
|
+
candidate.state = candidate.qualification.ok ? "qualified" : "soaking";
|
|
176
|
+
candidate.decision = {
|
|
177
|
+
reason: candidate.qualification.ok ? "qualification-satisfied" : passed ? "soaking" : "checks-incomplete",
|
|
178
|
+
updatedAt: iso(now, "now"),
|
|
179
|
+
actor: text(observation.actor),
|
|
180
|
+
};
|
|
181
|
+
ledger.updatedAt = iso(now, "now");
|
|
182
|
+
return ledger;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function revokeStableCandidate(ledgerInput, versionInput, { reason, actor = "", now = new Date().toISOString() } = {}) {
|
|
186
|
+
const ledger = normalizeStableCandidateLedger(ledgerInput);
|
|
187
|
+
const version = exactAlpha(versionInput).version;
|
|
188
|
+
const candidate = ledger.candidates.find((entry) => entry.version === version);
|
|
189
|
+
if (!candidate) throw new Error(`candidate ${version} is not registered`);
|
|
190
|
+
if (candidate.state === "promoted") throw new Error(`promoted candidate ${version} cannot be revoked`);
|
|
191
|
+
if (!text(reason)) throw new Error("candidate revocation requires a reason");
|
|
192
|
+
candidate.state = "revoked";
|
|
193
|
+
candidate.decision = { reason: text(reason), actor: text(actor), updatedAt: iso(now, "now") };
|
|
194
|
+
ledger.updatedAt = iso(now, "now");
|
|
195
|
+
return ledger;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function setStableCandidateHold(ledgerInput, enabled, { reason = "", now = new Date().toISOString() } = {}) {
|
|
199
|
+
const ledger = normalizeStableCandidateLedger(ledgerInput);
|
|
200
|
+
if (enabled && !text(reason)) throw new Error("enabling stable candidate hold requires a reason");
|
|
201
|
+
ledger.hold = { enabled: Boolean(enabled), reason: text(reason), updatedAt: iso(now, "now") };
|
|
202
|
+
ledger.updatedAt = iso(now, "now");
|
|
203
|
+
return ledger;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function selectStableCandidate(ledgerInput, { releaseNow = "", now = new Date().toISOString() } = {}) {
|
|
207
|
+
const ledger = normalizeStableCandidateLedger(ledgerInput);
|
|
208
|
+
if (ledger.hold.enabled && !releaseNow) {
|
|
209
|
+
return { selected: false, reason: "repository-held", hold: ledger.hold };
|
|
210
|
+
}
|
|
211
|
+
if (releaseNow) {
|
|
212
|
+
const version = exactAlpha(releaseNow).version;
|
|
213
|
+
const candidate = ledger.candidates.find((entry) => entry.version === version);
|
|
214
|
+
if (!candidate) return { selected: false, reason: "release-now-candidate-missing", version };
|
|
215
|
+
if (["revoked", "promoted"].includes(candidate.state)) {
|
|
216
|
+
return { selected: false, reason: `release-now-candidate-${candidate.state}`, candidate };
|
|
217
|
+
}
|
|
218
|
+
return { selected: true, reason: "human-release-now", authority: "human", candidate };
|
|
219
|
+
}
|
|
220
|
+
const candidates = ledger.candidates
|
|
221
|
+
.filter((candidate) => candidate.state === "qualified")
|
|
222
|
+
.sort(compareCandidates);
|
|
223
|
+
const candidate = candidates.at(-1);
|
|
224
|
+
return candidate
|
|
225
|
+
? { selected: true, reason: "latest-qualified", authority: "policy", candidate }
|
|
226
|
+
: { selected: false, reason: "no-qualified-candidate" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function markStableCandidatePromoted(
|
|
230
|
+
ledgerInput,
|
|
231
|
+
versionInput,
|
|
232
|
+
{ stableTag = "", stableSha = "", now = new Date().toISOString() } = {},
|
|
233
|
+
) {
|
|
234
|
+
const ledger = normalizeStableCandidateLedger(ledgerInput);
|
|
235
|
+
const version = exactAlpha(versionInput).version;
|
|
236
|
+
const candidate = ledger.candidates.find((entry) => entry.version === version);
|
|
237
|
+
if (!candidate) throw new Error(`candidate ${version} is not registered`);
|
|
238
|
+
if (!candidate.qualification?.ok && candidate.decision?.reason !== "human-release-now") {
|
|
239
|
+
throw new Error(`candidate ${version} is not qualified for promotion`);
|
|
240
|
+
}
|
|
241
|
+
candidate.state = "promoted";
|
|
242
|
+
candidate.promotion = {
|
|
243
|
+
stableTag: text(stableTag) || `v${candidate.stableVersion}`,
|
|
244
|
+
stableSha: stableSha ? sha(stableSha, "stable sha") : "",
|
|
245
|
+
promotedAt: iso(now, "now"),
|
|
246
|
+
};
|
|
247
|
+
candidate.decision = { reason: "promoted", actor: "", updatedAt: iso(now, "now") };
|
|
248
|
+
for (const entry of ledger.candidates) {
|
|
249
|
+
if (entry.version !== version && entry.stableVersion === candidate.stableVersion && entry.state !== "promoted") {
|
|
250
|
+
entry.state = "revoked";
|
|
251
|
+
entry.decision = {
|
|
252
|
+
reason: `stable-version-promoted-by:${version}`,
|
|
253
|
+
actor: "buildchain",
|
|
254
|
+
updatedAt: iso(now, "now"),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
ledger.updatedAt = iso(now, "now");
|
|
259
|
+
return ledger;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function stableCandidatePromotionRefs(candidateInput, targetBranch) {
|
|
263
|
+
const candidate = { ...candidateInput, ...exactAlpha(candidateInput.version) };
|
|
264
|
+
const normalizedTarget = text(targetBranch).replace(/^refs\/heads\//, "");
|
|
265
|
+
const match = normalizedTarget.match(/^release\/(v\d+)\/(v\d+\.\d+)$/);
|
|
266
|
+
if (!match) throw new Error(`targetBranch must be release/vN/vN.M, got ${targetBranch || "<empty>"}`);
|
|
267
|
+
if (`v${candidate.stableVersion.split(".").slice(0, 2).join(".")}` !== match[2]) {
|
|
268
|
+
throw new Error(`candidate ${candidate.version} does not belong to ${normalizedTarget}`);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
sourceRef: `publish-gate/release/${match[1]}/${match[2]}/${candidate.version}`,
|
|
272
|
+
targetRef: normalizedTarget,
|
|
273
|
+
exactAlphaTag: `v${candidate.version}`,
|
|
274
|
+
stableTag: `v${candidate.stableVersion}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
@@ -176,9 +176,10 @@ function bundleCli({ cwd, tempDir, version, logger }) {
|
|
|
176
176
|
sourcemap: false,
|
|
177
177
|
dts: false,
|
|
178
178
|
shims: true,
|
|
179
|
-
noExternal: ["smol-toml"],
|
|
179
|
+
noExternal: ["smol-toml", /^@kungfu-tech\\/kfd(?:\\/|$)/],
|
|
180
180
|
define: {
|
|
181
181
|
"process.env.BUILDCHAIN_EMBEDDED_PACKAGE_VERSION": ${JSON.stringify(JSON.stringify(version || packageVersion(cwd)))},
|
|
182
|
+
"process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT": ${JSON.stringify(JSON.stringify("1"))},
|
|
182
183
|
},
|
|
183
184
|
};
|
|
184
185
|
`);
|
|
@@ -683,6 +683,8 @@ for (const requiredSnippet of [
|
|
|
683
683
|
"checkout-cache-mode",
|
|
684
684
|
"BUILDCHAIN_CHECKOUT_CACHE_MIRROR_URL_TEMPLATE",
|
|
685
685
|
"sourceCheckout",
|
|
686
|
+
"Shifu Cache Profile Passthrough",
|
|
687
|
+
"opaque reference and digest",
|
|
686
688
|
]) {
|
|
687
689
|
if (!reusableBuildSurfaceDoc.includes(requiredSnippet)) {
|
|
688
690
|
throw new Error(`reusable build surface doc missing contract lock snippet: ${requiredSnippet}`);
|
|
@@ -732,7 +734,8 @@ for (const requiredSnippet of [
|
|
|
732
734
|
"../packages/core/logging.js",
|
|
733
735
|
"standalone.cli-bundle.create",
|
|
734
736
|
"BUILDCHAIN_EMBEDDED_PACKAGE_VERSION",
|
|
735
|
-
"
|
|
737
|
+
"BUILDCHAIN_EMBEDDED_ENTRYPOINT",
|
|
738
|
+
"noExternal: [\"smol-toml\",",
|
|
736
739
|
"--macho-segment-name",
|
|
737
740
|
"mainFormat: \"commonjs\"",
|
|
738
741
|
"standalone.sea-blob.create",
|
|
@@ -319,6 +319,7 @@ const manualMetaById = new Map(Object.entries({
|
|
|
319
319
|
"binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
|
|
320
320
|
"publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
|
|
321
321
|
"release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
|
|
322
|
+
"stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
|
|
322
323
|
"reusable-build-surface": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator"], maturity: "stable", order: 200 },
|
|
323
324
|
"lifecycle-protocol": { capabilityGroup: "reusable-build", audience: ["consumer", "developer"], maturity: "stable", order: 210 },
|
|
324
325
|
"runtime-train-validation": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer"], maturity: "stable", order: 220 },
|
|
@@ -394,6 +395,7 @@ function cliCommandMeta(id) {
|
|
|
394
395
|
"inspect-artifact": { group: "release-passport-trust", purpose: "Inspect artifact evidence." },
|
|
395
396
|
"inspect-release": { group: "release-passport-trust", purpose: "Inspect release passport evidence." },
|
|
396
397
|
kfd: { group: "kfd-trust", purpose: "Inspect KFD standards, schemas, and versioned KFD command families." },
|
|
398
|
+
layout: { group: "kfd-trust", purpose: "Return the versioned repository-layout and KFD registry discovery contract for tools such as Shifu." },
|
|
397
399
|
"kfd-schema": { group: "kfd-trust", purpose: "Inspect KFD schema command families." },
|
|
398
400
|
"kfd-schema-list": { group: "kfd-trust", purpose: "List machine-readable schemas exposed by the KFD package standards metadata." },
|
|
399
401
|
"kfd-schema-show": { group: "kfd-trust", purpose: "Print a machine-readable KFD schema from the KFD package standards metadata." },
|
|
@@ -472,12 +474,13 @@ function nodeApiMeta(exportName) {
|
|
|
472
474
|
"./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
|
|
473
475
|
"./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
|
|
474
476
|
"./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
|
|
477
|
+
"./stable-candidate-ledger": { group: "governance-versioning", summary: "Immutable alpha candidate ledger, qualification, revocation, selection, and exact stable source-lock APIs." },
|
|
475
478
|
"./release-propagation": { group: "site-and-propagation", summary: "Release propagation graph, plan, and exact upstream lock APIs." },
|
|
476
479
|
"./release-line-bootstrap": { group: "governance-versioning", summary: "Semver release-line bootstrap planning and version-state APIs." },
|
|
477
480
|
"./buildchain-contract": { group: "governance-versioning", summary: "Runtime contract world and compatibility digest APIs for floating-ref drift checks." },
|
|
478
481
|
"./surface-manifest": { group: "site-and-propagation", summary: "Surface manifest timestamp and reproducibility policy APIs." },
|
|
479
482
|
"./issue-reporting": { group: "observability-diagnostics", summary: "Buildchain-owned issue reporting API for workflow friction feedback." },
|
|
480
|
-
"./buildchain-layout": { group: "kfd-trust", summary: "
|
|
483
|
+
"./buildchain-layout": { group: "kfd-trust", summary: "Versioned Buildchain repository-layout discovery contract plus canonical .buildchain path resolution and migration APIs." },
|
|
481
484
|
"./kfd": { group: "kfd-trust", summary: "Unified KFD standards, schema discovery, KFD-1/KFD-2/KFD-3 grouped APIs, KFD-4 schema discovery, upstream KFD aggregate facts, and Buildchain KFD claim helpers." },
|
|
482
485
|
"./public-surface-audit": { group: "kfd-trust", summary: "Reverse audit APIs for CLI, workflow, action, site page, and documentation command surfaces." },
|
|
483
486
|
"./kfd-gate": { group: "kfd-trust", summary: "KFD-1/KFD-2/KFD-3 release gate evidence and validation APIs." },
|
|
@@ -747,6 +750,7 @@ function buildSiteBundle() {
|
|
|
747
750
|
"docs/kfd-support.md",
|
|
748
751
|
"docs/reusable-build-surface.md",
|
|
749
752
|
"docs/release-candidate.md",
|
|
753
|
+
"docs/stable-candidate-patrol.md",
|
|
750
754
|
"docs/release-governance.md",
|
|
751
755
|
"docs/release-passport.md",
|
|
752
756
|
"docs/publish-transaction.md",
|
|
@@ -827,6 +831,9 @@ function buildSiteBundle() {
|
|
|
827
831
|
["buildchain-patrol-daily", "repository-patrol"],
|
|
828
832
|
["buildchain-patrol-weekly", "repository-patrol"],
|
|
829
833
|
["buildchain-patrol-monthly", "repository-patrol"],
|
|
834
|
+
["stable-candidate-patrol", "repository-patrol"],
|
|
835
|
+
["buildchain-stable-candidate-patrol", "repository-patrol"],
|
|
836
|
+
["buildchain-stable-candidate-qualification", "repository-patrol"],
|
|
830
837
|
["patrol-daily", "repository-patrol"],
|
|
831
838
|
["patrol-weekly", "repository-patrol"],
|
|
832
839
|
["patrol-monthly", "repository-patrol"],
|
|
@@ -835,6 +842,7 @@ function buildSiteBundle() {
|
|
|
835
842
|
["release-propagation", "preview"],
|
|
836
843
|
["candidate-lab", "repository-internal"],
|
|
837
844
|
["build-surface-fixture", "repository-internal"],
|
|
845
|
+
["buildchain-stable-candidate-qualification", "repository-internal"],
|
|
838
846
|
["self-hosted-runner-smoke", "compatibility-fixture"],
|
|
839
847
|
]);
|
|
840
848
|
return {
|
package/scripts/init-repo.mjs
CHANGED
|
@@ -477,7 +477,7 @@ function readArg(name, fallback = "") {
|
|
|
477
477
|
return process.argv[index + 1] || "";
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
480
|
+
if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
481
481
|
try {
|
|
482
482
|
const result = initBuildchainRepo({
|
|
483
483
|
cwd: readArg("cwd", process.cwd()),
|
|
@@ -179,7 +179,13 @@ function hasCommit(targetPath, sha, timeoutMs) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
function checkoutFetchedCommit(targetPath, sha, timeoutMs) {
|
|
182
|
-
|
|
182
|
+
// The locked Git tree is also the byte-level source of release evidence.
|
|
183
|
+
// Override runner-global autocrlf only at checkout time so this also works
|
|
184
|
+
// when a container workspace uses an external Git metadata pointer.
|
|
185
|
+
git(["-c", "core.autocrlf=false", "-c", "core.eol=lf", "checkout", "--force", "--detach", sha], {
|
|
186
|
+
cwd: targetPath,
|
|
187
|
+
timeoutMs,
|
|
188
|
+
});
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
function retryableGitFetchError(error) {
|
|
@@ -194,6 +200,7 @@ export function fetchSourceCommit({
|
|
|
194
200
|
remoteUrl,
|
|
195
201
|
sha,
|
|
196
202
|
fetchRef,
|
|
203
|
+
sourceTreeSha = "",
|
|
197
204
|
timeoutMs,
|
|
198
205
|
env = {},
|
|
199
206
|
runGit = git,
|
|
@@ -216,10 +223,22 @@ export function fetchSourceCommit({
|
|
|
216
223
|
cwd: targetPath,
|
|
217
224
|
timeoutMs,
|
|
218
225
|
env,
|
|
219
|
-
stdio: "ignore",
|
|
220
226
|
});
|
|
221
227
|
if (containsCommit(targetPath, sha, timeoutMs)) {
|
|
222
|
-
return { selector: "ref" };
|
|
228
|
+
return { selector: "ref", checkoutSha: sha };
|
|
229
|
+
}
|
|
230
|
+
if (/^refs\/pull\/\d+\/merge$/.test(fetchRef) && sourceTreeSha) {
|
|
231
|
+
const fetchedSha = runGit(["rev-parse", "refs/buildchain/source-ref^{commit}"], {
|
|
232
|
+
cwd: targetPath,
|
|
233
|
+
timeoutMs,
|
|
234
|
+
});
|
|
235
|
+
const fetchedTree = runGit(["rev-parse", "refs/buildchain/source-ref^{tree}"], {
|
|
236
|
+
cwd: targetPath,
|
|
237
|
+
timeoutMs,
|
|
238
|
+
});
|
|
239
|
+
if (fetchedTree === sourceTreeSha) {
|
|
240
|
+
return { selector: "ref-tree", checkoutSha: fetchedSha };
|
|
241
|
+
}
|
|
223
242
|
}
|
|
224
243
|
} catch (error) {
|
|
225
244
|
// A retryable transport failure belongs to the bounded outer retry. Do
|
|
@@ -234,12 +253,11 @@ export function fetchSourceCommit({
|
|
|
234
253
|
cwd: targetPath,
|
|
235
254
|
timeoutMs,
|
|
236
255
|
env,
|
|
237
|
-
stdio: "ignore",
|
|
238
256
|
});
|
|
239
257
|
if (!containsCommit(targetPath, sha, timeoutMs)) {
|
|
240
258
|
throw new Error(`fetched ${fetchRef || sha}, but ${sha} is not available`);
|
|
241
259
|
}
|
|
242
|
-
return { selector: "sha" };
|
|
260
|
+
return { selector: "sha", checkoutSha: sha };
|
|
243
261
|
}
|
|
244
262
|
|
|
245
263
|
export function runBoundedFetch({ attempts = 1, fetch, onAttempt = () => {}, onRetry = () => {}, shouldRetry = () => true }) {
|
|
@@ -266,18 +284,30 @@ function githubRemoteUrl({ repository, serverUrl = "https://github.com" }) {
|
|
|
266
284
|
return `${base}/${repository}.git`;
|
|
267
285
|
}
|
|
268
286
|
|
|
269
|
-
function verifyCheckout({ targetPath, sourceSha, sourceTreeSha = "" }) {
|
|
287
|
+
function verifyCheckout({ targetPath, sourceSha, sourceTreeSha = "", fetchRef = "" }) {
|
|
270
288
|
const head = git(["rev-parse", "HEAD"], { cwd: targetPath });
|
|
271
289
|
const tree = git(["rev-parse", "HEAD^{tree}"], { cwd: targetPath });
|
|
272
290
|
const headOk = head === sourceSha;
|
|
273
291
|
const treeOk = !sourceTreeSha || tree === sourceTreeSha;
|
|
274
|
-
|
|
292
|
+
const pullMergeTreeEquivalent = !headOk
|
|
293
|
+
&& /^refs\/pull\/\d+\/merge$/.test(fetchRef)
|
|
294
|
+
&& Boolean(sourceTreeSha)
|
|
295
|
+
&& treeOk;
|
|
296
|
+
if (!headOk && !pullMergeTreeEquivalent) {
|
|
275
297
|
throw new Error(`locked source checkout head mismatch: expected ${sourceSha}, got ${head}`);
|
|
276
298
|
}
|
|
277
299
|
if (!treeOk) {
|
|
278
300
|
throw new Error(`locked source checkout tree mismatch: expected ${sourceTreeSha}, got ${tree}`);
|
|
279
301
|
}
|
|
280
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
head,
|
|
304
|
+
expectedHead: sourceSha,
|
|
305
|
+
tree,
|
|
306
|
+
headOk,
|
|
307
|
+
treeOk,
|
|
308
|
+
identityOk: headOk || pullMergeTreeEquivalent,
|
|
309
|
+
identityMode: pullMergeTreeEquivalent ? "tree-equivalent-pull-merge" : "commit",
|
|
310
|
+
};
|
|
281
311
|
}
|
|
282
312
|
|
|
283
313
|
function writeEvidence(filePath, evidence) {
|
|
@@ -359,6 +389,7 @@ export function lockedSourceCheckout({
|
|
|
359
389
|
durationMs: 0,
|
|
360
390
|
};
|
|
361
391
|
let checkoutError;
|
|
392
|
+
let checkoutSha = sha;
|
|
362
393
|
if (normalizedMode !== "off") {
|
|
363
394
|
try {
|
|
364
395
|
if (renderedReferenceRepository) {
|
|
@@ -374,15 +405,17 @@ export function lockedSourceCheckout({
|
|
|
374
405
|
checkoutFetchedCommit(targetPath, sha, timeoutMs);
|
|
375
406
|
} else if (renderedMirrorUrl) {
|
|
376
407
|
evidence.cache.transport = "mirror-url";
|
|
377
|
-
fetchSourceCommit({
|
|
408
|
+
const fetchResult = fetchSourceCommit({
|
|
378
409
|
targetPath,
|
|
379
410
|
remoteName: "buildchain-cache",
|
|
380
411
|
remoteUrl: renderedMirrorUrl,
|
|
381
412
|
sha,
|
|
382
413
|
fetchRef,
|
|
414
|
+
sourceTreeSha: treeSha,
|
|
383
415
|
timeoutMs,
|
|
384
416
|
});
|
|
385
|
-
|
|
417
|
+
checkoutSha = fetchResult.checkoutSha || sha;
|
|
418
|
+
checkoutFetchedCommit(targetPath, checkoutSha, timeoutMs);
|
|
386
419
|
} else {
|
|
387
420
|
throw new Error("checkout cache is enabled but no mirror URL or reference repository template was provided");
|
|
388
421
|
}
|
|
@@ -416,6 +449,7 @@ export function lockedSourceCheckout({
|
|
|
416
449
|
remoteUrl,
|
|
417
450
|
sha,
|
|
418
451
|
fetchRef,
|
|
452
|
+
sourceTreeSha: treeSha,
|
|
419
453
|
timeoutMs: githubTimeoutMs,
|
|
420
454
|
env: githubAuthEnv(githubToken),
|
|
421
455
|
}),
|
|
@@ -424,14 +458,20 @@ export function lockedSourceCheckout({
|
|
|
424
458
|
shouldRetry: retryableGitFetchError,
|
|
425
459
|
});
|
|
426
460
|
evidence.cache.githubFetchAttempts = fetchResult.attempts;
|
|
461
|
+
checkoutSha = fetchResult.value.checkoutSha || sha;
|
|
427
462
|
} catch (error) {
|
|
428
463
|
evidence.durationMs = Date.now() - startedAt;
|
|
429
464
|
writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
|
|
430
465
|
throw error;
|
|
431
466
|
}
|
|
432
|
-
checkoutFetchedCommit(targetPath,
|
|
467
|
+
checkoutFetchedCommit(targetPath, checkoutSha, timeoutMs);
|
|
433
468
|
}
|
|
434
|
-
evidence.verification = verifyCheckout({
|
|
469
|
+
evidence.verification = verifyCheckout({
|
|
470
|
+
targetPath,
|
|
471
|
+
sourceSha: sha,
|
|
472
|
+
sourceTreeSha: treeSha,
|
|
473
|
+
fetchRef,
|
|
474
|
+
});
|
|
435
475
|
evidence.durationMs = Date.now() - startedAt;
|
|
436
476
|
writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
|
|
437
477
|
return evidence;
|
|
@@ -155,7 +155,7 @@ function usage() {
|
|
|
155
155
|
`;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
158
|
+
if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
159
159
|
try {
|
|
160
160
|
const argv = process.argv.slice(2);
|
|
161
161
|
if (hasFlag(argv, "help") || hasFlag(argv, "h")) {
|
|
@@ -57,7 +57,7 @@ export function runPublicationArtifactCli(args = process.argv.slice(2)) {
|
|
|
57
57
|
return result;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
60
|
+
if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
61
61
|
try {
|
|
62
62
|
runPublicationArtifactCli();
|
|
63
63
|
} catch (error) {
|
|
@@ -28,7 +28,7 @@ export function runPublicationPackageCli(args = process.argv.slice(2)) {
|
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
31
|
+
if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
32
32
|
try {
|
|
33
33
|
runPublicationPackageCli();
|
|
34
34
|
} catch (error) {
|
|
@@ -93,6 +93,6 @@ export function runReleasePropagationCli(argv = process.argv.slice(2)) {
|
|
|
93
93
|
throw new Error(`unsupported release-propagation command: ${mode}`);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
96
|
+
if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
97
97
|
runReleasePropagationCli();
|
|
98
98
|
}
|