@kungfu-tech/buildchain 4.0.2-alpha.47 → 4.0.2-alpha.49
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/ci-lane-change-budget.json +18 -0
- package/architecture/maintainability-policy.json +4 -4
- package/architecture/v4-release-topology.json +56 -7
- package/architecture/v4-universal-workflow-bootstrap.json +6 -1
- package/architecture/v4-universal-workflow-train-admission.json +1 -1
- package/architecture/workflow-taxonomy.json +11 -0
- package/dist/site/buildchain-contract.json +5 -5
- package/dist/site/buildchain-site.json +21 -11
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/kfd-claims.json +21 -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 +9 -9
- package/dist/site/page-registry.json +16 -6
- package/dist/site/public-surface-audit.json +18 -3
- package/dist/site/publication-authority-registry.json +23 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +39 -5
- package/docs/node-api-reference.md +10 -10
- package/docs/oci-publication.md +75 -1
- package/docs/workflow-catalog.md +1 -0
- package/package.json +1 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/oci-compose-qualification.js +140 -0
- package/packages/core/oci-publication-bundle.js +59 -25
- package/packages/core/oci-publication-graph.js +206 -0
- package/scripts/check-v4-release-topology.mjs +12 -0
- package/scripts/generate-channel-promotion-workflow.mjs +1 -1
- package/scripts/generate-v4-universal-workflow-facades.mjs +53 -20
- package/scripts/oci-compose-preview.mjs +250 -0
- package/scripts/publication-candidate-kind.mjs +2 -1
- package/scripts/universal-facade-maintainability.mjs +10 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { verifyPublicationSettlement } from "./v4-publication-settlement.mjs";
|
|
7
|
+
import {
|
|
8
|
+
verifyComposePublication,
|
|
9
|
+
verifyComposeQualification,
|
|
10
|
+
} from "../packages/core/oci-compose-qualification.js";
|
|
11
|
+
import { releaseTailRoot } from "../packages/core/release-tail-provider-plane.js";
|
|
12
|
+
import { createRegistryClient } from "../actions/v4-release-candidate-promote/oci-registry-client.js";
|
|
13
|
+
import { ociPublicationTag } from "../packages/core/oci-publication-graph.js";
|
|
14
|
+
|
|
15
|
+
const digest = (bytes) =>
|
|
16
|
+
`sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
17
|
+
function check(condition, message) {
|
|
18
|
+
if (!condition) throw new Error(`Compose preview: ${message}`);
|
|
19
|
+
}
|
|
20
|
+
function gh(args) {
|
|
21
|
+
return execFileSync("gh", args, {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const api = (route) => JSON.parse(gh(["api", route]));
|
|
27
|
+
const read = (file) => JSON.parse(fs.readFileSync(file));
|
|
28
|
+
function output(name, value) {
|
|
29
|
+
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function promoteComposePreview({
|
|
33
|
+
context,
|
|
34
|
+
registry,
|
|
35
|
+
receipt,
|
|
36
|
+
fetchManifest,
|
|
37
|
+
}) {
|
|
38
|
+
const { application, policy } = context;
|
|
39
|
+
const exact = await fetchManifest(
|
|
40
|
+
ociPublicationTag(application, context.tag.slice(1)),
|
|
41
|
+
);
|
|
42
|
+
check(
|
|
43
|
+
exact.digest === application.digest,
|
|
44
|
+
"immutable application digest changed",
|
|
45
|
+
);
|
|
46
|
+
const current = await fetchManifest(policy.alias, true);
|
|
47
|
+
check(
|
|
48
|
+
current.digest === policy.previousDigest ||
|
|
49
|
+
current.digest === application.digest,
|
|
50
|
+
"preview changed since qualification",
|
|
51
|
+
);
|
|
52
|
+
const plan = {
|
|
53
|
+
schema: "kungfu-buildchain-compose-preview-plan/v1",
|
|
54
|
+
...context,
|
|
55
|
+
qualificationRoot: releaseTailRoot(receipt),
|
|
56
|
+
expectedOld: policy.previousDigest,
|
|
57
|
+
targetDigest: application.digest,
|
|
58
|
+
operation: "digest-preserving-preview-promotion",
|
|
59
|
+
};
|
|
60
|
+
if (current.digest !== application.digest) {
|
|
61
|
+
const result = await registry(application, `manifests/${policy.alias}`, {
|
|
62
|
+
write: true,
|
|
63
|
+
method: "PUT",
|
|
64
|
+
body: exact.bytes,
|
|
65
|
+
headers: {
|
|
66
|
+
"content-type": exact.mediaType,
|
|
67
|
+
"content-length": String(exact.bytes.length),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
check(
|
|
71
|
+
result.status === 201,
|
|
72
|
+
"preview write uncertain; retain the original qualification for readback",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const observed = await fetchManifest(policy.alias);
|
|
76
|
+
check(observed.digest === application.digest, "preview readback mismatch");
|
|
77
|
+
const body = {
|
|
78
|
+
schema: "kungfu-buildchain-compose-preview-receipt/v1",
|
|
79
|
+
repository: context.repository,
|
|
80
|
+
tag: context.tag,
|
|
81
|
+
sourceSha: context.sourceSha,
|
|
82
|
+
publicationReceiptRoot: context.publicationReceiptRoot,
|
|
83
|
+
planRoot: releaseTailRoot(plan),
|
|
84
|
+
qualificationRoot: plan.qualificationRoot,
|
|
85
|
+
previousDigest: policy.previousDigest,
|
|
86
|
+
application: `${application.repository}@${application.digest}`,
|
|
87
|
+
alias: `${application.repository}:${policy.alias}`,
|
|
88
|
+
observedDigest: observed.digest,
|
|
89
|
+
outcome: "complete",
|
|
90
|
+
};
|
|
91
|
+
return { ...body, receiptRoot: releaseTailRoot(body) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function run(mode) {
|
|
95
|
+
const repository = process.env.GITHUB_REPOSITORY,
|
|
96
|
+
event = read(process.env.GITHUB_EVENT_PATH);
|
|
97
|
+
check(
|
|
98
|
+
process.env.GITHUB_EVENT_NAME === "workflow_run" && event.workflow_run?.id,
|
|
99
|
+
"requires a completed qualification workflow event",
|
|
100
|
+
);
|
|
101
|
+
check(
|
|
102
|
+
/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/u.test(repository),
|
|
103
|
+
"invalid repository",
|
|
104
|
+
);
|
|
105
|
+
const id = String(event.workflow_run.id);
|
|
106
|
+
check(/^\d+$/u.test(id), "invalid qualification run");
|
|
107
|
+
const run = api(`repos/${repository}/actions/runs/${id}`),
|
|
108
|
+
tag = run.head_branch;
|
|
109
|
+
check(
|
|
110
|
+
/^v\d+\.\d+\.\d+-alpha\.\d+$/u.test(tag),
|
|
111
|
+
"qualification must run on an exact alpha tag",
|
|
112
|
+
);
|
|
113
|
+
const release = api(`repos/${repository}/releases/tags/${tag}`);
|
|
114
|
+
check(release.prerelease && !release.draft, "public alpha release required");
|
|
115
|
+
const root = path.resolve(".buildchain/compose-preview");
|
|
116
|
+
fs.mkdirSync(path.join(root, "public"), { recursive: true });
|
|
117
|
+
const names = [
|
|
118
|
+
"oci-family.json",
|
|
119
|
+
"oci-publication-readback.json",
|
|
120
|
+
"buildchain-publication-settlement.json",
|
|
121
|
+
"buildchain.release.json",
|
|
122
|
+
];
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
check(
|
|
125
|
+
release.assets.filter((asset) => asset.name === name).length === 1,
|
|
126
|
+
`missing exact public asset ${name}`,
|
|
127
|
+
);
|
|
128
|
+
gh([
|
|
129
|
+
"release",
|
|
130
|
+
"download",
|
|
131
|
+
tag,
|
|
132
|
+
"--repo",
|
|
133
|
+
repository,
|
|
134
|
+
"--pattern",
|
|
135
|
+
name,
|
|
136
|
+
"--dir",
|
|
137
|
+
path.join(root, "public"),
|
|
138
|
+
"--clobber",
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
const [family, readback, settlement, publicPassport] = names.map((name) =>
|
|
142
|
+
read(path.join(root, "public", name)),
|
|
143
|
+
);
|
|
144
|
+
const documents = settlement.documents;
|
|
145
|
+
const tagged = api(`repos/${repository}/commits/${tag}`).sha;
|
|
146
|
+
verifyPublicationSettlement(documents, {
|
|
147
|
+
repository,
|
|
148
|
+
tag,
|
|
149
|
+
sourceSha: tagged,
|
|
150
|
+
publicPassport,
|
|
151
|
+
});
|
|
152
|
+
const context = verifyComposePublication({
|
|
153
|
+
family,
|
|
154
|
+
readback,
|
|
155
|
+
documents,
|
|
156
|
+
repository,
|
|
157
|
+
tag,
|
|
158
|
+
});
|
|
159
|
+
verifyComposeQualification(context, run);
|
|
160
|
+
const artifactName = `oci-compose-qualification-${id}-${run.run_attempt}`;
|
|
161
|
+
const artifacts = api(
|
|
162
|
+
`repos/${repository}/actions/runs/${id}/artifacts?per_page=100`,
|
|
163
|
+
).artifacts.filter(
|
|
164
|
+
(artifact) => artifact.name === artifactName && !artifact.expired,
|
|
165
|
+
);
|
|
166
|
+
check(
|
|
167
|
+
artifacts.length === 1,
|
|
168
|
+
"qualification artifact is missing or ambiguous",
|
|
169
|
+
);
|
|
170
|
+
if (mode === "prepare") {
|
|
171
|
+
output("artifact-id", artifacts[0].id);
|
|
172
|
+
output("run-id", id);
|
|
173
|
+
output("source-sha", tagged);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
check(mode === "apply", "unsupported mode");
|
|
177
|
+
const evidenceRoot = path.join(root, "qualification");
|
|
178
|
+
const receipt = read(path.join(evidenceRoot, "qualification.json"));
|
|
179
|
+
verifyComposeQualification(context, run, receipt);
|
|
180
|
+
for (const item of receipt.evidence) {
|
|
181
|
+
const file = path.join(evidenceRoot, item.path);
|
|
182
|
+
check(
|
|
183
|
+
!fs.lstatSync(file).isSymbolicLink() &&
|
|
184
|
+
fs.statSync(file).size < 2_000_000 &&
|
|
185
|
+
digest(fs.readFileSync(file)) === item.sha256,
|
|
186
|
+
"qualification evidence bytes mismatch",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const actor = api("user").login;
|
|
190
|
+
const { registry } = createRegistryClient(process.env.GH_TOKEN, fetch, actor);
|
|
191
|
+
async function fetchManifest(ref, allowAbsent = false) {
|
|
192
|
+
const response = await registry(context.application, `manifests/${ref}`, {
|
|
193
|
+
headers: {
|
|
194
|
+
accept:
|
|
195
|
+
"application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json",
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
if (allowAbsent && response.status === 404) return { digest: "none" };
|
|
199
|
+
check(response.ok, "public registry readback unavailable");
|
|
200
|
+
const bytes = Buffer.from(await response.arrayBuffer()),
|
|
201
|
+
value = digest(bytes);
|
|
202
|
+
check(
|
|
203
|
+
!response.headers.get("docker-content-digest") ||
|
|
204
|
+
response.headers.get("docker-content-digest") === value,
|
|
205
|
+
"registry digest mismatch",
|
|
206
|
+
);
|
|
207
|
+
return { bytes, digest: value, mediaType: JSON.parse(bytes).mediaType };
|
|
208
|
+
}
|
|
209
|
+
const result = await promoteComposePreview({
|
|
210
|
+
context,
|
|
211
|
+
registry,
|
|
212
|
+
receipt,
|
|
213
|
+
fetchManifest,
|
|
214
|
+
});
|
|
215
|
+
const file = path.join(
|
|
216
|
+
root,
|
|
217
|
+
`buildchain-compose-preview-${id}-${run.run_attempt}.json`,
|
|
218
|
+
);
|
|
219
|
+
fs.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
|
|
220
|
+
const existing = release.assets.find(
|
|
221
|
+
(asset) => asset.name === path.basename(file),
|
|
222
|
+
);
|
|
223
|
+
if (existing) {
|
|
224
|
+
const previous = path.join(root, "previous");
|
|
225
|
+
fs.mkdirSync(previous, { recursive: true });
|
|
226
|
+
gh([
|
|
227
|
+
"release",
|
|
228
|
+
"download",
|
|
229
|
+
tag,
|
|
230
|
+
"--repo",
|
|
231
|
+
repository,
|
|
232
|
+
"--pattern",
|
|
233
|
+
path.basename(file),
|
|
234
|
+
"--dir",
|
|
235
|
+
previous,
|
|
236
|
+
"--clobber",
|
|
237
|
+
]);
|
|
238
|
+
check(
|
|
239
|
+
fs.readFileSync(path.join(previous, path.basename(file)), "utf8") ===
|
|
240
|
+
fs.readFileSync(file, "utf8"),
|
|
241
|
+
"existing preview receipt conflict",
|
|
242
|
+
);
|
|
243
|
+
} else gh(["release", "upload", tag, file, "--repo", repository]);
|
|
244
|
+
process.stdout.write(`Verified ${result.alias}@${result.observedDigest}\n`);
|
|
245
|
+
}
|
|
246
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
247
|
+
run(process.argv[2]).catch((error) => {
|
|
248
|
+
process.stderr.write(`${error.message}\n`);
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { ociPublicationTag } from "../packages/core/oci-publication-graph.js";
|
|
3
4
|
import { verifyOciPublicationBundle } from "../packages/core/oci-publication-bundle.js";
|
|
4
5
|
|
|
5
6
|
export function resolveOciCandidate({ payloadRoot, passport }) {
|
|
@@ -31,7 +32,7 @@ export function resolveOciCandidate({ payloadRoot, passport }) {
|
|
|
31
32
|
requiredArtifacts: manifest.images.map((image) => ({
|
|
32
33
|
kind: "oci",
|
|
33
34
|
name: image.repository,
|
|
34
|
-
ref:
|
|
35
|
+
ref: ociPublicationTag(image, manifest.version),
|
|
35
36
|
digest: image.digest,
|
|
36
37
|
platform: image.platform,
|
|
37
38
|
required: true,
|
|
@@ -20,7 +20,11 @@ function loadUniversalFacadeMigration(root) {
|
|
|
20
20
|
"universal facade migration requires an exact facadeSourceRevision",
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
|
-
return {
|
|
23
|
+
return {
|
|
24
|
+
paths: new Set(paths),
|
|
25
|
+
sourceRevision,
|
|
26
|
+
postMigration: new Set(contract.migration?.postMigrationWorkflows || []),
|
|
27
|
+
};
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
function verifyUniversalFacadeMigration(root) {
|
|
@@ -40,6 +44,11 @@ function governUniversalFacadeWorkflowMetrics({
|
|
|
40
44
|
verifyUniversalFacadeMigration(root);
|
|
41
45
|
const frozen = workflowMetricsAtRevision(root, migration.sourceRevision);
|
|
42
46
|
const governed = { ...current, workflows: { ...current.workflows } };
|
|
47
|
+
for (const file of migration.postMigration) {
|
|
48
|
+
if (frozen[file] || !migration.paths.has(file))
|
|
49
|
+
throw new Error(`invalid post-migration workflow declaration ${file}`);
|
|
50
|
+
migration.paths.delete(file);
|
|
51
|
+
}
|
|
43
52
|
for (const file of migration.paths) {
|
|
44
53
|
if (!frozen[file])
|
|
45
54
|
throw new Error(`frozen universal facade source is missing ${file}`);
|