@kungfu-tech/buildchain 4.0.1 → 4.0.2-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/architecture/agent-change-map.md +15 -6
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/internal-capabilities.json +9 -2
- package/architecture/maintainability-debt.json +66 -136
- package/architecture/maintainability-policy.json +17 -12
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-core-mechanism-inventory.json +3 -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 +316 -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 +9 -9
- package/dist/site/kfd-claims.json +28 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +92 -17
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +42 -10
- 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 +38 -11
- package/docs/dev-delivery-warrant.md +21 -0
- package/docs/node-api-reference.md +19 -16
- 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 +248 -0
- 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/release-tail-product-capabilities.js +29 -0
- package/packages/core/release-tail-provider-plane.js +2 -2
- package/packages/core/v4-canonical-contracts.js +14 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-product-publication.js +387 -0
- 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 +490 -0
- package/scripts/dev-delivery-warrant-options.mjs +15 -4
- package/scripts/dev-delivery-warrant.mjs +41 -7
- package/scripts/generate-channel-promotion-workflow.mjs +14 -55
- package/scripts/release-candidate-resolver.mjs +23 -24
- package/scripts/resume-from-candidate-run.mjs +16 -17
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/v4-product-publication-intent.mjs +114 -0
- 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,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
parseWorkflowDocument,
|
|
11
|
+
parseYamlUses,
|
|
12
|
+
} from "../packages/core/workflow-yaml-contract.js";
|
|
13
|
+
|
|
14
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const ledgerPath = path.join(root, "architecture/v4-release-topology.json");
|
|
16
|
+
|
|
17
|
+
function read(relative) {
|
|
18
|
+
return fs.readFileSync(path.join(root, relative), "utf8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LOCAL_MODULE_EXTENSIONS = ["", ".js", ".mjs", ".cjs"];
|
|
22
|
+
const PRIVILEGED_ENTRYPOINTS = [
|
|
23
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function localModuleSpecifiers(source) {
|
|
27
|
+
const specifiers = [];
|
|
28
|
+
const expression =
|
|
29
|
+
/(?:\bfrom\s+|\bimport\s*\(\s*|\bimport\s+)["'](\.[^"']+)["']/gu;
|
|
30
|
+
for (const match of source.matchAll(expression)) specifiers.push(match[1]);
|
|
31
|
+
return [...new Set(specifiers)].sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveLocalModule(importer, specifier) {
|
|
35
|
+
const unresolved = path.posix.normalize(
|
|
36
|
+
path.posix.join(path.posix.dirname(importer), specifier),
|
|
37
|
+
);
|
|
38
|
+
assert.ok(
|
|
39
|
+
!unresolved.startsWith("../") && !path.posix.isAbsolute(unresolved),
|
|
40
|
+
`local module import escapes the repository: ${importer} -> ${specifier}`,
|
|
41
|
+
);
|
|
42
|
+
const candidates = [
|
|
43
|
+
...LOCAL_MODULE_EXTENSIONS.map((extension) => `${unresolved}${extension}`),
|
|
44
|
+
...LOCAL_MODULE_EXTENSIONS.slice(1).map((extension) =>
|
|
45
|
+
path.posix.join(unresolved, `index${extension}`),
|
|
46
|
+
),
|
|
47
|
+
];
|
|
48
|
+
const matches = candidates.filter((relative) => {
|
|
49
|
+
const absolute = path.join(root, relative);
|
|
50
|
+
return fs.existsSync(absolute) && fs.statSync(absolute).isFile();
|
|
51
|
+
});
|
|
52
|
+
assert.equal(
|
|
53
|
+
matches.length,
|
|
54
|
+
1,
|
|
55
|
+
`local module import must resolve exactly once: ${importer} -> ${specifier}`,
|
|
56
|
+
);
|
|
57
|
+
return matches[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function discoverStaticModuleClosure(entrypoints) {
|
|
61
|
+
const pending = [...entrypoints];
|
|
62
|
+
const visited = new Set();
|
|
63
|
+
while (pending.length > 0) {
|
|
64
|
+
const relative = pending.pop();
|
|
65
|
+
if (visited.has(relative)) continue;
|
|
66
|
+
assert.ok(
|
|
67
|
+
fs.statSync(path.join(root, relative)).isFile(),
|
|
68
|
+
`privileged entry is not a file: ${relative}`,
|
|
69
|
+
);
|
|
70
|
+
visited.add(relative);
|
|
71
|
+
for (const specifier of localModuleSpecifiers(read(relative))) {
|
|
72
|
+
const dependency = resolveLocalModule(relative, specifier);
|
|
73
|
+
if (!visited.has(dependency)) pending.push(dependency);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...visited].sort();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function rootedPathSet(paths) {
|
|
80
|
+
return `sha256:${crypto
|
|
81
|
+
.createHash("sha256")
|
|
82
|
+
.update(JSON.stringify([...paths].sort()))
|
|
83
|
+
.digest("hex")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function productionFiles(relative, extensions) {
|
|
87
|
+
const absolute = path.join(root, relative);
|
|
88
|
+
if (!fs.existsSync(absolute)) return [];
|
|
89
|
+
const entries = fs.readdirSync(absolute, { withFileTypes: true });
|
|
90
|
+
return entries.flatMap((entry) => {
|
|
91
|
+
const child = path.posix.join(relative, entry.name);
|
|
92
|
+
if (
|
|
93
|
+
entry.isDirectory() &&
|
|
94
|
+
!["dist", "node_modules", ".git"].includes(entry.name)
|
|
95
|
+
)
|
|
96
|
+
return productionFiles(child, extensions);
|
|
97
|
+
if (
|
|
98
|
+
entry.isFile() &&
|
|
99
|
+
extensions.some((extension) => entry.name.endsWith(extension))
|
|
100
|
+
)
|
|
101
|
+
return [child];
|
|
102
|
+
return [];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function matchingProductionFiles(roots, extensions, pattern) {
|
|
107
|
+
return roots
|
|
108
|
+
.flatMap((relative) => productionFiles(relative, extensions))
|
|
109
|
+
.filter((relative) => relative !== "scripts/check-v4-release-topology.mjs")
|
|
110
|
+
.filter((relative) => pattern.test(read(relative)))
|
|
111
|
+
.sort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function discoverV4ReleaseAuthorityClosure() {
|
|
115
|
+
const providerPlane = matchingProductionFiles(
|
|
116
|
+
["actions", "packages/core"],
|
|
117
|
+
[".js", ".mjs", ".cjs"],
|
|
118
|
+
/(?:release-tail-provider-(?:adapters|plane)|create(?:GitHubReleaseAssets|SignedStaticChannel|SiteReleaseActivation|ReleasedEvidence)Adapter)/u,
|
|
119
|
+
);
|
|
120
|
+
const runtimeSelectors = matchingProductionFiles(
|
|
121
|
+
[".github/workflows", "scripts", "packages/core"],
|
|
122
|
+
[".yml", ".yaml", ".js", ".mjs", ".cjs"],
|
|
123
|
+
/(?:promotion-runtime-sha|resume-buildchain-runtime-sha|BUILDCHAIN_(?:CURRENT_RUNTIME_SHA|RUNTIME_REF|RESUME_CANDIDATE_RUN_ID)|authorizeV4RuntimeSelection|scanV4RuntimeSelectorPersistence|v4-release-candidate-adapter)/u,
|
|
124
|
+
);
|
|
125
|
+
const terminalProjections = matchingProductionFiles(
|
|
126
|
+
[".github/workflows", "actions", "packages/core", "scripts"],
|
|
127
|
+
[".yml", ".yaml", ".js", ".mjs", ".cjs"],
|
|
128
|
+
/(?:createV4ReleaseReceipt|release-receipt\.json|V4_RELEASE_RECEIPT_CONTRACT)/u,
|
|
129
|
+
);
|
|
130
|
+
const privilegedModules = discoverStaticModuleClosure(PRIVILEGED_ENTRYPOINTS);
|
|
131
|
+
const legacyEngineModules = productionFiles(
|
|
132
|
+
"actions/promote-buildchain-ref",
|
|
133
|
+
[".js", ".mjs", ".cjs"],
|
|
134
|
+
)
|
|
135
|
+
.filter((relative) =>
|
|
136
|
+
/(?:^|\/)(?:lib|promote-(?:alpha|release|major)-channel|durable-transaction-operations)\.js$/u.test(
|
|
137
|
+
relative,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
.sort();
|
|
141
|
+
return {
|
|
142
|
+
providerAdapters: [
|
|
143
|
+
...new Set([
|
|
144
|
+
...providerPlane,
|
|
145
|
+
"packages/core/release-tail-provider-plane.js",
|
|
146
|
+
]),
|
|
147
|
+
].sort(),
|
|
148
|
+
runtimeSelectors,
|
|
149
|
+
runtimeEngines: ["actions/v4-release-candidate-promote/index.js"],
|
|
150
|
+
terminalProjections,
|
|
151
|
+
privilegedExecutableClosure: {
|
|
152
|
+
entrypoints: PRIVILEGED_ENTRYPOINTS,
|
|
153
|
+
modules: privilegedModules,
|
|
154
|
+
root: rootedPathSet(privilegedModules),
|
|
155
|
+
},
|
|
156
|
+
legacyEngineModules,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function jobBlock(source, jobId) {
|
|
161
|
+
const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
162
|
+
return (
|
|
163
|
+
source.match(
|
|
164
|
+
new RegExp(
|
|
165
|
+
`^ ${escaped}:\\n([\\s\\S]*?)(?=^ [A-Za-z0-9_.-]+:|(?![\\s\\S]))`,
|
|
166
|
+
"mu",
|
|
167
|
+
),
|
|
168
|
+
)?.[1] || ""
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function permission(block, name) {
|
|
173
|
+
return (
|
|
174
|
+
new RegExp(`^ ${name}:\\s*([^#\\n]+)`, "mu")
|
|
175
|
+
.exec(block)?.[1]
|
|
176
|
+
?.trim() || null
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function workflowSnapshot(relative) {
|
|
181
|
+
const source = read(relative);
|
|
182
|
+
const document = parseWorkflowDocument(source);
|
|
183
|
+
const jobs = document.jobs.map((job) => {
|
|
184
|
+
const block = jobBlock(source, job.id);
|
|
185
|
+
const uses = job.uses || null;
|
|
186
|
+
return {
|
|
187
|
+
id: job.id,
|
|
188
|
+
kind: uses ? "reusable-call" : "runner",
|
|
189
|
+
uses,
|
|
190
|
+
permissions: {
|
|
191
|
+
contents: permission(block, "contents"),
|
|
192
|
+
idToken: permission(block, "id-token"),
|
|
193
|
+
},
|
|
194
|
+
carriers: {
|
|
195
|
+
artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
|
|
196
|
+
artifactUpload: /uses:\s+actions\/upload-artifact@/u.test(block),
|
|
197
|
+
jobOutput: /GITHUB_OUTPUT/u.test(block),
|
|
198
|
+
},
|
|
199
|
+
mutationSignals: [
|
|
200
|
+
/contents:\s*write/u.test(block) && "contents-write",
|
|
201
|
+
/id-token:\s*write/u.test(block) && "oidc-write",
|
|
202
|
+
/(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
|
|
203
|
+
block,
|
|
204
|
+
) && "promotion-runtime",
|
|
205
|
+
/(?:git push|npm publish|gh release (?:create|upload))/u.test(block) &&
|
|
206
|
+
"direct-publication-command",
|
|
207
|
+
].filter(Boolean),
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
path: relative,
|
|
212
|
+
triggers: document.triggers,
|
|
213
|
+
jobs,
|
|
214
|
+
reusableEdges: parseYamlUses(source)
|
|
215
|
+
.map(({ value }) => value)
|
|
216
|
+
.filter((value) => /\.github\/workflows\//u.test(value))
|
|
217
|
+
.sort(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function discoverV4ReleaseTopology(
|
|
222
|
+
workflowPaths,
|
|
223
|
+
semanticWorkflowPaths = workflowPaths,
|
|
224
|
+
) {
|
|
225
|
+
const workflows = workflowPaths.map(workflowSnapshot);
|
|
226
|
+
const jobs = workflows.flatMap((workflow) => workflow.jobs);
|
|
227
|
+
const mutationJobs = jobs.filter((job) => job.mutationSignals.length > 0);
|
|
228
|
+
const semanticPaths = new Set(semanticWorkflowPaths);
|
|
229
|
+
const semanticJobs = workflows
|
|
230
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
231
|
+
.flatMap((workflow) => workflow.jobs);
|
|
232
|
+
const semanticMutationJobs = semanticJobs.filter(
|
|
233
|
+
(job) => job.kind === "runner" && job.mutationSignals.length > 0,
|
|
234
|
+
);
|
|
235
|
+
return {
|
|
236
|
+
jobRecordFields: [
|
|
237
|
+
"id",
|
|
238
|
+
"kind",
|
|
239
|
+
"uses",
|
|
240
|
+
"contentsPermission",
|
|
241
|
+
"oidcPermission",
|
|
242
|
+
"carriers",
|
|
243
|
+
"mutationSignals",
|
|
244
|
+
],
|
|
245
|
+
workflows: workflows
|
|
246
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
247
|
+
.map((workflow) => ({
|
|
248
|
+
path: workflow.path,
|
|
249
|
+
triggers: workflow.triggers,
|
|
250
|
+
jobs: workflow.jobs.map((job) =>
|
|
251
|
+
[
|
|
252
|
+
job.id,
|
|
253
|
+
job.kind,
|
|
254
|
+
job.uses || "-",
|
|
255
|
+
job.permissions.contents || "-",
|
|
256
|
+
job.permissions.idToken || "-",
|
|
257
|
+
Object.entries(job.carriers)
|
|
258
|
+
.filter(([, present]) => present)
|
|
259
|
+
.map(([name]) => name)
|
|
260
|
+
.join(",") || "-",
|
|
261
|
+
job.mutationSignals.join(",") || "-",
|
|
262
|
+
].join("|"),
|
|
263
|
+
),
|
|
264
|
+
reusableEdges: workflow.reusableEdges,
|
|
265
|
+
})),
|
|
266
|
+
metrics: {
|
|
267
|
+
workflowCount: workflows.length,
|
|
268
|
+
jobCount: jobs.length,
|
|
269
|
+
reusableEdgeCount: workflows.reduce(
|
|
270
|
+
(count, workflow) => count + workflow.reusableEdges.length,
|
|
271
|
+
0,
|
|
272
|
+
),
|
|
273
|
+
mutationRelevantNodeCount: jobs.filter(
|
|
274
|
+
(job) =>
|
|
275
|
+
job.kind === "reusable-call" ||
|
|
276
|
+
job.mutationSignals.length > 0 ||
|
|
277
|
+
job.carriers.artifactDownload ||
|
|
278
|
+
job.carriers.artifactUpload,
|
|
279
|
+
).length,
|
|
280
|
+
contentsWriteJobCount: mutationJobs.filter((job) =>
|
|
281
|
+
job.mutationSignals.includes("contents-write"),
|
|
282
|
+
).length,
|
|
283
|
+
oidcWriteJobCount: mutationJobs.filter((job) =>
|
|
284
|
+
job.mutationSignals.includes("oidc-write"),
|
|
285
|
+
).length,
|
|
286
|
+
},
|
|
287
|
+
semanticMetrics: {
|
|
288
|
+
workflowCount: semanticPaths.size,
|
|
289
|
+
mutationRelevantNodeCount: semanticJobs.filter(
|
|
290
|
+
(job) =>
|
|
291
|
+
job.kind === "reusable-call" ||
|
|
292
|
+
job.mutationSignals.length > 0 ||
|
|
293
|
+
job.carriers.artifactDownload ||
|
|
294
|
+
job.carriers.artifactUpload,
|
|
295
|
+
).length,
|
|
296
|
+
contentsWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
297
|
+
job.mutationSignals.includes("contents-write"),
|
|
298
|
+
).length,
|
|
299
|
+
oidcWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
300
|
+
job.mutationSignals.includes("oidc-write"),
|
|
301
|
+
).length,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function findUnknownV4ReleaseTopology(
|
|
307
|
+
workflowPaths,
|
|
308
|
+
allWorkflowPaths = fs
|
|
309
|
+
.readdirSync(path.join(root, ".github/workflows"))
|
|
310
|
+
.filter((name) => /\.ya?ml$/u.test(name))
|
|
311
|
+
.map((name) => `.github/workflows/${name}`),
|
|
312
|
+
readWorkflow = read,
|
|
313
|
+
) {
|
|
314
|
+
const declared = new Set(workflowPaths);
|
|
315
|
+
return allWorkflowPaths
|
|
316
|
+
.filter((relative) => !declared.has(relative))
|
|
317
|
+
.filter((relative) => {
|
|
318
|
+
const source = readWorkflow(relative);
|
|
319
|
+
const usesReleaseAuthority = parseYamlUses(source).some(({ value }) =>
|
|
320
|
+
/(?:release-candidate-promote|promote-buildchain-ref|v4-release-candidate-promote|release-tail)/u.test(
|
|
321
|
+
value,
|
|
322
|
+
),
|
|
323
|
+
);
|
|
324
|
+
const hasReleaseLanguage =
|
|
325
|
+
/(?:^|[-_ ])(?:release|publish|promotion|distribution|tag)(?:$|[-_ :])/imu.test(
|
|
326
|
+
source,
|
|
327
|
+
);
|
|
328
|
+
const hasMutationAuthority =
|
|
329
|
+
/(?:contents|id-token):\s*write/u.test(source) ||
|
|
330
|
+
/(?:git push|npm publish|gh release (?:create|upload)|createRef|updateRef)/u.test(
|
|
331
|
+
source,
|
|
332
|
+
);
|
|
333
|
+
return (
|
|
334
|
+
usesReleaseAuthority || (hasReleaseLanguage && hasMutationAuthority)
|
|
335
|
+
);
|
|
336
|
+
})
|
|
337
|
+
.sort();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function assertClosedWorld(workflowPaths) {
|
|
341
|
+
const unknown = findUnknownV4ReleaseTopology(workflowPaths);
|
|
342
|
+
assert.deepEqual(
|
|
343
|
+
unknown,
|
|
344
|
+
[],
|
|
345
|
+
`unknown v4 release topology workflows: ${unknown.join(", ")}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function assertAuthorityClosure(ledger) {
|
|
350
|
+
const closure = ledger.authorityClosure;
|
|
351
|
+
assert.ok(
|
|
352
|
+
closure && typeof closure === "object",
|
|
353
|
+
"authority closure missing",
|
|
354
|
+
);
|
|
355
|
+
for (const className of [
|
|
356
|
+
"providerAdapters",
|
|
357
|
+
"runtimeSelectors",
|
|
358
|
+
"runtimeEngines",
|
|
359
|
+
"terminalProjections",
|
|
360
|
+
"legacyEngineModules",
|
|
361
|
+
]) {
|
|
362
|
+
assert.ok(
|
|
363
|
+
Array.isArray(closure[className]) && closure[className].length > 0,
|
|
364
|
+
`authority closure class ${className} is empty`,
|
|
365
|
+
);
|
|
366
|
+
for (const relative of closure[className])
|
|
367
|
+
assert.ok(
|
|
368
|
+
fs.statSync(path.join(root, relative)).isFile(),
|
|
369
|
+
`authority closure path is not a file: ${relative}`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const discovered = discoverV4ReleaseAuthorityClosure();
|
|
373
|
+
for (const className of Object.keys(discovered))
|
|
374
|
+
assert.deepEqual(
|
|
375
|
+
closure[className],
|
|
376
|
+
discovered[className],
|
|
377
|
+
`authority closure class drifted: ${className}`,
|
|
378
|
+
);
|
|
379
|
+
assert.deepEqual(
|
|
380
|
+
closure.privilegedExecutableClosure,
|
|
381
|
+
discovered.privilegedExecutableClosure,
|
|
382
|
+
"privileged executable transitive closure drifted",
|
|
383
|
+
);
|
|
384
|
+
assert.match(
|
|
385
|
+
closure.privilegedExecutableClosure.root,
|
|
386
|
+
/^sha256:[0-9a-f]{64}$/u,
|
|
387
|
+
);
|
|
388
|
+
const reachableLegacyEngines = closure.legacyEngineModules.filter(
|
|
389
|
+
(relative) =>
|
|
390
|
+
closure.privilegedExecutableClosure.modules.includes(relative),
|
|
391
|
+
);
|
|
392
|
+
assert.deepEqual(
|
|
393
|
+
reachableLegacyEngines,
|
|
394
|
+
[],
|
|
395
|
+
`legacy release engines remain reachable from APPLY: ${reachableLegacyEngines.join(", ")}`,
|
|
396
|
+
);
|
|
397
|
+
assert.deepEqual(closure.runtimeEngines, [
|
|
398
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
399
|
+
]);
|
|
400
|
+
assert.equal(
|
|
401
|
+
closure.freshEntry,
|
|
402
|
+
".github/workflows/release-candidate-promote.yml",
|
|
403
|
+
);
|
|
404
|
+
assert.equal(
|
|
405
|
+
closure.recoveryEntry,
|
|
406
|
+
".github/workflows/buildchain-ref-promotion-recovery.yml",
|
|
407
|
+
);
|
|
408
|
+
const engineSurface = [
|
|
409
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
410
|
+
...closure.privilegedExecutableClosure.modules,
|
|
411
|
+
]
|
|
412
|
+
.map(read)
|
|
413
|
+
.join("\n");
|
|
414
|
+
for (const pattern of closure.forbiddenLegacyEnginePatterns)
|
|
415
|
+
assert.doesNotMatch(
|
|
416
|
+
engineSurface,
|
|
417
|
+
new RegExp(pattern, "u"),
|
|
418
|
+
`legacy release engine remains reachable: ${pattern}`,
|
|
419
|
+
);
|
|
420
|
+
const canonicalWorkflow = read(
|
|
421
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
422
|
+
);
|
|
423
|
+
assert.match(
|
|
424
|
+
canonicalWorkflow,
|
|
425
|
+
/scripts\/v4-release-candidate-adapter\.mjs/u,
|
|
426
|
+
);
|
|
427
|
+
assert.match(
|
|
428
|
+
canonicalWorkflow,
|
|
429
|
+
/release-invocation\.json[\s\S]*release-transaction\.json[\s\S]*release-receipt\.json/u,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function checkV4ReleaseTopology() {
|
|
434
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
435
|
+
assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
|
|
436
|
+
assertClosedWorld(ledger.closedWorld.workflowPaths);
|
|
437
|
+
assertAuthorityClosure(ledger);
|
|
438
|
+
const actual = discoverV4ReleaseTopology(
|
|
439
|
+
ledger.closedWorld.workflowPaths,
|
|
440
|
+
ledger.semanticScope.workflowPaths,
|
|
441
|
+
);
|
|
442
|
+
assert.deepEqual(actual, ledger.observedTopology);
|
|
443
|
+
assert.equal(
|
|
444
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
445
|
+
Math.floor(ledger.baselineMetrics.semanticMutationRelevantNodeCount / 2),
|
|
446
|
+
);
|
|
447
|
+
if (ledger.enforcement === "converged") {
|
|
448
|
+
assert.ok(
|
|
449
|
+
actual.semanticMetrics.mutationRelevantNodeCount <=
|
|
450
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
451
|
+
);
|
|
452
|
+
assert.equal(actual.semanticMetrics.contentsWriteJobCount, 1);
|
|
453
|
+
assert.equal(actual.semanticMetrics.oidcWriteJobCount, 1);
|
|
454
|
+
const publisher = actual.workflows.find(
|
|
455
|
+
({ path: workflowPath }) =>
|
|
456
|
+
workflowPath === ".github/workflows/.release-candidate-promote.yml",
|
|
457
|
+
);
|
|
458
|
+
assert.deepEqual(
|
|
459
|
+
publisher.jobs.map((job) => job.split("|", 1)[0]),
|
|
460
|
+
["apply", "qualify", "settle"],
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return actual;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (
|
|
467
|
+
process.argv[1] &&
|
|
468
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
469
|
+
) {
|
|
470
|
+
if (process.argv.includes("--print-closure")) {
|
|
471
|
+
process.stdout.write(
|
|
472
|
+
`${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
|
|
473
|
+
);
|
|
474
|
+
} else if (process.argv.includes("--print")) {
|
|
475
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
476
|
+
process.stdout.write(
|
|
477
|
+
`${JSON.stringify(
|
|
478
|
+
discoverV4ReleaseTopology(
|
|
479
|
+
ledger.closedWorld.workflowPaths,
|
|
480
|
+
ledger.semanticScope.workflowPaths,
|
|
481
|
+
),
|
|
482
|
+
null,
|
|
483
|
+
2,
|
|
484
|
+
)}\n`,
|
|
485
|
+
);
|
|
486
|
+
} else {
|
|
487
|
+
checkV4ReleaseTopology();
|
|
488
|
+
process.stdout.write("v4 release topology: ok\n");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -33,11 +33,8 @@ function terminalEvidenceOptions(rest, environment) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
const [command = "", ...rest] = args;
|
|
36
|
+
function authorityStateOptions(rest, environment) {
|
|
38
37
|
return {
|
|
39
|
-
command,
|
|
40
|
-
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
41
38
|
branch: flag(
|
|
42
39
|
rest,
|
|
43
40
|
"branch",
|
|
@@ -53,6 +50,20 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
53
50
|
"expected-old",
|
|
54
51
|
environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_OLD,
|
|
55
52
|
),
|
|
53
|
+
legacyTerminalRecoveryPath: flag(
|
|
54
|
+
rest,
|
|
55
|
+
"legacy-terminal-recovery",
|
|
56
|
+
environment.BUILDCHAIN_DEV_DELIVERY_LEGACY_TERMINAL_RECOVERY,
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
62
|
+
const [command = "", ...rest] = args;
|
|
63
|
+
return {
|
|
64
|
+
command,
|
|
65
|
+
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
66
|
+
...authorityStateOptions(rest, environment),
|
|
56
67
|
pullRequestNumber: flag(
|
|
57
68
|
rest,
|
|
58
69
|
"pull-request",
|