@kungfu-tech/buildchain 4.0.2-alpha.2 → 4.0.2-alpha.20
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 +52 -1
- package/architecture/ci-lane-change-budget.json +349 -0
- package/architecture/internal-capabilities.json +44 -1
- package/architecture/maintainability-debt.json +53 -33
- package/architecture/maintainability-policy.json +20 -15
- package/architecture/v3-core-mechanism-inventory.json +1 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
- package/architecture/v4-release-invocation-fixtures.json +20 -0
- package/architecture/v4-release-topology.json +160 -84
- package/architecture/v4-runtime-semantic-closure.json +4 -1
- package/architecture/v4-universal-workflow-bootstrap.json +171 -0
- package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
- package/architecture/v4-universal-workflow-train-admission.json +40 -0
- package/contracts/buildchain-v2-residuals-v1.json +0 -54
- package/contracts/v4-release-invocation-v1.schema.json +50 -2
- package/dist/site/buildchain-contract.json +87 -42
- package/dist/site/buildchain-site.json +15 -10
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/controller-registry.json +46 -10
- package/dist/site/kfd-claims.json +174 -196
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +95 -20
- package/dist/site/page-registry.json +9 -4
- package/dist/site/public-surface-audit.json +178 -198
- package/dist/site/publication-authority-registry.json +67 -57
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/dist/site/workflow-registry.json +209 -259
- package/docs/node-api-reference.md +25 -22
- package/docs/reusable-build-surface.md +16 -0
- package/package.json +2 -2
- package/packages/core/buildchain-publication-authority.js +3 -2
- package/packages/core/ci-lane-change-budget.js +14 -1
- package/packages/core/dev-delivery-candidate-identity.js +18 -0
- package/packages/core/dev-delivery-execution-transfer.js +6 -7
- 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 +42 -26
- 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 +3 -3
- package/packages/core/v4-canonical-contracts.js +6 -0
- 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 +72 -3
- package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
- package/scripts/check-inventory.mjs +13 -22
- package/scripts/check-maintainability.mjs +16 -113
- package/scripts/check-v4-release-topology.mjs +296 -40
- package/scripts/check-v4-universal-workflow-bootstrap.mjs +265 -0
- package/scripts/dev-delivery-warrant-store.mjs +73 -7
- package/scripts/dev-delivery-warrant-transition.mjs +109 -0
- package/scripts/dev-delivery-warrant.mjs +84 -100
- package/scripts/dev-pr-auto-merge.mjs +15 -15
- package/scripts/generate-channel-build-workflow.mjs +5 -1
- package/scripts/generate-channel-promotion-workflow.mjs +5 -3
- package/scripts/generate-v4-universal-workflow-facades.mjs +415 -0
- package/scripts/maintainability-public-surface.mjs +115 -0
- package/scripts/release-candidate-resolver.mjs +23 -24
- package/scripts/resume-from-candidate-run.mjs +16 -17
- package/scripts/universal-facade-maintainability.mjs +82 -0
- package/scripts/v4-product-publication-intent.mjs +114 -0
- package/scripts/v4-release-candidate-adapter.mjs +41 -0
- package/scripts/v4-universal-workflow-backflow.mjs +212 -0
- package/scripts/v4-universal-workflow-engine.mjs +617 -0
- package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
- package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
- package/templates/universal-buildchain-bootstrap.yml +28 -0
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
evaluateTestBudgets,
|
|
20
20
|
evaluateWorkflowBudgets,
|
|
21
21
|
} from "./maintainability-governance.mjs";
|
|
22
|
+
import { governUniversalFacadeWorkflowMetrics } from "./universal-facade-maintainability.mjs";
|
|
23
|
+
import { evaluatePublicSurface } from "./maintainability-public-surface.mjs";
|
|
22
24
|
|
|
23
25
|
function collectHotspots(
|
|
24
26
|
root,
|
|
@@ -162,116 +164,6 @@ function selectedFunction(metrics, file, name) {
|
|
|
162
164
|
return metrics.files[file]?.functions.find((entry) => entry.name === name);
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
function readJsonAtRevision(root, revision, file) {
|
|
166
|
-
return JSON.parse(gitOutput(root, ["show", `${revision}:${file}`]));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function publicSurfaceContract(entry, kind) {
|
|
170
|
-
if (kind === "cli") return { id: entry.id, usage: entry.usage };
|
|
171
|
-
if (kind === "node")
|
|
172
|
-
return {
|
|
173
|
-
export: entry.export,
|
|
174
|
-
specifier: entry.specifier,
|
|
175
|
-
target: entry.target,
|
|
176
|
-
};
|
|
177
|
-
return {
|
|
178
|
-
id: entry.id,
|
|
179
|
-
path: entry.path,
|
|
180
|
-
reusable: entry.reusable,
|
|
181
|
-
inputs: entry.inputs || [],
|
|
182
|
-
secrets: entry.secrets || [],
|
|
183
|
-
outputs: entry.outputs || [],
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function evaluatePublicSurface({ root, revision, policy }) {
|
|
188
|
-
const issues = [];
|
|
189
|
-
const lifecycleFields = policy.publicSurfacePolicy.requiredLifecycleFields;
|
|
190
|
-
const capabilityGroups = new Set(
|
|
191
|
-
readJson(root, "dist/site/capability-registry.json").groups.map(
|
|
192
|
-
(entry) => entry.id,
|
|
193
|
-
),
|
|
194
|
-
);
|
|
195
|
-
const definitions = [
|
|
196
|
-
{
|
|
197
|
-
file: "dist/site/cli-registry.json",
|
|
198
|
-
collection: "commands",
|
|
199
|
-
kind: "cli",
|
|
200
|
-
key: "id",
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
file: "dist/site/node-api-registry.json",
|
|
204
|
-
collection: "exports",
|
|
205
|
-
kind: "node",
|
|
206
|
-
key: "export",
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
file: "dist/site/workflow-registry.json",
|
|
210
|
-
collection: "workflows",
|
|
211
|
-
kind: "workflow",
|
|
212
|
-
key: "id",
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
file: "dist/site/workflow-registry.json",
|
|
216
|
-
collection: "actions",
|
|
217
|
-
kind: "action",
|
|
218
|
-
key: "id",
|
|
219
|
-
},
|
|
220
|
-
];
|
|
221
|
-
for (const definition of definitions) {
|
|
222
|
-
const current =
|
|
223
|
-
readJson(root, definition.file)[definition.collection] || [];
|
|
224
|
-
const baseline =
|
|
225
|
-
readJsonAtRevision(root, revision, definition.file)[
|
|
226
|
-
definition.collection
|
|
227
|
-
] || [];
|
|
228
|
-
const baselineByKey = new Map(
|
|
229
|
-
baseline.map((entry) => [entry[definition.key], entry]),
|
|
230
|
-
);
|
|
231
|
-
for (const entry of current) {
|
|
232
|
-
const label = `${definition.kind}:${entry[definition.key]}`;
|
|
233
|
-
for (const field of lifecycleFields) {
|
|
234
|
-
if (
|
|
235
|
-
!Object.prototype.hasOwnProperty.call(entry, field) ||
|
|
236
|
-
typeof entry[field] !== "string"
|
|
237
|
-
) {
|
|
238
|
-
issues.push(`${label}: lifecycle field ${field} is missing`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (!capabilityGroups.has(entry.capabilityGroup)) {
|
|
242
|
-
issues.push(
|
|
243
|
-
`${label}: capability group ${entry.capabilityGroup || "<empty>"} is not registered`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
const previous = baselineByKey.get(entry[definition.key]);
|
|
247
|
-
const currentContract = publicSurfaceContract(entry, definition.kind);
|
|
248
|
-
if (
|
|
249
|
-
previous &&
|
|
250
|
-
JSON.stringify(currentContract) !==
|
|
251
|
-
JSON.stringify(publicSurfaceContract(previous, definition.kind))
|
|
252
|
-
) {
|
|
253
|
-
const approval = policy.approvedPublicSurfaceTransitions?.[label];
|
|
254
|
-
const approved =
|
|
255
|
-
approval?.fromRevision === revision &&
|
|
256
|
-
String(approval?.rationale || "").trim() &&
|
|
257
|
-
JSON.stringify(approval?.contract) ===
|
|
258
|
-
JSON.stringify(currentContract);
|
|
259
|
-
if (!approved) {
|
|
260
|
-
issues.push(
|
|
261
|
-
`${label}: existing public contract drifted from ${revision}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (!previous && !entry.nonDuplicationRationale) {
|
|
266
|
-
issues.push(
|
|
267
|
-
`${label}: new public surface requires a non-duplication rationale`,
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return issues;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
167
|
function evaluateAddedFunctionBudgets({
|
|
276
168
|
file,
|
|
277
169
|
metrics,
|
|
@@ -497,12 +389,18 @@ function checkMaintainability({ root = process.cwd() } = {}) {
|
|
|
497
389
|
root,
|
|
498
390
|
extendedCoverageRevision,
|
|
499
391
|
);
|
|
392
|
+
const { governed: governedCurrent, migration: facadeMigration } =
|
|
393
|
+
governUniversalFacadeWorkflowMetrics({
|
|
394
|
+
root,
|
|
395
|
+
current,
|
|
396
|
+
workflowMetricsAtRevision,
|
|
397
|
+
});
|
|
500
398
|
const issues = evaluateExceptionGovernance({ policy });
|
|
501
399
|
issues.push(...evaluateExceptionBudget({ policy }));
|
|
502
400
|
const hotspots = collectHotspots(root, current, 20, debt.hotspots || []);
|
|
503
401
|
issues.push(
|
|
504
402
|
...evaluateDebtAuthority({
|
|
505
|
-
current,
|
|
403
|
+
current: governedCurrent,
|
|
506
404
|
policy,
|
|
507
405
|
debt,
|
|
508
406
|
capabilityIds: new Set(
|
|
@@ -518,14 +416,19 @@ function checkMaintainability({ root = process.cwd() } = {}) {
|
|
|
518
416
|
);
|
|
519
417
|
issues.push(
|
|
520
418
|
...evaluateWorkflowBudgets({
|
|
521
|
-
current,
|
|
419
|
+
current: governedCurrent,
|
|
522
420
|
baselineFiles: baselineWorkflows,
|
|
523
421
|
policy,
|
|
524
422
|
}),
|
|
525
423
|
);
|
|
526
424
|
issues.push(...evaluateRepositoryBudgets({ current, policy }));
|
|
527
425
|
issues.push(
|
|
528
|
-
...evaluatePublicSurface({
|
|
426
|
+
...evaluatePublicSurface({
|
|
427
|
+
root,
|
|
428
|
+
revision: enforcementRevision,
|
|
429
|
+
policy,
|
|
430
|
+
migration: facadeMigration,
|
|
431
|
+
}),
|
|
529
432
|
);
|
|
530
433
|
if (issues.length > 0) {
|
|
531
434
|
throw new Error(`maintainability check failed:\n- ${issues.join("\n- ")}`);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
parseWorkflowDocument,
|
|
@@ -12,13 +13,150 @@ import {
|
|
|
12
13
|
|
|
13
14
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
15
|
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
16
|
|
|
18
17
|
function read(relative) {
|
|
19
18
|
return fs.readFileSync(path.join(root, relative), "utf8");
|
|
20
19
|
}
|
|
21
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
|
+
|
|
22
160
|
function jobBlock(source, jobId) {
|
|
23
161
|
const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
24
162
|
return (
|
|
@@ -42,7 +180,9 @@ function permission(block, name) {
|
|
|
42
180
|
function workflowSnapshot(relative) {
|
|
43
181
|
const source = read(relative);
|
|
44
182
|
const document = parseWorkflowDocument(source);
|
|
45
|
-
const jobs = document.jobs
|
|
183
|
+
const jobs = document.jobs
|
|
184
|
+
.filter((job) => job.id !== "universal-bootstrap")
|
|
185
|
+
.map((job) => {
|
|
46
186
|
const block = jobBlock(source, job.id);
|
|
47
187
|
const uses = job.uses || null;
|
|
48
188
|
return {
|
|
@@ -68,7 +208,7 @@ function workflowSnapshot(relative) {
|
|
|
68
208
|
"direct-publication-command",
|
|
69
209
|
].filter(Boolean),
|
|
70
210
|
};
|
|
71
|
-
|
|
211
|
+
});
|
|
72
212
|
return {
|
|
73
213
|
path: relative,
|
|
74
214
|
triggers: document.triggers,
|
|
@@ -76,6 +216,7 @@ function workflowSnapshot(relative) {
|
|
|
76
216
|
reusableEdges: parseYamlUses(source)
|
|
77
217
|
.map(({ value }) => value)
|
|
78
218
|
.filter((value) => /\.github\/workflows\//u.test(value))
|
|
219
|
+
.filter((value) => !/\/bootstrap\.yml(?:@v4)?$/u.test(value))
|
|
79
220
|
.sort(),
|
|
80
221
|
};
|
|
81
222
|
}
|
|
@@ -104,25 +245,27 @@ export function discoverV4ReleaseTopology(
|
|
|
104
245
|
"carriers",
|
|
105
246
|
"mutationSignals",
|
|
106
247
|
],
|
|
107
|
-
workflows: workflows
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
248
|
+
workflows: workflows
|
|
249
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
250
|
+
.map((workflow) => ({
|
|
251
|
+
path: workflow.path,
|
|
252
|
+
triggers: workflow.triggers,
|
|
253
|
+
jobs: workflow.jobs.map((job) =>
|
|
254
|
+
[
|
|
255
|
+
job.id,
|
|
256
|
+
job.kind,
|
|
257
|
+
job.uses || "-",
|
|
258
|
+
job.permissions.contents || "-",
|
|
259
|
+
job.permissions.idToken || "-",
|
|
260
|
+
Object.entries(job.carriers)
|
|
261
|
+
.filter(([, present]) => present)
|
|
262
|
+
.map(([name]) => name)
|
|
263
|
+
.join(",") || "-",
|
|
264
|
+
job.mutationSignals.join(",") || "-",
|
|
265
|
+
].join("|"),
|
|
266
|
+
),
|
|
267
|
+
reusableEdges: workflow.reusableEdges,
|
|
268
|
+
})),
|
|
126
269
|
metrics: {
|
|
127
270
|
workflowCount: workflows.length,
|
|
128
271
|
jobCount: jobs.length,
|
|
@@ -174,7 +317,26 @@ export function findUnknownV4ReleaseTopology(
|
|
|
174
317
|
const declared = new Set(workflowPaths);
|
|
175
318
|
return allWorkflowPaths
|
|
176
319
|
.filter((relative) => !declared.has(relative))
|
|
177
|
-
.filter((relative) =>
|
|
320
|
+
.filter((relative) => {
|
|
321
|
+
const source = readWorkflow(relative);
|
|
322
|
+
const usesReleaseAuthority = parseYamlUses(source).some(({ value }) =>
|
|
323
|
+
/(?:release-candidate-promote|promote-buildchain-ref|v4-release-candidate-promote|release-tail)/u.test(
|
|
324
|
+
value,
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
const hasReleaseLanguage =
|
|
328
|
+
/(?:^|[-_ ])(?:release|publish|promotion|distribution|tag)(?:$|[-_ :])/imu.test(
|
|
329
|
+
source,
|
|
330
|
+
);
|
|
331
|
+
const hasMutationAuthority =
|
|
332
|
+
/(?:contents|id-token):\s*write/u.test(source) ||
|
|
333
|
+
/(?:git push|npm publish|gh release (?:create|upload)|createRef|updateRef)/u.test(
|
|
334
|
+
source,
|
|
335
|
+
);
|
|
336
|
+
return (
|
|
337
|
+
usesReleaseAuthority || (hasReleaseLanguage && hasMutationAuthority)
|
|
338
|
+
);
|
|
339
|
+
})
|
|
178
340
|
.sort();
|
|
179
341
|
}
|
|
180
342
|
|
|
@@ -187,10 +349,95 @@ function assertClosedWorld(workflowPaths) {
|
|
|
187
349
|
);
|
|
188
350
|
}
|
|
189
351
|
|
|
352
|
+
function assertAuthorityClosure(ledger) {
|
|
353
|
+
const closure = ledger.authorityClosure;
|
|
354
|
+
assert.ok(
|
|
355
|
+
closure && typeof closure === "object",
|
|
356
|
+
"authority closure missing",
|
|
357
|
+
);
|
|
358
|
+
for (const className of [
|
|
359
|
+
"providerAdapters",
|
|
360
|
+
"runtimeSelectors",
|
|
361
|
+
"runtimeEngines",
|
|
362
|
+
"terminalProjections",
|
|
363
|
+
"legacyEngineModules",
|
|
364
|
+
]) {
|
|
365
|
+
assert.ok(
|
|
366
|
+
Array.isArray(closure[className]) && closure[className].length > 0,
|
|
367
|
+
`authority closure class ${className} is empty`,
|
|
368
|
+
);
|
|
369
|
+
for (const relative of closure[className])
|
|
370
|
+
assert.ok(
|
|
371
|
+
fs.statSync(path.join(root, relative)).isFile(),
|
|
372
|
+
`authority closure path is not a file: ${relative}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const discovered = discoverV4ReleaseAuthorityClosure();
|
|
376
|
+
for (const className of Object.keys(discovered))
|
|
377
|
+
assert.deepEqual(
|
|
378
|
+
closure[className],
|
|
379
|
+
discovered[className],
|
|
380
|
+
`authority closure class drifted: ${className}`,
|
|
381
|
+
);
|
|
382
|
+
assert.deepEqual(
|
|
383
|
+
closure.privilegedExecutableClosure,
|
|
384
|
+
discovered.privilegedExecutableClosure,
|
|
385
|
+
"privileged executable transitive closure drifted",
|
|
386
|
+
);
|
|
387
|
+
assert.match(
|
|
388
|
+
closure.privilegedExecutableClosure.root,
|
|
389
|
+
/^sha256:[0-9a-f]{64}$/u,
|
|
390
|
+
);
|
|
391
|
+
const reachableLegacyEngines = closure.legacyEngineModules.filter(
|
|
392
|
+
(relative) =>
|
|
393
|
+
closure.privilegedExecutableClosure.modules.includes(relative),
|
|
394
|
+
);
|
|
395
|
+
assert.deepEqual(
|
|
396
|
+
reachableLegacyEngines,
|
|
397
|
+
[],
|
|
398
|
+
`legacy release engines remain reachable from APPLY: ${reachableLegacyEngines.join(", ")}`,
|
|
399
|
+
);
|
|
400
|
+
assert.deepEqual(closure.runtimeEngines, [
|
|
401
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
402
|
+
]);
|
|
403
|
+
assert.equal(
|
|
404
|
+
closure.freshEntry,
|
|
405
|
+
".github/workflows/release-candidate-promote.yml",
|
|
406
|
+
);
|
|
407
|
+
assert.equal(
|
|
408
|
+
closure.recoveryEntry,
|
|
409
|
+
".github/workflows/buildchain-ref-promotion-recovery.yml",
|
|
410
|
+
);
|
|
411
|
+
const engineSurface = [
|
|
412
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
413
|
+
...closure.privilegedExecutableClosure.modules,
|
|
414
|
+
]
|
|
415
|
+
.map(read)
|
|
416
|
+
.join("\n");
|
|
417
|
+
for (const pattern of closure.forbiddenLegacyEnginePatterns)
|
|
418
|
+
assert.doesNotMatch(
|
|
419
|
+
engineSurface,
|
|
420
|
+
new RegExp(pattern, "u"),
|
|
421
|
+
`legacy release engine remains reachable: ${pattern}`,
|
|
422
|
+
);
|
|
423
|
+
const canonicalWorkflow = read(
|
|
424
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
425
|
+
);
|
|
426
|
+
assert.match(
|
|
427
|
+
canonicalWorkflow,
|
|
428
|
+
/scripts\/v4-release-candidate-adapter\.mjs/u,
|
|
429
|
+
);
|
|
430
|
+
assert.match(
|
|
431
|
+
canonicalWorkflow,
|
|
432
|
+
/release-invocation\.json[\s\S]*release-transaction\.json[\s\S]*release-receipt\.json/u,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
190
436
|
export function checkV4ReleaseTopology() {
|
|
191
437
|
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
192
438
|
assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
|
|
193
439
|
assertClosedWorld(ledger.closedWorld.workflowPaths);
|
|
440
|
+
assertAuthorityClosure(ledger);
|
|
194
441
|
const actual = discoverV4ReleaseTopology(
|
|
195
442
|
ledger.closedWorld.workflowPaths,
|
|
196
443
|
ledger.semanticScope.workflowPaths,
|
|
@@ -219,19 +466,28 @@ export function checkV4ReleaseTopology() {
|
|
|
219
466
|
return actual;
|
|
220
467
|
}
|
|
221
468
|
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
process.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
469
|
+
if (
|
|
470
|
+
process.argv[1] &&
|
|
471
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
472
|
+
) {
|
|
473
|
+
if (process.argv.includes("--print-closure")) {
|
|
474
|
+
process.stdout.write(
|
|
475
|
+
`${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
|
|
476
|
+
);
|
|
477
|
+
} else if (process.argv.includes("--print")) {
|
|
478
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
479
|
+
process.stdout.write(
|
|
480
|
+
`${JSON.stringify(
|
|
481
|
+
discoverV4ReleaseTopology(
|
|
482
|
+
ledger.closedWorld.workflowPaths,
|
|
483
|
+
ledger.semanticScope.workflowPaths,
|
|
484
|
+
),
|
|
485
|
+
null,
|
|
486
|
+
2,
|
|
487
|
+
)}\n`,
|
|
488
|
+
);
|
|
489
|
+
} else {
|
|
490
|
+
checkV4ReleaseTopology();
|
|
491
|
+
process.stdout.write("v4 release topology: ok\n");
|
|
492
|
+
}
|
|
237
493
|
}
|