@kungfu-tech/buildchain 2.12.7-alpha.9 → 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.
@@ -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.9",
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",
@@ -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;
@@ -22,27 +22,16 @@ function requiredFlag(name) {
22
22
  return value;
23
23
  }
24
24
 
25
- function filesNamed(root, name) {
26
- const matches = [];
27
- const pending = [path.resolve(root)];
28
- while (pending.length > 0) {
29
- const current = pending.pop();
30
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
31
- const full = path.join(current, entry.name);
32
- if (entry.isDirectory()) pending.push(full);
33
- else if (entry.isFile() && entry.name === name) matches.push(full);
34
- }
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}`);
35
30
  }
36
- return matches.sort();
37
- }
38
-
39
- function oneJson(root, name) {
40
- const matches = filesNamed(root, name);
41
- if (matches.length !== 1)
42
- throw new Error(
43
- `expected exactly one ${name} under ${root}, found ${matches.length}`,
44
- );
45
- 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"));
46
35
  }
47
36
 
48
37
  function collectFiles(root) {
@@ -84,12 +73,15 @@ export function buildPublicationArtifactCandidate({
84
73
  sourceSha,
85
74
  sourceTreeSha,
86
75
  runtimeSha,
87
- manifest: oneJson(resolvedArtifactRoot, "publication-artifact.json"),
88
- passport: oneJson(
76
+ manifest: exactJson(
77
+ resolvedArtifactRoot,
78
+ ".buildchain/publication/publication-artifact.json",
79
+ ),
80
+ passport: exactJson(
89
81
  resolvedArtifactRoot,
90
- "publication-artifact-passport.json",
82
+ ".buildchain/publication/publication-artifact-passport.json",
91
83
  ),
92
- controllerReceipt: oneJson(resolvedControllerRoot, "receipt.json"),
84
+ controllerReceipt: exactJson(resolvedControllerRoot, "receipt.json"),
93
85
  files: collectFiles(resolvedArtifactRoot),
94
86
  };
95
87
  const candidate = createPublicationArtifactCandidate(evidence);