@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.0
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/actions/report-buildchain-issue/README.md +1 -1
- package/contracts/buildchain-v2-residuals-v1.json +191 -0
- package/dist/site/buildchain-contract.json +58 -35
- package/dist/site/buildchain-site.json +33 -33
- package/dist/site/controller-registry.json +24 -3
- package/dist/site/kfd-claims.json +11 -6
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -7
- package/dist/site/node-api-registry.json +6 -6
- package/dist/site/page-registry.json +22 -22
- package/dist/site/public-surface-audit.json +10 -5
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +8 -3
- package/docs/auditable-demo.md +19 -1
- package/docs/consumer-issue-reporting.md +1 -1
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +23 -30
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/release-governance.md +8 -1
- package/docs/reusable-build-surface.md +124 -64
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/versioning.md +41 -21
- package/docs/web-surface-deployments.md +20 -1
- package/package.json +1 -1
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/release-passport.js +36 -27
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +147 -2
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +20 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +114 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/inspect-artifact-signing-requests.mjs +45 -14
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/runtime-ref-core.mjs +13 -2
- package/scripts/seal-artifact-signing-requests.mjs +51 -6
- package/scripts/stable-candidate-qualification.mjs +8 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-production-decision.mjs +19 -3
|
@@ -24,6 +24,40 @@ function assertHex(value, label) {
|
|
|
24
24
|
if (!/^[0-9a-f]{64}$/.test(String(value || ""))) throw new Error(`${label} must be a lowercase sha256 hex digest`);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function requiredHeader(value, label) {
|
|
28
|
+
const normalized = String(value || "").trim();
|
|
29
|
+
if (!normalized || /[\r\n]/.test(normalized)) throw new Error(`${label} must be a non-empty single-line value`);
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validatePublicationEntry({ entry, kind, root, snapshotId, requireSnapshotDocument = false, requireHeaders = false }) {
|
|
34
|
+
const source = safeRelative(entry.source, `${kind}.source`);
|
|
35
|
+
const key = safeRelative(entry.key, `${kind}.key`);
|
|
36
|
+
const file = path.resolve(root, source);
|
|
37
|
+
if (file !== root && !file.startsWith(`${root}${path.sep}`)) throw new Error(`${kind}.source escapes artifact root`);
|
|
38
|
+
if (!fs.statSync(file).isFile()) throw new Error(`${kind}.source is not a file: ${source}`);
|
|
39
|
+
assertHex(entry.sha256, `${kind}.sha256`);
|
|
40
|
+
const actual = sha256(file);
|
|
41
|
+
if (actual !== entry.sha256) throw new Error(`${kind}.sha256 does not match ${source}`);
|
|
42
|
+
if (requireSnapshotDocument) {
|
|
43
|
+
const document = readJson(file);
|
|
44
|
+
if (document.snapshotId !== snapshotId) throw new Error(`${kind} document snapshotId does not match manifest`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
kind,
|
|
48
|
+
source,
|
|
49
|
+
key,
|
|
50
|
+
file,
|
|
51
|
+
sha256: actual,
|
|
52
|
+
contentType: requireHeaders
|
|
53
|
+
? requiredHeader(entry.contentType, `${kind}.contentType`)
|
|
54
|
+
: String(entry.contentType || "application/json"),
|
|
55
|
+
cacheControl: requireHeaders
|
|
56
|
+
? requiredHeader(entry.cacheControl, `${kind}.cacheControl`)
|
|
57
|
+
: String(entry.cacheControl || ""),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
27
61
|
export function validateObservedEvidenceBundle({ manifestPath, artifactRoot = path.dirname(manifestPath) }) {
|
|
28
62
|
const resolvedManifest = path.resolve(manifestPath);
|
|
29
63
|
const root = path.resolve(artifactRoot);
|
|
@@ -36,26 +70,39 @@ export function validateObservedEvidenceBundle({ manifestPath, artifactRoot = pa
|
|
|
36
70
|
if (!Number.isFinite(Date.parse(manifest.snapshot?.observedAt || ""))) throw new Error("snapshot.observedAt must be ISO-8601");
|
|
37
71
|
const immutable = manifest.publication?.immutable || {};
|
|
38
72
|
const latest = manifest.publication?.latest || {};
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
if (
|
|
73
|
+
const immutableEntry = validatePublicationEntry({ entry: immutable, kind: "immutable", root, snapshotId, requireSnapshotDocument: true });
|
|
74
|
+
const latestEntry = validatePublicationEntry({ entry: latest, kind: "latest", root, snapshotId, requireSnapshotDocument: true });
|
|
75
|
+
const projectionInput = manifest.publication?.projections ?? [];
|
|
76
|
+
if (!Array.isArray(projectionInput) || projectionInput.length > 16) {
|
|
77
|
+
throw new Error("publication.projections must be an array with at most 16 entries");
|
|
78
|
+
}
|
|
79
|
+
const projections = projectionInput.map((entry, index) => validatePublicationEntry({
|
|
80
|
+
entry,
|
|
81
|
+
kind: `projection[${index}]`,
|
|
82
|
+
root,
|
|
83
|
+
snapshotId,
|
|
84
|
+
requireHeaders: true,
|
|
85
|
+
}));
|
|
86
|
+
const entries = [immutableEntry, ...projections, latestEntry];
|
|
87
|
+
if (!immutableEntry.key.includes(`/${snapshotId}.json`)) throw new Error("immutable key must bind snapshot.id below a versioned path");
|
|
88
|
+
if (new Set(entries.map((entry) => entry.key)).size !== entries.length) {
|
|
89
|
+
throw new Error("immutable, projection, and latest keys must be unique");
|
|
90
|
+
}
|
|
54
91
|
const invalidationPaths = [...new Set(manifest.publication?.invalidationPaths || [])];
|
|
55
92
|
if (invalidationPaths.some((entry) => typeof entry !== "string" || !entry.startsWith("/") || entry.includes(".."))) {
|
|
56
93
|
throw new Error("invalidation paths must be absolute viewer paths without traversal");
|
|
57
94
|
}
|
|
58
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
manifest,
|
|
97
|
+
manifestPath: resolvedManifest,
|
|
98
|
+
artifactRoot: root,
|
|
99
|
+
snapshotId,
|
|
100
|
+
entries,
|
|
101
|
+
immutable: immutableEntry,
|
|
102
|
+
latest: latestEntry,
|
|
103
|
+
projections,
|
|
104
|
+
invalidationPaths,
|
|
105
|
+
};
|
|
59
106
|
}
|
|
60
107
|
|
|
61
108
|
function defaultRunner(args) {
|
|
@@ -79,6 +126,8 @@ function headEvidence(result) {
|
|
|
79
126
|
snapshotId: String(value.Metadata?.["snapshot-id"] || ""),
|
|
80
127
|
etag: String(value.ETag || "").replaceAll('"', ""),
|
|
81
128
|
versionId: String(value.VersionId || ""),
|
|
129
|
+
contentType: String(value.ContentType || ""),
|
|
130
|
+
cacheControl: String(value.CacheControl || ""),
|
|
82
131
|
};
|
|
83
132
|
}
|
|
84
133
|
|
|
@@ -90,7 +139,7 @@ function putArgs({ bucket, entry, snapshotId, immutable }) {
|
|
|
90
139
|
const args = [
|
|
91
140
|
"s3api", "put-object", "--bucket", bucket, "--key", entry.key, "--body", entry.file,
|
|
92
141
|
"--content-type", entry.contentType,
|
|
93
|
-
"--cache-control", immutable ? "public,max-age=31536000,immutable" : "public,max-age=0,must-revalidate",
|
|
142
|
+
"--cache-control", entry.cacheControl || (immutable ? "public,max-age=31536000,immutable" : "public,max-age=0,must-revalidate"),
|
|
94
143
|
"--checksum-sha256", base64Digest(entry.sha256),
|
|
95
144
|
"--metadata", `snapshot-id=${snapshotId},sha256=${entry.sha256}`,
|
|
96
145
|
];
|
|
@@ -98,10 +147,39 @@ function putArgs({ bucket, entry, snapshotId, immutable }) {
|
|
|
98
147
|
return args;
|
|
99
148
|
}
|
|
100
149
|
|
|
150
|
+
function copySource(bucket, key, versionId) {
|
|
151
|
+
const encoded = `${encodeURIComponent(bucket)}/${key.split("/").map(encodeURIComponent).join("/")}`;
|
|
152
|
+
return `${encoded}?versionId=${encodeURIComponent(versionId)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rollbackMutableEntries({ commandRunner, bucket, applied }) {
|
|
156
|
+
const operations = [];
|
|
157
|
+
for (const { entry, previous } of [...applied].reverse()) {
|
|
158
|
+
const result = previous
|
|
159
|
+
? commandRunner([
|
|
160
|
+
"s3api", "copy-object", "--bucket", bucket, "--key", entry.key,
|
|
161
|
+
"--copy-source", copySource(bucket, entry.key, previous.versionId),
|
|
162
|
+
"--metadata-directive", "COPY",
|
|
163
|
+
])
|
|
164
|
+
: commandRunner(["s3api", "delete-object", "--bucket", bucket, "--key", entry.key]);
|
|
165
|
+
operations.push({ action: previous ? "restore-previous-version" : "remove-new-projection", key: entry.key, status: result.status });
|
|
166
|
+
if (result.status !== 0) {
|
|
167
|
+
throw new Error(`rollback failed for ${entry.key}: ${result.stderr || result.stdout}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return operations;
|
|
171
|
+
}
|
|
172
|
+
|
|
101
173
|
function assertHeadMatches(head, entry, snapshotId, label) {
|
|
102
174
|
if (!head || head.sha256 !== entry.sha256 || head.snapshotId !== snapshotId) {
|
|
103
175
|
throw new Error(`${label} object does not match the admitted snapshot and sha256`);
|
|
104
176
|
}
|
|
177
|
+
if (
|
|
178
|
+
entry.kind.startsWith("projection[") &&
|
|
179
|
+
(head.contentType !== entry.contentType || head.cacheControl !== entry.cacheControl)
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(`${label} object does not match the admitted content type and cache control`);
|
|
182
|
+
}
|
|
105
183
|
}
|
|
106
184
|
|
|
107
185
|
export function publishObservedEvidence(options, { commandRunner = defaultRunner } = {}) {
|
|
@@ -110,7 +188,7 @@ export function publishObservedEvidence(options, { commandRunner = defaultRunner
|
|
|
110
188
|
if (!bucket) throw new Error("bucket is required");
|
|
111
189
|
const distributionId = String(options.distributionId || "").trim();
|
|
112
190
|
const dryRun = options.dryRun !== false;
|
|
113
|
-
const
|
|
191
|
+
const { immutable, latest, projections } = bundle;
|
|
114
192
|
const receipt = {
|
|
115
193
|
schemaVersion: 1,
|
|
116
194
|
contract: "kungfu-buildchain-observed-evidence-publication-receipt",
|
|
@@ -121,8 +199,17 @@ export function publishObservedEvidence(options, { commandRunner = defaultRunner
|
|
|
121
199
|
bucket,
|
|
122
200
|
distributionId,
|
|
123
201
|
immutable: { key: immutable.key, sha256: immutable.sha256, status: "planned" },
|
|
202
|
+
projections: projections.map((entry) => ({
|
|
203
|
+
key: entry.key,
|
|
204
|
+
sha256: entry.sha256,
|
|
205
|
+
contentType: entry.contentType,
|
|
206
|
+
cacheControl: entry.cacheControl,
|
|
207
|
+
status: "planned",
|
|
208
|
+
})),
|
|
124
209
|
latest: { key: latest.key, sha256: latest.sha256, status: "planned" },
|
|
210
|
+
previousProjections: [],
|
|
125
211
|
previousLatest: null,
|
|
212
|
+
rollback: { status: "not-needed", operations: [] },
|
|
126
213
|
invalidationPaths: bundle.invalidationPaths,
|
|
127
214
|
operations: [],
|
|
128
215
|
};
|
|
@@ -147,21 +234,52 @@ export function publishObservedEvidence(options, { commandRunner = defaultRunner
|
|
|
147
234
|
assertHeadMatches(verifiedImmutable, immutable, bundle.snapshotId, "verified immutable");
|
|
148
235
|
receipt.immutable.verification = verifiedImmutable;
|
|
149
236
|
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
237
|
+
const mutableEntries = [...projections, latest];
|
|
238
|
+
const previousByKey = new Map(mutableEntries.map((entry) => [entry.key, headEvidence(headObject(commandRunner, bucket, entry.key))]));
|
|
239
|
+
if (projections.length > 0) {
|
|
240
|
+
for (const [key, previous] of previousByKey) {
|
|
241
|
+
if (previous && !previous.versionId) {
|
|
242
|
+
throw new Error(`mutable projection transaction requires bucket versioning before replacing ${key}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
receipt.previousProjections = projections.map((entry) => ({ key: entry.key, previous: previousByKey.get(entry.key) }));
|
|
247
|
+
receipt.previousLatest = previousByKey.get(latest.key);
|
|
248
|
+
const applied = [];
|
|
249
|
+
try {
|
|
250
|
+
for (const [index, projection] of projections.entries()) {
|
|
251
|
+
const put = commandRunner(putArgs({ bucket, entry: projection, snapshotId: bundle.snapshotId, immutable: false }));
|
|
252
|
+
receipt.operations.push({ action: "advance-projection", key: projection.key, status: put.status });
|
|
253
|
+
if (put.status !== 0) throw new Error(`projection update failed for ${projection.key}: ${put.stderr || put.stdout}`);
|
|
254
|
+
applied.push({ entry: projection, previous: previousByKey.get(projection.key) });
|
|
255
|
+
const verified = headEvidence(headObject(commandRunner, bucket, projection.key));
|
|
256
|
+
assertHeadMatches(verified, projection, bundle.snapshotId, `verified projection ${projection.key}`);
|
|
257
|
+
receipt.projections[index] = { ...receipt.projections[index], status: "written", verification: verified };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const latestPut = commandRunner(putArgs({ bucket, entry: latest, snapshotId: bundle.snapshotId, immutable: false }));
|
|
261
|
+
receipt.operations.push({ action: "advance-latest", status: latestPut.status });
|
|
262
|
+
if (latestPut.status !== 0) throw new Error(`latest alias update failed: ${latestPut.stderr || latestPut.stdout}`);
|
|
263
|
+
applied.push({ entry: latest, previous: previousByKey.get(latest.key) });
|
|
264
|
+
const verifiedLatest = headEvidence(headObject(commandRunner, bucket, latest.key));
|
|
265
|
+
assertHeadMatches(verifiedLatest, latest, bundle.snapshotId, "verified latest");
|
|
266
|
+
receipt.latest = { ...receipt.latest, status: "written", verification: verifiedLatest };
|
|
267
|
+
|
|
268
|
+
if (distributionId && bundle.invalidationPaths.length > 0) {
|
|
269
|
+
const invalidation = commandRunner([
|
|
270
|
+
"cloudfront", "create-invalidation", "--distribution-id", distributionId,
|
|
271
|
+
"--paths", ...bundle.invalidationPaths,
|
|
272
|
+
]);
|
|
273
|
+
receipt.operations.push({ action: "invalidate-cdn", status: invalidation.status, result: jsonOutput(invalidation) });
|
|
274
|
+
if (invalidation.status !== 0) throw new Error(`CloudFront invalidation failed: ${invalidation.stderr || invalidation.stdout}`);
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (projections.length > 0 && applied.length > 0) {
|
|
278
|
+
receipt.rollback.status = "applying";
|
|
279
|
+
receipt.rollback.operations = rollbackMutableEntries({ commandRunner, bucket, applied });
|
|
280
|
+
receipt.rollback.status = "restored";
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
165
283
|
}
|
|
166
284
|
receipt.status = "published";
|
|
167
285
|
receipt.publishedAt = new Date().toISOString();
|
|
@@ -96,6 +96,11 @@ function writeJson(filePath, value) {
|
|
|
96
96
|
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export function githubApiFailureIsAbsence(method, output) {
|
|
100
|
+
return String(method || "GET").toUpperCase() === "GET" &&
|
|
101
|
+
/404|not found/i.test(String(output || ""));
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
function githubApi(route, { method = "GET", body } = {}) {
|
|
100
105
|
const args = [
|
|
101
106
|
"api",
|
|
@@ -115,7 +120,9 @@ function githubApi(route, { method = "GET", body } = {}) {
|
|
|
115
120
|
});
|
|
116
121
|
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
117
122
|
if (result.status !== 0) {
|
|
118
|
-
if (
|
|
123
|
+
if (githubApiFailureIsAbsence(method, output)) {
|
|
124
|
+
return { exists: false, data: null };
|
|
125
|
+
}
|
|
119
126
|
throw new Error(`GitHub API ${method} ${route} failed closed`);
|
|
120
127
|
}
|
|
121
128
|
return {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
function normalize(value, name) {
|
|
7
|
+
const normalized = String(value ?? "").trim();
|
|
8
|
+
if (/\r|\n|\0/.test(normalized)) {
|
|
9
|
+
throw new Error(`${name} must be a single-line NO_PROXY value`);
|
|
10
|
+
}
|
|
11
|
+
return normalized;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveArtifactSigningUploadRoute({
|
|
15
|
+
requestedNoProxy = "",
|
|
16
|
+
noProxy = "",
|
|
17
|
+
lowerNoProxy = "",
|
|
18
|
+
} = {}) {
|
|
19
|
+
const requested = normalize(requestedNoProxy, "artifact signing request upload NO_PROXY");
|
|
20
|
+
const currentUpper = normalize(noProxy, "NO_PROXY");
|
|
21
|
+
const currentLower = normalize(lowerNoProxy, "no_proxy");
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
noProxy: requested || currentUpper || currentLower,
|
|
25
|
+
overrideApplied: requested !== "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeGitHubOutputs(route, outputPath = process.env.GITHUB_OUTPUT) {
|
|
30
|
+
if (!outputPath) return;
|
|
31
|
+
fs.appendFileSync(
|
|
32
|
+
outputPath,
|
|
33
|
+
[
|
|
34
|
+
`no-proxy=${route.noProxy}`,
|
|
35
|
+
`override-applied=${route.overrideApplied}`,
|
|
36
|
+
"",
|
|
37
|
+
].join("\n"),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function main() {
|
|
42
|
+
const route = resolveArtifactSigningUploadRoute({
|
|
43
|
+
requestedNoProxy: process.env.BUILDCHAIN_SIGNING_REQUEST_UPLOAD_NO_PROXY,
|
|
44
|
+
noProxy: process.env.NO_PROXY,
|
|
45
|
+
lowerNoProxy: process.env.no_proxy,
|
|
46
|
+
});
|
|
47
|
+
writeGitHubOutputs(route);
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
`Resolved artifact signing request upload route (override=${route.overrideApplied ? "yes" : "no"}).\n`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
54
|
+
main();
|
|
55
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import childProcess from "node:child_process";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { TextDecoder } from "node:util";
|
|
9
|
+
|
|
10
|
+
const EXACT_SHA = /^[0-9a-f]{40}$/u;
|
|
11
|
+
const MANAGED_MARKER = "<!-- buildchain-dev-alpha-candidate-state";
|
|
12
|
+
const MAX_PREFIX_CHARACTERS = 32768;
|
|
13
|
+
|
|
14
|
+
function required(value, name) {
|
|
15
|
+
const normalized = String(value ?? "").trim();
|
|
16
|
+
if (!normalized) throw new Error(`${name} is required`);
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function resolveRendererPath(consumerRootValue, rendererValue) {
|
|
21
|
+
const consumerRoot = fs.realpathSync(
|
|
22
|
+
path.resolve(required(consumerRootValue, "consumerRoot")),
|
|
23
|
+
);
|
|
24
|
+
const renderer = required(rendererValue, "renderer");
|
|
25
|
+
if (path.isAbsolute(renderer))
|
|
26
|
+
throw new Error("renderer must be repository-relative");
|
|
27
|
+
const candidate = path.resolve(consumerRoot, renderer);
|
|
28
|
+
const candidateRelative = path.relative(consumerRoot, candidate);
|
|
29
|
+
if (
|
|
30
|
+
!candidateRelative ||
|
|
31
|
+
candidateRelative.startsWith("..") ||
|
|
32
|
+
path.isAbsolute(candidateRelative)
|
|
33
|
+
)
|
|
34
|
+
throw new Error("renderer must resolve inside the consumer checkout");
|
|
35
|
+
const absolute = fs.realpathSync(candidate);
|
|
36
|
+
const relative = path.relative(consumerRoot, absolute);
|
|
37
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
38
|
+
throw new Error("renderer must resolve inside the consumer checkout");
|
|
39
|
+
const stat = fs.statSync(absolute);
|
|
40
|
+
if (!stat.isFile())
|
|
41
|
+
throw new Error("renderer must resolve to a regular file");
|
|
42
|
+
return { consumerRoot, renderer: absolute };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeEnvironment(values, source = process.env) {
|
|
46
|
+
const retained = [
|
|
47
|
+
"CI",
|
|
48
|
+
"HOME",
|
|
49
|
+
"LANG",
|
|
50
|
+
"LC_ALL",
|
|
51
|
+
"PATH",
|
|
52
|
+
"RUNNER_ARCH",
|
|
53
|
+
"RUNNER_OS",
|
|
54
|
+
"TEMP",
|
|
55
|
+
"TMP",
|
|
56
|
+
"TMPDIR",
|
|
57
|
+
"TZ",
|
|
58
|
+
];
|
|
59
|
+
return {
|
|
60
|
+
...Object.fromEntries(
|
|
61
|
+
retained
|
|
62
|
+
.filter((key) => source[key] !== undefined)
|
|
63
|
+
.map((key) => [key, source[key]]),
|
|
64
|
+
),
|
|
65
|
+
BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX_OUTPUT: values.outputPath,
|
|
66
|
+
BUILDCHAIN_CHANNEL_PATROL_SELECTED_SHA: values.selectedSha,
|
|
67
|
+
BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH: values.sourceBranch,
|
|
68
|
+
BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH: values.targetBranch,
|
|
69
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function checkedSpawn(command, args, options, label) {
|
|
74
|
+
const result = childProcess.spawnSync(command, args, {
|
|
75
|
+
...options,
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
});
|
|
78
|
+
if (result.error) throw result.error;
|
|
79
|
+
if (result.status !== 0) {
|
|
80
|
+
const detail = String(result.stderr || result.stdout || "").trim();
|
|
81
|
+
throw new Error(`${label} failed${detail ? `: ${detail}` : ""}`);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readPrefix(outputPath) {
|
|
87
|
+
const bytes = fs.readFileSync(outputPath);
|
|
88
|
+
let value;
|
|
89
|
+
try {
|
|
90
|
+
value = new TextDecoder("utf-8", { fatal: true }).decode(bytes).trim();
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error("renderer output must be valid UTF-8");
|
|
93
|
+
}
|
|
94
|
+
if (!value) throw new Error("renderer output must not be empty");
|
|
95
|
+
if (value.length > MAX_PREFIX_CHARACTERS)
|
|
96
|
+
throw new Error(
|
|
97
|
+
`renderer output exceeds ${MAX_PREFIX_CHARACTERS} characters`,
|
|
98
|
+
);
|
|
99
|
+
if (value.includes(MANAGED_MARKER))
|
|
100
|
+
throw new Error(
|
|
101
|
+
"renderer output must not contain the managed candidate state marker",
|
|
102
|
+
);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function appendMultilineOutput(outputFile, name, value) {
|
|
107
|
+
let delimiter;
|
|
108
|
+
do {
|
|
109
|
+
delimiter = `buildchain_${crypto.randomUUID()}`;
|
|
110
|
+
} while (value.split(/\r?\n/u).includes(delimiter));
|
|
111
|
+
fs.appendFileSync(
|
|
112
|
+
outputFile,
|
|
113
|
+
`${name}<<${delimiter}\n${value}\n${delimiter}\n`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function runCandidateBodyPrefixRenderer(options = {}) {
|
|
118
|
+
const { consumerRoot, renderer } = resolveRendererPath(
|
|
119
|
+
options.consumerRoot,
|
|
120
|
+
options.renderer,
|
|
121
|
+
);
|
|
122
|
+
const selectedSha = required(options.selectedSha, "selectedSha");
|
|
123
|
+
if (!EXACT_SHA.test(selectedSha))
|
|
124
|
+
throw new Error("selectedSha must be an exact 40-character commit SHA");
|
|
125
|
+
const sourceBranch = required(options.sourceBranch, "sourceBranch");
|
|
126
|
+
const targetBranch = required(options.targetBranch, "targetBranch");
|
|
127
|
+
const outputPath = path.resolve(required(options.outputPath, "outputPath"));
|
|
128
|
+
const githubOutput = required(options.githubOutput, "githubOutput");
|
|
129
|
+
|
|
130
|
+
const head = checkedSpawn(
|
|
131
|
+
"git",
|
|
132
|
+
["rev-parse", "HEAD"],
|
|
133
|
+
{
|
|
134
|
+
cwd: consumerRoot,
|
|
135
|
+
env: safeEnvironment({
|
|
136
|
+
outputPath,
|
|
137
|
+
selectedSha,
|
|
138
|
+
sourceBranch,
|
|
139
|
+
targetBranch,
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
"consumer HEAD resolution",
|
|
143
|
+
).stdout.trim();
|
|
144
|
+
if (head !== selectedSha)
|
|
145
|
+
throw new Error(
|
|
146
|
+
`consumer checkout HEAD ${head} does not match selected SHA ${selectedSha}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
150
|
+
fs.rmSync(outputPath, { force: true });
|
|
151
|
+
const env = safeEnvironment({
|
|
152
|
+
outputPath,
|
|
153
|
+
selectedSha,
|
|
154
|
+
sourceBranch,
|
|
155
|
+
targetBranch,
|
|
156
|
+
});
|
|
157
|
+
checkedSpawn(
|
|
158
|
+
process.execPath,
|
|
159
|
+
[renderer],
|
|
160
|
+
{ cwd: consumerRoot, env },
|
|
161
|
+
"PR body prefix renderer",
|
|
162
|
+
);
|
|
163
|
+
const value = readPrefix(outputPath);
|
|
164
|
+
appendMultilineOutput(githubOutput, "pull-request-body-prefix", value);
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function main() {
|
|
169
|
+
runCandidateBodyPrefixRenderer({
|
|
170
|
+
consumerRoot: process.env.BUILDCHAIN_CHANNEL_PATROL_CONSUMER_ROOT,
|
|
171
|
+
renderer: process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX_RENDERER,
|
|
172
|
+
outputPath: process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX_OUTPUT,
|
|
173
|
+
selectedSha: process.env.BUILDCHAIN_CHANNEL_PATROL_SELECTED_SHA,
|
|
174
|
+
sourceBranch: process.env.BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH,
|
|
175
|
+
targetBranch: process.env.BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH,
|
|
176
|
+
githubOutput: process.env.GITHUB_OUTPUT,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
181
|
+
try {
|
|
182
|
+
main();
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -3,7 +3,7 @@ const TRAIN_REF_RE = /^train\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
|
|
|
3
3
|
const AUTHORITY_REF_RE = /^authority\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
|
|
4
4
|
const OFFICIAL_CHANNEL_REF_RE = /^v\d+(?:\.\d+)?(?:-alpha)?$/;
|
|
5
5
|
|
|
6
|
-
export function parseWorkflowShellRef(workflowRef = "", fallback = "
|
|
6
|
+
export function parseWorkflowShellRef(workflowRef = "", fallback = "v3", buildchainRepository = "kungfu-systems/buildchain") {
|
|
7
7
|
const value = String(workflowRef || "");
|
|
8
8
|
const expectedPrefix = `${buildchainRepository}/.github/workflows/`;
|
|
9
9
|
if (!value.startsWith(expectedPrefix)) {
|
|
@@ -80,7 +80,7 @@ export function normalizeRequestedRuntimeRef(requestedRef = "") {
|
|
|
80
80
|
export function resolveRuntimeSelection({
|
|
81
81
|
requestedRef = "",
|
|
82
82
|
workflowRef = "",
|
|
83
|
-
defaultStableRef = "
|
|
83
|
+
defaultStableRef = "v3",
|
|
84
84
|
buildchainRepository = "kungfu-systems/buildchain",
|
|
85
85
|
} = {}) {
|
|
86
86
|
const requested = String(requestedRef || "").trim();
|
|
@@ -114,9 +114,11 @@ export function resolveRuntimeSelection({
|
|
|
114
114
|
export function validateRuntimeOverrideTrust({
|
|
115
115
|
requestedRef = "",
|
|
116
116
|
eventName = "",
|
|
117
|
+
eventAction = "",
|
|
117
118
|
actorPermission = "",
|
|
118
119
|
sameRepositoryPullRequest = false,
|
|
119
120
|
pullRequestHeadSha = "",
|
|
121
|
+
workflowShellSha = "",
|
|
120
122
|
} = {}) {
|
|
121
123
|
if (!String(requestedRef || "").trim()) {
|
|
122
124
|
return { ok: true, decision: "stable-default" };
|
|
@@ -126,6 +128,15 @@ export function validateRuntimeOverrideTrust({
|
|
|
126
128
|
}
|
|
127
129
|
const normalizedRequested = String(requestedRef || "").trim().toLowerCase();
|
|
128
130
|
const normalizedHeadSha = String(pullRequestHeadSha || "").trim().toLowerCase();
|
|
131
|
+
const normalizedWorkflowShellSha = String(workflowShellSha || "").trim().toLowerCase();
|
|
132
|
+
if (
|
|
133
|
+
eventName === "pull_request" &&
|
|
134
|
+
eventAction === "closed" &&
|
|
135
|
+
EXACT_SHA_RE.test(normalizedRequested) &&
|
|
136
|
+
normalizedRequested === normalizedWorkflowShellSha
|
|
137
|
+
) {
|
|
138
|
+
return { ok: true, decision: "closed-release-pr-shell-runtime" };
|
|
139
|
+
}
|
|
129
140
|
if (
|
|
130
141
|
eventName === "pull_request" &&
|
|
131
142
|
sameRepositoryPullRequest === true &&
|
|
@@ -52,6 +52,31 @@ function assertInside(root, target, label) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function containsPath(root, target) {
|
|
56
|
+
const relative = path.relative(root, target);
|
|
57
|
+
return (
|
|
58
|
+
!relative || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resetGeneratedOutputRoot({
|
|
63
|
+
workspace,
|
|
64
|
+
cwd,
|
|
65
|
+
outputRoot,
|
|
66
|
+
protectedPaths = [],
|
|
67
|
+
}) {
|
|
68
|
+
const protectedRoots = [workspace, cwd, ...protectedPaths];
|
|
69
|
+
for (const protectedPath of protectedRoots) {
|
|
70
|
+
if (containsPath(outputRoot, protectedPath)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"signing request output must not contain the workspace, working directory, manifest, or a declared artifact",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
77
|
+
fs.mkdirSync(outputRoot, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
function walkSubject(subjectRoot) {
|
|
56
81
|
const entries = [];
|
|
57
82
|
function visit(current) {
|
|
@@ -255,8 +280,12 @@ export function sealArtifactSigningRequests({
|
|
|
255
280
|
);
|
|
256
281
|
const resolvedOutputRoot = path.resolve(resolvedWorkspace, outputRoot);
|
|
257
282
|
assertInside(resolvedWorkspace, resolvedOutputRoot, "signing request output");
|
|
258
|
-
fs.mkdirSync(resolvedOutputRoot, { recursive: true });
|
|
259
283
|
if (selected.length === 0) {
|
|
284
|
+
resetGeneratedOutputRoot({
|
|
285
|
+
workspace: resolvedWorkspace,
|
|
286
|
+
cwd: resolvedCwd,
|
|
287
|
+
outputRoot: resolvedOutputRoot,
|
|
288
|
+
});
|
|
260
289
|
const index = { schemaVersion: 1, contract: INDEX_CONTRACT, requests: [] };
|
|
261
290
|
const indexPath = path.join(resolvedOutputRoot, "index.json");
|
|
262
291
|
fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
|
|
@@ -274,8 +303,7 @@ export function sealArtifactSigningRequests({
|
|
|
274
303
|
assertInside(resolvedWorkspace, resolvedManifest, "artifact manifest");
|
|
275
304
|
const manifest = JSON.parse(fs.readFileSync(resolvedManifest, "utf8"));
|
|
276
305
|
const platform = normalizePlatform(manifest.platform?.os || process.platform);
|
|
277
|
-
const
|
|
278
|
-
for (const declaration of selected) {
|
|
306
|
+
const prepared = selected.map((declaration) => {
|
|
279
307
|
const subjectPath = path.resolve(resolvedCwd, declaration.path);
|
|
280
308
|
assertInside(
|
|
281
309
|
resolvedWorkspace,
|
|
@@ -288,7 +316,7 @@ export function sealArtifactSigningRequests({
|
|
|
288
316
|
);
|
|
289
317
|
const realSubject = fs.realpathSync(subjectPath);
|
|
290
318
|
assertInside(
|
|
291
|
-
resolvedWorkspace,
|
|
319
|
+
fs.realpathSync(resolvedWorkspace),
|
|
292
320
|
realSubject,
|
|
293
321
|
`signing artifact ${declaration.id}`,
|
|
294
322
|
);
|
|
@@ -299,8 +327,25 @@ export function sealArtifactSigningRequests({
|
|
|
299
327
|
subjectPath,
|
|
300
328
|
descriptor,
|
|
301
329
|
});
|
|
302
|
-
|
|
303
|
-
|
|
330
|
+
return {
|
|
331
|
+
declaration,
|
|
332
|
+
subjectPath,
|
|
333
|
+
descriptor,
|
|
334
|
+
id: safeId(declaration.id),
|
|
335
|
+
kind: inferKind(subjectPath, declaration.kind),
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
resetGeneratedOutputRoot({
|
|
339
|
+
workspace: resolvedWorkspace,
|
|
340
|
+
cwd: resolvedCwd,
|
|
341
|
+
outputRoot: resolvedOutputRoot,
|
|
342
|
+
protectedPaths: [
|
|
343
|
+
resolvedManifest,
|
|
344
|
+
...prepared.map((entry) => entry.subjectPath),
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
const requests = [];
|
|
348
|
+
for (const { declaration, subjectPath, descriptor, id, kind } of prepared) {
|
|
304
349
|
const transport = archiveSubject({
|
|
305
350
|
subjectPath,
|
|
306
351
|
outputRoot: resolvedOutputRoot,
|