@kungfu-tech/buildchain 2.14.3-alpha.1 → 2.14.3-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/dist/site/buildchain-contract.json +8 -8
- package/dist/site/buildchain-site.json +10 -10
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +1 -1
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/docs/migration-inventory.md +6 -0
- package/docs/reusable-build-surface.md +10 -0
- package/package.json +1 -1
- package/packages/core/publication-control-plane-audit.js +2 -0
- package/scripts/audit-publication-control-plane.mjs +6 -1
- package/scripts/check-inventory.mjs +11 -2
- package/scripts/generate-channel-promotion-workflow.mjs +114 -32
- package/scripts/promotion-channel-router.mjs +0 -7
- package/scripts/stable-candidate-patrol.mjs +55 -6
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
8
|
const sourcePath = path.join(root, ".github/workflows/.release-candidate-promote.yml");
|
|
9
9
|
const targetPath = path.join(root, ".github/workflows/release-candidate-promote.yml");
|
|
10
|
+
const routingPath = path.join(root, ".buildchain/promotion-shell-routing.json");
|
|
10
11
|
const internalInputs = new Set([
|
|
11
12
|
"promotion-router-ref",
|
|
12
13
|
"promotion-router-sha",
|
|
@@ -89,32 +90,80 @@ function publicOutputs(outputBlock) {
|
|
|
89
90
|
return [...routed, ...forwarded].join("\n");
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
function forwardedInputs(inputNames) {
|
|
93
|
-
return inputNames
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
93
|
+
function forwardedInputs(inputNames, { includeInternal = true } = {}) {
|
|
94
|
+
return inputNames
|
|
95
|
+
.filter((name) => includeInternal || !internalInputs.has(name))
|
|
96
|
+
.map((name) => {
|
|
97
|
+
const routed = {
|
|
98
|
+
"buildchain-ref": "runtime-ref",
|
|
99
|
+
"buildchain-contract-lock-path": "contract-lock-path",
|
|
100
|
+
channel: "publication-channel",
|
|
101
|
+
"target-ref": "target-ref",
|
|
102
|
+
"promotion-router-ref": "router-ref",
|
|
103
|
+
"promotion-router-sha": "router-sha",
|
|
104
|
+
"promotion-shell-ref": "shell-ref",
|
|
105
|
+
"promotion-shell-sha": "shell-sha",
|
|
106
|
+
"promotion-runtime-ref": "runtime-ref",
|
|
107
|
+
"promotion-runtime-sha": "runtime-sha",
|
|
108
|
+
"promotion-contract-lock-path": "contract-lock-path",
|
|
109
|
+
"promotion-contract-lock-digest": "contract-lock-digest",
|
|
110
|
+
"promotion-publication-channel": "publication-channel",
|
|
111
|
+
"promotion-target-ref": "target-ref",
|
|
112
|
+
"promotion-override-used": "override-used",
|
|
113
|
+
}[name];
|
|
114
|
+
if (name === "promotion-override-used") {
|
|
115
|
+
return ` ${name}: \${{ needs.resolve-promotion.outputs.override-used == 'true' }}`;
|
|
116
|
+
}
|
|
117
|
+
return routed
|
|
118
|
+
? ` ${name}: \${{ needs.resolve-promotion.outputs.${routed} }}`
|
|
119
|
+
: ` ${name}: \${{ inputs.${name} }}`;
|
|
120
|
+
}).join("\n");
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
|
|
123
|
+
function validateWorkflowRoute(name, route, expectedLogicalRef) {
|
|
124
|
+
if (!route || typeof route !== "object") throw new Error(`promotion shell routing missing ${name} route`);
|
|
125
|
+
if (route.logicalRef !== expectedLogicalRef) {
|
|
126
|
+
throw new Error(`promotion shell ${name} logicalRef must be ${expectedLogicalRef}`);
|
|
127
|
+
}
|
|
128
|
+
if (!/^\.github\/workflows\/[.a-z0-9-]+\.ya?ml$/.test(route.workflowPath || "")) {
|
|
129
|
+
throw new Error(`promotion shell ${name} workflowPath must name a reusable workflow`);
|
|
130
|
+
}
|
|
131
|
+
if (!/^(?:v[0-9]+(?:-alpha)?|[0-9a-f]{40})$/.test(route.callRef || "")) {
|
|
132
|
+
throw new Error(`promotion shell ${name} callRef must be an official channel ref or exact SHA`);
|
|
133
|
+
}
|
|
134
|
+
if (typeof route.forwardInternalInputs !== "boolean") {
|
|
135
|
+
throw new Error(`promotion shell ${name} forwardInternalInputs must be boolean`);
|
|
136
|
+
}
|
|
137
|
+
return { ...route };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function parsePromotionShellRouting(source, { major = 2 } = {}) {
|
|
141
|
+
const parsed = JSON.parse(source);
|
|
142
|
+
if (parsed.schemaVersion !== 1) throw new Error("promotion shell routing schemaVersion must be 1");
|
|
143
|
+
if (parsed.major !== major) throw new Error(`promotion shell routing major must be ${major}`);
|
|
144
|
+
return {
|
|
145
|
+
alpha: validateWorkflowRoute("alpha", parsed.alpha, `v${major}-alpha`),
|
|
146
|
+
stable: validateWorkflowRoute("stable", parsed.stable, `v${major}`),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function generateChannelPromotionWorkflow(source, { major = 2, shellRouting } = {}) {
|
|
151
|
+
const routes = shellRouting || {
|
|
152
|
+
alpha: {
|
|
153
|
+
logicalRef: `v${major}-alpha`,
|
|
154
|
+
callRef: `v${major}-alpha`,
|
|
155
|
+
workflowPath: ".github/workflows/.release-candidate-promote.yml",
|
|
156
|
+
forwardInternalInputs: true,
|
|
157
|
+
},
|
|
158
|
+
stable: {
|
|
159
|
+
logicalRef: `v${major}`,
|
|
160
|
+
callRef: `v${major}`,
|
|
161
|
+
workflowPath: ".github/workflows/.release-candidate-promote.yml",
|
|
162
|
+
forwardInternalInputs: true,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
|
|
166
|
+
const stableRoute = validateWorkflowRoute("stable", routes.stable, `v${major}`);
|
|
118
167
|
const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
|
|
119
168
|
const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
|
|
120
169
|
const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
|
|
@@ -122,7 +171,12 @@ export function generateChannelPromotionWorkflow(source, { major = 2 } = {}) {
|
|
|
122
171
|
for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
|
|
123
172
|
if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
|
|
124
173
|
}
|
|
125
|
-
const
|
|
174
|
+
const alphaForwarded = forwardedInputs(inputNames, {
|
|
175
|
+
includeInternal: alphaRoute.forwardInternalInputs,
|
|
176
|
+
});
|
|
177
|
+
const stableForwarded = forwardedInputs(inputNames, {
|
|
178
|
+
includeInternal: stableRoute.forwardInternalInputs,
|
|
179
|
+
});
|
|
126
180
|
return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
|
|
127
181
|
name: Release Candidate Promote
|
|
128
182
|
|
|
@@ -269,12 +323,39 @@ jobs:
|
|
|
269
323
|
- name: Resolve immutable promotion identities
|
|
270
324
|
id: identities
|
|
271
325
|
shell: bash
|
|
326
|
+
env:
|
|
327
|
+
CHANNEL: \${{ steps.route.outputs.channel }}
|
|
328
|
+
SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
|
|
329
|
+
ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
|
|
330
|
+
ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
|
|
331
|
+
ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
|
|
332
|
+
STABLE_SHELL_REF: ${stableRoute.logicalRef}
|
|
333
|
+
STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
|
|
334
|
+
STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
|
|
272
335
|
run: |
|
|
273
336
|
set -euo pipefail
|
|
274
|
-
|
|
337
|
+
if [[ "\${CHANNEL}" = "alpha" ]]; then
|
|
338
|
+
expected_ref="\${ALPHA_SHELL_REF}"
|
|
339
|
+
call_ref="\${ALPHA_SHELL_CALL_REF}"
|
|
340
|
+
workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
|
|
341
|
+
else
|
|
342
|
+
expected_ref="\${STABLE_SHELL_REF}"
|
|
343
|
+
call_ref="\${STABLE_SHELL_CALL_REF}"
|
|
344
|
+
workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
|
|
345
|
+
fi
|
|
346
|
+
if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
|
|
347
|
+
echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
|
|
348
|
+
exit 1
|
|
349
|
+
fi
|
|
350
|
+
test -f ".buildchain/shell/\${workflow_path}"
|
|
351
|
+
shell_sha="$(git -C .buildchain/shell rev-parse HEAD)"
|
|
352
|
+
if [[ "\${call_ref}" =~ ^[0-9a-f]{40}$ && "\${shell_sha}" != "\${call_ref}" ]]; then
|
|
353
|
+
echo "::error::Pinned promotion shell \${call_ref} does not match selected \${shell_sha}"
|
|
354
|
+
exit 1
|
|
355
|
+
fi
|
|
275
356
|
{
|
|
276
357
|
echo "router-sha=$(git -C .buildchain/router rev-parse HEAD)"
|
|
277
|
-
echo "shell-sha
|
|
358
|
+
echo "shell-sha=\${shell_sha}"
|
|
278
359
|
echo "runtime-sha=$(git -C .buildchain/runtime rev-parse HEAD)"
|
|
279
360
|
} >> "\${GITHUB_OUTPUT}"
|
|
280
361
|
|
|
@@ -282,7 +363,7 @@ jobs:
|
|
|
282
363
|
name: Promote with alpha workflow shell
|
|
283
364
|
needs: resolve-promotion
|
|
284
365
|
if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
|
|
285
|
-
uses: kungfu-systems/buildchain
|
|
366
|
+
uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
|
|
286
367
|
permissions:
|
|
287
368
|
actions: read
|
|
288
369
|
checks: write
|
|
@@ -290,14 +371,14 @@ jobs:
|
|
|
290
371
|
id-token: write
|
|
291
372
|
issues: write
|
|
292
373
|
with:
|
|
293
|
-
${
|
|
374
|
+
${alphaForwarded}
|
|
294
375
|
secrets: inherit
|
|
295
376
|
|
|
296
377
|
stable:
|
|
297
378
|
name: Promote with stable workflow shell
|
|
298
379
|
needs: resolve-promotion
|
|
299
380
|
if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
|
|
300
|
-
uses: kungfu-systems/buildchain
|
|
381
|
+
uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
|
|
301
382
|
permissions:
|
|
302
383
|
actions: read
|
|
303
384
|
checks: write
|
|
@@ -305,7 +386,7 @@ ${forwarded}
|
|
|
305
386
|
id-token: write
|
|
306
387
|
issues: write
|
|
307
388
|
with:
|
|
308
|
-
${
|
|
389
|
+
${stableForwarded}
|
|
309
390
|
secrets: inherit
|
|
310
391
|
`;
|
|
311
392
|
}
|
|
@@ -315,7 +396,8 @@ function main() {
|
|
|
315
396
|
const version = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version;
|
|
316
397
|
const major = Number(String(version).match(/^(\d+)\./)?.[1]);
|
|
317
398
|
if (!Number.isInteger(major)) throw new Error("package version must expose a numeric major");
|
|
318
|
-
const
|
|
399
|
+
const shellRouting = parsePromotionShellRouting(fs.readFileSync(routingPath, "utf8"), { major });
|
|
400
|
+
const generated = generateChannelPromotionWorkflow(source, { major, shellRouting });
|
|
319
401
|
if (process.argv.includes("--check")) {
|
|
320
402
|
const current = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : "";
|
|
321
403
|
if (current !== generated) {
|
|
@@ -23,12 +23,10 @@ function targetIntent(targetRef, requestedPublicationChannel = "") {
|
|
|
23
23
|
if (requested && requested !== target.publicationChannel) {
|
|
24
24
|
throw new Error(`promotion channel ${requested} does not match target ref ${ref} (${target.publicationChannel})`);
|
|
25
25
|
}
|
|
26
|
-
const match = ref.match(target.pattern);
|
|
27
26
|
return {
|
|
28
27
|
targetRef: ref,
|
|
29
28
|
publicationChannel: target.publicationChannel,
|
|
30
29
|
shellChannel: target.shellChannel,
|
|
31
|
-
targetMajor: match?.[1] ? Number(match[1]) : undefined,
|
|
32
30
|
};
|
|
33
31
|
}
|
|
34
32
|
|
|
@@ -55,11 +53,6 @@ export function resolvePromotionChannel({
|
|
|
55
53
|
`promotion target ${intent.targetRef} requires ${intent.shellChannel} shell/runtime, got ${selected.channel}`,
|
|
56
54
|
);
|
|
57
55
|
}
|
|
58
|
-
if (Number.isInteger(intent.targetMajor) && selected.major !== intent.targetMajor) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`promotion target ${intent.targetRef} requires Buildchain v${intent.targetMajor}, got v${selected.major}`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
56
|
const shellRef = intent.shellChannel === "alpha" ? `v${selected.major}-alpha` : `v${selected.major}`;
|
|
64
57
|
return {
|
|
65
58
|
targetRef: intent.targetRef,
|
|
@@ -223,7 +223,7 @@ export async function runStableCandidatePatrol(optionsInput = {}, clientInput) {
|
|
|
223
223
|
`Buildchain Stable Candidate Patrol human release-now for ${selection.candidate.version}`,
|
|
224
224
|
);
|
|
225
225
|
}
|
|
226
|
-
await client.ensureBranch(refs.sourceRef, selection.candidate.sha);
|
|
226
|
+
await client.ensureBranch(refs.sourceRef, selection.candidate.sha, refs.targetRef);
|
|
227
227
|
const pullRequest = await client.ensurePromotionPullRequest({
|
|
228
228
|
head: refs.sourceRef,
|
|
229
229
|
base: refs.targetRef,
|
|
@@ -371,15 +371,64 @@ export function createGitHubStableCandidateClient({ repository: repositoryInput,
|
|
|
371
371
|
},
|
|
372
372
|
});
|
|
373
373
|
},
|
|
374
|
-
async ensureBranch(ref, candidateSha) {
|
|
374
|
+
async ensureBranch(ref, candidateSha, targetRef) {
|
|
375
375
|
const current = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`, { allow404: true });
|
|
376
|
-
if (!
|
|
376
|
+
if (!targetRef) {
|
|
377
|
+
if (!current) {
|
|
378
|
+
return api(`/repos/${owner}/${repo}/git/refs`, { method: "POST", body: { ref: `refs/heads/${ref}`, sha: candidateSha } });
|
|
379
|
+
}
|
|
380
|
+
if (current.object.sha !== candidateSha) {
|
|
381
|
+
throw new Error(`source-lock branch ${ref} already points to ${current.object.sha}, not ${candidateSha}`);
|
|
382
|
+
}
|
|
383
|
+
return current;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const candidateCommit = await api(`/repos/${owner}/${repo}/git/commits/${candidateSha}`);
|
|
387
|
+
let sourceSha = current?.object?.sha || candidateSha;
|
|
388
|
+
if (sourceSha !== candidateSha) {
|
|
389
|
+
const sourceCommit = await api(`/repos/${owner}/${repo}/git/commits/${sourceSha}`);
|
|
390
|
+
if (sourceCommit.tree?.sha !== candidateCommit.tree?.sha) {
|
|
391
|
+
throw new Error(`source-lock branch ${ref} no longer preserves candidate ${candidateSha} tree`);
|
|
392
|
+
}
|
|
393
|
+
const candidateLineage = await api(`/repos/${owner}/${repo}/compare/${candidateSha}...${sourceSha}`);
|
|
394
|
+
if (!["ahead", "identical"].includes(candidateLineage.status)) {
|
|
395
|
+
throw new Error(`source-lock branch ${ref} no longer descends from candidate ${candidateSha}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const target = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(targetRef)}`);
|
|
400
|
+
const targetSha = target.object?.sha || "";
|
|
401
|
+
const targetLineage = await api(`/repos/${owner}/${repo}/compare/${targetSha}...${sourceSha}`);
|
|
402
|
+
if (["ahead", "identical"].includes(targetLineage.status)) {
|
|
403
|
+
if (current) return current;
|
|
377
404
|
return api(`/repos/${owner}/${repo}/git/refs`, { method: "POST", body: { ref: `refs/heads/${ref}`, sha: candidateSha } });
|
|
378
405
|
}
|
|
379
|
-
|
|
380
|
-
|
|
406
|
+
|
|
407
|
+
const identity = { name: "Buildchain Patrol", email: "buildchain-patrol@kungfu.link" };
|
|
408
|
+
const reconciliation = await api(`/repos/${owner}/${repo}/git/commits`, {
|
|
409
|
+
method: "POST",
|
|
410
|
+
body: {
|
|
411
|
+
message: [
|
|
412
|
+
"chore(buildchain): reconcile stable source-lock ancestry",
|
|
413
|
+
"",
|
|
414
|
+
`Preserve the exact candidate tree from ${candidateSha} while admitting ${targetRef} at ${targetSha}.`,
|
|
415
|
+
"",
|
|
416
|
+
"Signed-off-by: Buildchain Patrol <buildchain-patrol@kungfu.link>",
|
|
417
|
+
].join("\n"),
|
|
418
|
+
tree: candidateCommit.tree.sha,
|
|
419
|
+
parents: [sourceSha, targetSha],
|
|
420
|
+
author: identity,
|
|
421
|
+
committer: identity,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
sourceSha = reconciliation.sha;
|
|
425
|
+
if (!current) {
|
|
426
|
+
return api(`/repos/${owner}/${repo}/git/refs`, { method: "POST", body: { ref: `refs/heads/${ref}`, sha: sourceSha } });
|
|
381
427
|
}
|
|
382
|
-
return
|
|
428
|
+
return api(`/repos/${owner}/${repo}/git/refs/heads/${encodeRef(ref)}`, {
|
|
429
|
+
method: "PATCH",
|
|
430
|
+
body: { sha: sourceSha, force: false },
|
|
431
|
+
});
|
|
383
432
|
},
|
|
384
433
|
async ensurePromotionPullRequest({ head, base, title, body }) {
|
|
385
434
|
const open = await api(`/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`);
|