@kungfu-tech/buildchain 2.12.7-alpha.8 → 2.12.7

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.
@@ -196,16 +196,21 @@ jobs:
196
196
  verify-command: make check
197
197
  artifact-paths: _build/paper-name.pdf
198
198
  buildchain-contract-lock-path: .buildchain/contract-lock.json
199
+ secrets:
200
+ BUILDCHAIN_PROMOTION_TOKEN: ${{ secrets.BUILDCHAIN_PROMOTION_TOKEN }}
199
201
  ```
200
202
 
201
- The sealed preset does not accept a long-lived promotion token. It builds and
202
- packages the paper in a read-only job, then a credential-free authority job
203
- downloads that exact candidate, audits the external control plane, and seals a
204
- capability over the source tree, Buildchain runtime, controller receipt, PDF,
205
- and npm package bytes. Only the final job receives write and OIDC permissions;
206
- it downloads the admitted candidate, recomputes the capability binding, and
207
- publishes without executing consumer build commands. npm binds the OIDC identity
208
- to the consumer workflow named by `publisher-workflow-path`.
203
+ The sealed preset does not use a long-lived token for npm publication. It may
204
+ accept an optional `BUILDCHAIN_PROMOTION_TOKEN` only for machine-generated
205
+ version-state updates on protected channel branches; npm publication remains
206
+ bound to GitHub OIDC trusted publishing. The preset builds and packages the
207
+ paper in a read-only job, then a credential-free authority job downloads that
208
+ exact candidate, audits the external control plane, and seals a capability over
209
+ the source tree, Buildchain runtime, controller receipt, PDF, and npm package
210
+ bytes. Only the final job receives write and OIDC permissions; it downloads the
211
+ admitted candidate, recomputes the capability binding, and publishes without
212
+ executing consumer build commands. npm binds the OIDC identity to the consumer
213
+ workflow named by `publisher-workflow-path`.
209
214
 
210
215
  The preset:
211
216
 
@@ -109,17 +109,20 @@ artifacts. The denial explicitly records that npm Trusted Publishing and OIDC
109
109
  were not evaluated, so downstream diagnostics cannot misclassify an admission
110
110
  assembly failure as an npm authentication failure.
111
111
 
112
- Buildchain's own `workflow_run` promotion lane may assemble those inputs only
113
- for `kungfu-systems/buildchain`. It downloads the exact prior RC passport,
112
+ An explicitly opted-in managed `workflow_run` promotion lane may assemble those
113
+ inputs from evidence owned by its caller repository. It downloads the exact prior RC passport,
114
114
  summary, referenced controller receipt, manifests, and product payloads; proves
115
115
  the admitted channel commit has the same Git tree as the RC; performs the live
116
116
  read-only control-plane audit; records the GitHub-hosted job as ephemeral runner
117
- provenance; and creates an explicit Buildchain-owned no-Gate decision. The
117
+ provenance; and consumes either a caller-supplied Gate aggregate or an explicit
118
+ caller no-Gate decision. The
118
119
  independent verifier then recomputes every receipt and payload digest exactly as
119
- it does for externally supplied admission. The self-assembly mode rejects other
120
- repositories, unknown refs, non-exact source SHAs, and any caller other than
121
- `.github/workflows/buildchain-ref-promotion.yml`. Manual apply and external
122
- consumer workflows still require their own explicit admission inputs.
120
+ it does for externally supplied admission. Automatic assembly keeps Buildchain
121
+ as the canonical authority repository, requires evidence to belong to the
122
+ caller, binds a repository-local publisher workflow, and rejects unknown refs,
123
+ non-exact source SHAs, package/target mismatches, or an undeclared Gate policy.
124
+ Manual apply and consumers that do not opt in still require their own explicit
125
+ admission inputs.
123
126
 
124
127
  Before authority verification, the reusable promotion controller runs the same
125
128
  release transaction selector in read-only mode. That plan supplies one exact
@@ -116,3 +116,22 @@ KFD-1 source/artifact contract surfaces, audits KFD-2 public release claims, and
116
116
  compares KFD-3 declared shipped public surfaces with artifact-exposed public
117
117
  surfaces. The release passport records the results under `kfd-1`, `kfd-2`, and
118
118
  the KFD-provided `kfd-3` section.
119
+
120
+ Managed consumers may also ask the promotion wrapper to assemble sealed
121
+ publication evidence from the exact release candidate instead of producing
122
+ short-lived admission JSON in repository-specific workflow code:
123
+
124
+ ```yaml
125
+ publication-auto-admission: true
126
+ publication-auto-no-gate: true
127
+ publication-publisher-workflow-path: .github/workflows/buildchain-ref-promotion.yml
128
+ publication-product: Example Product
129
+ publication-target: npm:@example/product
130
+ publication-package-name: "@example/product"
131
+ ```
132
+
133
+ `publication-auto-no-gate` is an explicit consumer decision, not a default. A
134
+ consumer with a Shifu Gate registry supplies `publication-gate-aggregate-json`
135
+ instead. Buildchain still requires caller-owned RC evidence, an exact authority
136
+ runtime and source SHA, a repository-local publisher workflow, matching npm
137
+ target/package identity, and a qualifying control-plane audit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.12.7-alpha.8",
3
+ "version": "2.12.7",
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",
@@ -157,6 +157,7 @@ export {
157
157
  PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
158
158
  createPublicationArtifactCandidate,
159
159
  publicationArtifactCandidateDigest,
160
+ resolvePublicationCandidateFile,
160
161
  } from "./publication-artifact-candidate.js";
161
162
 
162
163
  export {
@@ -20,6 +20,18 @@ export function publicationArtifactCandidateDigest(value) {
20
20
  return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
21
21
  }
22
22
 
23
+ export function resolvePublicationCandidateFile(files = [], candidatePath) {
24
+ const normalizedPath = requiredString(candidatePath, "candidatePath").replaceAll("\\", "/");
25
+ if (normalizedPath.startsWith("/") || normalizedPath.split("/").includes("..")) {
26
+ throw new Error(`publication artifact candidate contains an unsafe path: ${normalizedPath}`);
27
+ }
28
+ const matches = files.filter((entry) => entry.path === normalizedPath);
29
+ if (matches.length !== 1) {
30
+ throw new Error(`expected exactly one publication candidate file at ${normalizedPath}, found ${matches.length}`);
31
+ }
32
+ return matches[0].path;
33
+ }
34
+
23
35
  function requiredString(value, label) {
24
36
  const normalized = String(value || "").trim();
25
37
  if (!normalized) throw new Error(`${label} must be a non-empty string`);
@@ -115,13 +115,22 @@ async function main() {
115
115
  manifests,
116
116
  payloads: manifests.map((manifest) => payloadFor(manifest, path.join(evidenceRoot, "payloads"))),
117
117
  });
118
- const gateAggregate = createPublicationGateDecision({
119
- sourceSha,
120
- profile: process.env.BUILDCHAIN_GATE_PROFILE || "buildchain-self-publication",
121
- required: false,
122
- rationale: process.env.BUILDCHAIN_GATE_RATIONALE || "Buildchain self-publication has no consumer-owned Shifu Gate registry.",
123
- policy: { scope: "buildchain-self-publication", repository },
124
- });
118
+ const suppliedGateAggregate = String(process.env.BUILDCHAIN_GATE_AGGREGATE_JSON || "").trim();
119
+ let gateAggregate;
120
+ if (suppliedGateAggregate) {
121
+ gateAggregate = JSON.parse(suppliedGateAggregate);
122
+ } else {
123
+ if (process.env.BUILDCHAIN_ALLOW_NO_GATE !== "true") {
124
+ throw new Error("managed release-candidate admission requires a Gate aggregate or explicit no-Gate decision");
125
+ }
126
+ gateAggregate = createPublicationGateDecision({
127
+ sourceSha,
128
+ profile: process.env.BUILDCHAIN_GATE_PROFILE || "managed-release-candidate-no-gate",
129
+ required: false,
130
+ rationale: process.env.BUILDCHAIN_GATE_RATIONALE || "The consumer explicitly declared no Shifu Gate registry for this publication transaction.",
131
+ policy: { scope: "managed-release-candidate", repository },
132
+ });
133
+ }
125
134
  const runnerProvenance = createRunnerProvenance({
126
135
  runnerClass: "ephemeral",
127
136
  os: required("RUNNER_OS"),
@@ -178,6 +187,6 @@ async function main() {
178
187
  }
179
188
 
180
189
  main().catch((error) => {
181
- console.error(`assemble self publication admission: ${error.message}`);
190
+ console.error(`assemble release-candidate admission: ${error.message}`);
182
191
  process.exitCode = 1;
183
192
  });
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
8
  export const LOCKED_SOURCE_CHECKOUT_CONTRACT = "kungfu-buildchain-locked-source-checkout-cache";
9
+ export const ISOLATED_GIT_GLOBAL_CONFIG = process.platform === "win32" ? "NUL" : "/dev/null";
9
10
 
10
11
  const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
11
12
 
@@ -133,6 +134,24 @@ function githubAuthEnv(token = "") {
133
134
  };
134
135
  }
135
136
 
137
+ function isolatedGitFetchEnv(env = {}, targetPath) {
138
+ const configuredCount = Number.parseInt(env.GIT_CONFIG_COUNT || "0", 10);
139
+ const safeDirectoryIndex = Number.isInteger(configuredCount) && configuredCount >= 0
140
+ ? configuredCount
141
+ : 0;
142
+ return {
143
+ ...env,
144
+ // Runner-global URL rewrites are shared mutable state. A concurrent job
145
+ // may point the same repository URL at a different single-SHA bundle, so
146
+ // network fetches must not consult the account-level Git config.
147
+ GIT_CONFIG_GLOBAL: ISOLATED_GIT_GLOBAL_CONFIG,
148
+ GIT_CONFIG_NOSYSTEM: "1",
149
+ GIT_CONFIG_COUNT: String(safeDirectoryIndex + 1),
150
+ [`GIT_CONFIG_KEY_${safeDirectoryIndex}`]: "safe.directory",
151
+ [`GIT_CONFIG_VALUE_${safeDirectoryIndex}`]: path.resolve(targetPath),
152
+ };
153
+ }
154
+
136
155
  function markSafeDirectory(targetPath, timeoutMs) {
137
156
  try {
138
157
  git(["config", "--global", "--add", "safe.directory", path.resolve(targetPath)], {
@@ -203,9 +222,32 @@ export function fetchSourceCommit({
203
222
  sourceTreeSha = "",
204
223
  timeoutMs,
205
224
  env = {},
225
+ allowFullFetchRetry = false,
206
226
  runGit = git,
207
227
  containsCommit = hasCommit,
208
228
  }) {
229
+ const fetchEnv = isolatedGitFetchEnv(env, targetPath);
230
+ const fetch = (refspec) => {
231
+ const options = { cwd: targetPath, timeoutMs, env: fetchEnv };
232
+ try {
233
+ runGit(["fetch", "--no-tags", "--depth=1", remoteName, refspec], options);
234
+ return "shallow";
235
+ } catch (error) {
236
+ if (
237
+ !allowFullFetchRetry
238
+ || !/dumb http transport does not support shallow capabilities/i.test(
239
+ String(error?.message || error || ""),
240
+ )
241
+ ) {
242
+ throw error;
243
+ }
244
+ // Dumb HTTP mirrors are intentionally simple static cache endpoints.
245
+ // Retry only that explicit capability mismatch without --depth; all
246
+ // network fallbacks remain shallow and bounded by their own policy.
247
+ runGit(["fetch", "--no-tags", remoteName, refspec], options);
248
+ return "full";
249
+ }
250
+ };
209
251
  try {
210
252
  runGit(["remote", "remove", remoteName], { cwd: targetPath, timeoutMs, stdio: "ignore" });
211
253
  } catch {
@@ -219,13 +261,9 @@ export function fetchSourceCommit({
219
261
 
220
262
  if (fetchRef) {
221
263
  try {
222
- runGit(["fetch", "--no-tags", "--depth=1", remoteName, `+${fetchRef}:refs/buildchain/source-ref`], {
223
- cwd: targetPath,
224
- timeoutMs,
225
- env,
226
- });
264
+ const fetchMode = fetch(`+${fetchRef}:refs/buildchain/source-ref`);
227
265
  if (containsCommit(targetPath, sha, timeoutMs)) {
228
- return { selector: "ref", checkoutSha: sha };
266
+ return { selector: "ref", checkoutSha: sha, fetchMode };
229
267
  }
230
268
  if (/^refs\/pull\/\d+\/merge$/.test(fetchRef) && sourceTreeSha) {
231
269
  const fetchedSha = runGit(["rev-parse", "refs/buildchain/source-ref^{commit}"], {
@@ -237,7 +275,7 @@ export function fetchSourceCommit({
237
275
  timeoutMs,
238
276
  });
239
277
  if (fetchedTree === sourceTreeSha) {
240
- return { selector: "ref-tree", checkoutSha: fetchedSha };
278
+ return { selector: "ref-tree", checkoutSha: fetchedSha, fetchMode };
241
279
  }
242
280
  }
243
281
  } catch (error) {
@@ -249,15 +287,11 @@ export function fetchSourceCommit({
249
287
  }
250
288
  }
251
289
 
252
- runGit(["fetch", "--no-tags", "--depth=1", remoteName, `+${sha}:refs/buildchain/source`], {
253
- cwd: targetPath,
254
- timeoutMs,
255
- env,
256
- });
290
+ const fetchMode = fetch(`+${sha}:refs/buildchain/source`);
257
291
  if (!containsCommit(targetPath, sha, timeoutMs)) {
258
292
  throw new Error(`fetched ${fetchRef || sha}, but ${sha} is not available`);
259
293
  }
260
- return { selector: "sha", checkoutSha: sha };
294
+ return { selector: "sha", checkoutSha: sha, fetchMode };
261
295
  }
262
296
 
263
297
  export function runBoundedFetch({ attempts = 1, fetch, onAttempt = () => {}, onRetry = () => {}, shouldRetry = () => true }) {
@@ -413,7 +447,9 @@ export function lockedSourceCheckout({
413
447
  fetchRef,
414
448
  sourceTreeSha: treeSha,
415
449
  timeoutMs,
450
+ allowFullFetchRetry: true,
416
451
  });
452
+ evidence.cache.fetchMode = fetchResult.fetchMode;
417
453
  checkoutSha = fetchResult.checkoutSha || sha;
418
454
  checkoutFetchedCommit(targetPath, checkoutSha, timeoutMs);
419
455
  } else {
@@ -458,6 +494,7 @@ export function lockedSourceCheckout({
458
494
  shouldRetry: retryableGitFetchError,
459
495
  });
460
496
  evidence.cache.githubFetchAttempts = fetchResult.attempts;
497
+ evidence.cache.fetchMode = fetchResult.value.fetchMode;
461
498
  checkoutSha = fetchResult.value.checkoutSha || sha;
462
499
  } catch (error) {
463
500
  evidence.durationMs = Date.now() - startedAt;
@@ -4,7 +4,12 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
 
7
- import { createPublicationArtifactCandidate } from "../packages/core/publication-artifact-candidate.js";
7
+ import {
8
+ createPublicationArtifactCandidate,
9
+ resolvePublicationCandidateFile,
10
+ } from "../packages/core/publication-artifact-candidate.js";
11
+
12
+ export { resolvePublicationCandidateFile };
8
13
 
9
14
  function flag(name, fallback = "") {
10
15
  const index = process.argv.indexOf(`--${name}`);
@@ -17,27 +22,16 @@ function requiredFlag(name) {
17
22
  return value;
18
23
  }
19
24
 
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
- }
25
+ function exactJson(root, relativePath) {
26
+ const absoluteRoot = path.resolve(root);
27
+ const absolutePath = path.resolve(absoluteRoot, relativePath);
28
+ if (!absolutePath.startsWith(`${absoluteRoot}${path.sep}`)) {
29
+ throw new Error(`publication evidence path escapes artifact root: ${relativePath}`);
30
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"));
31
+ if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
32
+ throw new Error(`expected publication evidence at ${relativePath}`);
33
+ }
34
+ return JSON.parse(fs.readFileSync(absolutePath, "utf8"));
41
35
  }
42
36
 
43
37
  function collectFiles(root) {
@@ -79,12 +73,15 @@ export function buildPublicationArtifactCandidate({
79
73
  sourceSha,
80
74
  sourceTreeSha,
81
75
  runtimeSha,
82
- manifest: oneJson(resolvedArtifactRoot, "publication-artifact.json"),
83
- passport: oneJson(
76
+ manifest: exactJson(
77
+ resolvedArtifactRoot,
78
+ ".buildchain/publication/publication-artifact.json",
79
+ ),
80
+ passport: exactJson(
84
81
  resolvedArtifactRoot,
85
- "publication-artifact-passport.json",
82
+ ".buildchain/publication/publication-artifact-passport.json",
86
83
  ),
87
- controllerReceipt: oneJson(resolvedControllerRoot, "receipt.json"),
84
+ controllerReceipt: exactJson(resolvedControllerRoot, "receipt.json"),
88
85
  files: collectFiles(resolvedArtifactRoot),
89
86
  };
90
87
  const candidate = createPublicationArtifactCandidate(evidence);