@kungfu-tech/buildchain 2.10.9 → 2.10.10

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.
@@ -0,0 +1,245 @@
1
+ import crypto from "node:crypto";
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { loadBuildchainConfig } from "./buildchain-config.js";
6
+
7
+ export const PUBLICATION_ARTIFACT_MANIFEST_CONTRACT = "kungfu-buildchain-publication-artifact-manifest";
8
+ export const PUBLICATION_ARTIFACT_PASSPORT_CONTRACT = "kungfu-buildchain-publication-artifact-passport";
9
+
10
+ function toPosix(value) {
11
+ return String(value || "").split(path.sep).join("/");
12
+ }
13
+
14
+ function sha256Buffer(buffer) {
15
+ return crypto.createHash("sha256").update(buffer).digest("hex");
16
+ }
17
+
18
+ function sha256FilePath(filePath) {
19
+ return sha256Buffer(fs.readFileSync(filePath));
20
+ }
21
+
22
+ function repoRelative(cwd, filePath) {
23
+ const relative = path.relative(cwd, filePath);
24
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
25
+ ? toPosix(relative)
26
+ : toPosix(path.resolve(filePath));
27
+ }
28
+
29
+ function listFiles(cwd, relPath) {
30
+ const target = path.resolve(cwd, relPath);
31
+ if (!fs.existsSync(target)) {
32
+ return [];
33
+ }
34
+ const stat = fs.statSync(target);
35
+ if (stat.isFile()) {
36
+ return [target];
37
+ }
38
+ if (!stat.isDirectory()) {
39
+ return [];
40
+ }
41
+ return fs
42
+ .readdirSync(target, { withFileTypes: true })
43
+ .flatMap((entry) => listFiles(cwd, path.join(repoRelative(cwd, target), entry.name)));
44
+ }
45
+
46
+ function collectFiles(cwd, paths = []) {
47
+ const files = new Map();
48
+ for (const relPath of paths) {
49
+ for (const filePath of listFiles(cwd, relPath)) {
50
+ const relative = repoRelative(cwd, filePath);
51
+ files.set(relative, {
52
+ path: relative,
53
+ bytes: fs.statSync(filePath).size,
54
+ sha256: sha256FilePath(filePath),
55
+ });
56
+ }
57
+ }
58
+ return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
59
+ }
60
+
61
+ function gitValue(cwd, args, fallback = "") {
62
+ try {
63
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
64
+ } catch {
65
+ return fallback;
66
+ }
67
+ }
68
+
69
+ function createdAt(now = new Date()) {
70
+ const sourceDateEpoch = process.env.SOURCE_DATE_EPOCH || "";
71
+ if (/^\d+$/.test(sourceDateEpoch)) {
72
+ return new Date(Number(sourceDateEpoch) * 1000).toISOString();
73
+ }
74
+ return now.toISOString();
75
+ }
76
+
77
+ export function createPublicationSourceBundle({
78
+ cwd = process.cwd(),
79
+ sourcePaths = [],
80
+ output = ".buildchain/publication/source.tar.gz",
81
+ } = {}) {
82
+ const outputPath = path.resolve(cwd, output);
83
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
84
+ const paths = sourcePaths.length > 0 ? sourcePaths : ["."];
85
+ execFileSync("git", [
86
+ "archive",
87
+ "--format=tar.gz",
88
+ `--output=${outputPath}`,
89
+ "HEAD",
90
+ ...paths,
91
+ ], { cwd, stdio: "pipe" });
92
+ return {
93
+ path: toPosix(path.relative(cwd, outputPath)),
94
+ bytes: fs.statSync(outputPath).size,
95
+ sha256: sha256FilePath(outputPath),
96
+ };
97
+ }
98
+
99
+ export function collectPublicationArtifact({
100
+ cwd = process.cwd(),
101
+ sourceSha = "",
102
+ sourceBundle = true,
103
+ sourceBundlePath = "",
104
+ generatedAt = "",
105
+ } = {}) {
106
+ const loaded = loadBuildchainConfig(cwd);
107
+ if (!loaded?.config?.publication) {
108
+ throw new Error("publication artifact manifest requires [publication] in .buildchain/buildchain.toml");
109
+ }
110
+ if (loaded.config.project?.type !== "publication-artifact") {
111
+ throw new Error('publication artifact manifest requires project.type = "publication-artifact"');
112
+ }
113
+ const publication = loaded.config.publication;
114
+ const artifactPaths = [publication.primaryArtifact, ...publication.artifactPaths]
115
+ .filter(Boolean)
116
+ .filter((value, index, list) => list.indexOf(value) === index);
117
+ const artifacts = collectFiles(cwd, artifactPaths);
118
+ if (!artifacts.find((artifact) => artifact.path === publication.primaryArtifact)) {
119
+ throw new Error(`publication primary artifact is missing: ${publication.primaryArtifact}`);
120
+ }
121
+ const metadata = collectFiles(cwd, publication.metadataPaths);
122
+ const sourceFiles = collectFiles(cwd, publication.sourcePaths);
123
+ const bundle = sourceBundle
124
+ ? createPublicationSourceBundle({
125
+ cwd,
126
+ sourcePaths: publication.sourcePaths,
127
+ output: sourceBundlePath || publication.sourceBundlePath,
128
+ })
129
+ : undefined;
130
+ const resolvedSourceSha = sourceSha || gitValue(cwd, ["rev-parse", "HEAD"]);
131
+ const sourceTreeSha = gitValue(cwd, ["rev-parse", `${resolvedSourceSha}^{tree}`]);
132
+ const timestamp = generatedAt || createdAt();
133
+ const manifest = {
134
+ schemaVersion: 1,
135
+ contract: PUBLICATION_ARTIFACT_MANIFEST_CONTRACT,
136
+ project: {
137
+ name: loaded.config.project?.name || path.basename(path.resolve(cwd)),
138
+ type: "publication-artifact",
139
+ },
140
+ publication: {
141
+ kind: publication.kind,
142
+ title: publication.title,
143
+ version: publication.version,
144
+ abstract: publication.abstract,
145
+ authors: publication.authors,
146
+ primaryArtifact: publication.primaryArtifact,
147
+ siteConsumers: publication.siteConsumers,
148
+ },
149
+ source: {
150
+ repository: gitValue(cwd, ["config", "--get", "remote.origin.url"]),
151
+ sha: resolvedSourceSha,
152
+ treeSha: sourceTreeSha,
153
+ sourcePaths: publication.sourcePaths,
154
+ sourceFiles,
155
+ sourceBundle: bundle,
156
+ },
157
+ artifacts,
158
+ metadata,
159
+ generatedAt: timestamp,
160
+ publishedAt: timestamp,
161
+ reproducible: true,
162
+ timestampPolicy: "ci-injected",
163
+ deterministicInputs: [
164
+ "publication source paths",
165
+ "publication metadata paths",
166
+ "publication primary artifact",
167
+ "Buildchain publication contract",
168
+ "source SHA",
169
+ "source tree SHA",
170
+ ],
171
+ timestampPolicyDetails: {
172
+ contract: "kungfu-buildchain-surface-timestamp-policy",
173
+ timestampFields: ["generatedAt", "publishedAt"],
174
+ timestampFieldsParticipateInArtifactDigest: false,
175
+ artifactDigestScope: "publication artifact digests cover files and source bundle, not manifest timestamps",
176
+ },
177
+ };
178
+ return {
179
+ manifest,
180
+ passport: {
181
+ schemaVersion: 1,
182
+ contract: PUBLICATION_ARTIFACT_PASSPORT_CONTRACT,
183
+ status: "passed",
184
+ manifestDigest: `sha256:${sha256Buffer(Buffer.from(JSON.stringify(manifest, null, 2)))}`,
185
+ source: manifest.source,
186
+ artifacts: manifest.artifacts,
187
+ responsibility: {
188
+ producer: "publication repository",
189
+ renderer: "site repository",
190
+ buildchain: "artifact contract, manifest, source bundle, and evidence generation",
191
+ },
192
+ auditBoundary: {
193
+ machineVerified: [
194
+ "declared publication artifact files exist",
195
+ "declared publication metadata files exist",
196
+ "declared publication source files are hashed",
197
+ "source bundle digest is recorded",
198
+ ],
199
+ outsideBoundary: [
200
+ "paper scientific claims",
201
+ "bibliography quality",
202
+ "human review of publication content",
203
+ ],
204
+ },
205
+ residualRisk: [
206
+ {
207
+ riskType: "natural-language-semantic-risk",
208
+ trustImpact: "downgrade-warning",
209
+ machineProvability: "not-machine-verifiable",
210
+ agentAction: "semantic-review-required",
211
+ note: "Buildchain verifies declared files and hashes; it does not peer-review paper claims.",
212
+ },
213
+ ],
214
+ },
215
+ };
216
+ }
217
+
218
+ export function writePublicationArtifact({
219
+ cwd = process.cwd(),
220
+ output = "",
221
+ passportOutput = "",
222
+ sourceSha = "",
223
+ sourceBundle = true,
224
+ sourceBundlePath = "",
225
+ generatedAt = "",
226
+ } = {}) {
227
+ const collected = collectPublicationArtifact({
228
+ cwd,
229
+ sourceSha,
230
+ sourceBundle,
231
+ sourceBundlePath,
232
+ generatedAt,
233
+ });
234
+ const manifestOutput = output || loadBuildchainConfig(cwd).config.publication.manifestPath;
235
+ const passportPath = passportOutput || ".buildchain/publication/publication-artifact-passport.json";
236
+ fs.mkdirSync(path.dirname(path.resolve(cwd, manifestOutput)), { recursive: true });
237
+ fs.mkdirSync(path.dirname(path.resolve(cwd, passportPath)), { recursive: true });
238
+ fs.writeFileSync(path.resolve(cwd, manifestOutput), `${JSON.stringify(collected.manifest, null, 2)}\n`);
239
+ fs.writeFileSync(path.resolve(cwd, passportPath), `${JSON.stringify(collected.passport, null, 2)}\n`);
240
+ return {
241
+ ...collected,
242
+ manifestPath: toPosix(manifestOutput),
243
+ passportPath: toPosix(passportPath),
244
+ };
245
+ }
@@ -325,6 +325,7 @@ const manualMetaById = new Map(Object.entries({
325
325
  "site-bundle-contract": { capabilityGroup: "site-and-propagation", audience: ["site", "agent"], maturity: "stable", order: 400 },
326
326
  "web-surface-deployments": { capabilityGroup: "site-and-propagation", audience: ["site", "release-operator"], maturity: "stable", order: 410 },
327
327
  "release-propagation": { capabilityGroup: "site-and-propagation", audience: ["release-operator", "agent"], maturity: "preview", order: 420 },
328
+ "publication-artifacts": { capabilityGroup: "reusable-build", audience: ["consumer", "site", "agent"], maturity: "stable", order: 430 },
328
329
  "readme-badges": { capabilityGroup: "distribution-indexes", audience: ["consumer", "site"], maturity: "stable", order: 500 },
329
330
  homebrew: { capabilityGroup: "distribution-indexes", audience: ["consumer", "release-operator"], maturity: "stable", order: 510 },
330
331
  "build-facts": { capabilityGroup: "observability-diagnostics", audience: ["maintainer", "agent"], maturity: "stable", order: 600 },
@@ -427,6 +428,8 @@ function cliCommandMeta(id) {
427
428
  npm: { group: "release-passport-trust", purpose: "Inspect npm publishing command families." },
428
429
  "npm-dry-run": { group: "release-passport-trust", purpose: "Verify npm publish shape before a release transaction." },
429
430
  "publish-source": { group: "release-passport-trust", purpose: "Create, inspect, or verify publish-gate source-lock refs." },
431
+ "publication-artifact": { group: "reusable-build", purpose: "Generate publication artifact manifests, passports, and source bundles for paper/report repositories." },
432
+ "publication-artifact-manifest": { group: "reusable-build", purpose: "Write a site-consumable publication artifact manifest, publication passport, and source bundle." },
430
433
  "release-dry-run": { group: "governance-versioning", purpose: "Explain what a channel merge would publish before the PR is merged." },
431
434
  "release-line-open": { group: "governance-versioning", purpose: "Plan or write the initial version-state commit for a new minor release line." },
432
435
  "release-propagation": { group: "site-and-propagation", purpose: "Plan channel-preserving downstream release PRs and write exact upstream release locks." },
@@ -461,6 +464,7 @@ function nodeApiMeta(exportName) {
461
464
  "./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
462
465
  "./diagnostics": { group: "observability-diagnostics", summary: "Native diagnostics collection, summarization, cache, compiler, and process-sampler APIs." },
463
466
  "./logging": { group: "observability-diagnostics", summary: "Buildchain JSONL logging, span, summary, and verification APIs." },
467
+ "./publication-artifact": { group: "reusable-build", summary: "Publication artifact manifest, source bundle, and publication passport APIs." },
464
468
  "./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
465
469
  "./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
466
470
  "./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
@@ -531,7 +535,7 @@ function buildCapabilityRegistry({ docs, pages, cliRegistry, manualRegistry, nod
531
535
 
532
536
  function workflowCapabilityGroup(entry) {
533
537
  if (["web-surface", "release-propagation"].includes(entry.id)) return capabilityGroup("site-and-propagation");
534
- if (["build", "release-candidate-promote"].includes(entry.id)) return capabilityGroup("reusable-build");
538
+ if (["build", "release-candidate-promote", "publication-artifact"].includes(entry.id)) return capabilityGroup("reusable-build");
535
539
  if (["buildchain-ref-promotion", "release-line-bootstrap"].includes(entry.id)) return capabilityGroup("release-passport-trust");
536
540
  if (entry.id.includes("patrol") || entry.id.includes("dev-pr-auto-merge")) return capabilityGroup("governance-versioning");
537
541
  if (entry.status === "repository-internal" || entry.status === "compatibility-fixture") return capabilityGroup("api-cli-reference");
@@ -239,6 +239,33 @@ command = "buildchain infra-contract --mode ci"
239
239
  `;
240
240
  }
241
241
 
242
+ function publicationArtifactToml(cwd) {
243
+ const name = repoName(cwd);
244
+ return `schema = 1
245
+
246
+ [project]
247
+ type = "publication-artifact"
248
+ name = "${name}"
249
+
250
+ [publication]
251
+ kind = "paper"
252
+ title = "${name}"
253
+ primary_artifact = "_build/main.pdf"
254
+ artifact_paths = ["_build/main.pdf"]
255
+ metadata_paths = ["README.md", "docs/MAP.md"]
256
+ source_paths = ["paper", "README.md", "LICENSE", "Makefile"]
257
+ site_consumers = ["papers-site"]
258
+ manifest_path = ".buildchain/publication/publication-artifact.json"
259
+ source_bundle_path = ".buildchain/publication/source.tar.gz"
260
+
261
+ [lifecycle.build]
262
+ command = "make pdf"
263
+
264
+ [lifecycle.verify]
265
+ command = "make check"
266
+ `;
267
+ }
268
+
242
269
  function infraContractDesiredJson(cwd) {
243
270
  return `${JSON.stringify({
244
271
  service: repoName(cwd),
@@ -280,10 +307,48 @@ function workflowArtifactPaths(type) {
280
307
  .buildchain/infra-contract-evidence-bundle.json
281
308
  .buildchain/infra-contract-evidence-verification.json`;
282
309
  }
310
+ if (type === "publication-artifact") {
311
+ return `_build/main.pdf
312
+ .buildchain/publication/publication-artifact.json
313
+ .buildchain/publication/publication-artifact-passport.json
314
+ .buildchain/publication/source.tar.gz`;
315
+ }
283
316
  return `dist
284
317
  build/stage`;
285
318
  }
286
319
 
320
+ function publicationWorkflowYaml() {
321
+ return `name: Build
322
+
323
+ on:
324
+ workflow_dispatch:
325
+ inputs:
326
+ buildchain-ref:
327
+ description: "Temporary Buildchain runtime ref for trusted manual validation"
328
+ required: false
329
+ default: ""
330
+ pull_request:
331
+ push:
332
+ branches:
333
+ - "dev/**"
334
+ - "alpha/**"
335
+ - "release/**"
336
+
337
+ permissions:
338
+ contents: read
339
+ issues: write
340
+
341
+ jobs:
342
+ publication:
343
+ uses: kungfu-systems/buildchain/.github/workflows/publication-artifact.yml@v2
344
+ with:
345
+ buildchain-ref: \${{ inputs.buildchain-ref || '' }}
346
+ build-command: make pdf
347
+ verify-command: make check
348
+ artifact-name: publication-artifact
349
+ `;
350
+ }
351
+
287
352
  function workflowYaml({ type, runnerPreset, artifactName }) {
288
353
  return `name: Build
289
354
 
@@ -350,19 +415,28 @@ export function initBuildchainRepo({
350
415
  if (type === "infra-contract") {
351
416
  return infraContractToml(resolvedCwd);
352
417
  }
418
+ if (type === "publication-artifact") {
419
+ return publicationArtifactToml(resolvedCwd);
420
+ }
353
421
  if (type === "anchored-package") {
354
422
  return anchoredPackageToml(resolvedCwd, manager);
355
423
  }
356
- throw new Error("init --type must be one of package, native, web-surface, infra-contract, or anchored-package");
424
+ throw new Error("init --type must be one of package, native, web-surface, infra-contract, publication-artifact, or anchored-package");
357
425
  })();
358
426
 
359
427
  const written = [
360
428
  writeIfAllowed(path.join(resolvedCwd, BUILDCHAIN_CONFIG_PATH), toml, { force }),
361
- writeIfAllowed(path.join(resolvedCwd, ".github", "workflows", "build.yml"), workflowYaml({
362
- type,
363
- runnerPreset,
364
- artifactName,
365
- }), { force }),
429
+ writeIfAllowed(
430
+ path.join(resolvedCwd, ".github", "workflows", "build.yml"),
431
+ type === "publication-artifact"
432
+ ? publicationWorkflowYaml()
433
+ : workflowYaml({
434
+ type,
435
+ runnerPreset,
436
+ artifactName,
437
+ }),
438
+ { force },
439
+ ),
366
440
  ];
367
441
 
368
442
  if (type === "anchored-package" && !fs.existsSync(path.join(resolvedCwd, "release.json"))) {
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import { writePublicationArtifact } from "../packages/core/publication-artifact.js";
4
+
5
+ function readFlag(args, name, fallback = "") {
6
+ const index = args.indexOf(`--${name}`);
7
+ return index === -1 ? fallback : args[index + 1] || "";
8
+ }
9
+
10
+ function readBooleanFlag(args, name) {
11
+ return args.includes(`--${name}`);
12
+ }
13
+
14
+ export function runPublicationArtifactCli(args = process.argv.slice(2)) {
15
+ const [mode = "manifest"] = args;
16
+ if (!["manifest", "collect"].includes(mode)) {
17
+ throw new Error("usage: buildchain publication-artifact manifest [--cwd <dir>] [--source-sha <sha>] [--output <file>] [--passport-output <file>] [--source-bundle <file>] [--no-source-bundle] [--json]");
18
+ }
19
+ const result = writePublicationArtifact({
20
+ cwd: readFlag(args, "cwd", process.cwd()),
21
+ output: readFlag(args, "output", ""),
22
+ passportOutput: readFlag(args, "passport-output", ""),
23
+ sourceSha: readFlag(args, "source-sha", ""),
24
+ sourceBundlePath: readFlag(args, "source-bundle", ""),
25
+ sourceBundle: !readBooleanFlag(args, "no-source-bundle"),
26
+ generatedAt: readFlag(args, "generated-at", ""),
27
+ });
28
+ if (readBooleanFlag(args, "json")) {
29
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
30
+ } else {
31
+ process.stdout.write(`publication-manifest=${result.manifestPath}\n`);
32
+ process.stdout.write(`publication-passport=${result.passportPath}\n`);
33
+ process.stdout.write(`publication-primary-artifact=${result.manifest.publication.primaryArtifact}\n`);
34
+ }
35
+ return result;
36
+ }
37
+
38
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
39
+ try {
40
+ runPublicationArtifactCli();
41
+ } catch (error) {
42
+ console.error(`publication-artifact: ${error.message}`);
43
+ process.exitCode = 1;
44
+ }
45
+ }