@kungfu-tech/buildchain 2.12.7-alpha.22 → 2.12.7-alpha.3
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/promote-buildchain-ref/README.md +1 -5
- package/dist/site/buildchain-contract.json +23 -48
- package/dist/site/buildchain-site.json +16 -16
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/controller-registry.json +3 -23
- package/dist/site/kfd-claims.json +9 -63
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +6 -6
- package/dist/site/page-registry.json +10 -10
- package/dist/site/public-surface-audit.json +9 -72
- package/dist/site/publication-authority-registry.json +1 -25
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/dist/site/workflow-registry.json +6 -72
- package/docs/publication-artifacts.md +20 -30
- package/docs/publication-authority.md +9 -21
- package/docs/release-candidate.md +0 -19
- package/docs/shifu-gate-profiles.md +0 -6
- package/package.json +1 -1
- package/packages/core/buildchain-publication-authority.js +0 -1
- package/packages/core/controller-evidence.js +1 -1
- package/packages/core/index.js +0 -7
- package/packages/core/publication-authority.js +2 -41
- package/packages/core/publication-control-plane-audit.js +1 -3
- package/scripts/assemble-self-publication-admission.mjs +9 -19
- package/scripts/audit-publication-control-plane.mjs +2 -5
- package/scripts/check-inventory.mjs +0 -3
- package/scripts/locked-source-checkout.mjs +13 -50
- package/scripts/workflow-friction-report.mjs +2 -13
- package/packages/core/publication-artifact-candidate.js +0 -128
- package/scripts/assemble-publication-artifact-admission.mjs +0 -190
- package/scripts/publication-artifact-candidate.mjs +0 -122
|
@@ -840,9 +840,6 @@ for (const requiredSnippet of [
|
|
|
840
840
|
"publish-source-ref: ${{ steps.publish-gate.outputs.ref }}",
|
|
841
841
|
"publish-source-sha: ${{ steps.publish-gate.outputs.sha }}",
|
|
842
842
|
"publish-source-locked: ${{ steps.publish-gate.outputs.locked }}",
|
|
843
|
-
"expected-publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
844
|
-
"publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
845
|
-
"name: Plan exact publication version",
|
|
846
843
|
"DRY_RUN: ${{ inputs.dry-run }}",
|
|
847
844
|
"if (dryRun) {",
|
|
848
845
|
"Enforce Buildchain stable release canary gate",
|
|
@@ -6,7 +6,6 @@ import path from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
export const LOCKED_SOURCE_CHECKOUT_CONTRACT = "kungfu-buildchain-locked-source-checkout-cache";
|
|
9
|
-
export const ISOLATED_GIT_GLOBAL_CONFIG = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
10
9
|
|
|
11
10
|
const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
12
11
|
|
|
@@ -134,24 +133,6 @@ function githubAuthEnv(token = "") {
|
|
|
134
133
|
};
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
function isolatedGitFetchEnv(env = {}, targetPath) {
|
|
138
|
-
const configuredCount = Number.parseInt(env.GIT_CONFIG_COUNT || "0", 10);
|
|
139
|
-
const safeDirectoryIndex = Number.isInteger(configuredCount) && configuredCount >= 0
|
|
140
|
-
? configuredCount
|
|
141
|
-
: 0;
|
|
142
|
-
return {
|
|
143
|
-
...env,
|
|
144
|
-
// Runner-global URL rewrites are shared mutable state. A concurrent job
|
|
145
|
-
// may point the same repository URL at a different single-SHA bundle, so
|
|
146
|
-
// network fetches must not consult the account-level Git config.
|
|
147
|
-
GIT_CONFIG_GLOBAL: ISOLATED_GIT_GLOBAL_CONFIG,
|
|
148
|
-
GIT_CONFIG_NOSYSTEM: "1",
|
|
149
|
-
GIT_CONFIG_COUNT: String(safeDirectoryIndex + 1),
|
|
150
|
-
[`GIT_CONFIG_KEY_${safeDirectoryIndex}`]: "safe.directory",
|
|
151
|
-
[`GIT_CONFIG_VALUE_${safeDirectoryIndex}`]: path.resolve(targetPath),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
136
|
function markSafeDirectory(targetPath, timeoutMs) {
|
|
156
137
|
try {
|
|
157
138
|
git(["config", "--global", "--add", "safe.directory", path.resolve(targetPath)], {
|
|
@@ -222,32 +203,9 @@ export function fetchSourceCommit({
|
|
|
222
203
|
sourceTreeSha = "",
|
|
223
204
|
timeoutMs,
|
|
224
205
|
env = {},
|
|
225
|
-
allowFullFetchRetry = false,
|
|
226
206
|
runGit = git,
|
|
227
207
|
containsCommit = hasCommit,
|
|
228
208
|
}) {
|
|
229
|
-
const fetchEnv = isolatedGitFetchEnv(env, targetPath);
|
|
230
|
-
const fetch = (refspec) => {
|
|
231
|
-
const options = { cwd: targetPath, timeoutMs, env: fetchEnv };
|
|
232
|
-
try {
|
|
233
|
-
runGit(["fetch", "--no-tags", "--depth=1", remoteName, refspec], options);
|
|
234
|
-
return "shallow";
|
|
235
|
-
} catch (error) {
|
|
236
|
-
if (
|
|
237
|
-
!allowFullFetchRetry
|
|
238
|
-
|| !/dumb http transport does not support shallow capabilities/i.test(
|
|
239
|
-
String(error?.message || error || ""),
|
|
240
|
-
)
|
|
241
|
-
) {
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
// Dumb HTTP mirrors are intentionally simple static cache endpoints.
|
|
245
|
-
// Retry only that explicit capability mismatch without --depth; all
|
|
246
|
-
// network fallbacks remain shallow and bounded by their own policy.
|
|
247
|
-
runGit(["fetch", "--no-tags", remoteName, refspec], options);
|
|
248
|
-
return "full";
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
209
|
try {
|
|
252
210
|
runGit(["remote", "remove", remoteName], { cwd: targetPath, timeoutMs, stdio: "ignore" });
|
|
253
211
|
} catch {
|
|
@@ -261,9 +219,13 @@ export function fetchSourceCommit({
|
|
|
261
219
|
|
|
262
220
|
if (fetchRef) {
|
|
263
221
|
try {
|
|
264
|
-
|
|
222
|
+
runGit(["fetch", "--no-tags", "--depth=1", remoteName, `+${fetchRef}:refs/buildchain/source-ref`], {
|
|
223
|
+
cwd: targetPath,
|
|
224
|
+
timeoutMs,
|
|
225
|
+
env,
|
|
226
|
+
});
|
|
265
227
|
if (containsCommit(targetPath, sha, timeoutMs)) {
|
|
266
|
-
return { selector: "ref", checkoutSha: sha
|
|
228
|
+
return { selector: "ref", checkoutSha: sha };
|
|
267
229
|
}
|
|
268
230
|
if (/^refs\/pull\/\d+\/merge$/.test(fetchRef) && sourceTreeSha) {
|
|
269
231
|
const fetchedSha = runGit(["rev-parse", "refs/buildchain/source-ref^{commit}"], {
|
|
@@ -275,7 +237,7 @@ export function fetchSourceCommit({
|
|
|
275
237
|
timeoutMs,
|
|
276
238
|
});
|
|
277
239
|
if (fetchedTree === sourceTreeSha) {
|
|
278
|
-
return { selector: "ref-tree", checkoutSha: fetchedSha
|
|
240
|
+
return { selector: "ref-tree", checkoutSha: fetchedSha };
|
|
279
241
|
}
|
|
280
242
|
}
|
|
281
243
|
} catch (error) {
|
|
@@ -287,11 +249,15 @@ export function fetchSourceCommit({
|
|
|
287
249
|
}
|
|
288
250
|
}
|
|
289
251
|
|
|
290
|
-
|
|
252
|
+
runGit(["fetch", "--no-tags", "--depth=1", remoteName, `+${sha}:refs/buildchain/source`], {
|
|
253
|
+
cwd: targetPath,
|
|
254
|
+
timeoutMs,
|
|
255
|
+
env,
|
|
256
|
+
});
|
|
291
257
|
if (!containsCommit(targetPath, sha, timeoutMs)) {
|
|
292
258
|
throw new Error(`fetched ${fetchRef || sha}, but ${sha} is not available`);
|
|
293
259
|
}
|
|
294
|
-
return { selector: "sha", checkoutSha: sha
|
|
260
|
+
return { selector: "sha", checkoutSha: sha };
|
|
295
261
|
}
|
|
296
262
|
|
|
297
263
|
export function runBoundedFetch({ attempts = 1, fetch, onAttempt = () => {}, onRetry = () => {}, shouldRetry = () => true }) {
|
|
@@ -447,9 +413,7 @@ export function lockedSourceCheckout({
|
|
|
447
413
|
fetchRef,
|
|
448
414
|
sourceTreeSha: treeSha,
|
|
449
415
|
timeoutMs,
|
|
450
|
-
allowFullFetchRetry: true,
|
|
451
416
|
});
|
|
452
|
-
evidence.cache.fetchMode = fetchResult.fetchMode;
|
|
453
417
|
checkoutSha = fetchResult.checkoutSha || sha;
|
|
454
418
|
checkoutFetchedCommit(targetPath, checkoutSha, timeoutMs);
|
|
455
419
|
} else {
|
|
@@ -494,7 +458,6 @@ export function lockedSourceCheckout({
|
|
|
494
458
|
shouldRetry: retryableGitFetchError,
|
|
495
459
|
});
|
|
496
460
|
evidence.cache.githubFetchAttempts = fetchResult.attempts;
|
|
497
|
-
evidence.cache.fetchMode = fetchResult.value.fetchMode;
|
|
498
461
|
checkoutSha = fetchResult.value.checkoutSha || sha;
|
|
499
462
|
} catch (error) {
|
|
500
463
|
evidence.durationMs = Date.now() - startedAt;
|
|
@@ -202,8 +202,6 @@ export async function classifyWorkflowFriction({
|
|
|
202
202
|
buildWorkflowName = DEFAULT_BUILD_WORKFLOW_NAME,
|
|
203
203
|
releaseCandidateOutcome = env("BUILDCHAIN_RC_RESOLVE_OUTCOME"),
|
|
204
204
|
releaseCandidateDiagnosis = env("BUILDCHAIN_RC_DIAGNOSIS"),
|
|
205
|
-
promotionOutcome = env("BUILDCHAIN_PROMOTION_OUTCOME"),
|
|
206
|
-
promotionDiagnosis = env("BUILDCHAIN_PROMOTION_DIAGNOSIS"),
|
|
207
205
|
runUrl = env("BUILDCHAIN_WORKFLOW_RUN_URL"),
|
|
208
206
|
outputDir = ".buildchain/workflow-friction",
|
|
209
207
|
fetchImpl = globalThis.fetch,
|
|
@@ -258,23 +256,14 @@ export async function classifyWorkflowFriction({
|
|
|
258
256
|
if (releaseCandidateOutcome === "failure") {
|
|
259
257
|
diagnosisParts.push("Promotion reached the post-Verify workflow before required PR-stage RC evidence could be resolved.");
|
|
260
258
|
}
|
|
261
|
-
if (
|
|
259
|
+
if (releaseCandidateDiagnosis) {
|
|
262
260
|
diagnosisParts.push(releaseCandidateDiagnosis);
|
|
263
261
|
}
|
|
264
|
-
if (promotionOutcome === "failure") {
|
|
265
|
-
diagnosisParts.push(
|
|
266
|
-
promotionDiagnosis
|
|
267
|
-
? `Promotion failed: ${promotionDiagnosis}`
|
|
268
|
-
: "Promotion failed after PR-stage release-candidate evidence resolved successfully.",
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
262
|
diagnosisParts.push(...workflowRunDiagnostics);
|
|
272
263
|
const diagnosis = diagnosisParts.join(" ") || "Buildchain ref promotion failed after Verify succeeded; inspect the classified evidence and keep the fix in Buildchain.";
|
|
273
264
|
const nextAction = frictionClass === "late-fail-fast"
|
|
274
265
|
? "Move the missing/stale RC evidence check earlier or make the promotion workflow consume the exact PR-stage RC passport before any publish side effect."
|
|
275
|
-
:
|
|
276
|
-
? "Deduplicate the PR/build path or tighten Buildchain workflow gates so the next channel promotion reaches publish exactly once."
|
|
277
|
-
: "Fix the concrete promotion failure above, then rerun through the protected channel workflow; do not treat successful RC resolution as the failure diagnosis.";
|
|
266
|
+
: "Deduplicate the PR/build path or tighten Buildchain workflow gates so the next channel promotion reaches publish exactly once.";
|
|
278
267
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
279
268
|
const bodyFile = path.join(outputDir, "issue-body.md");
|
|
280
269
|
const body = buildWorkflowFrictionBody({
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
|
|
3
|
-
import { validateControllerReceipt } from "./controller-evidence.js";
|
|
4
|
-
|
|
5
|
-
export const PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT =
|
|
6
|
-
"kungfu-buildchain-publication-artifact-candidate";
|
|
7
|
-
|
|
8
|
-
const MANIFEST_CONTRACT = "kungfu-buildchain-publication-artifact-manifest";
|
|
9
|
-
const PASSPORT_CONTRACT = "kungfu-buildchain-publication-artifact-passport";
|
|
10
|
-
|
|
11
|
-
function stableJson(value) {
|
|
12
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
13
|
-
if (value && typeof value === "object") {
|
|
14
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
15
|
-
}
|
|
16
|
-
return JSON.stringify(value);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function publicationArtifactCandidateDigest(value) {
|
|
20
|
-
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function resolvePublicationCandidateFile(files = [], candidatePath) {
|
|
24
|
-
const normalizedPath = requiredString(candidatePath, "candidatePath").replaceAll("\\", "/");
|
|
25
|
-
if (normalizedPath.startsWith("/") || normalizedPath.split("/").includes("..")) {
|
|
26
|
-
throw new Error(`publication artifact candidate contains an unsafe path: ${normalizedPath}`);
|
|
27
|
-
}
|
|
28
|
-
const matches = files.filter((entry) => entry.path === normalizedPath);
|
|
29
|
-
if (matches.length !== 1) {
|
|
30
|
-
throw new Error(`expected exactly one publication candidate file at ${normalizedPath}, found ${matches.length}`);
|
|
31
|
-
}
|
|
32
|
-
return matches[0].path;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function requiredString(value, label) {
|
|
36
|
-
const normalized = String(value || "").trim();
|
|
37
|
-
if (!normalized) throw new Error(`${label} must be a non-empty string`);
|
|
38
|
-
return normalized;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizeDigest(value, label) {
|
|
42
|
-
const normalized = requiredString(value, label).replace(/^sha256:/, "").toLowerCase();
|
|
43
|
-
if (!/^[0-9a-f]{64}$/.test(normalized)) throw new Error(`${label} must be a sha256 digest`);
|
|
44
|
-
return normalized;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function normalizeGitSha(value, label) {
|
|
48
|
-
const normalized = requiredString(value, label).toLowerCase();
|
|
49
|
-
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(normalized)) {
|
|
50
|
-
throw new Error(`${label} must be a 40- or 64-character Git SHA`);
|
|
51
|
-
}
|
|
52
|
-
return normalized;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function createPublicationArtifactCandidate({
|
|
56
|
-
repository,
|
|
57
|
-
sourceSha,
|
|
58
|
-
sourceTreeSha,
|
|
59
|
-
runtimeSha,
|
|
60
|
-
manifest,
|
|
61
|
-
passport,
|
|
62
|
-
controllerReceipt,
|
|
63
|
-
files = [],
|
|
64
|
-
} = {}) {
|
|
65
|
-
const normalizedRepository = requiredString(repository, "repository");
|
|
66
|
-
const normalizedSourceSha = normalizeGitSha(sourceSha, "sourceSha");
|
|
67
|
-
const normalizedSourceTreeSha = normalizeGitSha(sourceTreeSha, "sourceTreeSha");
|
|
68
|
-
const normalizedRuntimeSha = normalizeGitSha(runtimeSha, "runtimeSha");
|
|
69
|
-
if (manifest?.contract !== MANIFEST_CONTRACT) throw new Error("publication artifact manifest contract mismatch");
|
|
70
|
-
if (passport?.contract !== PASSPORT_CONTRACT || passport.status !== "passed") {
|
|
71
|
-
throw new Error("publication artifact passport is not qualifying");
|
|
72
|
-
}
|
|
73
|
-
if (manifest.source?.sha !== normalizedSourceSha || passport.source?.sha !== normalizedSourceSha) {
|
|
74
|
-
throw new Error("publication artifact source SHA mismatch");
|
|
75
|
-
}
|
|
76
|
-
if (manifest.source?.treeSha !== normalizedSourceTreeSha || passport.source?.treeSha !== normalizedSourceTreeSha) {
|
|
77
|
-
throw new Error("publication artifact source tree mismatch");
|
|
78
|
-
}
|
|
79
|
-
const manifestDigest = crypto.createHash("sha256").update(JSON.stringify(manifest, null, 2)).digest("hex");
|
|
80
|
-
if (normalizeDigest(passport.manifestDigest, "passport.manifestDigest") !== manifestDigest) {
|
|
81
|
-
throw new Error("publication artifact passport manifest digest mismatch");
|
|
82
|
-
}
|
|
83
|
-
const controllerValidation = validateControllerReceipt(controllerReceipt, {
|
|
84
|
-
expectedSourceSha: normalizedSourceSha,
|
|
85
|
-
expectedRuntimeSha: normalizedRuntimeSha,
|
|
86
|
-
});
|
|
87
|
-
if (!controllerValidation.ok || !controllerValidation.qualifying) {
|
|
88
|
-
throw new Error(`publication artifact controller receipt did not qualify: ${controllerValidation.issues.join("; ")}`);
|
|
89
|
-
}
|
|
90
|
-
const evidenceKinds = new Set((controllerReceipt.evidence || []).map((entry) => entry.kind));
|
|
91
|
-
for (const kind of ["publication-manifest", "publication-passport"]) {
|
|
92
|
-
if (!evidenceKinds.has(kind)) throw new Error(`publication artifact controller receipt is missing ${kind} evidence`);
|
|
93
|
-
}
|
|
94
|
-
const normalizedFiles = files.map((entry, index) => {
|
|
95
|
-
const filePath = requiredString(entry.path, `files[${index}].path`).replaceAll("\\", "/");
|
|
96
|
-
if (filePath.startsWith("/") || filePath.split("/").includes("..")) {
|
|
97
|
-
throw new Error(`publication artifact candidate contains an unsafe path: ${filePath}`);
|
|
98
|
-
}
|
|
99
|
-
const size = Number(entry.size ?? entry.bytes);
|
|
100
|
-
if (!Number.isSafeInteger(size) || size < 0) throw new Error(`files[${index}].size must be a non-negative safe integer`);
|
|
101
|
-
return { path: filePath, size, sha256: normalizeDigest(entry.sha256, `files[${index}].sha256`) };
|
|
102
|
-
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
103
|
-
if (new Set(normalizedFiles.map((entry) => entry.path)).size !== normalizedFiles.length) {
|
|
104
|
-
throw new Error("publication artifact candidate file paths must be unique");
|
|
105
|
-
}
|
|
106
|
-
const byPath = new Map(normalizedFiles.map((entry) => [entry.path, entry]));
|
|
107
|
-
for (const [index, artifact] of (manifest.artifacts || []).entries()) {
|
|
108
|
-
const artifactPath = requiredString(artifact.path, `manifest.artifacts[${index}].path`);
|
|
109
|
-
const actual = byPath.get(artifactPath);
|
|
110
|
-
if (!actual) throw new Error(`publication artifact candidate is missing declared artifact: ${artifactPath}`);
|
|
111
|
-
if (actual.size !== Number(artifact.bytes) || actual.sha256 !== normalizeDigest(artifact.sha256, `manifest.artifacts[${index}].sha256`)) {
|
|
112
|
-
throw new Error(`publication artifact candidate bytes do not match manifest: ${artifactPath}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
const payload = {
|
|
116
|
-
schemaVersion: 1,
|
|
117
|
-
contract: PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
|
|
118
|
-
repository: normalizedRepository,
|
|
119
|
-
sourceSha: normalizedSourceSha,
|
|
120
|
-
sourceTreeSha: normalizedSourceTreeSha,
|
|
121
|
-
runtimeSha: normalizedRuntimeSha,
|
|
122
|
-
manifestDigest,
|
|
123
|
-
passportDigest: publicationArtifactCandidateDigest(passport),
|
|
124
|
-
controllerReceiptDigest: normalizeDigest(controllerReceipt.digest, "controllerReceipt.digest"),
|
|
125
|
-
files: normalizedFiles,
|
|
126
|
-
};
|
|
127
|
-
return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
|
|
128
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
createPublicationAdmission,
|
|
9
|
-
createPublicationGateDecision,
|
|
10
|
-
createRunnerProvenance,
|
|
11
|
-
} from "../packages/core/publication-authority.js";
|
|
12
|
-
import { buildPublicationArtifactCandidate } from "./publication-artifact-candidate.mjs";
|
|
13
|
-
|
|
14
|
-
function required(name) {
|
|
15
|
-
const value = String(process.env[name] || "").trim();
|
|
16
|
-
if (!value) throw new Error(`${name} is required`);
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function sha256(value) {
|
|
21
|
-
return crypto.createHash("sha256").update(String(value)).digest("hex");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function sourceTree(repository, sourceSha, token) {
|
|
25
|
-
const response = await fetch(
|
|
26
|
-
`${required("GITHUB_API_URL")}/repos/${repository}/git/commits/${sourceSha}`,
|
|
27
|
-
{
|
|
28
|
-
headers: {
|
|
29
|
-
accept: "application/vnd.github+json",
|
|
30
|
-
authorization: `Bearer ${token}`,
|
|
31
|
-
"x-github-api-version": "2022-11-28",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
);
|
|
35
|
-
if (!response.ok)
|
|
36
|
-
throw new Error(
|
|
37
|
-
`could not resolve admitted source tree: GitHub API ${response.status}`,
|
|
38
|
-
);
|
|
39
|
-
const commit = await response.json();
|
|
40
|
-
return String(commit.tree?.sha || "").toLowerCase();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function writeBundle(outputDir, values) {
|
|
44
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
45
|
-
for (const [name, value] of Object.entries(values)) {
|
|
46
|
-
fs.writeFileSync(
|
|
47
|
-
path.join(outputDir, `${name}.json`),
|
|
48
|
-
`${JSON.stringify(value, null, 2)}\n`,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (process.env.GITHUB_OUTPUT) {
|
|
52
|
-
const output = fs.createWriteStream(process.env.GITHUB_OUTPUT, {
|
|
53
|
-
flags: "a",
|
|
54
|
-
});
|
|
55
|
-
for (const [name, value] of Object.entries(values)) {
|
|
56
|
-
if (name === "candidate") continue;
|
|
57
|
-
output.write(
|
|
58
|
-
`${name.replaceAll("_", "-")}-json=${JSON.stringify(value)}\n`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
output.end();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function main() {
|
|
66
|
-
const repository = required("BUILDCHAIN_REPOSITORY");
|
|
67
|
-
const sourceSha = required("BUILDCHAIN_SOURCE_SHA").toLowerCase();
|
|
68
|
-
const runtimeRoot =
|
|
69
|
-
process.env.BUILDCHAIN_RUNTIME_ROOT || ".buildchain/authority-runtime";
|
|
70
|
-
const runtimeSha = execFileSync(
|
|
71
|
-
"git",
|
|
72
|
-
["-C", runtimeRoot, "rev-parse", "HEAD"],
|
|
73
|
-
{ encoding: "utf8" },
|
|
74
|
-
)
|
|
75
|
-
.trim()
|
|
76
|
-
.toLowerCase();
|
|
77
|
-
const token = required("GITHUB_TOKEN");
|
|
78
|
-
const treeSha = await sourceTree(repository, sourceSha, token);
|
|
79
|
-
const candidateBundle = buildPublicationArtifactCandidate({
|
|
80
|
-
artifactRoot:
|
|
81
|
-
process.env.BUILDCHAIN_ARTIFACT_ROOT ||
|
|
82
|
-
".buildchain/publication-evidence/artifact",
|
|
83
|
-
controllerRoot:
|
|
84
|
-
process.env.BUILDCHAIN_CONTROLLER_ROOT ||
|
|
85
|
-
".buildchain/publication-evidence/controller",
|
|
86
|
-
repository,
|
|
87
|
-
sourceSha,
|
|
88
|
-
sourceTreeSha: treeSha,
|
|
89
|
-
runtimeSha,
|
|
90
|
-
});
|
|
91
|
-
const controllerReceipt = candidateBundle.evidence.controllerReceipt;
|
|
92
|
-
const controlPlaneAudit = JSON.parse(
|
|
93
|
-
fs.readFileSync(required("BUILDCHAIN_CONTROL_PLANE_AUDIT_PATH"), "utf8"),
|
|
94
|
-
);
|
|
95
|
-
const registry = JSON.parse(
|
|
96
|
-
fs.readFileSync(
|
|
97
|
-
path.join(runtimeRoot, "dist/site/publication-authority-registry.json"),
|
|
98
|
-
"utf8",
|
|
99
|
-
),
|
|
100
|
-
);
|
|
101
|
-
const gateAggregate = createPublicationGateDecision({
|
|
102
|
-
sourceSha,
|
|
103
|
-
profile: process.env.BUILDCHAIN_GATE_PROFILE || "managed-paper-publication",
|
|
104
|
-
required: false,
|
|
105
|
-
rationale:
|
|
106
|
-
process.env.BUILDCHAIN_GATE_RATIONALE ||
|
|
107
|
-
"The managed paper repository declares no project-specific Shifu Gate registry.",
|
|
108
|
-
policy: { scope: "managed-paper-publication", repository },
|
|
109
|
-
});
|
|
110
|
-
const runnerProvenance = createRunnerProvenance({
|
|
111
|
-
runnerClass: "ephemeral",
|
|
112
|
-
os: required("RUNNER_OS"),
|
|
113
|
-
architecture: required("RUNNER_ARCH"),
|
|
114
|
-
imageDigest: sha256(
|
|
115
|
-
`${process.env.ImageOS || "unknown"}|${process.env.ImageVersion || "unknown"}`,
|
|
116
|
-
),
|
|
117
|
-
measurementDigest: sha256(
|
|
118
|
-
[
|
|
119
|
-
process.env.GITHUB_WORKFLOW,
|
|
120
|
-
process.env.GITHUB_JOB,
|
|
121
|
-
process.env.GITHUB_RUN_ID,
|
|
122
|
-
process.env.GITHUB_RUN_ATTEMPT,
|
|
123
|
-
process.env.RUNNER_ENVIRONMENT,
|
|
124
|
-
].join("|"),
|
|
125
|
-
),
|
|
126
|
-
isolation: "github-hosted-single-job",
|
|
127
|
-
});
|
|
128
|
-
const issuedAt = new Date();
|
|
129
|
-
const admission = createPublicationAdmission({
|
|
130
|
-
registryDigest: registry.registryDigest,
|
|
131
|
-
workflowPath:
|
|
132
|
-
process.env.BUILDCHAIN_AUTHORITY_WORKFLOW_PATH ||
|
|
133
|
-
".github/workflows/paper-release-sealed.yml",
|
|
134
|
-
publisherWorkflowPath: required("BUILDCHAIN_PUBLISHER_WORKFLOW_PATH"),
|
|
135
|
-
repository,
|
|
136
|
-
sourceSha,
|
|
137
|
-
runtimeSha,
|
|
138
|
-
contractDigest: controllerReceipt.runtime?.contractDigest,
|
|
139
|
-
policyDigest: gateAggregate.policyDigest,
|
|
140
|
-
controllerReceiptDigest: controllerReceipt.digest,
|
|
141
|
-
runnerProvenanceDigest: runnerProvenance.receiptDigest,
|
|
142
|
-
controlPlaneAuditDigest: controlPlaneAudit.receiptDigest,
|
|
143
|
-
gateAggregateDigest: gateAggregate.digest,
|
|
144
|
-
environment: "none",
|
|
145
|
-
product: required("BUILDCHAIN_PUBLICATION_PRODUCT"),
|
|
146
|
-
target: required("BUILDCHAIN_PUBLICATION_TARGET"),
|
|
147
|
-
version: required("BUILDCHAIN_PUBLICATION_VERSION"),
|
|
148
|
-
channel: required("BUILDCHAIN_PUBLICATION_CHANNEL"),
|
|
149
|
-
artifactDigest: candidateBundle.candidate.candidateDigest,
|
|
150
|
-
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}:paper`,
|
|
151
|
-
issuedAt: issuedAt.toISOString(),
|
|
152
|
-
expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
|
|
153
|
-
});
|
|
154
|
-
const bindingNames = [
|
|
155
|
-
"repository",
|
|
156
|
-
"publisherWorkflowPath",
|
|
157
|
-
"sourceSha",
|
|
158
|
-
"runtimeSha",
|
|
159
|
-
"contractDigest",
|
|
160
|
-
"policyDigest",
|
|
161
|
-
"controllerReceiptDigest",
|
|
162
|
-
"gateAggregateDigest",
|
|
163
|
-
"environment",
|
|
164
|
-
"product",
|
|
165
|
-
"target",
|
|
166
|
-
"version",
|
|
167
|
-
"channel",
|
|
168
|
-
"artifactDigest",
|
|
169
|
-
];
|
|
170
|
-
const expected = Object.fromEntries(
|
|
171
|
-
bindingNames.map((name) => [name, admission[name]]),
|
|
172
|
-
);
|
|
173
|
-
writeBundle(
|
|
174
|
-
process.env.BUILDCHAIN_OUTPUT_DIR ||
|
|
175
|
-
".buildchain/publication-authority/auto",
|
|
176
|
-
{
|
|
177
|
-
admission,
|
|
178
|
-
runner_provenance: runnerProvenance,
|
|
179
|
-
control_plane_audit: controlPlaneAudit,
|
|
180
|
-
gate_aggregate: gateAggregate,
|
|
181
|
-
expected,
|
|
182
|
-
candidate: candidateBundle.candidate,
|
|
183
|
-
},
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
main().catch((error) => {
|
|
188
|
-
console.error(`assemble publication artifact admission: ${error.message}`);
|
|
189
|
-
process.exitCode = 1;
|
|
190
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
createPublicationArtifactCandidate,
|
|
9
|
-
resolvePublicationCandidateFile,
|
|
10
|
-
} from "../packages/core/publication-artifact-candidate.js";
|
|
11
|
-
|
|
12
|
-
export { resolvePublicationCandidateFile };
|
|
13
|
-
|
|
14
|
-
function flag(name, fallback = "") {
|
|
15
|
-
const index = process.argv.indexOf(`--${name}`);
|
|
16
|
-
return index === -1 ? fallback : String(process.argv[index + 1] || "");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function requiredFlag(name) {
|
|
20
|
-
const value = flag(name).trim();
|
|
21
|
-
if (!value) throw new Error(`--${name} is required`);
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function exactJson(root, relativePath) {
|
|
26
|
-
const absoluteRoot = path.resolve(root);
|
|
27
|
-
const absolutePath = path.resolve(absoluteRoot, relativePath);
|
|
28
|
-
if (!absolutePath.startsWith(`${absoluteRoot}${path.sep}`)) {
|
|
29
|
-
throw new Error(`publication evidence path escapes artifact root: ${relativePath}`);
|
|
30
|
-
}
|
|
31
|
-
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
32
|
-
throw new Error(`expected publication evidence at ${relativePath}`);
|
|
33
|
-
}
|
|
34
|
-
return JSON.parse(fs.readFileSync(absolutePath, "utf8"));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function collectFiles(root) {
|
|
38
|
-
const absoluteRoot = path.resolve(root);
|
|
39
|
-
const files = [];
|
|
40
|
-
const pending = [absoluteRoot];
|
|
41
|
-
while (pending.length > 0) {
|
|
42
|
-
const current = pending.pop();
|
|
43
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
44
|
-
const full = path.join(current, entry.name);
|
|
45
|
-
if (entry.isDirectory()) pending.push(full);
|
|
46
|
-
else if (entry.isFile()) {
|
|
47
|
-
files.push({
|
|
48
|
-
path: path.relative(absoluteRoot, full).split(path.sep).join("/"),
|
|
49
|
-
size: fs.statSync(full).size,
|
|
50
|
-
sha256: crypto
|
|
51
|
-
.createHash("sha256")
|
|
52
|
-
.update(fs.readFileSync(full))
|
|
53
|
-
.digest("hex"),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function buildPublicationArtifactCandidate({
|
|
62
|
-
artifactRoot,
|
|
63
|
-
controllerRoot,
|
|
64
|
-
repository,
|
|
65
|
-
sourceSha,
|
|
66
|
-
sourceTreeSha,
|
|
67
|
-
runtimeSha,
|
|
68
|
-
} = {}) {
|
|
69
|
-
const resolvedArtifactRoot = path.resolve(artifactRoot);
|
|
70
|
-
const resolvedControllerRoot = path.resolve(controllerRoot);
|
|
71
|
-
const evidence = {
|
|
72
|
-
repository,
|
|
73
|
-
sourceSha,
|
|
74
|
-
sourceTreeSha,
|
|
75
|
-
runtimeSha,
|
|
76
|
-
manifest: exactJson(
|
|
77
|
-
resolvedArtifactRoot,
|
|
78
|
-
".buildchain/publication/publication-artifact.json",
|
|
79
|
-
),
|
|
80
|
-
passport: exactJson(
|
|
81
|
-
resolvedArtifactRoot,
|
|
82
|
-
".buildchain/publication/publication-artifact-passport.json",
|
|
83
|
-
),
|
|
84
|
-
controllerReceipt: exactJson(resolvedControllerRoot, "receipt.json"),
|
|
85
|
-
files: collectFiles(resolvedArtifactRoot),
|
|
86
|
-
};
|
|
87
|
-
const candidate = createPublicationArtifactCandidate(evidence);
|
|
88
|
-
return { schemaVersion: 1, candidate, evidence };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function main() {
|
|
92
|
-
const result = buildPublicationArtifactCandidate({
|
|
93
|
-
artifactRoot: requiredFlag("artifact-root"),
|
|
94
|
-
controllerRoot: requiredFlag("controller-root"),
|
|
95
|
-
repository: requiredFlag("repository"),
|
|
96
|
-
sourceSha: requiredFlag("source-sha"),
|
|
97
|
-
sourceTreeSha: requiredFlag("source-tree-sha"),
|
|
98
|
-
runtimeSha: requiredFlag("runtime-sha"),
|
|
99
|
-
});
|
|
100
|
-
const output = flag("output");
|
|
101
|
-
if (output) {
|
|
102
|
-
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
103
|
-
fs.writeFileSync(
|
|
104
|
-
path.resolve(output),
|
|
105
|
-
`${JSON.stringify(result, null, 2)}\n`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
if (process.argv.includes("--json") || !output)
|
|
109
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
process.argv[1] &&
|
|
114
|
-
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
115
|
-
) {
|
|
116
|
-
try {
|
|
117
|
-
main();
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error(`publication artifact candidate: ${error.message}`);
|
|
120
|
-
process.exitCode = 1;
|
|
121
|
-
}
|
|
122
|
-
}
|