@kungfu-tech/buildchain 4.0.2-alpha.0 → 4.0.2-alpha.2
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/architecture/agent-change-map.md +4 -4
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/maintainability-debt.json +41 -115
- package/architecture/maintainability-policy.json +12 -7
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-v4-live-capability-inventory.json +17 -17
- package/architecture/v4-release-invocation-fixtures.json +119 -0
- package/architecture/v4-release-topology.json +238 -0
- package/contracts/buildchain-v2-residuals-v1.json +0 -9
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-release-invocation-v1.schema.json +86 -0
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +7 -7
- package/dist/site/kfd-claims.json +7 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +17 -9
- package/dist/site/publication-authority-registry.json +2 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +19 -11
- package/docs/node-api-reference.md +3 -3
- package/package.json +2 -2
- package/packages/core/dev-delivery-candidate-identity.js +45 -17
- package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
- package/packages/core/dev-delivery-warrant-state.js +12 -28
- package/packages/core/v4-canonical-contracts.js +8 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-release-invocation.js +356 -0
- package/scripts/audit-publication-control-plane.mjs +0 -1
- package/scripts/check-inventory.mjs +27 -59
- package/scripts/check-maintainability.mjs +13 -5
- package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
- package/scripts/check-v4-release-topology.mjs +237 -0
- package/scripts/dev-delivery-warrant.mjs +13 -4
- package/scripts/generate-channel-promotion-workflow.mjs +12 -54
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/capture-package-release-propagation.mjs +0 -263
- package/scripts/publication-commit-evidence.mjs +0 -444
- package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
- package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
parseWorkflowDocument,
|
|
10
|
+
parseYamlUses,
|
|
11
|
+
} from "../packages/core/workflow-yaml-contract.js";
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const ledgerPath = path.join(root, "architecture/v4-release-topology.json");
|
|
15
|
+
const releaseMarker =
|
|
16
|
+
/(?:release-candidate-promote\.yml|promote-buildchain-ref|v4-release-candidate-promote|release-tail-runtime)/u;
|
|
17
|
+
|
|
18
|
+
function read(relative) {
|
|
19
|
+
return fs.readFileSync(path.join(root, relative), "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function jobBlock(source, jobId) {
|
|
23
|
+
const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
24
|
+
return (
|
|
25
|
+
source.match(
|
|
26
|
+
new RegExp(
|
|
27
|
+
`^ ${escaped}:\\n([\\s\\S]*?)(?=^ [A-Za-z0-9_.-]+:|(?![\\s\\S]))`,
|
|
28
|
+
"mu",
|
|
29
|
+
),
|
|
30
|
+
)?.[1] || ""
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function permission(block, name) {
|
|
35
|
+
return (
|
|
36
|
+
new RegExp(`^ ${name}:\\s*([^#\\n]+)`, "mu")
|
|
37
|
+
.exec(block)?.[1]
|
|
38
|
+
?.trim() || null
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function workflowSnapshot(relative) {
|
|
43
|
+
const source = read(relative);
|
|
44
|
+
const document = parseWorkflowDocument(source);
|
|
45
|
+
const jobs = document.jobs.map((job) => {
|
|
46
|
+
const block = jobBlock(source, job.id);
|
|
47
|
+
const uses = job.uses || null;
|
|
48
|
+
return {
|
|
49
|
+
id: job.id,
|
|
50
|
+
kind: uses ? "reusable-call" : "runner",
|
|
51
|
+
uses,
|
|
52
|
+
permissions: {
|
|
53
|
+
contents: permission(block, "contents"),
|
|
54
|
+
idToken: permission(block, "id-token"),
|
|
55
|
+
},
|
|
56
|
+
carriers: {
|
|
57
|
+
artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
|
|
58
|
+
artifactUpload: /uses:\s+actions\/upload-artifact@/u.test(block),
|
|
59
|
+
jobOutput: /GITHUB_OUTPUT/u.test(block),
|
|
60
|
+
},
|
|
61
|
+
mutationSignals: [
|
|
62
|
+
/contents:\s*write/u.test(block) && "contents-write",
|
|
63
|
+
/id-token:\s*write/u.test(block) && "oidc-write",
|
|
64
|
+
/(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
|
|
65
|
+
block,
|
|
66
|
+
) && "promotion-runtime",
|
|
67
|
+
/(?:git push|npm publish|gh release (?:create|upload))/u.test(block) &&
|
|
68
|
+
"direct-publication-command",
|
|
69
|
+
].filter(Boolean),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
path: relative,
|
|
74
|
+
triggers: document.triggers,
|
|
75
|
+
jobs,
|
|
76
|
+
reusableEdges: parseYamlUses(source)
|
|
77
|
+
.map(({ value }) => value)
|
|
78
|
+
.filter((value) => /\.github\/workflows\//u.test(value))
|
|
79
|
+
.sort(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function discoverV4ReleaseTopology(
|
|
84
|
+
workflowPaths,
|
|
85
|
+
semanticWorkflowPaths = workflowPaths,
|
|
86
|
+
) {
|
|
87
|
+
const workflows = workflowPaths.map(workflowSnapshot);
|
|
88
|
+
const jobs = workflows.flatMap((workflow) => workflow.jobs);
|
|
89
|
+
const mutationJobs = jobs.filter((job) => job.mutationSignals.length > 0);
|
|
90
|
+
const semanticPaths = new Set(semanticWorkflowPaths);
|
|
91
|
+
const semanticJobs = workflows
|
|
92
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
93
|
+
.flatMap((workflow) => workflow.jobs);
|
|
94
|
+
const semanticMutationJobs = semanticJobs.filter(
|
|
95
|
+
(job) => job.kind === "runner" && job.mutationSignals.length > 0,
|
|
96
|
+
);
|
|
97
|
+
return {
|
|
98
|
+
jobRecordFields: [
|
|
99
|
+
"id",
|
|
100
|
+
"kind",
|
|
101
|
+
"uses",
|
|
102
|
+
"contentsPermission",
|
|
103
|
+
"oidcPermission",
|
|
104
|
+
"carriers",
|
|
105
|
+
"mutationSignals",
|
|
106
|
+
],
|
|
107
|
+
workflows: workflows.map((workflow) => ({
|
|
108
|
+
path: workflow.path,
|
|
109
|
+
triggers: workflow.triggers,
|
|
110
|
+
jobs: workflow.jobs.map((job) =>
|
|
111
|
+
[
|
|
112
|
+
job.id,
|
|
113
|
+
job.kind,
|
|
114
|
+
job.uses || "-",
|
|
115
|
+
job.permissions.contents || "-",
|
|
116
|
+
job.permissions.idToken || "-",
|
|
117
|
+
Object.entries(job.carriers)
|
|
118
|
+
.filter(([, present]) => present)
|
|
119
|
+
.map(([name]) => name)
|
|
120
|
+
.join(",") || "-",
|
|
121
|
+
job.mutationSignals.join(",") || "-",
|
|
122
|
+
].join("|"),
|
|
123
|
+
),
|
|
124
|
+
reusableEdges: workflow.reusableEdges,
|
|
125
|
+
})),
|
|
126
|
+
metrics: {
|
|
127
|
+
workflowCount: workflows.length,
|
|
128
|
+
jobCount: jobs.length,
|
|
129
|
+
reusableEdgeCount: workflows.reduce(
|
|
130
|
+
(count, workflow) => count + workflow.reusableEdges.length,
|
|
131
|
+
0,
|
|
132
|
+
),
|
|
133
|
+
mutationRelevantNodeCount: jobs.filter(
|
|
134
|
+
(job) =>
|
|
135
|
+
job.kind === "reusable-call" ||
|
|
136
|
+
job.mutationSignals.length > 0 ||
|
|
137
|
+
job.carriers.artifactDownload ||
|
|
138
|
+
job.carriers.artifactUpload,
|
|
139
|
+
).length,
|
|
140
|
+
contentsWriteJobCount: mutationJobs.filter((job) =>
|
|
141
|
+
job.mutationSignals.includes("contents-write"),
|
|
142
|
+
).length,
|
|
143
|
+
oidcWriteJobCount: mutationJobs.filter((job) =>
|
|
144
|
+
job.mutationSignals.includes("oidc-write"),
|
|
145
|
+
).length,
|
|
146
|
+
},
|
|
147
|
+
semanticMetrics: {
|
|
148
|
+
workflowCount: semanticPaths.size,
|
|
149
|
+
mutationRelevantNodeCount: semanticJobs.filter(
|
|
150
|
+
(job) =>
|
|
151
|
+
job.kind === "reusable-call" ||
|
|
152
|
+
job.mutationSignals.length > 0 ||
|
|
153
|
+
job.carriers.artifactDownload ||
|
|
154
|
+
job.carriers.artifactUpload,
|
|
155
|
+
).length,
|
|
156
|
+
contentsWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
157
|
+
job.mutationSignals.includes("contents-write"),
|
|
158
|
+
).length,
|
|
159
|
+
oidcWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
160
|
+
job.mutationSignals.includes("oidc-write"),
|
|
161
|
+
).length,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function findUnknownV4ReleaseTopology(
|
|
167
|
+
workflowPaths,
|
|
168
|
+
allWorkflowPaths = fs
|
|
169
|
+
.readdirSync(path.join(root, ".github/workflows"))
|
|
170
|
+
.filter((name) => /\.ya?ml$/u.test(name))
|
|
171
|
+
.map((name) => `.github/workflows/${name}`),
|
|
172
|
+
readWorkflow = read,
|
|
173
|
+
) {
|
|
174
|
+
const declared = new Set(workflowPaths);
|
|
175
|
+
return allWorkflowPaths
|
|
176
|
+
.filter((relative) => !declared.has(relative))
|
|
177
|
+
.filter((relative) => releaseMarker.test(readWorkflow(relative)))
|
|
178
|
+
.sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertClosedWorld(workflowPaths) {
|
|
182
|
+
const unknown = findUnknownV4ReleaseTopology(workflowPaths);
|
|
183
|
+
assert.deepEqual(
|
|
184
|
+
unknown,
|
|
185
|
+
[],
|
|
186
|
+
`unknown v4 release topology workflows: ${unknown.join(", ")}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function checkV4ReleaseTopology() {
|
|
191
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
192
|
+
assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
|
|
193
|
+
assertClosedWorld(ledger.closedWorld.workflowPaths);
|
|
194
|
+
const actual = discoverV4ReleaseTopology(
|
|
195
|
+
ledger.closedWorld.workflowPaths,
|
|
196
|
+
ledger.semanticScope.workflowPaths,
|
|
197
|
+
);
|
|
198
|
+
assert.deepEqual(actual, ledger.observedTopology);
|
|
199
|
+
assert.equal(
|
|
200
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
201
|
+
Math.floor(ledger.baselineMetrics.semanticMutationRelevantNodeCount / 2),
|
|
202
|
+
);
|
|
203
|
+
if (ledger.enforcement === "converged") {
|
|
204
|
+
assert.ok(
|
|
205
|
+
actual.semanticMetrics.mutationRelevantNodeCount <=
|
|
206
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
207
|
+
);
|
|
208
|
+
assert.equal(actual.semanticMetrics.contentsWriteJobCount, 1);
|
|
209
|
+
assert.equal(actual.semanticMetrics.oidcWriteJobCount, 1);
|
|
210
|
+
const publisher = actual.workflows.find(
|
|
211
|
+
({ path: workflowPath }) =>
|
|
212
|
+
workflowPath === ".github/workflows/.release-candidate-promote.yml",
|
|
213
|
+
);
|
|
214
|
+
assert.deepEqual(
|
|
215
|
+
publisher.jobs.map((job) => job.split("|", 1)[0]),
|
|
216
|
+
["apply", "qualify", "settle"],
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return actual;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (process.argv.includes("--print")) {
|
|
223
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
224
|
+
process.stdout.write(
|
|
225
|
+
`${JSON.stringify(
|
|
226
|
+
discoverV4ReleaseTopology(
|
|
227
|
+
ledger.closedWorld.workflowPaths,
|
|
228
|
+
ledger.semanticScope.workflowPaths,
|
|
229
|
+
),
|
|
230
|
+
null,
|
|
231
|
+
2,
|
|
232
|
+
)}\n`,
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
checkV4ReleaseTopology();
|
|
236
|
+
process.stdout.write("v4 release topology: ok\n");
|
|
237
|
+
}
|
|
@@ -430,20 +430,29 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
430
430
|
options.command,
|
|
431
431
|
),
|
|
432
432
|
});
|
|
433
|
+
let concurrencyRecovery = null;
|
|
433
434
|
if (
|
|
434
435
|
options.expectedOldStateRoot &&
|
|
435
436
|
loaded.queue.stateRoot !== options.expectedOldStateRoot
|
|
436
437
|
) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
if (options.command !== "heartbeat") {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
concurrencyRecovery = {
|
|
444
|
+
schema: "kungfu.buildchain.dev-delivery-concurrency-recovery/v1",
|
|
445
|
+
action: "heartbeat-state-root-rebased",
|
|
446
|
+
requestedStateRoot: options.expectedOldStateRoot,
|
|
447
|
+
observedStateRoot: loaded.queue.stateRoot,
|
|
448
|
+
observedCommitSha: loaded.commitSha,
|
|
449
|
+
};
|
|
440
450
|
}
|
|
441
451
|
if (options.command === "observe") return observeQueue(loaded, options);
|
|
442
452
|
const initialLoaded = loaded;
|
|
443
453
|
let changed = transitionFor(options.command, loaded.queue, options);
|
|
444
454
|
let mutates = changed.queue.stateRoot !== loaded.queue.stateRoot;
|
|
445
455
|
let write = null;
|
|
446
|
-
let concurrencyRecovery = null;
|
|
447
456
|
if (options.execute && mutates) {
|
|
448
457
|
if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
|
|
449
458
|
throw new Error(
|
|
@@ -74,7 +74,7 @@ function publicInputs(inputBlock) {
|
|
|
74
74
|
function publicOutputs(outputBlock) {
|
|
75
75
|
const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
|
|
76
76
|
/^ value:.*$/m,
|
|
77
|
-
` value: \${{ jobs.
|
|
77
|
+
` value: \${{ jobs.invoke.outputs.${entry.name} }}`,
|
|
78
78
|
));
|
|
79
79
|
const routed = [
|
|
80
80
|
["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
|
|
@@ -292,7 +292,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
|
|
|
292
292
|
},
|
|
293
293
|
};
|
|
294
294
|
const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
|
|
295
|
-
|
|
295
|
+
validateWorkflowRoute("stable", routes.stable, `v${major}`);
|
|
296
296
|
const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
|
|
297
297
|
const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
|
|
298
298
|
const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
|
|
@@ -300,8 +300,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
|
|
|
300
300
|
for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
|
|
301
301
|
if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
|
|
302
302
|
}
|
|
303
|
-
const
|
|
304
|
-
const stableForwarded = routedInputs(inputNames, stableRoute, major);
|
|
303
|
+
const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
|
|
305
304
|
return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
|
|
306
305
|
name: Release Candidate Promote
|
|
307
306
|
|
|
@@ -336,7 +335,7 @@ jobs:
|
|
|
336
335
|
target-ref: \${{ steps.route.outputs.target-ref }}
|
|
337
336
|
router-ref: \${{ steps.router.outputs.ref }}
|
|
338
337
|
router-sha: \${{ steps.router.outputs.sha }}
|
|
339
|
-
shell-ref: \${{ steps.
|
|
338
|
+
shell-ref: \${{ steps.identities.outputs.shell-ref }}
|
|
340
339
|
shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
|
|
341
340
|
shell-sha: \${{ steps.identities.outputs.shell-sha }}
|
|
342
341
|
runtime-ref: \${{ steps.route.outputs.runtime-ref }}
|
|
@@ -425,32 +424,17 @@ jobs:
|
|
|
425
424
|
shell: bash
|
|
426
425
|
env:
|
|
427
426
|
GITHUB_TOKEN: \${{ github.token }}
|
|
428
|
-
|
|
429
|
-
SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
|
|
430
|
-
ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
|
|
431
|
-
ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
|
|
432
|
-
STABLE_SHELL_REF: ${stableRoute.logicalRef}
|
|
433
|
-
STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
|
|
427
|
+
SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
|
|
434
428
|
run: |
|
|
435
429
|
set -euo pipefail
|
|
436
|
-
if [[ "\${CHANNEL}" = "alpha" ]]; then
|
|
437
|
-
expected_ref="\${ALPHA_SHELL_REF}"
|
|
438
|
-
call_ref="\${ALPHA_SHELL_CALL_REF}"
|
|
439
|
-
else
|
|
440
|
-
expected_ref="\${STABLE_SHELL_REF}"
|
|
441
|
-
call_ref="\${STABLE_SHELL_CALL_REF}"
|
|
442
|
-
fi
|
|
443
|
-
if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
|
|
444
|
-
echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
|
|
445
|
-
exit 1
|
|
446
|
-
fi
|
|
447
430
|
node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
|
|
448
431
|
--repository "\${{ steps.router.outputs.repository }}" \\
|
|
449
432
|
--router-ref "\${{ steps.router.outputs.ref }}" \\
|
|
450
433
|
--router-sha "\${{ steps.router.outputs.sha }}" \\
|
|
451
434
|
--shell-ref "\${SELECTED_SHELL_REF}" \\
|
|
452
|
-
--shell-call-ref "\${
|
|
435
|
+
--shell-call-ref "\${SELECTED_SHELL_REF}" \\
|
|
453
436
|
--runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
|
|
437
|
+
echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
|
|
454
438
|
|
|
455
439
|
- name: Checkout selected promotion workflow shell
|
|
456
440
|
uses: actions/checkout@v7.0.0
|
|
@@ -500,29 +484,21 @@ jobs:
|
|
|
500
484
|
- name: Verify immutable promotion checkouts
|
|
501
485
|
shell: bash
|
|
502
486
|
env:
|
|
503
|
-
|
|
504
|
-
ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
|
|
505
|
-
STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
|
|
487
|
+
SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
|
|
506
488
|
ROUTER_SHA: \${{ steps.router.outputs.sha }}
|
|
507
489
|
SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
|
|
508
490
|
RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
|
|
509
491
|
run: |
|
|
510
492
|
set -euo pipefail
|
|
511
|
-
|
|
512
|
-
workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
|
|
513
|
-
else
|
|
514
|
-
workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
|
|
515
|
-
fi
|
|
516
|
-
test -f ".buildchain/shell/\${workflow_path}"
|
|
493
|
+
test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
|
|
517
494
|
[[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
|
|
518
495
|
[[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
|
|
519
496
|
[[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
|
|
520
497
|
|
|
521
498
|
${consumerAdmissionJob()}
|
|
522
|
-
|
|
523
|
-
name:
|
|
499
|
+
invoke:
|
|
500
|
+
name: Invoke the single v4 publisher adapter
|
|
524
501
|
needs: [resolve-promotion, consumer-admission]
|
|
525
|
-
if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
|
|
526
502
|
uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
|
|
527
503
|
permissions:
|
|
528
504
|
actions: write
|
|
@@ -534,25 +510,7 @@ ${consumerAdmissionJob()}
|
|
|
534
510
|
issues: write
|
|
535
511
|
pull-requests: write
|
|
536
512
|
with:
|
|
537
|
-
${
|
|
538
|
-
secrets: inherit
|
|
539
|
-
|
|
540
|
-
stable:
|
|
541
|
-
name: Promote with stable workflow shell
|
|
542
|
-
needs: [resolve-promotion, consumer-admission]
|
|
543
|
-
if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
|
|
544
|
-
uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
|
|
545
|
-
permissions:
|
|
546
|
-
actions: write
|
|
547
|
-
artifact-metadata: write
|
|
548
|
-
attestations: write
|
|
549
|
-
checks: write
|
|
550
|
-
contents: write
|
|
551
|
-
id-token: write
|
|
552
|
-
issues: write
|
|
553
|
-
pull-requests: write
|
|
554
|
-
with:
|
|
555
|
-
${stableForwarded}
|
|
513
|
+
${invokeForwarded}
|
|
556
514
|
secrets: inherit
|
|
557
515
|
`;
|
|
558
516
|
}
|
|
@@ -23,6 +23,9 @@ const PLATFORM_MARKERS = Object.freeze({
|
|
|
23
23
|
"self-hosted": /\bself-hosted\b/iu,
|
|
24
24
|
windows: /\bwindows\b/iu,
|
|
25
25
|
});
|
|
26
|
+
const LIVE_V4_PROTECTED_BRANCH = "refs/heads/dev/v4/v4.0";
|
|
27
|
+
const LIVE_V4_PROTECTED_LINEAGE_REF = "refs/remotes/origin/dev/v4/v4.0";
|
|
28
|
+
const PROTECTED_LINEAGE_DEPTH = 256;
|
|
26
29
|
|
|
27
30
|
export function sha256(value) {
|
|
28
31
|
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
@@ -47,6 +50,110 @@ export function git(root, args, { trim = true } = {}) {
|
|
|
47
50
|
return trim ? output.trim() : output;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
export function assertCapabilityCutAncestor({
|
|
54
|
+
root = process.cwd(),
|
|
55
|
+
revision,
|
|
56
|
+
descendant = "HEAD",
|
|
57
|
+
label = "capability cut",
|
|
58
|
+
} = {}) {
|
|
59
|
+
try {
|
|
60
|
+
execFileSync("git", ["merge-base", "--is-ancestor", revision, descendant], {
|
|
61
|
+
cwd: root,
|
|
62
|
+
stdio: "ignore",
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${label} ${revision} must be an ancestor of ${descendant}; regenerate the cut after rebasing instead of relying on a retained local object`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ensureCapabilityCutAncestor({
|
|
72
|
+
root = process.cwd(),
|
|
73
|
+
revision,
|
|
74
|
+
descendant = "HEAD",
|
|
75
|
+
label = "capability cut",
|
|
76
|
+
} = {}) {
|
|
77
|
+
try {
|
|
78
|
+
assertCapabilityCutAncestor({ root, revision, descendant, label });
|
|
79
|
+
return;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (git(root, ["rev-parse", "--is-shallow-repository"]) !== "true")
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const descendantCommit = git(root, ["rev-parse", `${descendant}^{commit}`]);
|
|
85
|
+
try {
|
|
86
|
+
execFileSync(
|
|
87
|
+
"git",
|
|
88
|
+
["fetch", "--no-tags", "--depth=128", "origin", descendantCommit],
|
|
89
|
+
{ cwd: root, stdio: "ignore" },
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${label} ${revision} ancestry could not be hydrated from ${descendantCommit} through a bounded origin fetch`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
assertCapabilityCutAncestor({ root, revision, descendant, label });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function protectedV4TreeWitness(root, descendant) {
|
|
100
|
+
try {
|
|
101
|
+
execFileSync(
|
|
102
|
+
"git",
|
|
103
|
+
[
|
|
104
|
+
"fetch",
|
|
105
|
+
"--no-tags",
|
|
106
|
+
`--depth=${PROTECTED_LINEAGE_DEPTH}`,
|
|
107
|
+
"origin",
|
|
108
|
+
`${LIVE_V4_PROTECTED_BRANCH}:${LIVE_V4_PROTECTED_LINEAGE_REF}`,
|
|
109
|
+
],
|
|
110
|
+
{ cwd: root, stdio: "ignore" },
|
|
111
|
+
);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`protected v4 lineage ${LIVE_V4_PROTECTED_BRANCH} could not be hydrated through a bounded origin fetch`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const tree = git(root, ["rev-parse", `${descendant}^{tree}`]);
|
|
118
|
+
return (
|
|
119
|
+
git(root, [
|
|
120
|
+
"log",
|
|
121
|
+
`--max-count=${PROTECTED_LINEAGE_DEPTH}`,
|
|
122
|
+
"--format=%H %T",
|
|
123
|
+
LIVE_V4_PROTECTED_LINEAGE_REF,
|
|
124
|
+
])
|
|
125
|
+
.split("\n")
|
|
126
|
+
.map((row) => row.split(" "))
|
|
127
|
+
.find(([, candidateTree]) => candidateTree === tree)?.[0] || ""
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function ensureCapabilityCutLineage({
|
|
132
|
+
root = process.cwd(),
|
|
133
|
+
revision,
|
|
134
|
+
descendant = "HEAD",
|
|
135
|
+
label = "capability cut",
|
|
136
|
+
} = {}) {
|
|
137
|
+
try {
|
|
138
|
+
ensureCapabilityCutAncestor({ root, revision, descendant, label });
|
|
139
|
+
return { mode: "direct-ancestry", witness: descendant };
|
|
140
|
+
} catch (directError) {
|
|
141
|
+
const witness = protectedV4TreeWitness(root, descendant);
|
|
142
|
+
if (!witness)
|
|
143
|
+
throw new Error(
|
|
144
|
+
`${label} ${revision} is not an ancestor of ${descendant}, and ${descendant} has no tree-equivalent commit in the bounded protected v4 lineage`,
|
|
145
|
+
{ cause: directError },
|
|
146
|
+
);
|
|
147
|
+
ensureCapabilityCutAncestor({
|
|
148
|
+
root,
|
|
149
|
+
revision,
|
|
150
|
+
descendant: witness,
|
|
151
|
+
label: `${label} protected-lineage witness`,
|
|
152
|
+
});
|
|
153
|
+
return { mode: "protected-tree-equivalent", witness };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
50
157
|
function gitJson(root, revision, relPath) {
|
|
51
158
|
return JSON.parse(
|
|
52
159
|
git(root, ["show", `${revision}:${relPath}`], { trim: false }),
|
|
@@ -8,7 +8,10 @@ export function admitV4DeclarativePromotion({
|
|
|
8
8
|
runtimeRef,
|
|
9
9
|
declarative,
|
|
10
10
|
}) {
|
|
11
|
-
if (
|
|
11
|
+
if (
|
|
12
|
+
declarative !== true &&
|
|
13
|
+
!/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))
|
|
14
|
+
) {
|
|
12
15
|
return Object.freeze({ mode: "legacy", admitted: true });
|
|
13
16
|
}
|
|
14
17
|
if (declarative !== true) {
|