@kungfu-tech/buildchain 4.0.2-alpha.43 → 4.0.2-alpha.45
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/maintainability-debt.json +18 -19
- package/architecture/maintainability-policy.json +4 -4
- package/architecture/v4-release-topology.json +9 -6
- package/architecture/v4-universal-workflow-train-admission.json +1 -1
- package/dist/site/buildchain-contract.json +7 -7
- package/dist/site/buildchain-site.json +50 -14
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/kfd-claims.json +29 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +154 -13
- package/dist/site/page-registry.json +43 -7
- package/dist/site/public-surface-audit.json +7 -3
- package/dist/site/publication-authority-registry.json +5 -17
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -1
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +5 -5
- package/docs/MAP.md +1 -0
- package/docs/cli.md +3 -0
- package/docs/node-api-reference.md +21 -10
- package/docs/oci-publication.md +82 -0
- package/package.json +3 -2
- package/packages/core/buildchain-v4-domain.wasm +0 -0
- package/packages/core/oci-publication-bundle.js +241 -0
- package/packages/core/paper-agent-entry.js +26 -2
- package/packages/core/paper.js +4 -4
- package/packages/core/release-tail-product-capabilities.js +9 -0
- package/packages/core/v4-canonical-contracts.js +1 -0
- package/packages/core/v4-domain-wasm-artifact.js +2 -2
- package/scripts/check-v4-release-topology.mjs +14 -4
- package/scripts/generate-channel-promotion-workflow.mjs +2 -19
- package/scripts/generate-v4-universal-workflow-facades.mjs +8 -2
- package/scripts/publication-candidate-kind.mjs +92 -0
- package/scripts/publication-candidate-sealer.mjs +9 -0
- package/scripts/release-candidate-resolver.mjs +6 -17
- package/scripts/resume-from-candidate-run.mjs +10 -17
- package/scripts/site-capability-metadata.mjs +1 -0
- package/scripts/v4-product-publication-intent.mjs +1 -1
- package/scripts/v4-universal-workflow-engine.mjs +2 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { v4ContentRoot } from "./v4-canonical-contracts.js";
|
|
5
|
+
|
|
6
|
+
export const OCI_FAMILY_SCHEMA = "kungfu-buildchain-oci-family/v1";
|
|
7
|
+
const digestPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
8
|
+
const namePattern = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u;
|
|
9
|
+
|
|
10
|
+
export function sealOciPublicationBundle({ bundleRoot, body }) {
|
|
11
|
+
const manifest = {
|
|
12
|
+
...body,
|
|
13
|
+
root: v4ContentRoot("oci-publication-family", body),
|
|
14
|
+
};
|
|
15
|
+
verifyOciPublicationBundle({
|
|
16
|
+
bundleRoot,
|
|
17
|
+
manifest,
|
|
18
|
+
repository: body.repository,
|
|
19
|
+
sourceSha: body.sourceSha,
|
|
20
|
+
version: body.version,
|
|
21
|
+
});
|
|
22
|
+
return manifest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function requireValue(condition, message) {
|
|
26
|
+
if (!condition) throw new Error(`OCI bundle: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ociBundleFile(root, relative) {
|
|
30
|
+
requireValue(
|
|
31
|
+
typeof relative === "string" &&
|
|
32
|
+
relative.length > 0 &&
|
|
33
|
+
!relative.includes("\\") &&
|
|
34
|
+
!path.isAbsolute(relative) &&
|
|
35
|
+
!relative.split(/[\\/]/u).some((p) => p === ".." || p === "." || !p),
|
|
36
|
+
"unsafe file path",
|
|
37
|
+
);
|
|
38
|
+
const base = fs.realpathSync(root);
|
|
39
|
+
let current = base;
|
|
40
|
+
for (const segment of relative.split("/")) {
|
|
41
|
+
current = path.join(current, segment);
|
|
42
|
+
requireValue(
|
|
43
|
+
!fs.lstatSync(current).isSymbolicLink(),
|
|
44
|
+
"symlinks are forbidden",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
requireValue(
|
|
48
|
+
fs.statSync(current).isFile() &&
|
|
49
|
+
fs.realpathSync(current).startsWith(`${base}${path.sep}`),
|
|
50
|
+
"file escapes bundle",
|
|
51
|
+
);
|
|
52
|
+
return current;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fileDigest(file) {
|
|
56
|
+
const hash = crypto.createHash("sha256"),
|
|
57
|
+
buffer = Buffer.alloc(1024 * 1024);
|
|
58
|
+
const fd = fs.openSync(file, "r");
|
|
59
|
+
try {
|
|
60
|
+
for (;;) {
|
|
61
|
+
const size = fs.readSync(fd, buffer);
|
|
62
|
+
if (!size) break;
|
|
63
|
+
hash.update(buffer.subarray(0, size));
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
fs.closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
return `sha256:${hash.digest("hex")}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function verifyImageProvenance(image, config, sourceSha, version) {
|
|
72
|
+
const labels = config.config?.Labels || {};
|
|
73
|
+
requireValue(
|
|
74
|
+
image.content &&
|
|
75
|
+
/^[0-9a-f]{40}$/u.test(image.content.sourceSha) &&
|
|
76
|
+
typeof image.content.version === "string" &&
|
|
77
|
+
image.content.version.length > 0,
|
|
78
|
+
"missing image content provenance",
|
|
79
|
+
);
|
|
80
|
+
requireValue(
|
|
81
|
+
labels["org.opencontainers.image.revision"] === image.content.sourceSha &&
|
|
82
|
+
labels["org.opencontainers.image.version"] === image.content.version,
|
|
83
|
+
"image content provenance mismatch",
|
|
84
|
+
);
|
|
85
|
+
if (image.action === "built")
|
|
86
|
+
requireValue(
|
|
87
|
+
image.content.sourceSha === sourceSha &&
|
|
88
|
+
image.content.version === version,
|
|
89
|
+
"built image is not from the candidate source",
|
|
90
|
+
);
|
|
91
|
+
if (image.contractMajor !== undefined)
|
|
92
|
+
requireValue(
|
|
93
|
+
Number.isInteger(image.contractMajor) &&
|
|
94
|
+
image.contractMajor > 0 &&
|
|
95
|
+
labels["io.kungfu.image.contract-major"] ===
|
|
96
|
+
String(image.contractMajor),
|
|
97
|
+
"image contract major mismatch",
|
|
98
|
+
);
|
|
99
|
+
if (image.parentDigest)
|
|
100
|
+
requireValue(
|
|
101
|
+
digestPattern.test(image.parentDigest) &&
|
|
102
|
+
labels["io.kungfu.image.parent-digest"] === image.parentDigest,
|
|
103
|
+
"image parent digest mismatch",
|
|
104
|
+
);
|
|
105
|
+
if (image.content.materialSha)
|
|
106
|
+
requireValue(
|
|
107
|
+
/^[0-9a-f]{40}$/u.test(image.content.materialSha) &&
|
|
108
|
+
labels["io.kungfu.buildchain.release-material-sha"] ===
|
|
109
|
+
image.content.materialSha,
|
|
110
|
+
"image material provenance mismatch",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function verifyOciPublicationBundle({
|
|
115
|
+
bundleRoot,
|
|
116
|
+
manifest,
|
|
117
|
+
repository,
|
|
118
|
+
sourceSha,
|
|
119
|
+
version,
|
|
120
|
+
}) {
|
|
121
|
+
requireValue(
|
|
122
|
+
manifest?.schema === OCI_FAMILY_SCHEMA,
|
|
123
|
+
"unsupported manifest schema",
|
|
124
|
+
);
|
|
125
|
+
const { root, ...body } = manifest;
|
|
126
|
+
const computed = v4ContentRoot("oci-publication-family", body);
|
|
127
|
+
requireValue(root === computed, "manifest root mismatch");
|
|
128
|
+
requireValue(
|
|
129
|
+
manifest.repository === repository &&
|
|
130
|
+
manifest.sourceSha === sourceSha &&
|
|
131
|
+
manifest.version === version,
|
|
132
|
+
"candidate identity mismatch",
|
|
133
|
+
);
|
|
134
|
+
requireValue(
|
|
135
|
+
/^[a-z0-9-]+\/[a-z0-9._-]+$/u.test(repository),
|
|
136
|
+
"invalid repository",
|
|
137
|
+
);
|
|
138
|
+
requireValue(
|
|
139
|
+
Array.isArray(manifest.images) && manifest.images.length > 0,
|
|
140
|
+
"empty image family",
|
|
141
|
+
);
|
|
142
|
+
const expected = manifest.expectedImages;
|
|
143
|
+
requireValue(
|
|
144
|
+
Array.isArray(expected) &&
|
|
145
|
+
expected.length === new Set(expected).size &&
|
|
146
|
+
expected.every((n) => namePattern.test(n)),
|
|
147
|
+
"invalid expected image family",
|
|
148
|
+
);
|
|
149
|
+
const actual = manifest.images.map((i) => i.name).sort();
|
|
150
|
+
requireValue(
|
|
151
|
+
JSON.stringify(actual) === JSON.stringify([...expected].sort()),
|
|
152
|
+
"incomplete or duplicate image family",
|
|
153
|
+
);
|
|
154
|
+
const files = new Map();
|
|
155
|
+
for (const image of manifest.images) {
|
|
156
|
+
requireValue(
|
|
157
|
+
image.repository === `ghcr.io/${repository}/${image.name}`,
|
|
158
|
+
"destination must belong to the consumer repository",
|
|
159
|
+
);
|
|
160
|
+
requireValue(
|
|
161
|
+
digestPattern.test(image.digest) &&
|
|
162
|
+
["built", "reused"].includes(image.action),
|
|
163
|
+
"invalid image identity",
|
|
164
|
+
);
|
|
165
|
+
requireValue(
|
|
166
|
+
/^linux\/(amd64|arm64)$/u.test(image.platform),
|
|
167
|
+
"unsupported image platform",
|
|
168
|
+
);
|
|
169
|
+
const prefix = image.layout;
|
|
170
|
+
const descriptorFile = (descriptor) => {
|
|
171
|
+
requireValue(
|
|
172
|
+
digestPattern.test(descriptor.digest) &&
|
|
173
|
+
Number.isSafeInteger(descriptor.size) &&
|
|
174
|
+
descriptor.size >= 0,
|
|
175
|
+
"invalid OCI descriptor",
|
|
176
|
+
);
|
|
177
|
+
const relative = `${prefix}/blobs/sha256/${descriptor.digest.slice(7)}`;
|
|
178
|
+
const file = ociBundleFile(bundleRoot, relative);
|
|
179
|
+
requireValue(
|
|
180
|
+
fs.statSync(file).size === descriptor.size,
|
|
181
|
+
"OCI blob size mismatch",
|
|
182
|
+
);
|
|
183
|
+
if (!files.has(file))
|
|
184
|
+
requireValue(
|
|
185
|
+
fileDigest(file) === descriptor.digest,
|
|
186
|
+
"OCI blob digest mismatch",
|
|
187
|
+
);
|
|
188
|
+
files.set(file, descriptor);
|
|
189
|
+
return file;
|
|
190
|
+
};
|
|
191
|
+
const layout = JSON.parse(
|
|
192
|
+
fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/oci-layout`)),
|
|
193
|
+
);
|
|
194
|
+
requireValue(
|
|
195
|
+
layout.imageLayoutVersion === "1.0.0",
|
|
196
|
+
"unsupported OCI layout",
|
|
197
|
+
);
|
|
198
|
+
const index = JSON.parse(
|
|
199
|
+
fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/index.json`)),
|
|
200
|
+
);
|
|
201
|
+
const selected = (index.manifests || []).filter(
|
|
202
|
+
(descriptor) =>
|
|
203
|
+
descriptor.digest === image.digest &&
|
|
204
|
+
(descriptor.annotations?.["org.opencontainers.image.ref.name"] ===
|
|
205
|
+
image.name ||
|
|
206
|
+
index.manifests.length === 1),
|
|
207
|
+
);
|
|
208
|
+
requireValue(
|
|
209
|
+
index.schemaVersion === 2 && selected.length === 1,
|
|
210
|
+
"OCI index must select one exact image manifest",
|
|
211
|
+
);
|
|
212
|
+
const document = JSON.parse(fs.readFileSync(descriptorFile(selected[0])));
|
|
213
|
+
requireValue(
|
|
214
|
+
document.schemaVersion === 2 &&
|
|
215
|
+
[
|
|
216
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
217
|
+
"application/vnd.docker.distribution.manifest.v2+json",
|
|
218
|
+
].includes(document.mediaType) &&
|
|
219
|
+
Array.isArray(document.layers),
|
|
220
|
+
"unsupported OCI image manifest",
|
|
221
|
+
);
|
|
222
|
+
const config = JSON.parse(fs.readFileSync(descriptorFile(document.config)));
|
|
223
|
+
requireValue(
|
|
224
|
+
`${config.os}/${config.architecture}` === image.platform,
|
|
225
|
+
"OCI platform mismatch",
|
|
226
|
+
);
|
|
227
|
+
verifyImageProvenance(image, config, sourceSha, version);
|
|
228
|
+
for (const layer of document.layers) descriptorFile(layer);
|
|
229
|
+
const smokeFile = ociBundleFile(bundleRoot, image.smoke.path);
|
|
230
|
+
requireValue(
|
|
231
|
+
fileDigest(smokeFile) === image.smoke.sha256,
|
|
232
|
+
"smoke evidence digest mismatch",
|
|
233
|
+
);
|
|
234
|
+
const smoke = JSON.parse(fs.readFileSync(smokeFile));
|
|
235
|
+
requireValue(
|
|
236
|
+
smoke.passed === true && smoke.image === image.name,
|
|
237
|
+
"smoke qualification failed",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return { root: computed, manifest, bundleRoot: fs.realpathSync(bundleRoot) };
|
|
241
|
+
}
|
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
stableJson,
|
|
14
14
|
workCheck,
|
|
15
15
|
} from "./paper-repository.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
mergeNextDevelopmentAgentInstructions,
|
|
18
|
+
NEXT_DEVELOPMENT_AGENT_SECTION_START,
|
|
19
|
+
NEXT_DEVELOPMENT_AGENT_SECTION_END,
|
|
20
|
+
NEXT_DEVELOPMENT_LOCAL_COMMAND,
|
|
21
|
+
} from "./next-development-projection.js";
|
|
22
|
+
import { NEXT_DEVELOPMENT_ADR } from "./next-development-transition.js";
|
|
17
23
|
|
|
18
24
|
export const PAPER_AGENT_ENTRY_CONTRACT = "kungfu-buildchain-paper-agent-entry";
|
|
19
25
|
export const PAPER_AGENT_ENTRY_SCHEMA_VERSION = 1;
|
|
@@ -149,6 +155,24 @@ export function createPaperAgentEntry({
|
|
|
149
155
|
};
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
function mergePaperNextDevelopmentInstructions(current) {
|
|
159
|
+
const merged = mergeNextDevelopmentAgentInstructions(current);
|
|
160
|
+
const start = merged.indexOf(NEXT_DEVELOPMENT_AGENT_SECTION_START);
|
|
161
|
+
const end = merged.indexOf(NEXT_DEVELOPMENT_AGENT_SECTION_END);
|
|
162
|
+
const section = merged.slice(start, end)
|
|
163
|
+
.replace(
|
|
164
|
+
`\`${NEXT_DEVELOPMENT_ADR}\``,
|
|
165
|
+
`[Buildchain next-development ADR](https://github.com/kungfu-systems/buildchain/blob/v4/${NEXT_DEVELOPMENT_ADR})`,
|
|
166
|
+
)
|
|
167
|
+
.replace(
|
|
168
|
+
NEXT_DEVELOPMENT_LOCAL_COMMAND,
|
|
169
|
+
NEXT_DEVELOPMENT_LOCAL_COMMAND.replace(
|
|
170
|
+
"node scripts/", "node node_modules/@kungfu-tech/buildchain/scripts/",
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
return `${merged.slice(0, start)}${section}${merged.slice(end)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
152
176
|
export function paperAgentEntryFiles({
|
|
153
177
|
cwd,
|
|
154
178
|
buildchainVersion,
|
|
@@ -169,7 +193,7 @@ export function paperAgentEntryFiles({
|
|
|
169
193
|
[PAPER_PATHS.agentEntry, jsonText(entry)],
|
|
170
194
|
[
|
|
171
195
|
PAPER_PATHS.agentInstructions,
|
|
172
|
-
|
|
196
|
+
mergePaperNextDevelopmentInstructions(
|
|
173
197
|
mergePaperAgentEntryInstructions(currentAgents, {
|
|
174
198
|
developmentRef: resolvedDevelopmentRef,
|
|
175
199
|
}),
|
package/packages/core/paper.js
CHANGED
|
@@ -410,7 +410,7 @@ jobs:
|
|
|
410
410
|
`;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
function scaffoldVerifyWorkflow(buildchainSha) {
|
|
413
|
+
function scaffoldVerifyWorkflow(buildchainSha, buildchainVersion = "") {
|
|
414
414
|
return `${nextDevelopmentWorkflowHeader()}name: Verify
|
|
415
415
|
|
|
416
416
|
on:
|
|
@@ -423,7 +423,7 @@ on:
|
|
|
423
423
|
workflow_dispatch:
|
|
424
424
|
|
|
425
425
|
permissions:
|
|
426
|
-
contents: read
|
|
426
|
+
${buildchainVersion.startsWith("4.") ? " actions: read\n contents: read\n pull-requests: read" : " contents: read"}
|
|
427
427
|
|
|
428
428
|
jobs:
|
|
429
429
|
check:
|
|
@@ -621,7 +621,7 @@ function scaffoldFiles({
|
|
|
621
621
|
artifactPaths: "_build/main.pdf",
|
|
622
622
|
releasePassportProductName: title,
|
|
623
623
|
});
|
|
624
|
-
const verifyWorkflow = scaffoldVerifyWorkflow(buildchainSha);
|
|
624
|
+
const verifyWorkflow = scaffoldVerifyWorkflow(buildchainSha, buildchainVersion);
|
|
625
625
|
const agentEntry = paperAgentEntryFiles({
|
|
626
626
|
cwd,
|
|
627
627
|
buildchainVersion,
|
|
@@ -756,7 +756,7 @@ function migrationFiles({
|
|
|
756
756
|
artifactPaths: config.publication.artifactPaths.join(","),
|
|
757
757
|
releasePassportProductName: config.publication.title,
|
|
758
758
|
});
|
|
759
|
-
const verifyWorkflow = scaffoldVerifyWorkflow(runtimeSha);
|
|
759
|
+
const verifyWorkflow = scaffoldVerifyWorkflow(runtimeSha, runtimeIdentity.version);
|
|
760
760
|
const agentEntry = paperAgentEntryFiles({
|
|
761
761
|
cwd,
|
|
762
762
|
buildchainVersion: runtimeIdentity.version,
|
|
@@ -17,6 +17,15 @@ export const RELEASE_TAIL_PRODUCT_CAPABILITIES = Object.freeze([
|
|
|
17
17
|
receiptKind: "product-package-publication",
|
|
18
18
|
transactionState: "publishing",
|
|
19
19
|
}),
|
|
20
|
+
Object.freeze({
|
|
21
|
+
id: "product.oci.publish",
|
|
22
|
+
executor: "provider-adapter",
|
|
23
|
+
adapter: "oci-image-family",
|
|
24
|
+
effectKind: "oci-family-publication",
|
|
25
|
+
observationKind: "oci-family-readback",
|
|
26
|
+
receiptKind: "oci-family-publication",
|
|
27
|
+
transactionState: "publishing",
|
|
28
|
+
}),
|
|
20
29
|
Object.freeze({
|
|
21
30
|
id: "product.release-refs.converge",
|
|
22
31
|
executor: "provider-adapter",
|
|
@@ -58,6 +58,7 @@ const ROOT_DOMAINS = new Set([
|
|
|
58
58
|
"partial-mutation-recovery-checkpoint",
|
|
59
59
|
"partial-mutation-recovery-plan",
|
|
60
60
|
"v4-product-required-artifacts",
|
|
61
|
+
"oci-publication-family",
|
|
61
62
|
"v4-product-publication-intent",
|
|
62
63
|
"v4-product-publication-operation",
|
|
63
64
|
"v4-product-publication-plan",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export const V4_DOMAIN_WASM_ABI_VERSION = 1;
|
|
3
3
|
export const V4_DOMAIN_WASM_RUSTC = "rustc 1.96.0 (ac68faa20 2026-05-25)";
|
|
4
4
|
export const V4_DOMAIN_WASM_SOURCE_SHA256 =
|
|
5
|
-
"
|
|
5
|
+
"7297279bce5344b8fc58688be5c1933a823306c82dac7dccfabdf07de41d652f";
|
|
6
6
|
export const V4_DOMAIN_WASM_SHA256 =
|
|
7
|
-
"
|
|
7
|
+
"3a4909bb4b2bbd2676eac83f259c22faf1ce43479036992c5e53d5e2dcc512ce";
|
|
@@ -233,13 +233,21 @@ function workflowSnapshot(relative) {
|
|
|
233
233
|
.map((job) => {
|
|
234
234
|
const block = jobBlock(source, job.id);
|
|
235
235
|
const uses = job.uses || null;
|
|
236
|
+
// A built-in publisher still requires mutation authority when the caller supplies its envelope.
|
|
237
|
+
const inheritedPublisher =
|
|
238
|
+
!/^ permissions:/mu.test(block) &&
|
|
239
|
+
/uses:.*actions\/v4-release-candidate-promote/u.test(block);
|
|
236
240
|
return {
|
|
237
241
|
id: job.id,
|
|
238
242
|
kind: uses ? "reusable-call" : "runner",
|
|
239
243
|
uses,
|
|
240
244
|
permissions: {
|
|
241
|
-
contents:
|
|
242
|
-
|
|
245
|
+
contents:
|
|
246
|
+
permission(block, "contents") ||
|
|
247
|
+
(inheritedPublisher ? "inherited" : null),
|
|
248
|
+
idToken:
|
|
249
|
+
permission(block, "id-token") ||
|
|
250
|
+
(inheritedPublisher ? "inherited" : null),
|
|
243
251
|
},
|
|
244
252
|
carriers: {
|
|
245
253
|
artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
|
|
@@ -247,8 +255,10 @@ function workflowSnapshot(relative) {
|
|
|
247
255
|
jobOutput: /GITHUB_OUTPUT/u.test(block),
|
|
248
256
|
},
|
|
249
257
|
mutationSignals: [
|
|
250
|
-
/contents:\s*write/u.test(block) &&
|
|
251
|
-
|
|
258
|
+
(/contents:\s*write/u.test(block) || inheritedPublisher) &&
|
|
259
|
+
"contents-write",
|
|
260
|
+
(/id-token:\s*write/u.test(block) || inheritedPublisher) &&
|
|
261
|
+
"oidc-write",
|
|
252
262
|
/(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
|
|
253
263
|
block,
|
|
254
264
|
) && "promotion-runtime",
|
|
@@ -316,16 +316,7 @@ ${secrets.trimEnd()}
|
|
|
316
316
|
outputs:
|
|
317
317
|
${publicOutputs(outputs)}
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
actions: write
|
|
321
|
-
artifact-metadata: write
|
|
322
|
-
attestations: write
|
|
323
|
-
checks: write
|
|
324
|
-
contents: write
|
|
325
|
-
id-token: write
|
|
326
|
-
issues: write
|
|
327
|
-
pull-requests: write
|
|
328
|
-
|
|
319
|
+
# Provider authority is inherited from the explicit caller envelope.
|
|
329
320
|
jobs:
|
|
330
321
|
resolve-promotion:
|
|
331
322
|
name: Resolve promotion workflow shell and runtime
|
|
@@ -503,15 +494,7 @@ ${consumerAdmissionJob()}
|
|
|
503
494
|
name: Invoke the single v4 publisher adapter
|
|
504
495
|
needs: [resolve-promotion, consumer-admission]
|
|
505
496
|
uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
|
|
506
|
-
|
|
507
|
-
actions: write
|
|
508
|
-
artifact-metadata: write
|
|
509
|
-
attestations: write
|
|
510
|
-
checks: write
|
|
511
|
-
contents: write
|
|
512
|
-
id-token: write
|
|
513
|
-
issues: write
|
|
514
|
-
pull-requests: write
|
|
497
|
+
# Provider authority is inherited from the explicit caller envelope.
|
|
515
498
|
with:
|
|
516
499
|
${invokeForwarded}
|
|
517
500
|
secrets: inherit
|
|
@@ -216,11 +216,17 @@ function applyPublicationFacadePatches(source, relative) {
|
|
|
216
216
|
return source;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function inheritPublicationProviderAuthority(source, relative) {
|
|
220
|
+
if (relative !== ".github/workflows/.release-candidate-promote.yml") return source;
|
|
221
|
+
return source.replace("permissions:\n contents: read\n", "# Provider authority is inherited from the explicit caller envelope.\n")
|
|
222
|
+
.replace(" permissions:\n actions: read\n checks: write\n contents: write\n id-token: write\n pull-requests: write\n", " # Provider authority is inherited from the explicit caller envelope.\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
export function migrateV4UniversalWorkflowFacade(source, relative) {
|
|
220
|
-
return rewriteRepositoryWorkflowPaths(root, applyPublicationFacadePatches(guardCompatibilityJobs(
|
|
226
|
+
return rewriteRepositoryWorkflowPaths(root, inheritPublicationProviderAuthority(applyPublicationFacadePatches(guardCompatibilityJobs(
|
|
221
227
|
addUniversalInput(addRuntimeBootstrapDependencies(source, relative), relative),
|
|
222
228
|
relative,
|
|
223
|
-
), relative));
|
|
229
|
+
), relative), relative));
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
function verify(source, relative) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { verifyOciPublicationBundle } from "../packages/core/oci-publication-bundle.js";
|
|
4
|
+
|
|
5
|
+
export function resolveOciCandidate({ payloadRoot, passport }) {
|
|
6
|
+
const matches = [];
|
|
7
|
+
function visit(directory) {
|
|
8
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
9
|
+
if (entry.isSymbolicLink())
|
|
10
|
+
throw new Error("OCI candidate cannot contain symlinks");
|
|
11
|
+
const file = path.join(directory, entry.name);
|
|
12
|
+
if (entry.isDirectory()) visit(file);
|
|
13
|
+
else if (entry.name === "oci-family.json") matches.push(file);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
visit(payloadRoot);
|
|
17
|
+
if (matches.length !== 1)
|
|
18
|
+
throw new Error("OCI candidate requires exactly one sealed image family");
|
|
19
|
+
const manifest = JSON.parse(fs.readFileSync(matches[0]));
|
|
20
|
+
const bundleRoot = path.dirname(matches[0]);
|
|
21
|
+
const verified = verifyOciPublicationBundle({
|
|
22
|
+
bundleRoot,
|
|
23
|
+
manifest,
|
|
24
|
+
repository: passport.repository,
|
|
25
|
+
sourceSha: passport.source.headSha,
|
|
26
|
+
version: passport.target.version,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
root: bundleRoot,
|
|
30
|
+
manifest: verified.manifest,
|
|
31
|
+
requiredArtifacts: manifest.images.map((image) => ({
|
|
32
|
+
kind: "oci",
|
|
33
|
+
name: image.repository,
|
|
34
|
+
ref: `v${manifest.version}`,
|
|
35
|
+
digest: image.digest,
|
|
36
|
+
platform: image.platform,
|
|
37
|
+
required: true,
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveRecoveredPublicationVersion({
|
|
43
|
+
artifactVersion,
|
|
44
|
+
channel,
|
|
45
|
+
rematerializeOnResume = false,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const version = String(artifactVersion || "").trim(),
|
|
48
|
+
match = version.match(
|
|
49
|
+
/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,
|
|
50
|
+
);
|
|
51
|
+
if (!match)
|
|
52
|
+
throw new Error(
|
|
53
|
+
`recovered npm artifact has invalid publication version: ${version || "<empty>"}`,
|
|
54
|
+
);
|
|
55
|
+
return channel === "release" && rematerializeOnResume ? match[1] : version;
|
|
56
|
+
}
|
|
57
|
+
export function resolveRecoveredCandidateVersion({
|
|
58
|
+
artifactVersion,
|
|
59
|
+
publicationVersion,
|
|
60
|
+
channel,
|
|
61
|
+
rematerializeOnResume = false,
|
|
62
|
+
targetRef = "",
|
|
63
|
+
candidateRef = "",
|
|
64
|
+
} = {}) {
|
|
65
|
+
const version = String(artifactVersion || "").trim(),
|
|
66
|
+
target = String(targetRef || "").replace(/^refs\/heads\//u, ""),
|
|
67
|
+
candidate = String(candidateRef || "").replace(/^refs\/heads\//u, "");
|
|
68
|
+
if (channel !== "release" || !rematerializeOnResume) return version;
|
|
69
|
+
const prefix = `publish-gate/${target}/`;
|
|
70
|
+
if (!target || !candidate.startsWith(prefix))
|
|
71
|
+
throw new Error(
|
|
72
|
+
`stable recovery candidate ref must descend from ${prefix || "publish-gate/<target>/"}`,
|
|
73
|
+
);
|
|
74
|
+
const candidateVersion = candidate.slice(prefix.length);
|
|
75
|
+
if (!/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(candidateVersion))
|
|
76
|
+
throw new Error(
|
|
77
|
+
`stable recovery candidate ref must bind an exact alpha version, got ${candidateVersion || "<empty>"}`,
|
|
78
|
+
);
|
|
79
|
+
if (candidateVersion.replace(/-alpha\.\d+$/u, "") !== publicationVersion)
|
|
80
|
+
throw new Error(
|
|
81
|
+
`stable recovery candidate ${candidateVersion} does not match publication ${publicationVersion || "<empty>"}`,
|
|
82
|
+
);
|
|
83
|
+
if (
|
|
84
|
+
(version.match(
|
|
85
|
+
/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,
|
|
86
|
+
)?.[1] || "") !== publicationVersion
|
|
87
|
+
)
|
|
88
|
+
throw new Error(
|
|
89
|
+
`recovered npm artifact version ${version || "<empty>"} does not match publication ${publicationVersion}`,
|
|
90
|
+
);
|
|
91
|
+
return candidateVersion;
|
|
92
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveOciCandidate } from "./publication-candidate-kind.mjs";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -102,3 +103,11 @@ export function createResolvedPublicationSealedBundle({
|
|
|
102
103
|
});
|
|
103
104
|
return { root: resolvedRoot, manifest, npmArtifacts: resolvedNpmArtifacts, files };
|
|
104
105
|
}
|
|
106
|
+
|
|
107
|
+
export function resolveCandidatePublicationBundle({ kind, payloadDir, passport, runtimeSha, npmArtifacts, releaseAssetPaths }) {
|
|
108
|
+
if (kind === "oci") return resolveOciCandidate({ payloadRoot: payloadDir, passport });
|
|
109
|
+
if (kind !== "npm") return undefined;
|
|
110
|
+
return createResolvedPublicationSealedBundle({ bundleRoot: payloadDir, repository: passport.repository,
|
|
111
|
+
sourceSha: passport.source?.headSha, sourceTreeSha: passport.source?.treeHash,
|
|
112
|
+
runtimeSha, releaseCandidateRoot: passport.candidateHash, npmArtifacts, releaseAssetPaths });
|
|
113
|
+
}
|
|
@@ -7,7 +7,7 @@ import { execFileSync } from "node:child_process";
|
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveCandidatePublicationBundle } from "./publication-candidate-sealer.mjs";
|
|
11
11
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
12
12
|
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
13
13
|
import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
|
|
@@ -725,23 +725,12 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
725
725
|
const noun = publishArtifactKind === "npm" ? "npm package tarballs" : "platform manifests";
|
|
726
726
|
throw new Error(`expected at least ${minimumPayloadCount} downloaded ${noun}, found ${downloadedRequiredArtifactCount}`);
|
|
727
727
|
}
|
|
728
|
-
const sealedBundle = publishArtifactKind
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
sourceSha: passport.source?.headSha,
|
|
733
|
-
sourceTreeSha: passport.source?.treeHash,
|
|
734
|
-
runtimeSha: releaseCandidateRuntimeSha(passport),
|
|
735
|
-
releaseCandidateRoot: passport.candidateHash,
|
|
736
|
-
npmArtifacts: npmTarballPaths.map((tarballPath) => ({
|
|
737
|
-
path: tarballPath,
|
|
738
|
-
...readNpmPackageArtifact({ tarballPath, mainPackage: publishPackageMain }),
|
|
739
|
-
})),
|
|
740
|
-
releaseAssetPaths,
|
|
741
|
-
})
|
|
742
|
-
: undefined;
|
|
728
|
+
const sealedBundle = resolveCandidatePublicationBundle({ kind: publishArtifactKind, payloadDir, passport,
|
|
729
|
+
runtimeSha: releaseCandidateRuntimeSha(passport), releaseAssetPaths,
|
|
730
|
+
npmArtifacts: npmTarballPaths.map((tarballPath) => ({ path: tarballPath,
|
|
731
|
+
...readNpmPackageArtifact({ tarballPath, mainPackage: publishPackageMain }) })) });
|
|
743
732
|
const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8"))), publicationVersion = resolveFreshPublicationVersion({ sealedBundle, candidateVersion: passport.target?.version });
|
|
744
|
-
const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
|
|
733
|
+
const generatedRequiredArtifacts = sealedBundle?.requiredArtifacts || generatePublishRequiredArtifacts({
|
|
745
734
|
manifests,
|
|
746
735
|
version: publicationVersion,
|
|
747
736
|
kind: publishArtifactKind,
|
|
@@ -6,6 +6,8 @@ import path from "node:path";
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
|
|
9
|
+
import { resolveOciCandidate, resolveRecoveredPublicationVersion, resolveRecoveredCandidateVersion } from "./publication-candidate-kind.mjs";
|
|
10
|
+
export { resolveRecoveredPublicationVersion, resolveRecoveredCandidateVersion } from "./publication-candidate-kind.mjs";
|
|
9
11
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
10
12
|
import { compareSemver } from "./publication-registry-hydrate.mjs";
|
|
11
13
|
import { normalizeAnchorProvenance, normalizeCandidateRun, recoverCandidateProvenance } from "./release-candidate-anchor-provenance.mjs";
|
|
@@ -695,22 +697,6 @@ export function createRecoveredPublicationCandidate({
|
|
|
695
697
|
};
|
|
696
698
|
return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
|
|
697
699
|
}
|
|
698
|
-
export function resolveRecoveredPublicationVersion({ artifactVersion, channel, rematerializeOnResume = false } = {}) {
|
|
699
|
-
const version = String(artifactVersion || "").trim(), match = version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u);
|
|
700
|
-
if (!match) throw new Error(`recovered npm artifact has invalid publication version: ${version || "<empty>"}`);
|
|
701
|
-
return channel === "release" && rematerializeOnResume ? match[1] : version;
|
|
702
|
-
}
|
|
703
|
-
export function resolveRecoveredCandidateVersion({ artifactVersion, publicationVersion, channel, rematerializeOnResume = false, targetRef = "", candidateRef = "" } = {}) {
|
|
704
|
-
const version = String(artifactVersion || "").trim(), target = String(targetRef || "").replace(/^refs\/heads\//u, ""), candidate = String(candidateRef || "").replace(/^refs\/heads\//u, "");
|
|
705
|
-
if (channel !== "release" || !rematerializeOnResume) return version;
|
|
706
|
-
const prefix = `publish-gate/${target}/`;
|
|
707
|
-
if (!target || !candidate.startsWith(prefix)) throw new Error(`stable recovery candidate ref must descend from ${prefix || "publish-gate/<target>/"}`);
|
|
708
|
-
const candidateVersion = candidate.slice(prefix.length);
|
|
709
|
-
if (!/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(candidateVersion)) throw new Error(`stable recovery candidate ref must bind an exact alpha version, got ${candidateVersion || "<empty>"}`);
|
|
710
|
-
if (candidateVersion.replace(/-alpha\.\d+$/u, "") !== publicationVersion) throw new Error(`stable recovery candidate ${candidateVersion} does not match publication ${publicationVersion || "<empty>"}`);
|
|
711
|
-
if ((version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u)?.[1] || "") !== publicationVersion) throw new Error(`recovered npm artifact version ${version || "<empty>"} does not match publication ${publicationVersion}`);
|
|
712
|
-
return candidateVersion;
|
|
713
|
-
}
|
|
714
700
|
export function createRecoveredPublication({ downloads, bundleRoot, repository, passport, candidateRuntimeSha, publishArtifactKind, publishPackageMain, releasePatterns, platformManifests, channel, targetRef = "", candidateRef = "", rematerializeOnResume = false }) {
|
|
715
701
|
const allFiles = downloads.flatMap((download) => download.files.map((file) => ({ path: path.relative(bundleRoot, file.absolutePath).split(path.sep).join("/"),
|
|
716
702
|
size: file.size, sha256: file.sha256.replace(/^sha256:/, ""), absolutePath: file.absolutePath })))
|
|
@@ -718,6 +704,13 @@ export function createRecoveredPublication({ downloads, bundleRoot, repository,
|
|
|
718
704
|
const kind = String(publishArtifactKind || "npm");
|
|
719
705
|
const releaseMatchers = splitPatterns(releasePatterns).map(patternMatcher);
|
|
720
706
|
const releaseAssets = allFiles.filter((file) => releaseMatchers.some((matcher) => matcher.test(path.basename(file.path))));
|
|
707
|
+
if (kind === "oci") {
|
|
708
|
+
createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
|
|
709
|
+
const sealed = resolveOciCandidate({ payloadRoot: bundleRoot, passport });
|
|
710
|
+
return { manifest: sealed.manifest, bundleRoot: sealed.root, npmArtifacts: [], allFiles, releaseAssets,
|
|
711
|
+
version: sealed.manifest.version, candidateVersion: sealed.manifest.version,
|
|
712
|
+
publishRequiredArtifacts: sealed.requiredArtifacts };
|
|
713
|
+
}
|
|
721
714
|
if (kind !== "npm") {
|
|
722
715
|
createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
|
|
723
716
|
const version = String(passport.target?.version || "").trim();
|
|
@@ -1019,7 +1012,7 @@ export async function resumeFromCandidateRun({
|
|
|
1019
1012
|
npmTarballs: tarballs,
|
|
1020
1013
|
releaseAssets: publication.releaseAssets.map((asset) => outputPath(asset.absolutePath)),
|
|
1021
1014
|
publishRequiredArtifacts: outputPath(requiredArtifactsPath),
|
|
1022
|
-
sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
|
|
1015
|
+
sealedBundleRoot: publication.manifest ? outputPath(publication.bundleRoot || bundleRoot) : "",
|
|
1023
1016
|
sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
|
|
1024
1017
|
recoveryReceipt: outputPath(recoveryReceiptPath),
|
|
1025
1018
|
stageCapsules: stageCapsuleFile ? outputPath(stageCapsuleFile.absolutePath) : "",
|
|
@@ -206,6 +206,7 @@ export function nodeApiMeta(exportName) {
|
|
|
206
206
|
"./publication-artifact": { group: "reusable-build", summary: "Publication artifact manifest, source bundle, and publication passport APIs." },
|
|
207
207
|
"./publication-package": { group: "reusable-build", summary: "Publication npm package synthesis APIs for Buildchain-managed paper release presets." },
|
|
208
208
|
"./publication-reproducibility": { group: "reusable-build", summary: "Two-clean-build publication byte reproducibility receipt APIs." },
|
|
209
|
+
"./oci-publication": { group: "reusable-build", summary: "Sealed OCI image family verification, provenance, and smoke evidence APIs." },
|
|
209
210
|
"./publication-sealed-bundle": { group: "reusable-build", summary: "Build-once publication bundle manifests and exact-byte resume verification APIs." },
|
|
210
211
|
"./paper": { group: "reusable-build", summary: "Unified Paper work, fleet, scaffold, preflight, npm bootstrap, build, Alpha, status, and resume planning APIs." },
|
|
211
212
|
"./publication-authority": { group: "release-passport-trust", summary: "Sealed publication authority registry, runner provenance, control-plane audit, admission, and independent verification APIs." },
|