@kungfu-tech/buildchain 2.10.11 → 2.11.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/bin/buildchain.mjs +2 -1
- package/dist/site/buildchain-contract.json +5 -5
- package/dist/site/buildchain-site.json +17 -17
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +5 -5
- package/dist/site/page-registry.json +10 -10
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/site-manifest.json +7 -7
- package/docs/MAP.md +5 -5
- package/docs/cli.md +5 -3
- package/docs/publication-artifacts.md +44 -3
- package/docs/release-propagation.md +50 -1
- package/package.json +1 -1
- package/packages/core/README.md +3 -1
- package/packages/core/buildchain-config.js +35 -0
- package/packages/core/index.js +2 -0
- package/packages/core/publication-artifact.js +294 -3
- package/packages/core/release-propagation.js +71 -2
- package/scripts/publication-artifact.mjs +5 -1
|
@@ -27,6 +27,7 @@ name = "paper-observer-declared-timelines"
|
|
|
27
27
|
[publication]
|
|
28
28
|
kind = "paper"
|
|
29
29
|
title = "Observer-Declared Timelines for Real-World Agent Work"
|
|
30
|
+
version = "0.1.0"
|
|
30
31
|
primary_artifact = "_build/main.pdf"
|
|
31
32
|
artifact_paths = ["_build/main.pdf"]
|
|
32
33
|
metadata_paths = ["README.md", "docs/MAP.md"]
|
|
@@ -35,6 +36,14 @@ site_consumers = ["papers.libkungfu.dev"]
|
|
|
35
36
|
manifest_path = ".buildchain/publication/publication-artifact.json"
|
|
36
37
|
source_bundle_path = ".buildchain/publication/source.tar.gz"
|
|
37
38
|
|
|
39
|
+
[publication.archive]
|
|
40
|
+
id = "observer-declared-timelines"
|
|
41
|
+
canonical_url = "https://papers.libkungfu.dev/observer-declared-timelines/"
|
|
42
|
+
latest_url = "https://papers.libkungfu.dev/observer-declared-timelines/latest/"
|
|
43
|
+
latest_evidence_url = "https://papers.libkungfu.dev/observer-declared-timelines/latest/buildchain.release.json"
|
|
44
|
+
immutable_base_url = "https://papers.libkungfu.dev/archive"
|
|
45
|
+
registry_path = ".buildchain/publication/publication-registry.json"
|
|
46
|
+
|
|
38
47
|
[publication.toolchain]
|
|
39
48
|
type = "latex-docker"
|
|
40
49
|
image = "ghcr.io/kungfu-systems/latex"
|
|
@@ -52,6 +61,26 @@ command = "make check"
|
|
|
52
61
|
`source_paths` are archived into a source bundle. `metadata_paths` are hashed
|
|
53
62
|
and recorded so a site can consume the paper facts without scraping prose.
|
|
54
63
|
|
|
64
|
+
`publication.archive` turns the publication into an append-only public archive
|
|
65
|
+
contract:
|
|
66
|
+
|
|
67
|
+
- `canonical_url` is the stable human reader page.
|
|
68
|
+
- `latest_url` and `latest_evidence_url` are movable aliases for the latest
|
|
69
|
+
reader page and latest evidence.
|
|
70
|
+
- `immutable_base_url` plus `id` and `publication.version` produce a versioned
|
|
71
|
+
prefix such as
|
|
72
|
+
`https://papers.libkungfu.dev/archive/observer-declared-timelines/v0.1.0/`.
|
|
73
|
+
- `immutable_url_prefix` can be used instead when the repository already owns
|
|
74
|
+
the full version prefix.
|
|
75
|
+
- `registry_path` records every published version and its manifest, passport,
|
|
76
|
+
source bundle, primary artifact, URLs, and SHA-256 digests.
|
|
77
|
+
|
|
78
|
+
Immutable archive prefixes are append-only. Do not run site deployment commands
|
|
79
|
+
with `sync --delete` or equivalent deletion semantics over those prefixes. A
|
|
80
|
+
same-version republish is allowed only when the immutable digest is unchanged;
|
|
81
|
+
if PDF, source bundle, route, metadata, or toolchain evidence changes for an
|
|
82
|
+
existing version, Buildchain fails before the registry is rewritten.
|
|
83
|
+
|
|
55
84
|
`publication.toolchain` makes the source-to-PDF transformation part of the
|
|
56
85
|
machine-readable contract. `latex-docker` is the preferred LaTeX profile: the
|
|
57
86
|
workflow pulls the declared image by digest and runs the declared command in
|
|
@@ -88,8 +117,11 @@ The workflow:
|
|
|
88
117
|
- creates a source bundle from `publication.source_paths`;
|
|
89
118
|
- writes `.buildchain/publication/publication-artifact.json`;
|
|
90
119
|
- writes `.buildchain/publication/publication-artifact-passport.json`;
|
|
91
|
-
-
|
|
92
|
-
|
|
120
|
+
- when `[publication.archive]` is configured, writes
|
|
121
|
+
`.buildchain/publication/publication-registry.json` and verifies same-version
|
|
122
|
+
immutability;
|
|
123
|
+
- uploads one GitHub artifact containing the PDF, manifest, passport, optional
|
|
124
|
+
registry, and source bundle.
|
|
93
125
|
|
|
94
126
|
The wrapper is build-only. It does not publish npm packages, deploy web pages,
|
|
95
127
|
or create GitHub Releases. Release publication can be layered later by a
|
|
@@ -123,12 +155,19 @@ the reusable workflow. The generated manifest records:
|
|
|
123
155
|
- publication toolchain type, image, digest, command, invocation mode, and trust
|
|
124
156
|
classification;
|
|
125
157
|
- timestamp and reproducibility policy;
|
|
126
|
-
- downstream site-consumption hints
|
|
158
|
+
- downstream site-consumption hints;
|
|
159
|
+
- optional archive routes for canonical, latest, latest evidence, immutable
|
|
160
|
+
version prefix, and public artifact URLs.
|
|
127
161
|
|
|
128
162
|
The companion publication artifact passport records the same source and
|
|
129
163
|
artifact evidence plus an explicit responsibility split. Buildchain proves
|
|
130
164
|
declared files and hashes; it does not peer-review paper claims.
|
|
131
165
|
|
|
166
|
+
When archive config is present, the registry uses the
|
|
167
|
+
`kungfu-buildchain-publication-artifact-registry` contract. A site repository
|
|
168
|
+
can render latest pages and historical version indexes from that registry
|
|
169
|
+
without rebuilding old PDFs from the latest npm package or paper source.
|
|
170
|
+
|
|
132
171
|
## Site Consumption
|
|
133
172
|
|
|
134
173
|
A downstream papers site should treat the publication manifest as the single
|
|
@@ -139,7 +178,9 @@ For `paper-observer-declared-timelines`, the expected adoption path is:
|
|
|
139
178
|
|
|
140
179
|
```text
|
|
141
180
|
paper repo builds PDF + manifest + source bundle
|
|
181
|
+
paper repo updates publication-registry.json
|
|
142
182
|
papers site consumes publication-artifact.json
|
|
183
|
+
papers site consumes publication-registry.json for history
|
|
143
184
|
site renders paper page and links the PDF/source bundle
|
|
144
185
|
```
|
|
145
186
|
|
|
@@ -85,6 +85,53 @@ The upstream release envelope is the post-finalization fact set:
|
|
|
85
85
|
The package version and integrity must be exact. Downstream build logic should
|
|
86
86
|
install that version directly, not resolve `alpha` or `latest` again.
|
|
87
87
|
|
|
88
|
+
Publication repositories can propagate immutable publication archive evidence
|
|
89
|
+
without npm package facts. The upstream envelope then includes
|
|
90
|
+
`publicationArtifact`:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"repository": "kungfu-systems/paper-observer-declared-timelines",
|
|
95
|
+
"channel": "alpha",
|
|
96
|
+
"tag": "v0.1.0-alpha.1",
|
|
97
|
+
"sourceSha": "4444444444444444444444444444444444444444",
|
|
98
|
+
"releasePassport": {
|
|
99
|
+
"url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/buildchain.release.json",
|
|
100
|
+
"sha256": "5555555555555555555555555555555555555555555555555555555555555555"
|
|
101
|
+
},
|
|
102
|
+
"publicationArtifact": {
|
|
103
|
+
"id": "observer-declared-timelines",
|
|
104
|
+
"kind": "paper",
|
|
105
|
+
"version": "0.1.0-alpha.1",
|
|
106
|
+
"canonicalUrl": "https://papers.libkungfu.dev/observer-declared-timelines/",
|
|
107
|
+
"latestUrl": "https://papers.libkungfu.dev/observer-declared-timelines/latest/",
|
|
108
|
+
"latestEvidenceUrl": "https://papers.libkungfu.dev/observer-declared-timelines/latest/buildchain.release.json",
|
|
109
|
+
"immutableVersionUrl": "https://papers.libkungfu.dev/archive/observer-declared-timelines/v0.1.0-alpha.1/",
|
|
110
|
+
"registry": {
|
|
111
|
+
"url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/publication-registry.json",
|
|
112
|
+
"sha256": "6666666666666666666666666666666666666666666666666666666666666666"
|
|
113
|
+
},
|
|
114
|
+
"manifest": {
|
|
115
|
+
"url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/publication-artifact.json",
|
|
116
|
+
"sha256": "7777777777777777777777777777777777777777777777777777777777777777"
|
|
117
|
+
},
|
|
118
|
+
"passport": {
|
|
119
|
+
"url": "https://github.com/kungfu-systems/paper-observer-declared-timelines/releases/download/v0.1.0-alpha.1/publication-artifact-passport.json",
|
|
120
|
+
"sha256": "8888888888888888888888888888888888888888888888888888888888888888"
|
|
121
|
+
},
|
|
122
|
+
"primaryArtifact": {
|
|
123
|
+
"path": "_build/main.pdf",
|
|
124
|
+
"url": "https://papers.libkungfu.dev/archive/observer-declared-timelines/v0.1.0-alpha.1/main.pdf",
|
|
125
|
+
"sha256": "9999999999999999999999999999999999999999999999999999999999999999"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This lets a site repository render the latest reader page and historical
|
|
132
|
+
version index from release facts while keeping old PDFs, source bundles,
|
|
133
|
+
manifests, and passports immutable.
|
|
134
|
+
|
|
88
135
|
## CLI
|
|
89
136
|
|
|
90
137
|
Generate a propagation plan:
|
|
@@ -111,7 +158,9 @@ The written lock has contract
|
|
|
111
158
|
`kungfu-buildchain-release-propagation-lock` and records:
|
|
112
159
|
|
|
113
160
|
- upstream repository, channel, exact tag, source SHA;
|
|
114
|
-
- npm package name, exact version, and sha512 integrity;
|
|
161
|
+
- optional npm package name, exact version, and sha512 integrity;
|
|
162
|
+
- optional publication artifact canonical/latest/immutable URLs, registry,
|
|
163
|
+
manifest, passport, source bundle, and primary artifact digests;
|
|
115
164
|
- release passport URL and SHA-256;
|
|
116
165
|
- optional site bundle manifest SHA-256;
|
|
117
166
|
- downstream repository, channel, base ref, lock path;
|
package/package.json
CHANGED
package/packages/core/README.md
CHANGED
|
@@ -67,7 +67,9 @@ writeBuildFacts({ fact, output: ".buildchain/facts/native-core.json" });
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
Publication repositories can produce site-consumable paper/report facts without
|
|
70
|
-
becoming web-surface repositories
|
|
70
|
+
becoming web-surface repositories. When `[publication.archive]` is configured,
|
|
71
|
+
the same API also maintains the append-only publication registry used by site
|
|
72
|
+
repositories for latest and historical version pages:
|
|
71
73
|
|
|
72
74
|
```js
|
|
73
75
|
import { writePublicationArtifact } from "@kungfu-tech/buildchain/publication-artifact";
|
|
@@ -397,6 +397,41 @@ function normalizePublicationSection(publication) {
|
|
|
397
397
|
sourceBundlePath: publication.source_bundle_path === undefined
|
|
398
398
|
? ".buildchain/publication/source.tar.gz"
|
|
399
399
|
: posixPath(assertString(publication.source_bundle_path, "publication.source_bundle_path")),
|
|
400
|
+
archive: normalizePublicationArchive(publication.archive),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function normalizePublicationArchive(archive = undefined) {
|
|
405
|
+
if (archive === undefined) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
assertPlainObject(archive, "publication.archive");
|
|
409
|
+
const immutableUrlPrefix = archive.immutable_url_prefix === undefined
|
|
410
|
+
? ""
|
|
411
|
+
: assertString(archive.immutable_url_prefix, "publication.archive.immutable_url_prefix");
|
|
412
|
+
const immutableBaseUrl = archive.immutable_base_url === undefined
|
|
413
|
+
? ""
|
|
414
|
+
: assertString(archive.immutable_base_url, "publication.archive.immutable_base_url");
|
|
415
|
+
if (!immutableUrlPrefix && !immutableBaseUrl) {
|
|
416
|
+
throw new Error("publication.archive requires immutable_url_prefix or immutable_base_url");
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
id: archive.id === undefined ? "" : assertString(archive.id, "publication.archive.id"),
|
|
420
|
+
canonicalUrl: assertString(archive.canonical_url, "publication.archive.canonical_url"),
|
|
421
|
+
latestUrl: archive.latest_url === undefined
|
|
422
|
+
? ""
|
|
423
|
+
: assertString(archive.latest_url, "publication.archive.latest_url"),
|
|
424
|
+
latestEvidenceUrl: archive.latest_evidence_url === undefined
|
|
425
|
+
? ""
|
|
426
|
+
: assertString(archive.latest_evidence_url, "publication.archive.latest_evidence_url"),
|
|
427
|
+
immutableBaseUrl,
|
|
428
|
+
immutableUrlPrefix,
|
|
429
|
+
artifactUrlPrefix: archive.artifact_url_prefix === undefined
|
|
430
|
+
? ""
|
|
431
|
+
: assertString(archive.artifact_url_prefix, "publication.archive.artifact_url_prefix"),
|
|
432
|
+
registryPath: archive.registry_path === undefined
|
|
433
|
+
? ".buildchain/publication/publication-registry.json"
|
|
434
|
+
: posixPath(assertString(archive.registry_path, "publication.archive.registry_path")),
|
|
400
435
|
};
|
|
401
436
|
}
|
|
402
437
|
|
package/packages/core/index.js
CHANGED
|
@@ -186,8 +186,10 @@ export {
|
|
|
186
186
|
} from "./public-surface-audit.js";
|
|
187
187
|
|
|
188
188
|
export {
|
|
189
|
+
PUBLICATION_ARTIFACT_ARCHIVE_CONTRACT,
|
|
189
190
|
PUBLICATION_ARTIFACT_MANIFEST_CONTRACT,
|
|
190
191
|
PUBLICATION_ARTIFACT_PASSPORT_CONTRACT,
|
|
192
|
+
PUBLICATION_ARTIFACT_REGISTRY_CONTRACT,
|
|
191
193
|
collectPublicationArtifact,
|
|
192
194
|
createPublicationSourceBundle,
|
|
193
195
|
writePublicationArtifact,
|
|
@@ -6,6 +6,8 @@ import { loadBuildchainConfig } from "./buildchain-config.js";
|
|
|
6
6
|
|
|
7
7
|
export const PUBLICATION_ARTIFACT_MANIFEST_CONTRACT = "kungfu-buildchain-publication-artifact-manifest";
|
|
8
8
|
export const PUBLICATION_ARTIFACT_PASSPORT_CONTRACT = "kungfu-buildchain-publication-artifact-passport";
|
|
9
|
+
export const PUBLICATION_ARTIFACT_ARCHIVE_CONTRACT = "kungfu-buildchain-publication-artifact-archive";
|
|
10
|
+
export const PUBLICATION_ARTIFACT_REGISTRY_CONTRACT = "kungfu-buildchain-publication-artifact-registry";
|
|
9
11
|
const KFD2_RESIDUAL_RISK_SCHEMA = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk";
|
|
10
12
|
|
|
11
13
|
function toPosix(value) {
|
|
@@ -20,6 +22,28 @@ function sha256FilePath(filePath) {
|
|
|
20
22
|
return sha256Buffer(fs.readFileSync(filePath));
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function sortJson(value) {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value.map(sortJson);
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== "object") {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(value)
|
|
34
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
35
|
+
.map(([key, entry]) => [key, sortJson(entry)]),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stableJson(value) {
|
|
40
|
+
return `${JSON.stringify(sortJson(value), null, 2)}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sha256Json(value) {
|
|
44
|
+
return sha256Buffer(Buffer.from(stableJson(value)));
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
function repoRelative(cwd, filePath) {
|
|
24
48
|
const relative = path.relative(cwd, filePath);
|
|
25
49
|
return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
|
|
@@ -75,6 +99,29 @@ function createdAt(now = new Date()) {
|
|
|
75
99
|
return now.toISOString();
|
|
76
100
|
}
|
|
77
101
|
|
|
102
|
+
function ensureTrailingSlash(value) {
|
|
103
|
+
const text = String(value || "").trim();
|
|
104
|
+
return text.endsWith("/") ? text : `${text}/`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function joinUrl(prefix, ...segments) {
|
|
108
|
+
const base = ensureTrailingSlash(prefix);
|
|
109
|
+
const suffix = segments
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.map((segment) => String(segment).replace(/^\/+/, "").replace(/\\/g, "/"))
|
|
112
|
+
.join("/");
|
|
113
|
+
return suffix ? `${base}${suffix}` : base;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function versionLabel(version) {
|
|
117
|
+
const text = String(version || "").trim();
|
|
118
|
+
return text.startsWith("v") ? text : `v${text}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function archiveFileUrl(prefix, relPath) {
|
|
122
|
+
return joinUrl(prefix, path.posix.basename(toPosix(relPath)));
|
|
123
|
+
}
|
|
124
|
+
|
|
78
125
|
function publicationToolchainFacts(publication) {
|
|
79
126
|
const envType = String(process.env.BUILDCHAIN_PUBLICATION_TOOLCHAIN_TYPE || "").trim();
|
|
80
127
|
const envToolchain = envType
|
|
@@ -138,6 +185,67 @@ function publicationResidualRisk(toolchainFacts) {
|
|
|
138
185
|
return risks;
|
|
139
186
|
}
|
|
140
187
|
|
|
188
|
+
function publicationArchiveFacts({ loaded, publication, manifestPath, passportPath, artifacts, bundle }) {
|
|
189
|
+
if (!publication.archive) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const version = String(publication.version || "").trim();
|
|
193
|
+
if (!version) {
|
|
194
|
+
throw new Error("publication.archive requires publication.version");
|
|
195
|
+
}
|
|
196
|
+
const archive = publication.archive;
|
|
197
|
+
const id = archive.id || loaded.config.project?.name || "publication";
|
|
198
|
+
const immutableVersionPrefix = archive.immutableUrlPrefix
|
|
199
|
+
? ensureTrailingSlash(archive.immutableUrlPrefix)
|
|
200
|
+
: ensureTrailingSlash(joinUrl(archive.immutableBaseUrl, id, versionLabel(version)));
|
|
201
|
+
const artifactUrlPrefix = archive.artifactUrlPrefix
|
|
202
|
+
? ensureTrailingSlash(archive.artifactUrlPrefix)
|
|
203
|
+
: immutableVersionPrefix;
|
|
204
|
+
return {
|
|
205
|
+
contract: PUBLICATION_ARTIFACT_ARCHIVE_CONTRACT,
|
|
206
|
+
id,
|
|
207
|
+
version,
|
|
208
|
+
appendOnly: true,
|
|
209
|
+
sameVersionRepublish: "fail-on-digest-change",
|
|
210
|
+
immutablePathPolicy: "do-not-sync-delete-immutable-prefixes",
|
|
211
|
+
routes: {
|
|
212
|
+
canonicalUrl: archive.canonicalUrl,
|
|
213
|
+
latestUrl: archive.latestUrl || archive.canonicalUrl,
|
|
214
|
+
latestEvidenceUrl: archive.latestEvidenceUrl,
|
|
215
|
+
immutableVersionPrefix,
|
|
216
|
+
immutableVersionUrl: immutableVersionPrefix,
|
|
217
|
+
artifactUrlPrefix,
|
|
218
|
+
},
|
|
219
|
+
registry: {
|
|
220
|
+
contract: PUBLICATION_ARTIFACT_REGISTRY_CONTRACT,
|
|
221
|
+
path: archive.registryPath,
|
|
222
|
+
},
|
|
223
|
+
publicArtifacts: {
|
|
224
|
+
manifest: {
|
|
225
|
+
path: toPosix(manifestPath),
|
|
226
|
+
url: archiveFileUrl(artifactUrlPrefix, manifestPath),
|
|
227
|
+
},
|
|
228
|
+
passport: {
|
|
229
|
+
path: toPosix(passportPath),
|
|
230
|
+
url: archiveFileUrl(artifactUrlPrefix, passportPath),
|
|
231
|
+
},
|
|
232
|
+
sourceBundle: bundle
|
|
233
|
+
? {
|
|
234
|
+
path: bundle.path,
|
|
235
|
+
url: archiveFileUrl(artifactUrlPrefix, bundle.path),
|
|
236
|
+
sha256: bundle.sha256,
|
|
237
|
+
bytes: bundle.bytes,
|
|
238
|
+
}
|
|
239
|
+
: undefined,
|
|
240
|
+
artifacts: artifacts.map((artifact) => ({
|
|
241
|
+
...artifact,
|
|
242
|
+
role: artifact.path === publication.primaryArtifact ? "primary" : "attachment",
|
|
243
|
+
url: archiveFileUrl(artifactUrlPrefix, artifact.path),
|
|
244
|
+
})),
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
141
249
|
export function createPublicationSourceBundle({
|
|
142
250
|
cwd = process.cwd(),
|
|
143
251
|
sourcePaths = [],
|
|
@@ -166,6 +274,8 @@ export function collectPublicationArtifact({
|
|
|
166
274
|
sourceBundle = true,
|
|
167
275
|
sourceBundlePath = "",
|
|
168
276
|
generatedAt = "",
|
|
277
|
+
manifestPath = "",
|
|
278
|
+
passportPath = "",
|
|
169
279
|
} = {}) {
|
|
170
280
|
const loaded = loadBuildchainConfig(cwd);
|
|
171
281
|
if (!loaded?.config?.publication) {
|
|
@@ -195,6 +305,16 @@ export function collectPublicationArtifact({
|
|
|
195
305
|
const sourceTreeSha = gitValue(cwd, ["rev-parse", `${resolvedSourceSha}^{tree}`]);
|
|
196
306
|
const timestamp = generatedAt || createdAt();
|
|
197
307
|
const toolchain = publicationToolchainFacts(publication);
|
|
308
|
+
const resolvedManifestPath = manifestPath || publication.manifestPath;
|
|
309
|
+
const resolvedPassportPath = passportPath || ".buildchain/publication/publication-artifact-passport.json";
|
|
310
|
+
const archive = publicationArchiveFacts({
|
|
311
|
+
loaded,
|
|
312
|
+
publication,
|
|
313
|
+
manifestPath: resolvedManifestPath,
|
|
314
|
+
passportPath: resolvedPassportPath,
|
|
315
|
+
artifacts,
|
|
316
|
+
bundle,
|
|
317
|
+
});
|
|
198
318
|
const manifest = {
|
|
199
319
|
schemaVersion: 1,
|
|
200
320
|
contract: PUBLICATION_ARTIFACT_MANIFEST_CONTRACT,
|
|
@@ -210,6 +330,7 @@ export function collectPublicationArtifact({
|
|
|
210
330
|
authors: publication.authors,
|
|
211
331
|
primaryArtifact: publication.primaryArtifact,
|
|
212
332
|
siteConsumers: publication.siteConsumers,
|
|
333
|
+
archive,
|
|
213
334
|
},
|
|
214
335
|
toolchain,
|
|
215
336
|
source: {
|
|
@@ -241,16 +362,38 @@ export function collectPublicationArtifact({
|
|
|
241
362
|
artifactDigestScope: "publication artifact digests cover files and source bundle, not manifest timestamps",
|
|
242
363
|
},
|
|
243
364
|
};
|
|
365
|
+
const manifestDigest = `sha256:${sha256Buffer(Buffer.from(JSON.stringify(manifest, null, 2)))}`;
|
|
244
366
|
return {
|
|
245
367
|
manifest,
|
|
246
368
|
passport: {
|
|
247
369
|
schemaVersion: 1,
|
|
248
370
|
contract: PUBLICATION_ARTIFACT_PASSPORT_CONTRACT,
|
|
249
371
|
status: "passed",
|
|
250
|
-
manifestDigest
|
|
372
|
+
manifestDigest,
|
|
251
373
|
source: manifest.source,
|
|
252
374
|
toolchain,
|
|
253
375
|
artifacts: manifest.artifacts,
|
|
376
|
+
publicationArchive: archive
|
|
377
|
+
? {
|
|
378
|
+
contract: archive.contract,
|
|
379
|
+
id: archive.id,
|
|
380
|
+
version: archive.version,
|
|
381
|
+
routes: archive.routes,
|
|
382
|
+
registry: archive.registry,
|
|
383
|
+
publicArtifacts: {
|
|
384
|
+
manifest: {
|
|
385
|
+
...archive.publicArtifacts.manifest,
|
|
386
|
+
sha256: manifestDigest.replace(/^sha256:/, ""),
|
|
387
|
+
},
|
|
388
|
+
passport: archive.publicArtifacts.passport,
|
|
389
|
+
sourceBundle: archive.publicArtifacts.sourceBundle,
|
|
390
|
+
artifacts: archive.publicArtifacts.artifacts,
|
|
391
|
+
},
|
|
392
|
+
appendOnly: archive.appendOnly,
|
|
393
|
+
sameVersionRepublish: archive.sameVersionRepublish,
|
|
394
|
+
immutablePathPolicy: archive.immutablePathPolicy,
|
|
395
|
+
}
|
|
396
|
+
: undefined,
|
|
254
397
|
responsibility: {
|
|
255
398
|
producer: "publication repository",
|
|
256
399
|
renderer: "site repository",
|
|
@@ -277,31 +420,179 @@ export function collectPublicationArtifact({
|
|
|
277
420
|
};
|
|
278
421
|
}
|
|
279
422
|
|
|
423
|
+
function publicationArchiveRecord({ collected, manifestPath, passportPath, registryPath, generatedAt }) {
|
|
424
|
+
const archive = collected.manifest.publication.archive;
|
|
425
|
+
if (!archive) {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
const manifestDigest = sha256FilePath(manifestPath);
|
|
429
|
+
const passportDigest = sha256FilePath(passportPath);
|
|
430
|
+
const immutableEvidence = {
|
|
431
|
+
version: archive.version,
|
|
432
|
+
source: {
|
|
433
|
+
sha: collected.manifest.source.sha,
|
|
434
|
+
treeSha: collected.manifest.source.treeSha,
|
|
435
|
+
sourceBundle: archive.publicArtifacts.sourceBundle,
|
|
436
|
+
},
|
|
437
|
+
routes: archive.routes,
|
|
438
|
+
artifacts: archive.publicArtifacts.artifacts.map((artifact) => ({
|
|
439
|
+
path: artifact.path,
|
|
440
|
+
role: artifact.role,
|
|
441
|
+
url: artifact.url,
|
|
442
|
+
bytes: artifact.bytes,
|
|
443
|
+
sha256: artifact.sha256,
|
|
444
|
+
})),
|
|
445
|
+
metadata: collected.manifest.metadata,
|
|
446
|
+
toolchain: collected.manifest.toolchain,
|
|
447
|
+
};
|
|
448
|
+
const immutableDigest = sha256Json(immutableEvidence);
|
|
449
|
+
const record = {
|
|
450
|
+
version: archive.version,
|
|
451
|
+
status: "published",
|
|
452
|
+
publishedAt: generatedAt || collected.manifest.publishedAt,
|
|
453
|
+
immutableDigest: `sha256:${immutableDigest}`,
|
|
454
|
+
source: {
|
|
455
|
+
repository: collected.manifest.source.repository,
|
|
456
|
+
sha: collected.manifest.source.sha,
|
|
457
|
+
treeSha: collected.manifest.source.treeSha,
|
|
458
|
+
sourceBundle: archive.publicArtifacts.sourceBundle,
|
|
459
|
+
},
|
|
460
|
+
routes: archive.routes,
|
|
461
|
+
artifacts: archive.publicArtifacts.artifacts,
|
|
462
|
+
metadata: collected.manifest.metadata,
|
|
463
|
+
manifest: {
|
|
464
|
+
...archive.publicArtifacts.manifest,
|
|
465
|
+
sha256: manifestDigest,
|
|
466
|
+
},
|
|
467
|
+
passport: {
|
|
468
|
+
...archive.publicArtifacts.passport,
|
|
469
|
+
sha256: passportDigest,
|
|
470
|
+
},
|
|
471
|
+
immutableEvidenceSha256: immutableDigest,
|
|
472
|
+
};
|
|
473
|
+
record.recordSha256 = sha256Json({ ...record, recordSha256: undefined });
|
|
474
|
+
return {
|
|
475
|
+
registryPath: archive.registry.path || registryPath,
|
|
476
|
+
record,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function updatePublicationArchiveRegistry({ cwd, collected, manifestPath, passportPath, registryOutput, generatedAt }) {
|
|
481
|
+
const archive = collected.manifest.publication.archive;
|
|
482
|
+
if (!archive) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const registryPath = path.resolve(cwd, registryOutput || archive.registry.path);
|
|
486
|
+
const { record } = publicationArchiveRecord({
|
|
487
|
+
collected,
|
|
488
|
+
manifestPath,
|
|
489
|
+
passportPath,
|
|
490
|
+
registryPath,
|
|
491
|
+
generatedAt,
|
|
492
|
+
});
|
|
493
|
+
let registry = {
|
|
494
|
+
schemaVersion: 1,
|
|
495
|
+
contract: PUBLICATION_ARTIFACT_REGISTRY_CONTRACT,
|
|
496
|
+
publication: {
|
|
497
|
+
id: archive.id,
|
|
498
|
+
kind: collected.manifest.publication.kind,
|
|
499
|
+
title: collected.manifest.publication.title,
|
|
500
|
+
canonicalUrl: archive.routes.canonicalUrl,
|
|
501
|
+
latestUrl: archive.routes.latestUrl,
|
|
502
|
+
latestEvidenceUrl: archive.routes.latestEvidenceUrl,
|
|
503
|
+
siteConsumers: collected.manifest.publication.siteConsumers,
|
|
504
|
+
},
|
|
505
|
+
appendOnly: true,
|
|
506
|
+
sameVersionRepublish: "fail-on-digest-change",
|
|
507
|
+
immutablePathPolicy: archive.immutablePathPolicy,
|
|
508
|
+
versions: [],
|
|
509
|
+
};
|
|
510
|
+
if (fs.existsSync(registryPath)) {
|
|
511
|
+
registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
|
512
|
+
if (registry.contract !== PUBLICATION_ARTIFACT_REGISTRY_CONTRACT) {
|
|
513
|
+
throw new Error(`publication archive registry has unsupported contract: ${registry.contract || "(missing)"}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const versions = Array.isArray(registry.versions) ? registry.versions : [];
|
|
517
|
+
const existingIndex = versions.findIndex((entry) => entry.version === record.version);
|
|
518
|
+
if (existingIndex !== -1) {
|
|
519
|
+
const existing = versions[existingIndex];
|
|
520
|
+
if (existing.immutableDigest !== record.immutableDigest) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`publication archive version ${record.version} is immutable: existing digest ${existing.immutableDigest}, new digest ${record.immutableDigest}`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
versions[existingIndex] = {
|
|
526
|
+
...existing,
|
|
527
|
+
latestObservedAt: generatedAt || collected.manifest.publishedAt,
|
|
528
|
+
};
|
|
529
|
+
} else {
|
|
530
|
+
versions.push(record);
|
|
531
|
+
}
|
|
532
|
+
registry = {
|
|
533
|
+
...registry,
|
|
534
|
+
publication: {
|
|
535
|
+
...registry.publication,
|
|
536
|
+
id: archive.id,
|
|
537
|
+
kind: collected.manifest.publication.kind,
|
|
538
|
+
title: collected.manifest.publication.title,
|
|
539
|
+
canonicalUrl: archive.routes.canonicalUrl,
|
|
540
|
+
latestUrl: archive.routes.latestUrl,
|
|
541
|
+
latestEvidenceUrl: archive.routes.latestEvidenceUrl,
|
|
542
|
+
siteConsumers: collected.manifest.publication.siteConsumers,
|
|
543
|
+
},
|
|
544
|
+
versions: versions.sort((left, right) => left.version.localeCompare(right.version)),
|
|
545
|
+
};
|
|
546
|
+
registry.registrySha256 = sha256Json({ ...registry, registrySha256: undefined });
|
|
547
|
+
fs.mkdirSync(path.dirname(registryPath), { recursive: true });
|
|
548
|
+
fs.writeFileSync(registryPath, stableJson(registry));
|
|
549
|
+
return {
|
|
550
|
+
path: toPosix(path.relative(cwd, registryPath)),
|
|
551
|
+
sha256: sha256FilePath(registryPath),
|
|
552
|
+
record,
|
|
553
|
+
registry,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
280
557
|
export function writePublicationArtifact({
|
|
281
558
|
cwd = process.cwd(),
|
|
282
559
|
output = "",
|
|
283
560
|
passportOutput = "",
|
|
561
|
+
registryOutput = "",
|
|
284
562
|
sourceSha = "",
|
|
285
563
|
sourceBundle = true,
|
|
286
564
|
sourceBundlePath = "",
|
|
287
565
|
generatedAt = "",
|
|
288
566
|
} = {}) {
|
|
567
|
+
const loaded = loadBuildchainConfig(cwd);
|
|
568
|
+
const manifestOutput = output || loaded.config.publication.manifestPath;
|
|
569
|
+
const passportPath = passportOutput || ".buildchain/publication/publication-artifact-passport.json";
|
|
289
570
|
const collected = collectPublicationArtifact({
|
|
290
571
|
cwd,
|
|
291
572
|
sourceSha,
|
|
292
573
|
sourceBundle,
|
|
293
574
|
sourceBundlePath,
|
|
294
575
|
generatedAt,
|
|
576
|
+
manifestPath: manifestOutput,
|
|
577
|
+
passportPath,
|
|
295
578
|
});
|
|
296
|
-
const manifestOutput = output || loadBuildchainConfig(cwd).config.publication.manifestPath;
|
|
297
|
-
const passportPath = passportOutput || ".buildchain/publication/publication-artifact-passport.json";
|
|
298
579
|
fs.mkdirSync(path.dirname(path.resolve(cwd, manifestOutput)), { recursive: true });
|
|
299
580
|
fs.mkdirSync(path.dirname(path.resolve(cwd, passportPath)), { recursive: true });
|
|
300
581
|
fs.writeFileSync(path.resolve(cwd, manifestOutput), `${JSON.stringify(collected.manifest, null, 2)}\n`);
|
|
301
582
|
fs.writeFileSync(path.resolve(cwd, passportPath), `${JSON.stringify(collected.passport, null, 2)}\n`);
|
|
583
|
+
const registry = updatePublicationArchiveRegistry({
|
|
584
|
+
cwd,
|
|
585
|
+
collected,
|
|
586
|
+
manifestPath: path.resolve(cwd, manifestOutput),
|
|
587
|
+
passportPath: path.resolve(cwd, passportPath),
|
|
588
|
+
registryOutput,
|
|
589
|
+
generatedAt,
|
|
590
|
+
});
|
|
302
591
|
return {
|
|
303
592
|
...collected,
|
|
304
593
|
manifestPath: toPosix(manifestOutput),
|
|
305
594
|
passportPath: toPosix(passportPath),
|
|
595
|
+
registryPath: registry?.path,
|
|
596
|
+
registry,
|
|
306
597
|
};
|
|
307
598
|
}
|