@kungfu-tech/buildchain 2.11.14-alpha.1 → 2.11.14-alpha.2

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.
@@ -412,6 +412,7 @@
412
412
  "buildchain-ref",
413
413
  "buildchain-repository",
414
414
  "checkout-cache-fallback",
415
+ "checkout-cache-fetch-attempts",
415
416
  "checkout-cache-mirror-url-template",
416
417
  "checkout-cache-mode",
417
418
  "checkout-cache-reference-repository-template",
@@ -442,7 +443,7 @@
442
443
  "verify-command",
443
444
  "working-directory"
444
445
  ],
445
- "inputCount": 48
446
+ "inputCount": 49
446
447
  },
447
448
  {
448
449
  "id": ".bump-minor-version",
@@ -2538,8 +2539,8 @@
2538
2539
  "workflowRegistryPath": "dist/site/workflow-registry.json",
2539
2540
  "pageRegistryPath": "dist/site/page-registry.json",
2540
2541
  "cliRegistryDigest": "3c0729f5351eb82fe26625fa5aabf30903f45f68af2431e02860f85ada468ac9",
2541
- "workflowRegistryDigest": "4041321df08640a802a87ff0fbe8584d95dc416945b43778727adb820cab9097",
2542
- "pageRegistryDigest": "172c596669f7793af188430bde105d1e940c4927ba6446dc9f7ecf0456f1a4e1"
2542
+ "workflowRegistryDigest": "7052aa984ab49647ce2c9f96669caa32f92da57d8b2e506ac172089bedd793cf",
2543
+ "pageRegistryDigest": "784424e0ecb730449947fc0a3aba0bfcff77c34d097d63d71a9d60d5db1383df"
2543
2544
  },
2544
2545
  "comparison": {
2545
2546
  "missingCliRegistry": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-publication-release-registry",
4
- "generatedAt": "2026-07-10T11:01:58.300Z",
5
- "publishedAt": "2026-07-10T11:01:58.300Z",
4
+ "generatedAt": "2026-07-10T13:03:42.866Z",
5
+ "publishedAt": "2026-07-10T13:03:42.866Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "5a95cdcb4e1114635046a0765aec1ea1ecc60dcd",
22
+ "sourceRevision": "d0be2b26d092ef5fa411d30888ea0ee3fa044c85",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "2.11.14-alpha.1",
35
+ "version": "2.11.14-alpha.2",
36
36
  "versionSource": "package.json#version"
37
37
  },
38
38
  "sourceKind": "package-site-bundle",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-07-10T11:01:58.300Z",
5
- "publishedAt": "2026-07-10T11:01:58.300Z",
4
+ "generatedAt": "2026-07-10T13:03:42.866Z",
5
+ "publishedAt": "2026-07-10T13:03:42.866Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "5a95cdcb4e1114635046a0765aec1ea1ecc60dcd",
22
+ "sourceRevision": "d0be2b26d092ef5fa411d30888ea0ee3fa044c85",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "2.11.14-alpha.1",
40
+ "version": "2.11.14-alpha.2",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -197,7 +197,7 @@
197
197
  "path": "docs/lifecycle-protocol.md",
198
198
  "plane": "use",
199
199
  "exists": true,
200
- "digest": "sha256:16c8e4994276142e6b57a61a18a88998727dce5911cb2da96d3900c049fb76dc"
200
+ "digest": "sha256:60c39693878258d275e9d47983b306faed35d6416297c14570429de73284604c"
201
201
  },
202
202
  {
203
203
  "id": "reusable-build-surface",
@@ -205,7 +205,7 @@
205
205
  "path": "docs/reusable-build-surface.md",
206
206
  "plane": "use",
207
207
  "exists": true,
208
- "digest": "sha256:5f56d66ad2798bc46acde3f8091c2e5ccee80412c1b26b70c8197ff9c7f6a406"
208
+ "digest": "sha256:4f3699841c5a189d199db045d526b11f76ad0dff1464d6a63ca98ceadf1bb215"
209
209
  },
210
210
  {
211
211
  "id": "publish-transaction",
@@ -221,7 +221,7 @@
221
221
  "path": "docs/release-governance.md",
222
222
  "plane": "why",
223
223
  "exists": true,
224
- "digest": "sha256:f2eae65a8b678203da26e223bda9cffd7939852deea406f7328ce9e617da3c6f"
224
+ "digest": "sha256:59858b6e02e42377b8a1f3d6d33ba50c5ffba476f7e1443fd61e03919046eff2"
225
225
  },
226
226
  {
227
227
  "id": "release-flow",
@@ -253,7 +253,7 @@
253
253
  "path": "docs/web-surface-deployments.md",
254
254
  "plane": "use",
255
255
  "exists": true,
256
- "digest": "sha256:4c88b27af8aeb3f72fc8b979529b68d5fbc910dce70fe4219192c09aeb404e5c"
256
+ "digest": "sha256:f1e92ba87417dc9aac4f8cf080565f7f2c114b5007bb68b90f33692c73fdad88"
257
257
  }
258
258
  ],
259
259
  "facts": [
@@ -26,6 +26,7 @@
26
26
  "buildchain-ref",
27
27
  "buildchain-repository",
28
28
  "checkout-cache-fallback",
29
+ "checkout-cache-fetch-attempts",
29
30
  "checkout-cache-mirror-url-template",
30
31
  "checkout-cache-mode",
31
32
  "checkout-cache-reference-repository-template",
@@ -56,7 +57,7 @@
56
57
  "verify-command",
57
58
  "working-directory"
58
59
  ],
59
- "inputCount": 48,
60
+ "inputCount": 49,
60
61
  "surface": "reusable-workflow",
61
62
  "capabilityGroup": "api-cli-reference",
62
63
  "status": "active"
@@ -293,7 +293,10 @@ checked-out release material. Any code or undeclared file change still fails the
293
293
  release tree gate.
294
294
 
295
295
  The action input `verification-command` remains supported. When it is provided,
296
- it overrides `lifecycle.verify` for that invocation.
296
+ it overrides `lifecycle.verify` for that invocation. The override inherits
297
+ `[lifecycle.env]` and `lifecycle.verify.env`; when the configured stage declares
298
+ `shell`, it also inherits that shell. If the stage has no explicit shell,
299
+ Buildchain preserves the platform-default shell for compatibility.
297
300
 
298
301
  ## Migration Preflight
299
302
 
@@ -150,6 +150,15 @@ The build-only workflow:
150
150
 
151
151
  It does not publish npm packages, deploy web pages, or create GitHub Releases.
152
152
 
153
+ The paper release preset additionally hydrates every prior published package
154
+ registry from the npm registry before generating the current manifest. npm
155
+ package integrity authenticates each downloaded source; Buildchain verifies the
156
+ registry self-digest, merges immutable records, and fails if a cumulative
157
+ registry drops an accepted version or changes immutable route/artifact facts.
158
+ The synthesized package therefore carries complete history even on a clean
159
+ runner. Its cumulative registry and file SHA-256 values are bound into the paper
160
+ release build summary and release passport evidence.
161
+
153
162
  ## Paper Release Preset
154
163
 
155
164
  Paper repositories that publish a versioned npm package should use the
@@ -318,8 +318,14 @@ After each merge, the next PR is re-evaluated before it can move the protected
318
318
  dev branch. This prevents one merge from silently making the next candidate
319
319
  stale or conflicting.
320
320
 
321
- The required check should be the `check` job. Repositories can keep that job
322
- name stable while changing the actual verification command declaratively in
321
+ The canonical consumer required check context is `check / check`, matching the
322
+ reusable workflow call plus its `check` job. Buildchain's own `Verify` workflow
323
+ emits the repository-local context `check`, so Buildchain self-promotion,
324
+ release-line bootstrap, and dogfood patrol wrappers pass `check` explicitly.
325
+ Release governance preserves the exact context emitted by the repository and
326
+ records branch-protection policy before/after facts; it does not rewrite one
327
+ context shape into the other. Consumer repositories can keep their context
328
+ stable while changing the actual verification command declaratively in
323
329
  `buildchain.toml`:
324
330
 
325
331
  ```toml
@@ -359,7 +365,7 @@ jobs:
359
365
  statuses: read
360
366
  with:
361
367
  target-branch: dev/v2/v2.6
362
- required-status-checks: check
368
+ required-status-checks: check / check
363
369
  ready-label: ready
364
370
  block-labels: blocked,do-not-merge
365
371
  max-merges: 1
@@ -233,6 +233,7 @@ jobs:
233
233
  checkout-cache-reference-repository-template: ${{ vars.BUILDCHAIN_CHECKOUT_CACHE_REFERENCE_REPOSITORY_TEMPLATE }}
234
234
  checkout-cache-fallback: github
235
235
  checkout-cache-timeout-seconds: 60
236
+ checkout-cache-fetch-attempts: 3
236
237
  ```
237
238
 
238
239
  `checkout-cache-mode` accepts:
@@ -255,7 +256,10 @@ Do not read cache URLs or reference paths from PR-controlled files such as
255
256
  `.buildchain/buildchain.toml`. These values are trusted workflow inputs or repo/org
256
257
  variables. Buildchain does not pass GitHub credentials to cache mirrors or
257
258
  reference repositories. If it must fall back to GitHub, the workflow token is
258
- used only for the GitHub fetch path.
259
+ used only for the GitHub fetch path. Retryable timeout and transient network
260
+ failures use the bounded `checkout-cache-fetch-attempts` budget; permanent
261
+ failures stop immediately. Diagnostics record the configured budget and actual
262
+ GitHub fetch attempts before exact HEAD/tree verification.
259
263
 
260
264
  Each platform diagnostics artifact includes `source-checkout.json` and embeds a
261
265
  compact `sourceCheckout` summary in `diagnostics.json`: mode, transport,
@@ -592,7 +596,7 @@ jobs:
592
596
  runner-preset: github-hosted
593
597
  trusted-publishing: true
594
598
  github-release: true
595
- required-status-check: check
599
+ required-status-check: check / check
596
600
  required-artifact-count: 3
597
601
  publish-dist-tag: alpha
598
602
  publish-package-set-order: platforms-first-main-last
@@ -351,6 +351,16 @@ The CLI emits GitHub outputs when `GITHUB_OUTPUT` is present:
351
351
  - `web-surface-artifact-hash`
352
352
  - `web-surface-manifest-json`
353
353
 
354
+ The reusable workflow resolves that same effective channel before running the
355
+ caller build or verify command. Both steps receive
356
+ `BUILDCHAIN_WEB_SURFACE_CHANNEL` (`preview`, `staging`, or `production`) and
357
+ `BUILDCHAIN_PREVIEW_ALIAS` for previews. Compatibility aliases
358
+ `BUILDCHAIN_SURFACE_CHANNEL` and `BUILDCHAIN_WEB_SURFACE_ALIAS` are also
359
+ provided. Callers should consume these variables instead of reconstructing the
360
+ release-intent state machine from raw GitHub events.
361
+ An unapproved manual canary resolves to `staging`; a trusted manual dispatch
362
+ with `production-approved=true` resolves to `production` through the same path.
363
+
354
364
  ## Explicit Apply
355
365
 
356
366
  `deploy-apply` and `cleanup-apply` are explicit execution modes for the
@@ -452,7 +462,11 @@ HTTP check is visible from the consumer run.
452
462
 
453
463
  It can also execute a previously saved deploy plan. In that mode Buildchain
454
464
  recomputes the local artifact hash before running AWS commands and fails closed
455
- if the artifact no longer matches the saved plan:
465
+ if the artifact no longer matches the saved plan. Before any adapter operation,
466
+ it also checks channel-aware JSON manifests in the artifact: top-level
467
+ `canonicalHost` and declared `pages[].host` facts must belong to the plan's
468
+ surface hosts. A production plan therefore rejects staging/preview host facts
469
+ before AWS apply:
456
470
 
457
471
  ```bash
458
472
  node scripts/web-surface.mjs \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.11.14-alpha.1",
3
+ "version": "2.11.14-alpha.2",
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",
@@ -96,6 +96,10 @@ export function createBuildchainContractWorld({ root = process.cwd(), packageJso
96
96
  "platforms-json",
97
97
  "release-candidate",
98
98
  "artifact-transfer-mode",
99
+ "checkout-cache-mode",
100
+ "checkout-cache-fallback",
101
+ "checkout-cache-timeout-seconds",
102
+ "checkout-cache-fetch-attempts",
99
103
  "buildchain-contract-lock-path",
100
104
  "buildchain-contract-drift-issue-mode",
101
105
  ],
@@ -104,6 +108,7 @@ export function createBuildchainContractWorld({ root = process.cwd(), packageJso
104
108
  "publish source locks are verified before heavy build jobs",
105
109
  "release-candidate builds do not publish registry artifacts",
106
110
  "contract drift is checked before heavy build jobs for stable floating refs",
111
+ "retryable GitHub fallback fetches use a bounded attempt budget before exact source SHA and tree verification",
107
112
  ],
108
113
  }),
109
114
  surface(root, {
@@ -115,7 +120,7 @@ export function createBuildchainContractWorld({ root = process.cwd(), packageJso
115
120
  requiredOutputs: ["promoted-sha", "built-source-sha", "release-candidate-artifact"],
116
121
  breakingDefaults: {
117
122
  promoteOnlyReleaseCandidate: true,
118
- requiredStatusCheck: "check",
123
+ requiredStatusCheck: "check / check",
119
124
  },
120
125
  optionalInputs: [
121
126
  "buildchain-ref",
@@ -177,6 +182,8 @@ export function createBuildchainContractWorld({ root = process.cwd(), packageJso
177
182
  "contract drift is checked before caller build, web-surface render, deploy planning, and apply side effects",
178
183
  "compatible contract drift continues and produces a consumer issue or summary",
179
184
  "breaking contract drift fails closed before rendering, deployment, or release publication",
185
+ "effective channel and preview alias are injected before caller build and verify commands",
186
+ "channel-aware artifact host facts are checked against the deploy plan before adapter side effects",
180
187
  ],
181
188
  }),
182
189
  surface(root, {
@@ -189,6 +196,7 @@ export function createBuildchainContractWorld({ root = process.cwd(), packageJso
189
196
  breakingDefaults: {
190
197
  requireGovernance: false,
191
198
  releasePassport: true,
199
+ requiredStatusCheck: "check / check",
192
200
  },
193
201
  optionalInputs: [
194
202
  "publish-transaction",
@@ -477,7 +477,54 @@ function publicationArchiveRecord({ collected, manifestPath, passportPath, regis
477
477
  };
478
478
  }
479
479
 
480
- function updatePublicationArchiveRegistry({ cwd, collected, manifestPath, passportPath, registryOutput, generatedAt }) {
480
+ function immutablePublicationRecord(record = {}) {
481
+ const { publishedAt: _publishedAt, latestObservedAt: _latestObservedAt, recordSha256: _recordSha256, ...immutable } = record;
482
+ return immutable;
483
+ }
484
+
485
+ function readVerifiedPublicationRegistry(registryPath) {
486
+ const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
487
+ if (registry.contract !== PUBLICATION_ARTIFACT_REGISTRY_CONTRACT) {
488
+ throw new Error(`publication archive registry has unsupported contract: ${registry.contract || "(missing)"}`);
489
+ }
490
+ const expectedDigest = sha256Json({ ...registry, registrySha256: undefined });
491
+ if (registry.registrySha256 !== expectedDigest) {
492
+ throw new Error(`publication archive registry digest mismatch for ${registryPath}: expected ${expectedDigest}, got ${registry.registrySha256 || "(missing)"}`);
493
+ }
494
+ if (!Array.isArray(registry.versions)) throw new Error(`publication archive registry versions must be an array: ${registryPath}`);
495
+ return registry;
496
+ }
497
+
498
+ function mergePublicationRegistryVersions({ registries = [], publicationId }) {
499
+ const versions = new Map();
500
+ for (const registry of registries) {
501
+ if (registry.publication?.id !== publicationId) {
502
+ throw new Error(`publication archive registry id mismatch: expected ${publicationId}, got ${registry.publication?.id || "(missing)"}`);
503
+ }
504
+ if (registry.historyPolicy === "cumulative-authenticated-hydration") {
505
+ const declaredVersions = new Set(registry.versions.map((record) => String(record?.version || "")));
506
+ const missingVersions = [...versions.keys()].filter((version) => !declaredVersions.has(version));
507
+ if (missingVersions.length > 0) throw new Error(`cumulative publication archive registry dropped accepted versions: ${missingVersions.join(", ")}`);
508
+ }
509
+ for (const record of registry.versions) {
510
+ const version = String(record?.version || "").trim();
511
+ if (!version) throw new Error("publication archive registry record is missing version");
512
+ const existing = versions.get(version);
513
+ if (!existing) {
514
+ versions.set(version, record);
515
+ continue;
516
+ }
517
+ if (existing.immutableDigest !== record.immutableDigest || stableJson(immutablePublicationRecord(existing)) !== stableJson(immutablePublicationRecord(record))) {
518
+ throw new Error(`publication archive version ${version} changed across accepted registries: ${existing.immutableDigest || "(missing)"} != ${record.immutableDigest || "(missing)"}`);
519
+ }
520
+ const latestObservedAt = [existing.latestObservedAt, record.latestObservedAt].filter(Boolean).sort().at(-1);
521
+ versions.set(version, latestObservedAt ? { ...existing, latestObservedAt } : existing);
522
+ }
523
+ }
524
+ return [...versions.values()];
525
+ }
526
+
527
+ function updatePublicationArchiveRegistry({ cwd, collected, manifestPath, passportPath, registryOutput, registryInputs = [], generatedAt }) {
481
528
  const archive = collected.manifest.publication.archive;
482
529
  if (!archive) {
483
530
  return undefined;
@@ -503,17 +550,16 @@ function updatePublicationArchiveRegistry({ cwd, collected, manifestPath, passpo
503
550
  siteConsumers: collected.manifest.publication.siteConsumers,
504
551
  },
505
552
  appendOnly: true,
553
+ historyPolicy: "cumulative-authenticated-hydration",
506
554
  sameVersionRepublish: "fail-on-digest-change",
507
555
  immutablePathPolicy: archive.immutablePathPolicy,
508
556
  versions: [],
509
557
  };
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 : [];
558
+ const inputPaths = [...registryInputs.map((entry) => path.resolve(cwd, entry)), ...(fs.existsSync(registryPath) ? [registryPath] : [])];
559
+ const versions = mergePublicationRegistryVersions({
560
+ registries: inputPaths.map(readVerifiedPublicationRegistry),
561
+ publicationId: archive.id,
562
+ });
517
563
  const existingIndex = versions.findIndex((entry) => entry.version === record.version);
518
564
  if (existingIndex !== -1) {
519
565
  const existing = versions[existingIndex];
@@ -559,6 +605,7 @@ export function writePublicationArtifact({
559
605
  output = "",
560
606
  passportOutput = "",
561
607
  registryOutput = "",
608
+ registryInputs = [],
562
609
  sourceSha = "",
563
610
  sourceBundle = true,
564
611
  sourceBundlePath = "",
@@ -586,6 +633,7 @@ export function writePublicationArtifact({
586
633
  manifestPath: path.resolve(cwd, manifestOutput),
587
634
  passportPath: path.resolve(cwd, passportPath),
588
635
  registryOutput,
636
+ registryInputs,
589
637
  generatedAt,
590
638
  });
591
639
  return {
@@ -106,6 +106,7 @@ export function collectPublicationPackageFacts({
106
106
  manifestPath: publication.manifestPath,
107
107
  passportPath: ".buildchain/publication/publication-artifact-passport.json",
108
108
  registryPath: publication.archive?.registryPath || "",
109
+ registryHydrationPath: publication.archive ? ".buildchain/publication/registry-hydration.json" : "",
109
110
  sourceBundlePath: publication.sourceBundlePath,
110
111
  artifactPaths: [publication.primaryArtifact, ...publication.artifactPaths].filter(Boolean),
111
112
  metadataPaths: publication.metadataPaths,
@@ -131,6 +132,7 @@ export function preparePublicationNpmPackage({
131
132
  facts.publication.manifestPath,
132
133
  facts.publication.passportPath,
133
134
  facts.publication.registryPath,
135
+ facts.publication.registryHydrationPath,
134
136
  facts.publication.sourceBundlePath,
135
137
  ...facts.publication.artifactPaths,
136
138
  ...facts.publication.metadataPaths,
@@ -197,6 +199,9 @@ export function preparePublicationNpmPackage({
197
199
  publicationManifest: facts.publication.manifestPath,
198
200
  publicationPassport: facts.publication.passportPath,
199
201
  publicationRegistry: facts.publication.registryPath || undefined,
202
+ publicationRegistryHydration: files.some((file) => file.path === facts.publication.registryHydrationPath)
203
+ ? facts.publication.registryHydrationPath
204
+ : undefined,
200
205
  primaryArtifact: facts.publication.primaryArtifact,
201
206
  sourceBundle: facts.publication.sourceBundlePath,
202
207
  siteConsumers: facts.publication.siteConsumers,
@@ -213,6 +213,31 @@ function fetchSha({ targetPath, remoteName, remoteUrl, sha, fetchRef, timeoutMs,
213
213
  }
214
214
  }
215
215
 
216
+ export function runBoundedFetch({ attempts = 1, fetch, onAttempt = () => {}, onRetry = () => {}, shouldRetry = () => true }) {
217
+ const limit = Math.max(1, Math.floor(Number(attempts) || 1));
218
+ let lastError;
219
+ for (let attempt = 1; attempt <= limit; attempt += 1) {
220
+ onAttempt({ attempt, limit });
221
+ try {
222
+ return { value: fetch({ attempt, limit }), attempts: attempt };
223
+ } catch (error) {
224
+ lastError = error;
225
+ if (attempt >= limit || !shouldRetry(error)) {
226
+ error.fetchAttempts = attempt;
227
+ throw error;
228
+ }
229
+ onRetry({ attempt, limit, error });
230
+ }
231
+ }
232
+ throw lastError;
233
+ }
234
+
235
+ function retryableGitFetchError(error) {
236
+ const code = String(error?.code || "").toUpperCase();
237
+ if (["ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "ENETUNREACH", "EPIPE"].includes(code)) return true;
238
+ return /timed?\s*out|timeout|connection (?:reset|refused)|remote end hung up|early eof|rpc failed|http (?:429|5\d\d)|temporary failure|network is unreachable/i.test(String(error?.message || error || ""));
239
+ }
240
+
216
241
  function githubRemoteUrl({ repository, serverUrl = "https://github.com" }) {
217
242
  const base = String(serverUrl || "https://github.com").replace(/\/+$/, "");
218
243
  return `${base}/${repository}.git`;
@@ -252,6 +277,7 @@ export function lockedSourceCheckout({
252
277
  referenceRepositoryTemplate = readEnv("BUILDCHAIN_CHECKOUT_CACHE_REFERENCE_REPOSITORY_TEMPLATE"),
253
278
  fallback = readEnv("BUILDCHAIN_CHECKOUT_CACHE_FALLBACK", "github"),
254
279
  timeoutSeconds = Number(readEnv("BUILDCHAIN_CHECKOUT_CACHE_TIMEOUT_SECONDS", "60") || 60),
280
+ fetchAttempts = Number(readEnv("BUILDCHAIN_CHECKOUT_CACHE_FETCH_ATTEMPTS", "3") || 3),
255
281
  diagnosticsPath = readEnv("BUILDCHAIN_SOURCE_CHECKOUT_DIAGNOSTICS_PATH", ".buildchain/diagnostics/source-checkout.json"),
256
282
  githubToken = readEnv("GITHUB_TOKEN"),
257
283
  githubServerUrl = readEnv("GITHUB_SERVER_URL", "https://github.com"),
@@ -265,6 +291,7 @@ export function lockedSourceCheckout({
265
291
  const treeSha = sourceTreeSha ? assertSha(sourceTreeSha, "sourceTreeSha") : "";
266
292
  const repoParts = splitRepository(repository);
267
293
  const timeoutMs = Math.max(1, Number(timeoutSeconds || 60)) * 1000;
294
+ const normalizedFetchAttempts = Math.max(1, Math.floor(Number(fetchAttempts) || 3));
268
295
  const targetPath = ensureCheckoutTarget(checkoutPath, workspace);
269
296
  git(["init"], { cwd: targetPath, timeoutMs });
270
297
  const renderedMirrorUrl = renderTemplate(mirrorUrlTemplate, { ...repoParts, sha });
@@ -285,6 +312,7 @@ export function lockedSourceCheckout({
285
312
  mode: normalizedMode,
286
313
  fallback: normalizedFallback,
287
314
  timeoutSeconds: Math.max(1, Number(timeoutSeconds || 60)),
315
+ fetchAttempts: normalizedFetchAttempts,
288
316
  mirror: sanitizeIdentity(renderedMirrorUrl),
289
317
  referenceRepository: sanitizeIdentity(renderedReferenceRepository),
290
318
  },
@@ -294,6 +322,7 @@ export function lockedSourceCheckout({
294
322
  transport: "github",
295
323
  fallbackUsed: normalizedMode === "off",
296
324
  fallbackReason: normalizedMode === "off" ? "cache disabled" : "",
325
+ githubFetchAttempts: 0,
297
326
  },
298
327
  verification: {
299
328
  head: "",
@@ -352,15 +381,20 @@ export function lockedSourceCheckout({
352
381
  if (!evidence.cache.fallbackReason && checkoutError) {
353
382
  evidence.cache.fallbackReason = checkoutError.message;
354
383
  }
355
- fetchSha({
356
- targetPath,
357
- remoteName: "origin",
358
- remoteUrl,
359
- sha,
360
- fetchRef,
361
- timeoutMs,
362
- env: githubAuthEnv(githubToken),
363
- });
384
+ try {
385
+ const fetchResult = runBoundedFetch({
386
+ attempts: normalizedFetchAttempts,
387
+ fetch: () => fetchSha({ targetPath, remoteName: "origin", remoteUrl, sha, fetchRef, timeoutMs, env: githubAuthEnv(githubToken) }),
388
+ onAttempt: ({ attempt }) => { evidence.cache.githubFetchAttempts = attempt; },
389
+ onRetry: ({ attempt, limit, error }) => console.log(`buildchain: GitHub source fetch failed, retry ${attempt + 1}/${limit}: ${error.message}`),
390
+ shouldRetry: retryableGitFetchError,
391
+ });
392
+ evidence.cache.githubFetchAttempts = fetchResult.attempts;
393
+ } catch (error) {
394
+ evidence.durationMs = Date.now() - startedAt;
395
+ writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
396
+ throw error;
397
+ }
364
398
  checkoutFetchedCommit(targetPath, sha, timeoutMs);
365
399
  }
366
400
  evidence.verification = verifyCheckout({ targetPath, sourceSha: sha, sourceTreeSha: treeSha });
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { pathToFileURL } from "node:url";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
3
5
  import { writePublicationArtifact } from "../packages/core/publication-artifact.js";
4
6
 
5
7
  function readFlag(args, name, fallback = "") {
@@ -11,16 +13,32 @@ function readBooleanFlag(args, name) {
11
13
  return args.includes(`--${name}`);
12
14
  }
13
15
 
16
+ function readRegistryInputs(args, cwd) {
17
+ const inputs = [];
18
+ for (let index = 0; index < args.length; index += 1) {
19
+ if (args[index] === "--registry-input" && args[index + 1]) inputs.push(args[index + 1]);
20
+ }
21
+ const inputDir = readFlag(args, "registry-input-dir", "");
22
+ if (inputDir) {
23
+ const resolved = path.resolve(cwd, inputDir);
24
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) throw new Error(`publication registry input directory is missing: ${inputDir}`);
25
+ inputs.push(...fs.readdirSync(resolved).filter((entry) => entry.endsWith(".json")).sort().map((entry) => path.join(resolved, entry)));
26
+ }
27
+ return inputs;
28
+ }
29
+
14
30
  export function runPublicationArtifactCli(args = process.argv.slice(2)) {
15
31
  const [mode = "manifest"] = args;
16
32
  if (!["manifest", "collect"].includes(mode)) {
17
- throw new Error("usage: buildchain publication-artifact manifest [--cwd <dir>] [--source-sha <sha>] [--output <file>] [--passport-output <file>] [--registry-output <file>] [--source-bundle <file>] [--no-source-bundle] [--json]");
33
+ throw new Error("usage: buildchain publication-artifact manifest [--cwd <dir>] [--source-sha <sha>] [--output <file>] [--passport-output <file>] [--registry-output <file>] [--registry-input <file>] [--registry-input-dir <dir>] [--source-bundle <file>] [--no-source-bundle] [--json]");
18
34
  }
35
+ const cwd = readFlag(args, "cwd", process.cwd());
19
36
  const result = writePublicationArtifact({
20
- cwd: readFlag(args, "cwd", process.cwd()),
37
+ cwd,
21
38
  output: readFlag(args, "output", ""),
22
39
  passportOutput: readFlag(args, "passport-output", ""),
23
40
  registryOutput: readFlag(args, "registry-output", ""),
41
+ registryInputs: readRegistryInputs(args, cwd),
24
42
  sourceSha: readFlag(args, "source-sha", ""),
25
43
  sourceBundlePath: readFlag(args, "source-bundle", ""),
26
44
  sourceBundle: !readBooleanFlag(args, "no-source-bundle"),