@kungfu-tech/buildchain 2.11.13-alpha.1 → 2.11.13-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 +6 -0
- package/dist/site/buildchain-contract.json +3 -3
- package/dist/site/buildchain-site.json +19 -14
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/page-registry.json +13 -8
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/docs/lifecycle-protocol.md +16 -0
- package/docs/publication-artifacts.md +23 -0
- package/docs/web-surface-deployments.md +35 -1
- package/package.json +1 -1
- package/packages/core/buildchain-config.js +53 -5
- package/scripts/web-surface-core.mjs +350 -27
- package/scripts/web-surface-immutable-object.mjs +123 -0
- package/scripts/web-surface.mjs +4 -0
|
@@ -59,6 +59,14 @@ path = "pyproject.toml"
|
|
|
59
59
|
key = "project.version"
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Configured JSON and TOML version files are semantic no-ops when the declared
|
|
63
|
+
key already equals the requested version. Buildchain preserves the repository's
|
|
64
|
+
original TOML bytes in that case instead of serializing the whole document and
|
|
65
|
+
creating formatter-only release state. When a TOML version really changes,
|
|
66
|
+
Buildchain applies a parser-verified lossless edit to that key and fails closed
|
|
67
|
+
if it cannot prove a unique edit; unrelated arrays, comments, and formatting
|
|
68
|
+
remain repository-owned.
|
|
69
|
+
|
|
62
70
|
Regex files must expose the current version through a named capture group called
|
|
63
71
|
`version`:
|
|
64
72
|
|
|
@@ -248,6 +256,14 @@ refs. After the command finishes, Buildchain checks that only declared
|
|
|
248
256
|
version-state files changed. This prevents verification from quietly adding
|
|
249
257
|
extra source changes to the release commit.
|
|
250
258
|
|
|
259
|
+
Buildchain-owned untracked runtime evidence is excluded only through an exact
|
|
260
|
+
internal allowlist. This includes contract-drift issue material under
|
|
261
|
+
`.buildchain/contract-drift/` and the paper workflow's
|
|
262
|
+
`.buildchain/publication-result.json`, alongside release-candidate, passport,
|
|
263
|
+
release-state, KFD, and runtime evidence directories. Tracked changes, ordinary
|
|
264
|
+
source files, and undeclared `.buildchain/*` paths still fail version
|
|
265
|
+
verification.
|
|
266
|
+
|
|
251
267
|
On protected alpha and release branches, the generated version-state commit is
|
|
252
268
|
applied by the promotion automation after the reviewed channel PR has merged.
|
|
253
269
|
Buildchain keeps review requirements, conversation resolution, strict status
|
|
@@ -86,6 +86,16 @@ same-version republish is allowed only when the immutable digest is unchanged;
|
|
|
86
86
|
if PDF, source bundle, route, metadata, or toolchain evidence changes for an
|
|
87
87
|
existing version, Buildchain fails before the registry is rewritten.
|
|
88
88
|
|
|
89
|
+
The Buildchain web-surface adapter consumes this boundary from a surface-local
|
|
90
|
+
`manifest.json` whose `archivePolicy.contract` is
|
|
91
|
+
`kungfu-buildchain-publication-archive-policy`. It excludes the derived archive
|
|
92
|
+
root from every owning or parent `sync --delete`, verifies existing object
|
|
93
|
+
digests, uploads only missing immutable files with `--no-overwrite`, and verifies
|
|
94
|
+
them again before mutable site content is synchronized. A current package set
|
|
95
|
+
does not need to rebuild or enumerate every historical version: the protected
|
|
96
|
+
archive root remains outside deletion even when older versions disappear from
|
|
97
|
+
the current artifact.
|
|
98
|
+
|
|
89
99
|
`publication.toolchain` makes the source-to-PDF transformation part of the
|
|
90
100
|
machine-readable contract. `latex-docker` is the preferred LaTeX profile. The
|
|
91
101
|
Buildchain paper scaffold and reusable workflow default to
|
|
@@ -169,6 +179,8 @@ jobs:
|
|
|
169
179
|
contents: write
|
|
170
180
|
id-token: write
|
|
171
181
|
issues: write
|
|
182
|
+
secrets:
|
|
183
|
+
BUILDCHAIN_PROMOTION_TOKEN: ${{ secrets.RELEASE_AUTHORITY_TOKEN }}
|
|
172
184
|
with:
|
|
173
185
|
buildchain-ref: ${{ inputs.buildchain-ref || '' }}
|
|
174
186
|
toolchain-type: config
|
|
@@ -176,10 +188,21 @@ jobs:
|
|
|
176
188
|
buildchain-contract-lock-path: .buildchain/contract-lock.json
|
|
177
189
|
```
|
|
178
190
|
|
|
191
|
+
`RELEASE_AUTHORITY_TOKEN` is a caller-chosen release authority secret name.
|
|
192
|
+
Map whichever repository or organization secret owns protected release
|
|
193
|
+
bookkeeping into the reusable workflow's `BUILDCHAIN_PROMOTION_TOKEN` contract;
|
|
194
|
+
the reusable workflow does not require that provider-side secret to use a
|
|
195
|
+
specific name. Before it builds the paper, the workflow uses that authority to
|
|
196
|
+
read the target channel's branch protection and fails with a configuration
|
|
197
|
+
diagnostic if the protection is not readable. `github.token` remains a fallback
|
|
198
|
+
for repositories where its permissions are sufficient.
|
|
199
|
+
|
|
179
200
|
The preset:
|
|
180
201
|
|
|
181
202
|
- resolves the same floating Buildchain runtime and contract lock as the build
|
|
182
203
|
workflow;
|
|
204
|
+
- verifies that the declared promotion authority can read the protected target
|
|
205
|
+
channel before starting the publication build;
|
|
183
206
|
- builds the PDF through the declared pinned LaTeX Docker toolchain or custom
|
|
184
207
|
command;
|
|
185
208
|
- verifies the paper repository;
|
|
@@ -371,6 +371,39 @@ node scripts/web-surface.mjs \
|
|
|
371
371
|
--output .buildchain/web-surface-staging-apply.json
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
+
### Immutable publication paths
|
|
375
|
+
|
|
376
|
+
When a surface artifact contains `manifest.json` with
|
|
377
|
+
`archivePolicy.contract = "kungfu-buildchain-publication-archive-policy"`,
|
|
378
|
+
Buildchain treats every declared `publications[].versions[].immutablePath` as
|
|
379
|
+
an append-only publication boundary. This applies identically to preview,
|
|
380
|
+
staging, and production adapters.
|
|
381
|
+
|
|
382
|
+
The adapter derives the protected archive root from those declared version
|
|
383
|
+
paths and applies four ordered safeguards:
|
|
384
|
+
|
|
385
|
+
1. every local immutable file is checked against an existing S3 object;
|
|
386
|
+
2. missing files are uploaded with `aws s3 sync --no-overwrite` and a SHA-256
|
|
387
|
+
checksum;
|
|
388
|
+
3. every immutable file is checked again after upload, closing the race between
|
|
389
|
+
the first check and the no-overwrite transfer;
|
|
390
|
+
4. mutable site content keeps normal `sync --delete` behavior, but every parent
|
|
391
|
+
or owning surface sync excludes the protected archive root from deletion.
|
|
392
|
+
|
|
393
|
+
An existing object with a different SHA-256 digest fails apply before mutable
|
|
394
|
+
content is changed. Older objects without a stored S3 SHA-256 checksum are read
|
|
395
|
+
and byte-hashed for compatibility. Directory-index alias writes are skipped
|
|
396
|
+
under protected roots so they cannot overwrite immutable route objects; viewer
|
|
397
|
+
request rewriting remains the directory-index authority.
|
|
398
|
+
|
|
399
|
+
The deploy plan and manifest record `immutablePublication`,
|
|
400
|
+
`mutableDeleteExcludes`, and the parent-surface coverage. Apply output records
|
|
401
|
+
`immutablePreservation` plus every pre-check, no-overwrite sync, post-check, and
|
|
402
|
+
mutable sync operation. Health output adds an `__immutable__` check proving that
|
|
403
|
+
the owning and parent surface syncs carried their required delete exclusions.
|
|
404
|
+
The runner must provide an AWS CLI version whose `s3 sync` supports
|
|
405
|
+
`--no-overwrite`.
|
|
406
|
+
|
|
374
407
|
For multi-surface sites, each surface host is treated as a root-relative view
|
|
375
408
|
of that surface's artifact path prefix. For example, a `buildchain` surface with
|
|
376
409
|
`path = "/buildchain/"` and preview URL
|
|
@@ -457,7 +490,8 @@ node scripts/web-surface.mjs \
|
|
|
457
490
|
|
|
458
491
|
Apply output records the channel, alias, source SHA, artifact hash, target
|
|
459
492
|
bucket, object prefix, manifest key, all surface URLs, all surface bindings, CDN
|
|
460
|
-
invalidation paths, actor/run metadata,
|
|
493
|
+
invalidation paths, actor/run metadata, immutable preservation evidence, and
|
|
494
|
+
every adapter operation with
|
|
461
495
|
`executed`, `exitCode`, `stdout`, and `stderr`. If an operation fails,
|
|
462
496
|
Buildchain records the failed operation, stops subsequent adapter operations,
|
|
463
497
|
and exits non-zero after writing the result JSON. Buildchain records secret
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.11.13-alpha.
|
|
3
|
+
"version": "2.11.13-alpha.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -2,7 +2,8 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
|
+
import { parse } from "smol-toml";
|
|
6
7
|
import {
|
|
7
8
|
BUILDCHAIN_CONFIG_PATH,
|
|
8
9
|
resolveBuildchainConfigPath,
|
|
@@ -103,6 +104,44 @@ function setByDottedKey(target, key, value) {
|
|
|
103
104
|
current[segments[segments.length - 1]] = value;
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
function updateTomlStringValuePreservingSource({ source, content, key, value, filePath }) {
|
|
108
|
+
const currentValue = getByDottedKey(content, key);
|
|
109
|
+
if (currentValue === value) {
|
|
110
|
+
return source;
|
|
111
|
+
}
|
|
112
|
+
if (typeof currentValue !== "string" || currentValue.length === 0) {
|
|
113
|
+
throw new Error(`Configured TOML version key is missing or empty: ${filePath}:${key}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const expected = structuredClone(content);
|
|
117
|
+
setByDottedKey(expected, key, value);
|
|
118
|
+
const candidates = [];
|
|
119
|
+
let offset = 0;
|
|
120
|
+
while (offset <= source.length - currentValue.length) {
|
|
121
|
+
const index = source.indexOf(currentValue, offset);
|
|
122
|
+
if (index === -1) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const candidate = `${source.slice(0, index)}${value}${source.slice(index + currentValue.length)}`;
|
|
126
|
+
try {
|
|
127
|
+
if (isDeepStrictEqual(parse(candidate), expected)) {
|
|
128
|
+
candidates.push(candidate);
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Keep searching. Only a parser-confirmed single-field update is safe.
|
|
132
|
+
}
|
|
133
|
+
offset = index + currentValue.length;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (candidates.length !== 1) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Configured TOML version key cannot be updated losslessly: ${filePath}:${key} ` +
|
|
139
|
+
`(matching candidates: ${candidates.length})`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return candidates[0];
|
|
143
|
+
}
|
|
144
|
+
|
|
106
145
|
export function loadBuildchainConfig(cwd = process.cwd()) {
|
|
107
146
|
const configPath = resolveBuildchainConfigPath(cwd);
|
|
108
147
|
const filePath = path.join(cwd, configPath);
|
|
@@ -1359,14 +1398,23 @@ export function updateConfiguredVersionStateContents(files, version) {
|
|
|
1359
1398
|
return files
|
|
1360
1399
|
.map((file) => {
|
|
1361
1400
|
let content;
|
|
1362
|
-
if (
|
|
1401
|
+
if (
|
|
1402
|
+
(file.type === "json" || file.type === "toml") &&
|
|
1403
|
+
getByDottedKey(file.content, file.key) === version
|
|
1404
|
+
) {
|
|
1405
|
+
content = file.source;
|
|
1406
|
+
} else if (file.type === "json") {
|
|
1363
1407
|
const next = structuredClone(file.content);
|
|
1364
1408
|
setByDottedKey(next, file.key, version);
|
|
1365
1409
|
content = `${JSON.stringify(next, null, 2)}\n`;
|
|
1366
1410
|
} else if (file.type === "toml") {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1411
|
+
content = updateTomlStringValuePreservingSource({
|
|
1412
|
+
source: file.source,
|
|
1413
|
+
content: file.content,
|
|
1414
|
+
key: file.key,
|
|
1415
|
+
value: version,
|
|
1416
|
+
filePath: file.path,
|
|
1417
|
+
});
|
|
1370
1418
|
} else if (file.type === "regex") {
|
|
1371
1419
|
content = file.source.replace(file.pattern, (...args) => {
|
|
1372
1420
|
const groups = args.at(-1) || {};
|