@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.
@@ -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, and every adapter operation with
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.1",
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 { parse, stringify } from "smol-toml";
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 (file.type === "json") {
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
- const next = structuredClone(file.content);
1368
- setByDottedKey(next, file.key, version);
1369
- content = stringify(next);
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) || {};