@kungfu-tech/buildchain 4.0.2-alpha.0 → 4.0.2-alpha.4
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 +7 -4
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/internal-capabilities.json +2 -0
- package/architecture/maintainability-debt.json +52 -124
- package/architecture/maintainability-policy.json +15 -10
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-core-mechanism-inventory.json +1 -0
- package/architecture/v3-v4-live-capability-inventory.json +17 -17
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
- package/architecture/v4-release-invocation-fixtures.json +139 -0
- package/architecture/v4-release-topology.json +243 -0
- package/architecture/v4-runtime-semantic-closure.json +4 -1
- 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 +134 -0
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +7 -7
- package/dist/site/kfd-claims.json +24 -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 +90 -15
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +34 -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 +36 -11
- package/docs/node-api-reference.md +16 -13
- package/package.json +2 -2
- package/packages/core/dev-delivery-candidate-identity.js +45 -17
- package/packages/core/dev-delivery-execution-transfer.js +6 -7
- package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
- package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
- package/packages/core/dev-delivery-warrant-state.js +54 -54
- package/packages/core/dev-delivery-warrant.js +8 -0
- package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
- package/packages/core/v4-canonical-contracts.js +10 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-protected-publication-source.js +93 -0
- package/packages/core/v4-publication-qualification.js +1 -0
- package/packages/core/v4-release-invocation.js +425 -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 +383 -0
- package/scripts/dev-delivery-warrant.mjs +26 -5
- package/scripts/generate-channel-promotion-workflow.mjs +14 -55
- package/scripts/release-candidate-resolver.mjs +17 -17
- package/scripts/resume-from-candidate-run.mjs +15 -16
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/v4-release-candidate-adapter.mjs +41 -0
- 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
|
@@ -52,16 +52,11 @@ function assertOrdered(relative, markers) {
|
|
|
52
52
|
|
|
53
53
|
export function assertPromotionCertificationWiring(source) {
|
|
54
54
|
for (const marker of [
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"path: .buildchain/runtime/policy-caller",
|
|
61
|
-
'--caller-root "${{ github.workspace }}/.buildchain/runtime/policy-caller"',
|
|
62
|
-
'--stable-lock "${{ inputs.buildchain-stable-contract-lock-path }}"',
|
|
63
|
-
'--alpha-lock "${{ inputs.buildchain-alpha-contract-lock-path }}"',
|
|
64
|
-
"release-passport-v4-consumer-policy-certification-root: ${{ steps.v4-policy-certification.outputs.v4-consumer-policy-certification-root }}",
|
|
55
|
+
"Translate and admit legacy-compatible inputs",
|
|
56
|
+
"BUILDCHAIN_PROMOTION_INPUTS_JSON: ${{ toJSON(inputs) }}",
|
|
57
|
+
"Resolve and qualify the sealed release candidate",
|
|
58
|
+
"BUILDCHAIN_RUNTIME_AUTHORIZATION_JSON",
|
|
59
|
+
"QUALIFY canonical v4 release invocation inputs",
|
|
65
60
|
]) {
|
|
66
61
|
if (!source.includes(marker)) {
|
|
67
62
|
fail(`promotion certification is missing ${marker}`);
|
|
@@ -165,8 +160,7 @@ export function checkV4FloatingConsumerPolicyContract() {
|
|
|
165
160
|
const promotion = read(".github/workflows/release-candidate-promote.yml");
|
|
166
161
|
if (
|
|
167
162
|
!promotion.includes("consumer-admission:") ||
|
|
168
|
-
!jobDependsOn(promotion, "
|
|
169
|
-
!jobDependsOn(promotion, "stable", "consumer-admission")
|
|
163
|
+
!jobDependsOn(promotion, "invoke", "consumer-admission")
|
|
170
164
|
) {
|
|
171
165
|
fail(
|
|
172
166
|
"release candidate promotion is not transitively gated by consumer admission",
|
|
@@ -179,16 +173,12 @@ export function checkV4FloatingConsumerPolicyContract() {
|
|
|
179
173
|
["packages/core/release-candidate.js", "consumerPolicy"],
|
|
180
174
|
["packages/core/release-passport.js", "v4ConsumerPolicy"],
|
|
181
175
|
[
|
|
182
|
-
"actions/
|
|
183
|
-
"
|
|
176
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
177
|
+
"candidate.consumerPolicy?.receiptRoot",
|
|
184
178
|
],
|
|
185
179
|
[
|
|
186
|
-
"actions/
|
|
187
|
-
"
|
|
188
|
-
],
|
|
189
|
-
[
|
|
190
|
-
".github/workflows/.release-candidate-promote.yml",
|
|
191
|
-
"v4-policy-certification",
|
|
180
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
181
|
+
"createV4ReleaseInvocation",
|
|
192
182
|
],
|
|
193
183
|
]) {
|
|
194
184
|
if (!read(relative).includes(marker)) {
|
|
@@ -0,0 +1,383 @@
|
|
|
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
|
+
|
|
16
|
+
function read(relative) {
|
|
17
|
+
return fs.readFileSync(path.join(root, relative), "utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function productionFiles(relative, extensions) {
|
|
21
|
+
const absolute = path.join(root, relative);
|
|
22
|
+
if (!fs.existsSync(absolute)) return [];
|
|
23
|
+
const entries = fs.readdirSync(absolute, { withFileTypes: true });
|
|
24
|
+
return entries.flatMap((entry) => {
|
|
25
|
+
const child = path.posix.join(relative, entry.name);
|
|
26
|
+
if (
|
|
27
|
+
entry.isDirectory() &&
|
|
28
|
+
!["dist", "node_modules", ".git"].includes(entry.name)
|
|
29
|
+
)
|
|
30
|
+
return productionFiles(child, extensions);
|
|
31
|
+
if (
|
|
32
|
+
entry.isFile() &&
|
|
33
|
+
extensions.some((extension) => entry.name.endsWith(extension))
|
|
34
|
+
)
|
|
35
|
+
return [child];
|
|
36
|
+
return [];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function matchingProductionFiles(roots, extensions, pattern) {
|
|
41
|
+
return roots
|
|
42
|
+
.flatMap((relative) => productionFiles(relative, extensions))
|
|
43
|
+
.filter((relative) => relative !== "scripts/check-v4-release-topology.mjs")
|
|
44
|
+
.filter((relative) => pattern.test(read(relative)))
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function discoverV4ReleaseAuthorityClosure() {
|
|
49
|
+
const providerPlane = matchingProductionFiles(
|
|
50
|
+
["actions", "packages/core"],
|
|
51
|
+
[".js", ".mjs", ".cjs"],
|
|
52
|
+
/(?:release-tail-provider-(?:adapters|plane)|create(?:GitHubReleaseAssets|SignedStaticChannel|SiteReleaseActivation|ReleasedEvidence)Adapter)/u,
|
|
53
|
+
);
|
|
54
|
+
const runtimeSelectors = matchingProductionFiles(
|
|
55
|
+
[".github/workflows", "scripts", "packages/core"],
|
|
56
|
+
[".yml", ".yaml", ".js", ".mjs", ".cjs"],
|
|
57
|
+
/(?:promotion-runtime-sha|resume-buildchain-runtime-sha|BUILDCHAIN_(?:CURRENT_RUNTIME_SHA|RUNTIME_REF|RESUME_CANDIDATE_RUN_ID)|authorizeV4RuntimeSelection|scanV4RuntimeSelectorPersistence|v4-release-candidate-adapter)/u,
|
|
58
|
+
);
|
|
59
|
+
const terminalProjections = matchingProductionFiles(
|
|
60
|
+
[".github/workflows", "actions", "packages/core", "scripts"],
|
|
61
|
+
[".yml", ".yaml", ".js", ".mjs", ".cjs"],
|
|
62
|
+
/(?:createV4ReleaseReceipt|release-receipt\.json|V4_RELEASE_RECEIPT_CONTRACT)/u,
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
providerAdapters: [
|
|
66
|
+
...new Set([
|
|
67
|
+
...providerPlane,
|
|
68
|
+
"packages/core/release-tail-provider-plane.js",
|
|
69
|
+
]),
|
|
70
|
+
].sort(),
|
|
71
|
+
runtimeSelectors,
|
|
72
|
+
runtimeEngines: ["actions/v4-release-candidate-promote/index.js"],
|
|
73
|
+
terminalProjections,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function jobBlock(source, jobId) {
|
|
78
|
+
const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
79
|
+
return (
|
|
80
|
+
source.match(
|
|
81
|
+
new RegExp(
|
|
82
|
+
`^ ${escaped}:\\n([\\s\\S]*?)(?=^ [A-Za-z0-9_.-]+:|(?![\\s\\S]))`,
|
|
83
|
+
"mu",
|
|
84
|
+
),
|
|
85
|
+
)?.[1] || ""
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function permission(block, name) {
|
|
90
|
+
return (
|
|
91
|
+
new RegExp(`^ ${name}:\\s*([^#\\n]+)`, "mu")
|
|
92
|
+
.exec(block)?.[1]
|
|
93
|
+
?.trim() || null
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function workflowSnapshot(relative) {
|
|
98
|
+
const source = read(relative);
|
|
99
|
+
const document = parseWorkflowDocument(source);
|
|
100
|
+
const jobs = document.jobs.map((job) => {
|
|
101
|
+
const block = jobBlock(source, job.id);
|
|
102
|
+
const uses = job.uses || null;
|
|
103
|
+
return {
|
|
104
|
+
id: job.id,
|
|
105
|
+
kind: uses ? "reusable-call" : "runner",
|
|
106
|
+
uses,
|
|
107
|
+
permissions: {
|
|
108
|
+
contents: permission(block, "contents"),
|
|
109
|
+
idToken: permission(block, "id-token"),
|
|
110
|
+
},
|
|
111
|
+
carriers: {
|
|
112
|
+
artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
|
|
113
|
+
artifactUpload: /uses:\s+actions\/upload-artifact@/u.test(block),
|
|
114
|
+
jobOutput: /GITHUB_OUTPUT/u.test(block),
|
|
115
|
+
},
|
|
116
|
+
mutationSignals: [
|
|
117
|
+
/contents:\s*write/u.test(block) && "contents-write",
|
|
118
|
+
/id-token:\s*write/u.test(block) && "oidc-write",
|
|
119
|
+
/(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
|
|
120
|
+
block,
|
|
121
|
+
) && "promotion-runtime",
|
|
122
|
+
/(?:git push|npm publish|gh release (?:create|upload))/u.test(block) &&
|
|
123
|
+
"direct-publication-command",
|
|
124
|
+
].filter(Boolean),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
path: relative,
|
|
129
|
+
triggers: document.triggers,
|
|
130
|
+
jobs,
|
|
131
|
+
reusableEdges: parseYamlUses(source)
|
|
132
|
+
.map(({ value }) => value)
|
|
133
|
+
.filter((value) => /\.github\/workflows\//u.test(value))
|
|
134
|
+
.sort(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function discoverV4ReleaseTopology(
|
|
139
|
+
workflowPaths,
|
|
140
|
+
semanticWorkflowPaths = workflowPaths,
|
|
141
|
+
) {
|
|
142
|
+
const workflows = workflowPaths.map(workflowSnapshot);
|
|
143
|
+
const jobs = workflows.flatMap((workflow) => workflow.jobs);
|
|
144
|
+
const mutationJobs = jobs.filter((job) => job.mutationSignals.length > 0);
|
|
145
|
+
const semanticPaths = new Set(semanticWorkflowPaths);
|
|
146
|
+
const semanticJobs = workflows
|
|
147
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
148
|
+
.flatMap((workflow) => workflow.jobs);
|
|
149
|
+
const semanticMutationJobs = semanticJobs.filter(
|
|
150
|
+
(job) => job.kind === "runner" && job.mutationSignals.length > 0,
|
|
151
|
+
);
|
|
152
|
+
return {
|
|
153
|
+
jobRecordFields: [
|
|
154
|
+
"id",
|
|
155
|
+
"kind",
|
|
156
|
+
"uses",
|
|
157
|
+
"contentsPermission",
|
|
158
|
+
"oidcPermission",
|
|
159
|
+
"carriers",
|
|
160
|
+
"mutationSignals",
|
|
161
|
+
],
|
|
162
|
+
workflows: workflows
|
|
163
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
164
|
+
.map((workflow) => ({
|
|
165
|
+
path: workflow.path,
|
|
166
|
+
triggers: workflow.triggers,
|
|
167
|
+
jobs: workflow.jobs.map((job) =>
|
|
168
|
+
[
|
|
169
|
+
job.id,
|
|
170
|
+
job.kind,
|
|
171
|
+
job.uses || "-",
|
|
172
|
+
job.permissions.contents || "-",
|
|
173
|
+
job.permissions.idToken || "-",
|
|
174
|
+
Object.entries(job.carriers)
|
|
175
|
+
.filter(([, present]) => present)
|
|
176
|
+
.map(([name]) => name)
|
|
177
|
+
.join(",") || "-",
|
|
178
|
+
job.mutationSignals.join(",") || "-",
|
|
179
|
+
].join("|"),
|
|
180
|
+
),
|
|
181
|
+
reusableEdges: workflow.reusableEdges,
|
|
182
|
+
})),
|
|
183
|
+
metrics: {
|
|
184
|
+
workflowCount: workflows.length,
|
|
185
|
+
jobCount: jobs.length,
|
|
186
|
+
reusableEdgeCount: workflows.reduce(
|
|
187
|
+
(count, workflow) => count + workflow.reusableEdges.length,
|
|
188
|
+
0,
|
|
189
|
+
),
|
|
190
|
+
mutationRelevantNodeCount: jobs.filter(
|
|
191
|
+
(job) =>
|
|
192
|
+
job.kind === "reusable-call" ||
|
|
193
|
+
job.mutationSignals.length > 0 ||
|
|
194
|
+
job.carriers.artifactDownload ||
|
|
195
|
+
job.carriers.artifactUpload,
|
|
196
|
+
).length,
|
|
197
|
+
contentsWriteJobCount: mutationJobs.filter((job) =>
|
|
198
|
+
job.mutationSignals.includes("contents-write"),
|
|
199
|
+
).length,
|
|
200
|
+
oidcWriteJobCount: mutationJobs.filter((job) =>
|
|
201
|
+
job.mutationSignals.includes("oidc-write"),
|
|
202
|
+
).length,
|
|
203
|
+
},
|
|
204
|
+
semanticMetrics: {
|
|
205
|
+
workflowCount: semanticPaths.size,
|
|
206
|
+
mutationRelevantNodeCount: semanticJobs.filter(
|
|
207
|
+
(job) =>
|
|
208
|
+
job.kind === "reusable-call" ||
|
|
209
|
+
job.mutationSignals.length > 0 ||
|
|
210
|
+
job.carriers.artifactDownload ||
|
|
211
|
+
job.carriers.artifactUpload,
|
|
212
|
+
).length,
|
|
213
|
+
contentsWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
214
|
+
job.mutationSignals.includes("contents-write"),
|
|
215
|
+
).length,
|
|
216
|
+
oidcWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
217
|
+
job.mutationSignals.includes("oidc-write"),
|
|
218
|
+
).length,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function findUnknownV4ReleaseTopology(
|
|
224
|
+
workflowPaths,
|
|
225
|
+
allWorkflowPaths = fs
|
|
226
|
+
.readdirSync(path.join(root, ".github/workflows"))
|
|
227
|
+
.filter((name) => /\.ya?ml$/u.test(name))
|
|
228
|
+
.map((name) => `.github/workflows/${name}`),
|
|
229
|
+
readWorkflow = read,
|
|
230
|
+
) {
|
|
231
|
+
const declared = new Set(workflowPaths);
|
|
232
|
+
return allWorkflowPaths
|
|
233
|
+
.filter((relative) => !declared.has(relative))
|
|
234
|
+
.filter((relative) => {
|
|
235
|
+
const source = readWorkflow(relative);
|
|
236
|
+
const usesReleaseAuthority = parseYamlUses(source).some(({ value }) =>
|
|
237
|
+
/(?:release-candidate-promote|promote-buildchain-ref|v4-release-candidate-promote|release-tail)/u.test(
|
|
238
|
+
value,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
const hasReleaseLanguage =
|
|
242
|
+
/(?:^|[-_ ])(?:release|publish|promotion|distribution|tag)(?:$|[-_ :])/imu.test(
|
|
243
|
+
source,
|
|
244
|
+
);
|
|
245
|
+
const hasMutationAuthority =
|
|
246
|
+
/(?:contents|id-token):\s*write/u.test(source) ||
|
|
247
|
+
/(?:git push|npm publish|gh release (?:create|upload)|createRef|updateRef)/u.test(
|
|
248
|
+
source,
|
|
249
|
+
);
|
|
250
|
+
return (
|
|
251
|
+
usesReleaseAuthority || (hasReleaseLanguage && hasMutationAuthority)
|
|
252
|
+
);
|
|
253
|
+
})
|
|
254
|
+
.sort();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function assertClosedWorld(workflowPaths) {
|
|
258
|
+
const unknown = findUnknownV4ReleaseTopology(workflowPaths);
|
|
259
|
+
assert.deepEqual(
|
|
260
|
+
unknown,
|
|
261
|
+
[],
|
|
262
|
+
`unknown v4 release topology workflows: ${unknown.join(", ")}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function assertAuthorityClosure(ledger) {
|
|
267
|
+
const closure = ledger.authorityClosure;
|
|
268
|
+
assert.ok(
|
|
269
|
+
closure && typeof closure === "object",
|
|
270
|
+
"authority closure missing",
|
|
271
|
+
);
|
|
272
|
+
for (const className of [
|
|
273
|
+
"providerAdapters",
|
|
274
|
+
"runtimeSelectors",
|
|
275
|
+
"runtimeEngines",
|
|
276
|
+
"terminalProjections",
|
|
277
|
+
]) {
|
|
278
|
+
assert.ok(
|
|
279
|
+
Array.isArray(closure[className]) && closure[className].length > 0,
|
|
280
|
+
`authority closure class ${className} is empty`,
|
|
281
|
+
);
|
|
282
|
+
for (const relative of closure[className])
|
|
283
|
+
assert.ok(
|
|
284
|
+
fs.statSync(path.join(root, relative)).isFile(),
|
|
285
|
+
`authority closure path is not a file: ${relative}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const discovered = discoverV4ReleaseAuthorityClosure();
|
|
289
|
+
for (const className of Object.keys(discovered))
|
|
290
|
+
assert.deepEqual(
|
|
291
|
+
closure[className],
|
|
292
|
+
discovered[className],
|
|
293
|
+
`authority closure class drifted: ${className}`,
|
|
294
|
+
);
|
|
295
|
+
assert.deepEqual(closure.runtimeEngines, [
|
|
296
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
297
|
+
]);
|
|
298
|
+
assert.equal(
|
|
299
|
+
closure.freshEntry,
|
|
300
|
+
".github/workflows/release-candidate-promote.yml",
|
|
301
|
+
);
|
|
302
|
+
assert.equal(
|
|
303
|
+
closure.recoveryEntry,
|
|
304
|
+
".github/workflows/buildchain-ref-promotion-recovery.yml",
|
|
305
|
+
);
|
|
306
|
+
const engineSurface = [
|
|
307
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
308
|
+
...closure.runtimeEngines,
|
|
309
|
+
]
|
|
310
|
+
.map(read)
|
|
311
|
+
.join("\n");
|
|
312
|
+
for (const pattern of closure.forbiddenLegacyEnginePatterns)
|
|
313
|
+
assert.doesNotMatch(
|
|
314
|
+
engineSurface,
|
|
315
|
+
new RegExp(pattern, "u"),
|
|
316
|
+
`legacy release engine remains reachable: ${pattern}`,
|
|
317
|
+
);
|
|
318
|
+
const canonicalWorkflow = read(
|
|
319
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
320
|
+
);
|
|
321
|
+
assert.match(
|
|
322
|
+
canonicalWorkflow,
|
|
323
|
+
/scripts\/v4-release-candidate-adapter\.mjs/u,
|
|
324
|
+
);
|
|
325
|
+
assert.match(
|
|
326
|
+
canonicalWorkflow,
|
|
327
|
+
/release-invocation\.json[\s\S]*release-transaction\.json[\s\S]*release-receipt\.json/u,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function checkV4ReleaseTopology() {
|
|
332
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
333
|
+
assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
|
|
334
|
+
assertClosedWorld(ledger.closedWorld.workflowPaths);
|
|
335
|
+
assertAuthorityClosure(ledger);
|
|
336
|
+
const actual = discoverV4ReleaseTopology(
|
|
337
|
+
ledger.closedWorld.workflowPaths,
|
|
338
|
+
ledger.semanticScope.workflowPaths,
|
|
339
|
+
);
|
|
340
|
+
assert.deepEqual(actual, ledger.observedTopology);
|
|
341
|
+
assert.equal(
|
|
342
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
343
|
+
Math.floor(ledger.baselineMetrics.semanticMutationRelevantNodeCount / 2),
|
|
344
|
+
);
|
|
345
|
+
if (ledger.enforcement === "converged") {
|
|
346
|
+
assert.ok(
|
|
347
|
+
actual.semanticMetrics.mutationRelevantNodeCount <=
|
|
348
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
349
|
+
);
|
|
350
|
+
assert.equal(actual.semanticMetrics.contentsWriteJobCount, 1);
|
|
351
|
+
assert.equal(actual.semanticMetrics.oidcWriteJobCount, 1);
|
|
352
|
+
const publisher = actual.workflows.find(
|
|
353
|
+
({ path: workflowPath }) =>
|
|
354
|
+
workflowPath === ".github/workflows/.release-candidate-promote.yml",
|
|
355
|
+
);
|
|
356
|
+
assert.deepEqual(
|
|
357
|
+
publisher.jobs.map((job) => job.split("|", 1)[0]),
|
|
358
|
+
["apply", "qualify", "settle"],
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return actual;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (process.argv.includes("--print-closure")) {
|
|
365
|
+
process.stdout.write(
|
|
366
|
+
`${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
|
|
367
|
+
);
|
|
368
|
+
} else if (process.argv.includes("--print")) {
|
|
369
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
370
|
+
process.stdout.write(
|
|
371
|
+
`${JSON.stringify(
|
|
372
|
+
discoverV4ReleaseTopology(
|
|
373
|
+
ledger.closedWorld.workflowPaths,
|
|
374
|
+
ledger.semanticScope.workflowPaths,
|
|
375
|
+
),
|
|
376
|
+
null,
|
|
377
|
+
2,
|
|
378
|
+
)}\n`,
|
|
379
|
+
);
|
|
380
|
+
} else {
|
|
381
|
+
checkV4ReleaseTopology();
|
|
382
|
+
process.stdout.write("v4 release topology: ok\n");
|
|
383
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
closeDevDeliveryWarrant,
|
|
7
7
|
createDevDeliveryQueue,
|
|
8
8
|
createNativeCommandContract,
|
|
9
|
+
fenceDevDeliveryWriterProtocol,
|
|
9
10
|
heartbeatDevDeliveryWarrant,
|
|
10
11
|
observeDevDeliveryQueue,
|
|
11
12
|
qualifyDevDeliveryWarrant,
|
|
@@ -91,6 +92,13 @@ function requireTerminalEvidenceCas(options) {
|
|
|
91
92
|
"terminal evidence reconciliation execute requires expected-old CAS",
|
|
92
93
|
);
|
|
93
94
|
}
|
|
95
|
+
if (
|
|
96
|
+
options.command === "fence-writer-protocol" &&
|
|
97
|
+
options.execute &&
|
|
98
|
+
!options.expectedOldStateRoot
|
|
99
|
+
) {
|
|
100
|
+
throw new Error("writer protocol fence execute requires expected-old CAS");
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
function normalizeRepository(value) {
|
|
@@ -175,6 +183,9 @@ function jsonObject(value, label) {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
function transitionFor(command, queue, options) {
|
|
186
|
+
if (command === "fence-writer-protocol") {
|
|
187
|
+
return fenceDevDeliveryWriterProtocol(queue, { now: options.now });
|
|
188
|
+
}
|
|
178
189
|
if (command === "submit") {
|
|
179
190
|
const nativeCommandContract = options.environmentRoot
|
|
180
191
|
? createNativeCommandContract(options.nativeCommand)
|
|
@@ -430,20 +441,29 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
430
441
|
options.command,
|
|
431
442
|
),
|
|
432
443
|
});
|
|
444
|
+
let concurrencyRecovery = null;
|
|
433
445
|
if (
|
|
434
446
|
options.expectedOldStateRoot &&
|
|
435
447
|
loaded.queue.stateRoot !== options.expectedOldStateRoot
|
|
436
448
|
) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
449
|
+
if (options.command !== "heartbeat") {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
concurrencyRecovery = {
|
|
455
|
+
schema: "kungfu.buildchain.dev-delivery-concurrency-recovery/v1",
|
|
456
|
+
action: "heartbeat-state-root-rebased",
|
|
457
|
+
requestedStateRoot: options.expectedOldStateRoot,
|
|
458
|
+
observedStateRoot: loaded.queue.stateRoot,
|
|
459
|
+
observedCommitSha: loaded.commitSha,
|
|
460
|
+
};
|
|
440
461
|
}
|
|
441
462
|
if (options.command === "observe") return observeQueue(loaded, options);
|
|
442
463
|
const initialLoaded = loaded;
|
|
443
464
|
let changed = transitionFor(options.command, loaded.queue, options);
|
|
444
465
|
let mutates = changed.queue.stateRoot !== loaded.queue.stateRoot;
|
|
445
466
|
let write = null;
|
|
446
|
-
let concurrencyRecovery = null;
|
|
447
467
|
if (options.execute && mutates) {
|
|
448
468
|
if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
|
|
449
469
|
throw new Error(
|
|
@@ -508,7 +528,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
508
528
|
}
|
|
509
529
|
|
|
510
530
|
function usage() {
|
|
511
|
-
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
531
|
+
return "Usage:\n buildchain dev warrant <fence-writer-protocol|submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nWriter protocol fence:\n fence-writer-protocol --expected-old sha256:... [--execute]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
512
532
|
}
|
|
513
533
|
|
|
514
534
|
async function main() {
|
|
@@ -520,6 +540,7 @@ async function main() {
|
|
|
520
540
|
const options = devDeliveryCliOptions(args);
|
|
521
541
|
if (
|
|
522
542
|
![
|
|
543
|
+
"fence-writer-protocol",
|
|
523
544
|
"submit",
|
|
524
545
|
"select",
|
|
525
546
|
"heartbeat",
|
|
@@ -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"],
|
|
@@ -214,8 +214,9 @@ function consumerAdmissionJob() {
|
|
|
214
214
|
id: policy
|
|
215
215
|
env:
|
|
216
216
|
BUILDCHAIN_CONSUMER_ROOT: .buildchain/consumer
|
|
217
|
+
BUILDCHAIN_INVOCATION_SOURCE_PATH: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/release-candidate-promote.yml' || inputs.publication-publisher-workflow-path }}
|
|
217
218
|
BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: \${{ needs.resolve-promotion.outputs.channel }}
|
|
218
|
-
BUILDCHAIN_INVOKED_WORKFLOW: .github/workflows/release-candidate-promote.yml
|
|
219
|
+
BUILDCHAIN_INVOKED_WORKFLOW: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/.release-candidate-promote.yml' || '.github/workflows/release-candidate-promote.yml' }}
|
|
219
220
|
BUILDCHAIN_WORKFLOW_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
220
221
|
BUILDCHAIN_RUNTIME_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
221
222
|
BUILDCHAIN_STABLE_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
|
|
@@ -292,7 +293,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
|
|
|
292
293
|
},
|
|
293
294
|
};
|
|
294
295
|
const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
|
|
295
|
-
|
|
296
|
+
validateWorkflowRoute("stable", routes.stable, `v${major}`);
|
|
296
297
|
const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
|
|
297
298
|
const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
|
|
298
299
|
const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
|
|
@@ -300,8 +301,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
|
|
|
300
301
|
for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
|
|
301
302
|
if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
|
|
302
303
|
}
|
|
303
|
-
const
|
|
304
|
-
const stableForwarded = routedInputs(inputNames, stableRoute, major);
|
|
304
|
+
const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
|
|
305
305
|
return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
|
|
306
306
|
name: Release Candidate Promote
|
|
307
307
|
|
|
@@ -336,7 +336,7 @@ jobs:
|
|
|
336
336
|
target-ref: \${{ steps.route.outputs.target-ref }}
|
|
337
337
|
router-ref: \${{ steps.router.outputs.ref }}
|
|
338
338
|
router-sha: \${{ steps.router.outputs.sha }}
|
|
339
|
-
shell-ref: \${{ steps.
|
|
339
|
+
shell-ref: \${{ steps.identities.outputs.shell-ref }}
|
|
340
340
|
shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
|
|
341
341
|
shell-sha: \${{ steps.identities.outputs.shell-sha }}
|
|
342
342
|
runtime-ref: \${{ steps.route.outputs.runtime-ref }}
|
|
@@ -425,32 +425,17 @@ jobs:
|
|
|
425
425
|
shell: bash
|
|
426
426
|
env:
|
|
427
427
|
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}
|
|
428
|
+
SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
|
|
434
429
|
run: |
|
|
435
430
|
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
431
|
node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
|
|
448
432
|
--repository "\${{ steps.router.outputs.repository }}" \\
|
|
449
433
|
--router-ref "\${{ steps.router.outputs.ref }}" \\
|
|
450
434
|
--router-sha "\${{ steps.router.outputs.sha }}" \\
|
|
451
435
|
--shell-ref "\${SELECTED_SHELL_REF}" \\
|
|
452
|
-
--shell-call-ref "\${
|
|
436
|
+
--shell-call-ref "\${SELECTED_SHELL_REF}" \\
|
|
453
437
|
--runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
|
|
438
|
+
echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
|
|
454
439
|
|
|
455
440
|
- name: Checkout selected promotion workflow shell
|
|
456
441
|
uses: actions/checkout@v7.0.0
|
|
@@ -500,29 +485,21 @@ jobs:
|
|
|
500
485
|
- name: Verify immutable promotion checkouts
|
|
501
486
|
shell: bash
|
|
502
487
|
env:
|
|
503
|
-
|
|
504
|
-
ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
|
|
505
|
-
STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
|
|
488
|
+
SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
|
|
506
489
|
ROUTER_SHA: \${{ steps.router.outputs.sha }}
|
|
507
490
|
SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
|
|
508
491
|
RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
|
|
509
492
|
run: |
|
|
510
493
|
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}"
|
|
494
|
+
test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
|
|
517
495
|
[[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
|
|
518
496
|
[[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
|
|
519
497
|
[[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
|
|
520
498
|
|
|
521
499
|
${consumerAdmissionJob()}
|
|
522
|
-
|
|
523
|
-
name:
|
|
500
|
+
invoke:
|
|
501
|
+
name: Invoke the single v4 publisher adapter
|
|
524
502
|
needs: [resolve-promotion, consumer-admission]
|
|
525
|
-
if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
|
|
526
503
|
uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
|
|
527
504
|
permissions:
|
|
528
505
|
actions: write
|
|
@@ -534,25 +511,7 @@ ${consumerAdmissionJob()}
|
|
|
534
511
|
issues: write
|
|
535
512
|
pull-requests: write
|
|
536
513
|
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}
|
|
514
|
+
${invokeForwarded}
|
|
556
515
|
secrets: inherit
|
|
557
516
|
`;
|
|
558
517
|
}
|