@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.
@@ -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.requiredStatusChecks.includes("check") &&
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 === "check");
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 === "check" &&
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
+ }