@kungfu-tech/buildchain 2.12.7-alpha.6 → 2.12.7-alpha.8
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/dist/site/buildchain-contract.json +34 -14
- package/dist/site/buildchain-site.json +6 -6
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/controller-registry.json +18 -2
- package/dist/site/kfd-claims.json +56 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/node-api-registry.json +5 -5
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +63 -7
- package/dist/site/publication-authority-registry.json +25 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +4 -4
- package/dist/site/workflow-registry.json +63 -4
- package/docs/publication-artifacts.md +17 -19
- package/package.json +1 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/index.js +6 -0
- package/packages/core/publication-artifact-candidate.js +116 -0
- package/packages/core/publication-authority.js +41 -2
- package/packages/core/publication-control-plane-audit.js +3 -1
- package/scripts/assemble-publication-artifact-admission.mjs +190 -0
- package/scripts/audit-publication-control-plane.mjs +5 -2
- package/scripts/publication-artifact-candidate.mjs +125 -0
|
@@ -16,6 +16,7 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
16
16
|
branch,
|
|
17
17
|
packageName,
|
|
18
18
|
publisherMode = "npm-trusted-publisher",
|
|
19
|
+
requiredStatusCheck = "check",
|
|
19
20
|
snapshot,
|
|
20
21
|
observedAt,
|
|
21
22
|
expiresAt,
|
|
@@ -72,7 +73,8 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
72
73
|
branchPolicy.protected === true &&
|
|
73
74
|
branchPolicy.enforcementLevel === "everyone" &&
|
|
74
75
|
Array.isArray(branchPolicy.requiredStatusChecks) &&
|
|
75
|
-
branchPolicy.
|
|
76
|
+
branchPolicy.requiredStatusCheck === requiredStatusCheck &&
|
|
77
|
+
branchPolicy.requiredStatusChecks.includes(requiredStatusCheck) &&
|
|
76
78
|
branchPolicy.requiredCheckPassed === true &&
|
|
77
79
|
branchPolicy.sourceSha === branchPolicy.headSha &&
|
|
78
80
|
branchPolicy.mergedPullRequest === true &&
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
});
|
|
@@ -191,6 +191,7 @@ function main() {
|
|
|
191
191
|
const workflowPath = flag("workflow", ".github/workflows/release-candidate-promote.yml");
|
|
192
192
|
const workflowRef = flag("workflow-ref");
|
|
193
193
|
const publisherWorkflowPath = flag("publisher-workflow", workflowPath);
|
|
194
|
+
const requiredStatusCheck = flag("required-status-check", "check");
|
|
194
195
|
const jobId = flag("job", "promote");
|
|
195
196
|
const environment = flag("environment", "none");
|
|
196
197
|
const providerEnvironment = environment === "none" ? "" : environment;
|
|
@@ -280,15 +281,16 @@ function main() {
|
|
|
280
281
|
const checkRuns = githubJson(`repos/${repository}/commits/${sourceSha}/check-runs?per_page=100`, "source check runs");
|
|
281
282
|
const requiredStatusCheckPolicy = branchState.protection?.required_status_checks || {};
|
|
282
283
|
const requiredStatusChecks = requiredStatusCheckPolicy.contexts || [];
|
|
283
|
-
const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) => entry.context ===
|
|
284
|
+
const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) => entry.context === requiredStatusCheck);
|
|
284
285
|
branchPolicy = {
|
|
285
286
|
ref: branch,
|
|
286
287
|
policyMode: "provider-enforced-transaction",
|
|
287
288
|
protected: branchState.protected === true,
|
|
288
289
|
enforcementLevel: branchState.protection?.required_status_checks?.enforcement_level || "",
|
|
289
290
|
requiredStatusChecks,
|
|
291
|
+
requiredStatusCheck,
|
|
290
292
|
requiredCheckPassed: (checkRuns.check_runs || []).some((entry) =>
|
|
291
|
-
entry.name ===
|
|
293
|
+
entry.name === requiredStatusCheck &&
|
|
292
294
|
entry.conclusion === "success" &&
|
|
293
295
|
(!requiredCheckSource?.app_id || entry.app?.id === requiredCheckSource.app_id)
|
|
294
296
|
),
|
|
@@ -350,6 +352,7 @@ function main() {
|
|
|
350
352
|
branch,
|
|
351
353
|
packageName,
|
|
352
354
|
publisherMode,
|
|
355
|
+
requiredStatusCheck,
|
|
353
356
|
observedAt: observedAt.toISOString(),
|
|
354
357
|
expiresAt: expiresAt.toISOString(),
|
|
355
358
|
snapshot: {
|
|
@@ -0,0 +1,125 @@
|
|
|
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 { createPublicationArtifactCandidate } from "../packages/core/publication-artifact-candidate.js";
|
|
8
|
+
|
|
9
|
+
function flag(name, fallback = "") {
|
|
10
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
11
|
+
return index === -1 ? fallback : String(process.argv[index + 1] || "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requiredFlag(name) {
|
|
15
|
+
const value = flag(name).trim();
|
|
16
|
+
if (!value) throw new Error(`--${name} is required`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function filesNamed(root, name) {
|
|
21
|
+
const matches = [];
|
|
22
|
+
const pending = [path.resolve(root)];
|
|
23
|
+
while (pending.length > 0) {
|
|
24
|
+
const current = pending.pop();
|
|
25
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
26
|
+
const full = path.join(current, entry.name);
|
|
27
|
+
if (entry.isDirectory()) pending.push(full);
|
|
28
|
+
else if (entry.isFile() && entry.name === name) matches.push(full);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return matches.sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function oneJson(root, name) {
|
|
35
|
+
const matches = filesNamed(root, name);
|
|
36
|
+
if (matches.length !== 1)
|
|
37
|
+
throw new Error(
|
|
38
|
+
`expected exactly one ${name} under ${root}, found ${matches.length}`,
|
|
39
|
+
);
|
|
40
|
+
return JSON.parse(fs.readFileSync(matches[0], "utf8"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function collectFiles(root) {
|
|
44
|
+
const absoluteRoot = path.resolve(root);
|
|
45
|
+
const files = [];
|
|
46
|
+
const pending = [absoluteRoot];
|
|
47
|
+
while (pending.length > 0) {
|
|
48
|
+
const current = pending.pop();
|
|
49
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
50
|
+
const full = path.join(current, entry.name);
|
|
51
|
+
if (entry.isDirectory()) pending.push(full);
|
|
52
|
+
else if (entry.isFile()) {
|
|
53
|
+
files.push({
|
|
54
|
+
path: path.relative(absoluteRoot, full).split(path.sep).join("/"),
|
|
55
|
+
size: fs.statSync(full).size,
|
|
56
|
+
sha256: crypto
|
|
57
|
+
.createHash("sha256")
|
|
58
|
+
.update(fs.readFileSync(full))
|
|
59
|
+
.digest("hex"),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildPublicationArtifactCandidate({
|
|
68
|
+
artifactRoot,
|
|
69
|
+
controllerRoot,
|
|
70
|
+
repository,
|
|
71
|
+
sourceSha,
|
|
72
|
+
sourceTreeSha,
|
|
73
|
+
runtimeSha,
|
|
74
|
+
} = {}) {
|
|
75
|
+
const resolvedArtifactRoot = path.resolve(artifactRoot);
|
|
76
|
+
const resolvedControllerRoot = path.resolve(controllerRoot);
|
|
77
|
+
const evidence = {
|
|
78
|
+
repository,
|
|
79
|
+
sourceSha,
|
|
80
|
+
sourceTreeSha,
|
|
81
|
+
runtimeSha,
|
|
82
|
+
manifest: oneJson(resolvedArtifactRoot, "publication-artifact.json"),
|
|
83
|
+
passport: oneJson(
|
|
84
|
+
resolvedArtifactRoot,
|
|
85
|
+
"publication-artifact-passport.json",
|
|
86
|
+
),
|
|
87
|
+
controllerReceipt: oneJson(resolvedControllerRoot, "receipt.json"),
|
|
88
|
+
files: collectFiles(resolvedArtifactRoot),
|
|
89
|
+
};
|
|
90
|
+
const candidate = createPublicationArtifactCandidate(evidence);
|
|
91
|
+
return { schemaVersion: 1, candidate, evidence };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function main() {
|
|
95
|
+
const result = buildPublicationArtifactCandidate({
|
|
96
|
+
artifactRoot: requiredFlag("artifact-root"),
|
|
97
|
+
controllerRoot: requiredFlag("controller-root"),
|
|
98
|
+
repository: requiredFlag("repository"),
|
|
99
|
+
sourceSha: requiredFlag("source-sha"),
|
|
100
|
+
sourceTreeSha: requiredFlag("source-tree-sha"),
|
|
101
|
+
runtimeSha: requiredFlag("runtime-sha"),
|
|
102
|
+
});
|
|
103
|
+
const output = flag("output");
|
|
104
|
+
if (output) {
|
|
105
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.resolve(output),
|
|
108
|
+
`${JSON.stringify(result, null, 2)}\n`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (process.argv.includes("--json") || !output)
|
|
112
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
process.argv[1] &&
|
|
117
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
118
|
+
) {
|
|
119
|
+
try {
|
|
120
|
+
main();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`publication artifact candidate: ${error.message}`);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
}
|
|
125
|
+
}
|