@kungfu-tech/buildchain 2.8.16 → 2.8.17

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.
Files changed (55) hide show
  1. package/README.md +4 -4
  2. package/dist/site/agent-index.json +2 -1
  3. package/dist/site/artifact-schemas.json +1 -0
  4. package/dist/site/badge-endpoint-registry.json +659 -0
  5. package/dist/site/badges/v1/buildchain-release-passport/aligned.json +14 -0
  6. package/dist/site/badges/v1/buildchain-release-passport/declared.json +14 -0
  7. package/dist/site/badges/v1/buildchain-release-passport/downgraded.json +14 -0
  8. package/dist/site/badges/v1/buildchain-release-passport/draft.json +14 -0
  9. package/dist/site/badges/v1/buildchain-release-passport/failed.json +14 -0
  10. package/dist/site/badges/v1/buildchain-release-passport/missing.json +14 -0
  11. package/dist/site/badges/v1/buildchain-release-passport/passed.json +14 -0
  12. package/dist/site/badges/v1/buildchain-release-passport/planned.json +14 -0
  13. package/dist/site/badges/v1/kfd-1/aligned.json +14 -0
  14. package/dist/site/badges/v1/kfd-1/declared.json +14 -0
  15. package/dist/site/badges/v1/kfd-1/downgraded.json +14 -0
  16. package/dist/site/badges/v1/kfd-1/draft.json +14 -0
  17. package/dist/site/badges/v1/kfd-1/failed.json +14 -0
  18. package/dist/site/badges/v1/kfd-1/missing.json +14 -0
  19. package/dist/site/badges/v1/kfd-1/passed.json +14 -0
  20. package/dist/site/badges/v1/kfd-1/planned.json +14 -0
  21. package/dist/site/badges/v1/kfd-2/aligned.json +14 -0
  22. package/dist/site/badges/v1/kfd-2/declared.json +14 -0
  23. package/dist/site/badges/v1/kfd-2/downgraded.json +14 -0
  24. package/dist/site/badges/v1/kfd-2/draft.json +14 -0
  25. package/dist/site/badges/v1/kfd-2/failed.json +14 -0
  26. package/dist/site/badges/v1/kfd-2/missing.json +14 -0
  27. package/dist/site/badges/v1/kfd-2/passed.json +14 -0
  28. package/dist/site/badges/v1/kfd-2/planned.json +14 -0
  29. package/dist/site/badges/v1/kfd-3/aligned.json +14 -0
  30. package/dist/site/badges/v1/kfd-3/declared.json +14 -0
  31. package/dist/site/badges/v1/kfd-3/downgraded.json +14 -0
  32. package/dist/site/badges/v1/kfd-3/draft.json +14 -0
  33. package/dist/site/badges/v1/kfd-3/failed.json +14 -0
  34. package/dist/site/badges/v1/kfd-3/missing.json +14 -0
  35. package/dist/site/badges/v1/kfd-3/passed.json +14 -0
  36. package/dist/site/badges/v1/kfd-3/planned.json +14 -0
  37. package/dist/site/buildchain-contract.json +6 -6
  38. package/dist/site/buildchain-site.json +25 -19
  39. package/dist/site/manual-registry.json +4 -4
  40. package/dist/site/node-api-registry.json +5 -5
  41. package/dist/site/page-registry.json +15 -10
  42. package/dist/site/site-manifest.json +9 -8
  43. package/docs/MAP.md +3 -0
  44. package/docs/readme-badges.md +48 -7
  45. package/docs/reusable-build-surface.md +49 -0
  46. package/docs/web-surface-deployments.md +7 -5
  47. package/package.json +1 -1
  48. package/packages/core/diagnostics.js +16 -0
  49. package/packages/core/index.js +3 -0
  50. package/packages/core/readme-badges.js +235 -15
  51. package/scripts/check-inventory.mjs +22 -1
  52. package/scripts/generate-site-bundle.mjs +17 -0
  53. package/scripts/locked-source-checkout.mjs +386 -0
  54. package/scripts/run-lifecycle-core.mjs +12 -0
  55. package/scripts/web-surface-core.mjs +1 -8
@@ -181,6 +181,7 @@ const installDoc = fs.readFileSync(path.join(root, "docs/install.md"), "utf8");
181
181
  const siteBundle = JSON.parse(fs.readFileSync(path.join(root, "dist/site/buildchain-site.json"), "utf8"));
182
182
  const siteManifest = JSON.parse(fs.readFileSync(path.join(root, "dist/site/site-manifest.json"), "utf8"));
183
183
  const pageRegistry = JSON.parse(fs.readFileSync(path.join(root, "dist/site/page-registry.json"), "utf8"));
184
+ const badgeEndpointRegistry = JSON.parse(fs.readFileSync(path.join(root, "dist/site/badge-endpoint-registry.json"), "utf8"));
184
185
  if (!cliSource.startsWith("#!/usr/bin/env node")) {
185
186
  throw new Error("bin/buildchain.mjs must be executable with a node shebang");
186
187
  }
@@ -213,6 +214,7 @@ for (const requiredSnippet of [
213
214
  "renderReadmeBadgeBlock",
214
215
  "checkReadmeBadgeBlock",
215
216
  "updateReadmeBadgeBlock",
217
+ "createReadmeBadgeEndpointRegistry",
216
218
  ]) {
217
219
  if (!coreIndexSource.includes(requiredSnippet)) {
218
220
  throw new Error(`packages/core/index.js must export README badge API: ${requiredSnippet}`);
@@ -417,7 +419,12 @@ for (const requiredSnippet of [
417
419
  "buildchain badges readme --check",
418
420
  "collectReadmeBadgeFacts",
419
421
  "kungfu-buildchain-readme-badge-facts",
422
+ "@kungfu-tech/kfd/standards.json",
423
+ "badge-endpoint-registry.json",
424
+ "badge_endpoint_base_url",
425
+ "Buildchain-owned badges use stable hosted image URLs",
420
426
  "KFD passed",
427
+ "Buildchain Release Passport",
421
428
  "release passport",
422
429
  "<!-- buildchain:badges:start -->",
423
430
  ]) {
@@ -444,6 +451,10 @@ for (const requiredSnippet of [
444
451
  "dist/site/buildchain-contract.json",
445
452
  "buildchain-contract-drift-issue-mode",
446
453
  "compatible drift",
454
+ "Locked Source Checkout Cache",
455
+ "checkout-cache-mode",
456
+ "BUILDCHAIN_CHECKOUT_CACHE_MIRROR_URL_TEMPLATE",
457
+ "sourceCheckout",
447
458
  ]) {
448
459
  if (!reusableBuildSurfaceDoc.includes(requiredSnippet)) {
449
460
  throw new Error(`reusable build surface doc missing contract lock snippet: ${requiredSnippet}`);
@@ -768,7 +779,17 @@ for (const artifact of [
768
779
  }
769
780
  }
770
781
 
771
- for (const siteFile of ["buildchain-site.json", "site-manifest.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "release-model.json", "buildchain-contract.json"]) {
782
+ if (badgeEndpointRegistry.contract !== "kungfu-buildchain-readme-badge-endpoint-registry") {
783
+ throw new Error("badge-endpoint-registry.json must expose the README badge endpoint registry contract");
784
+ }
785
+ if (badgeEndpointRegistry.consumerActionForLogoChange !== "none") {
786
+ throw new Error("badge endpoint registry must declare no consumer action for logo changes");
787
+ }
788
+ if (!badgeEndpointRegistry.badges?.some((entry) => entry.id === "buildchain-release-passport")) {
789
+ throw new Error("badge endpoint registry must include Buildchain Release Passport badge");
790
+ }
791
+
792
+ for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "release-model.json", "buildchain-contract.json"]) {
772
793
  if (!fs.existsSync(path.join(root, "dist", "site", siteFile))) {
773
794
  throw new Error(`site bundle missing ${siteFile}`);
774
795
  }
@@ -8,6 +8,9 @@ import {
8
8
  BUILDCHAIN_AGENT_MANUALS,
9
9
  createBuildchainKfdClaimRegistry,
10
10
  } from "../packages/core/buildchain-kfd-claims.js";
11
+ import {
12
+ createReadmeBadgeEndpointRegistry,
13
+ } from "../packages/core/readme-badges.js";
11
14
  import { createSurfaceTimestampPolicy } from "../packages/core/surface-manifest.js";
12
15
 
13
16
  const SITE_BUNDLE_CONTRACT = "kungfu-buildchain-site-bundle";
@@ -448,6 +451,7 @@ function buildSiteBundle() {
448
451
  site: [
449
452
  "buildchain-site.json",
450
453
  "site-manifest.json",
454
+ "badge-endpoint-registry.json",
451
455
  "page-registry.json",
452
456
  "cli-registry.json",
453
457
  "manual-registry.json",
@@ -533,6 +537,7 @@ function buildSiteBundle() {
533
537
  "workflow-registry.json",
534
538
  "release-model.json",
535
539
  "artifact-schemas.json",
540
+ "badge-endpoint-registry.json",
536
541
  "product-mechanism.json",
537
542
  "release-provenance.json",
538
543
  "kfd-claims.json",
@@ -555,9 +560,17 @@ function buildSiteBundle() {
555
560
  "artifact-schemas.json",
556
561
  "buildchain-contract.json",
557
562
  "kfd-claims.json",
563
+ "badge-endpoint-registry.json",
558
564
  ],
559
565
  instruction: "Use this bundle as the package-owned fact source for Buildchain pages. Do not infer current release mechanics from prose alone.",
560
566
  };
567
+ const badgeEndpointRegistry = createReadmeBadgeEndpointRegistry({
568
+ kfdSpecs: [
569
+ { key: "kfd-1", label: "KFD-1", text: "contract world" },
570
+ { key: "kfd-2", label: "KFD-2", text: "release trust passport" },
571
+ { key: "kfd-3", label: "KFD-3", text: "collaboration interface" },
572
+ ],
573
+ });
561
574
 
562
575
  const homepageSections = [
563
576
  homepageSection({
@@ -701,11 +714,15 @@ function buildSiteBundle() {
701
714
  "workflow-registry.json": workflowRegistry,
702
715
  "release-model.json": releaseModel,
703
716
  "artifact-schemas.json": artifactSchemas,
717
+ "badge-endpoint-registry.json": badgeEndpointRegistry,
704
718
  "buildchain-contract.json": createBuildchainContractWorld({ root }),
705
719
  "kfd-claims.json": createBuildchainKfdClaimRegistry({ root }),
706
720
  "product-mechanism.json": productMechanism,
707
721
  "release-provenance.json": releaseProvenance,
708
722
  "agent-index.json": agentIndex,
723
+ ...Object.fromEntries(badgeEndpointRegistry.badges.flatMap((badge) => (
724
+ badge.states.map((state) => [state.path, state.payload])
725
+ ))),
709
726
  };
710
727
  }
711
728
 
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ export const LOCKED_SOURCE_CHECKOUT_CONTRACT = "kungfu-buildchain-locked-source-checkout-cache";
9
+
10
+ const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
11
+
12
+ function nowIso() {
13
+ return new Date().toISOString();
14
+ }
15
+
16
+ function hashText(value) {
17
+ return crypto.createHash("sha256").update(String(value || "")).digest("hex");
18
+ }
19
+
20
+ function readEnv(name, fallback = "") {
21
+ return process.env[name] || fallback;
22
+ }
23
+
24
+ function assertSha(value, label) {
25
+ const sha = String(value || "").trim();
26
+ if (!GIT_SHA_PATTERN.test(sha)) {
27
+ throw new Error(`${label} must be a 40-character Git SHA`);
28
+ }
29
+ return sha;
30
+ }
31
+
32
+ function normalizeMode(value = "off") {
33
+ const mode = String(value || "off").trim().toLowerCase() || "off";
34
+ if (!["off", "auto", "require"].includes(mode)) {
35
+ throw new Error(`checkout-cache-mode must be off, auto, or require; got ${value}`);
36
+ }
37
+ return mode;
38
+ }
39
+
40
+ function normalizeFallback(value = "github") {
41
+ const fallback = String(value || "github").trim().toLowerCase() || "github";
42
+ if (!["github", "fail"].includes(fallback)) {
43
+ throw new Error(`checkout-cache-fallback must be github or fail; got ${value}`);
44
+ }
45
+ return fallback;
46
+ }
47
+
48
+ function splitRepository(repository) {
49
+ const match = String(repository || "").trim().match(/^([^/\s]+)\/([^/\s]+)$/);
50
+ if (!match) {
51
+ throw new Error(`repository must be owner/repo, got ${repository || "<empty>"}`);
52
+ }
53
+ return { owner: match[1], repo: match[2], repository: `${match[1]}/${match[2]}` };
54
+ }
55
+
56
+ function renderTemplate(template = "", { owner, repo, repository, sha }) {
57
+ return String(template || "")
58
+ .replaceAll("{owner}", owner)
59
+ .replaceAll("{repo}", repo)
60
+ .replaceAll("{repository}", repository)
61
+ .replaceAll("{repositorySlug}", repository.replaceAll("/", "-"))
62
+ .replaceAll("{sha}", sha);
63
+ }
64
+
65
+ function sanitizeIdentity(value = "") {
66
+ const raw = String(value || "").trim();
67
+ if (!raw) {
68
+ return { display: "", fingerprint: "" };
69
+ }
70
+ try {
71
+ const url = new URL(raw);
72
+ url.username = "";
73
+ url.password = "";
74
+ return {
75
+ display: url.toString(),
76
+ fingerprint: hashText(raw).slice(0, 16),
77
+ };
78
+ } catch {
79
+ return {
80
+ display: path.basename(raw.replace(/[\\/]+$/, "")) || "reference",
81
+ fingerprint: hashText(raw).slice(0, 16),
82
+ };
83
+ }
84
+ }
85
+
86
+ function ensureCheckoutTarget(targetPath, workspace) {
87
+ const resolvedWorkspace = path.resolve(workspace || process.cwd());
88
+ const resolvedTarget = path.resolve(resolvedWorkspace, targetPath || ".");
89
+ const relative = path.relative(resolvedWorkspace, resolvedTarget);
90
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
91
+ throw new Error(`checkout path must stay inside the workspace: ${targetPath}`);
92
+ }
93
+ fs.mkdirSync(resolvedTarget, { recursive: true });
94
+ for (const entry of fs.readdirSync(resolvedTarget)) {
95
+ if ((relative === "" || relative === ".") && entry === ".buildchain") {
96
+ continue;
97
+ }
98
+ fs.rmSync(path.join(resolvedTarget, entry), { recursive: true, force: true });
99
+ }
100
+ return resolvedTarget;
101
+ }
102
+
103
+ function git(args, { cwd, env = {}, timeoutMs = 60000, stdio = ["ignore", "pipe", "pipe"] } = {}) {
104
+ try {
105
+ const output = execFileSync("git", args, {
106
+ cwd,
107
+ env: { ...process.env, ...env },
108
+ encoding: "utf8",
109
+ stdio,
110
+ timeout: timeoutMs,
111
+ });
112
+ return output ? String(output).trim() : "";
113
+ } catch (error) {
114
+ const stderr = error?.stderr ? String(error.stderr).trim() : "";
115
+ const stdout = error?.stdout ? String(error.stdout).trim() : "";
116
+ const detail = [stderr, stdout].filter(Boolean).join("\n").trim();
117
+ if (detail && !String(error.message || "").includes(detail)) {
118
+ error.message = `${error.message}\n${detail}`;
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ function githubAuthEnv(token = "") {
125
+ if (!token) {
126
+ return {};
127
+ }
128
+ const encoded = Buffer.from(`x-access-token:${token}`).toString("base64");
129
+ return {
130
+ GIT_CONFIG_COUNT: "1",
131
+ GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader",
132
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${encoded}`,
133
+ };
134
+ }
135
+
136
+ function markSafeDirectory(targetPath, timeoutMs) {
137
+ try {
138
+ git(["config", "--global", "--add", "safe.directory", path.resolve(targetPath)], {
139
+ cwd: targetPath,
140
+ timeoutMs,
141
+ stdio: "ignore",
142
+ });
143
+ } catch {
144
+ // Git safe.directory is best-effort. If the runner disallows global config,
145
+ // the next git command will fail with the original actionable error.
146
+ }
147
+ }
148
+
149
+ function gitObjectDirectory(referencePath) {
150
+ if (!referencePath) {
151
+ return "";
152
+ }
153
+ const resolved = path.resolve(referencePath);
154
+ const candidates = [
155
+ path.join(resolved, "objects"),
156
+ path.join(resolved, ".git", "objects"),
157
+ ];
158
+ return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) || "";
159
+ }
160
+
161
+ function writeAlternates(targetPath, referencePath) {
162
+ const objectDirectory = gitObjectDirectory(referencePath);
163
+ if (!objectDirectory) {
164
+ return false;
165
+ }
166
+ const alternatesPath = path.join(targetPath, ".git", "objects", "info", "alternates");
167
+ fs.mkdirSync(path.dirname(alternatesPath), { recursive: true });
168
+ fs.writeFileSync(alternatesPath, `${objectDirectory}\n`);
169
+ return true;
170
+ }
171
+
172
+ function hasCommit(targetPath, sha, timeoutMs) {
173
+ try {
174
+ git(["cat-file", "-e", `${sha}^{commit}`], { cwd: targetPath, timeoutMs });
175
+ return true;
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ function checkoutFetchedCommit(targetPath, sha, timeoutMs) {
182
+ git(["checkout", "--force", "--detach", sha], { cwd: targetPath, timeoutMs });
183
+ }
184
+
185
+ function fetchSha({ targetPath, remoteName, remoteUrl, sha, fetchRef, timeoutMs, env = {} }) {
186
+ try {
187
+ git(["remote", "remove", remoteName], { cwd: targetPath, timeoutMs, stdio: "ignore" });
188
+ } catch {
189
+ // The remote is optional; a fresh checkout target will not have it yet.
190
+ }
191
+ try {
192
+ git(["remote", "add", remoteName, remoteUrl], { cwd: targetPath, timeoutMs });
193
+ } catch {
194
+ git(["remote", "set-url", remoteName, remoteUrl], { cwd: targetPath, timeoutMs });
195
+ }
196
+ const fetchArgs = ["fetch", "--no-tags", "--depth=1", remoteName, `+${sha}:refs/buildchain/source`];
197
+ try {
198
+ git(fetchArgs, { cwd: targetPath, timeoutMs, env, stdio: "ignore" });
199
+ return;
200
+ } catch (error) {
201
+ if (!fetchRef) {
202
+ throw error;
203
+ }
204
+ }
205
+ git(["fetch", "--no-tags", "--depth=1", remoteName, `+${fetchRef}:refs/buildchain/source-ref`], {
206
+ cwd: targetPath,
207
+ timeoutMs,
208
+ env,
209
+ stdio: "ignore",
210
+ });
211
+ if (!hasCommit(targetPath, sha, timeoutMs)) {
212
+ throw new Error(`fetched ${fetchRef}, but ${sha} is not available`);
213
+ }
214
+ }
215
+
216
+ function githubRemoteUrl({ repository, serverUrl = "https://github.com" }) {
217
+ const base = String(serverUrl || "https://github.com").replace(/\/+$/, "");
218
+ return `${base}/${repository}.git`;
219
+ }
220
+
221
+ function verifyCheckout({ targetPath, sourceSha, sourceTreeSha = "" }) {
222
+ const head = git(["rev-parse", "HEAD"], { cwd: targetPath });
223
+ const tree = git(["rev-parse", "HEAD^{tree}"], { cwd: targetPath });
224
+ const headOk = head === sourceSha;
225
+ const treeOk = !sourceTreeSha || tree === sourceTreeSha;
226
+ if (!headOk) {
227
+ throw new Error(`locked source checkout head mismatch: expected ${sourceSha}, got ${head}`);
228
+ }
229
+ if (!treeOk) {
230
+ throw new Error(`locked source checkout tree mismatch: expected ${sourceTreeSha}, got ${tree}`);
231
+ }
232
+ return { head, tree, headOk, treeOk };
233
+ }
234
+
235
+ function writeEvidence(filePath, evidence) {
236
+ if (!filePath) {
237
+ return;
238
+ }
239
+ fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true });
240
+ fs.writeFileSync(filePath, `${JSON.stringify(evidence, null, 2)}\n`);
241
+ }
242
+
243
+ export function lockedSourceCheckout({
244
+ workspace = process.cwd(),
245
+ checkoutPath = ".",
246
+ repository = readEnv("GITHUB_REPOSITORY"),
247
+ sourceSha = readEnv("BUILDCHAIN_SOURCE_SHA"),
248
+ sourceTreeSha = readEnv("BUILDCHAIN_SOURCE_TREE_SHA"),
249
+ fetchRef = readEnv("BUILDCHAIN_SOURCE_REF", readEnv("GITHUB_REF")),
250
+ mode = readEnv("BUILDCHAIN_CHECKOUT_CACHE_MODE", "off"),
251
+ mirrorUrlTemplate = readEnv("BUILDCHAIN_CHECKOUT_CACHE_MIRROR_URL_TEMPLATE"),
252
+ referenceRepositoryTemplate = readEnv("BUILDCHAIN_CHECKOUT_CACHE_REFERENCE_REPOSITORY_TEMPLATE"),
253
+ fallback = readEnv("BUILDCHAIN_CHECKOUT_CACHE_FALLBACK", "github"),
254
+ timeoutSeconds = Number(readEnv("BUILDCHAIN_CHECKOUT_CACHE_TIMEOUT_SECONDS", "60") || 60),
255
+ diagnosticsPath = readEnv("BUILDCHAIN_SOURCE_CHECKOUT_DIAGNOSTICS_PATH", ".buildchain/diagnostics/source-checkout.json"),
256
+ githubToken = readEnv("GITHUB_TOKEN"),
257
+ githubServerUrl = readEnv("GITHUB_SERVER_URL", "https://github.com"),
258
+ githubRemote = "",
259
+ now = nowIso,
260
+ } = {}) {
261
+ const startedAt = Date.now();
262
+ const normalizedMode = normalizeMode(mode);
263
+ const normalizedFallback = normalizeFallback(fallback);
264
+ const sha = assertSha(sourceSha, "sourceSha");
265
+ const treeSha = sourceTreeSha ? assertSha(sourceTreeSha, "sourceTreeSha") : "";
266
+ const repoParts = splitRepository(repository);
267
+ const timeoutMs = Math.max(1, Number(timeoutSeconds || 60)) * 1000;
268
+ const targetPath = ensureCheckoutTarget(checkoutPath, workspace);
269
+ git(["init"], { cwd: targetPath, timeoutMs });
270
+ const renderedMirrorUrl = renderTemplate(mirrorUrlTemplate, { ...repoParts, sha });
271
+ const renderedReferenceRepository = renderTemplate(referenceRepositoryTemplate, { ...repoParts, sha });
272
+ markSafeDirectory(targetPath, timeoutMs);
273
+ const evidence = {
274
+ schemaVersion: 1,
275
+ contract: LOCKED_SOURCE_CHECKOUT_CONTRACT,
276
+ generatedAt: now(),
277
+ repository: repoParts.repository,
278
+ checkoutPath: path.relative(path.resolve(workspace), targetPath).split(path.sep).join("/") || ".",
279
+ source: {
280
+ sha,
281
+ treeSha,
282
+ fetchRef: fetchRef || "",
283
+ },
284
+ policy: {
285
+ mode: normalizedMode,
286
+ fallback: normalizedFallback,
287
+ timeoutSeconds: Math.max(1, Number(timeoutSeconds || 60)),
288
+ mirror: sanitizeIdentity(renderedMirrorUrl),
289
+ referenceRepository: sanitizeIdentity(renderedReferenceRepository),
290
+ },
291
+ cache: {
292
+ attempted: normalizedMode !== "off",
293
+ hit: false,
294
+ transport: "github",
295
+ fallbackUsed: normalizedMode === "off",
296
+ fallbackReason: normalizedMode === "off" ? "cache disabled" : "",
297
+ },
298
+ verification: {
299
+ head: "",
300
+ tree: "",
301
+ headOk: false,
302
+ treeOk: false,
303
+ },
304
+ durationMs: 0,
305
+ };
306
+ let checkoutError;
307
+ if (normalizedMode !== "off") {
308
+ try {
309
+ if (renderedReferenceRepository) {
310
+ const alternates = writeAlternates(targetPath, renderedReferenceRepository);
311
+ evidence.cache.transport = "reference-repository";
312
+ evidence.cache.referenceAvailable = alternates;
313
+ if (!alternates) {
314
+ throw new Error("reference repository is unavailable");
315
+ }
316
+ if (!hasCommit(targetPath, sha, timeoutMs)) {
317
+ throw new Error("reference repository does not contain source commit");
318
+ }
319
+ checkoutFetchedCommit(targetPath, sha, timeoutMs);
320
+ } else if (renderedMirrorUrl) {
321
+ evidence.cache.transport = "mirror-url";
322
+ fetchSha({
323
+ targetPath,
324
+ remoteName: "buildchain-cache",
325
+ remoteUrl: renderedMirrorUrl,
326
+ sha,
327
+ fetchRef,
328
+ timeoutMs,
329
+ });
330
+ checkoutFetchedCommit(targetPath, sha, timeoutMs);
331
+ } else {
332
+ throw new Error("checkout cache is enabled but no mirror URL or reference repository template was provided");
333
+ }
334
+ evidence.cache.hit = true;
335
+ evidence.cache.fallbackUsed = false;
336
+ } catch (error) {
337
+ checkoutError = error;
338
+ evidence.cache.hit = false;
339
+ evidence.cache.fallbackReason = error.message;
340
+ if (normalizedMode === "require" || normalizedFallback === "fail") {
341
+ evidence.durationMs = Date.now() - startedAt;
342
+ writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
343
+ throw new Error(`locked source checkout cache unavailable: ${error.message}`);
344
+ }
345
+ }
346
+ }
347
+ if (!evidence.cache.hit) {
348
+ const remoteUrl = githubRemote || githubRemoteUrl({ repository: repoParts.repository, serverUrl: githubServerUrl });
349
+ evidence.cache.transport = "github";
350
+ evidence.cache.fallbackUsed = normalizedMode !== "off";
351
+ evidence.cache.github = sanitizeIdentity(remoteUrl);
352
+ if (!evidence.cache.fallbackReason && checkoutError) {
353
+ evidence.cache.fallbackReason = checkoutError.message;
354
+ }
355
+ fetchSha({
356
+ targetPath,
357
+ remoteName: "origin",
358
+ remoteUrl,
359
+ sha,
360
+ fetchRef,
361
+ timeoutMs,
362
+ env: githubAuthEnv(githubToken),
363
+ });
364
+ checkoutFetchedCommit(targetPath, sha, timeoutMs);
365
+ }
366
+ evidence.verification = verifyCheckout({ targetPath, sourceSha: sha, sourceTreeSha: treeSha });
367
+ evidence.durationMs = Date.now() - startedAt;
368
+ writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
369
+ return evidence;
370
+ }
371
+
372
+ function isMain() {
373
+ return process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
374
+ }
375
+
376
+ if (isMain()) {
377
+ try {
378
+ const evidence = lockedSourceCheckout();
379
+ console.log(`locked-source-checkout=${evidence.cache.hit ? "cache-hit" : evidence.cache.fallbackUsed ? "fallback" : "github"}`);
380
+ console.log(`locked-source-checkout-sha=${evidence.verification.head}`);
381
+ console.log(`locked-source-checkout-tree=${evidence.verification.tree}`);
382
+ } catch (error) {
383
+ console.error(`::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`);
384
+ process.exitCode = 1;
385
+ }
386
+ }
@@ -405,10 +405,13 @@ export function runLifecycle({
405
405
  const resolvedDiagnosticsEventsPath = path.join(diagnosticsDir, "events.jsonl");
406
406
  const resolvedDiagnosticsProcessSummaryPath = path.join(diagnosticsDir, "process-summary.json");
407
407
  const resolvedDiagnosticsProcessSamplesPath = path.join(diagnosticsDir, "process-samples.jsonl");
408
+ const resolvedSourceCheckoutPath = path.resolve(resolvedWorkspace, ".buildchain/diagnostics/source-checkout.json");
409
+ const resolvedDiagnosticsSourceCheckoutPath = path.join(diagnosticsDir, "source-checkout.json");
408
410
  const resolvedDiagnosticsManifestPath = path.join(diagnosticsDir, "diagnostics-manifest.json");
409
411
  const relativeDiagnosticsEventsPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsEventsPath));
410
412
  const relativeDiagnosticsProcessSummaryPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSummaryPath));
411
413
  const relativeDiagnosticsProcessSamplesPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSamplesPath));
414
+ const relativeDiagnosticsSourceCheckoutPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsSourceCheckoutPath));
412
415
  const relativeDiagnosticsManifestPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsManifestPath));
413
416
  const logRunId = crypto.randomUUID();
414
417
  const frameworkLog = createBuildchainLogger({
@@ -567,6 +570,9 @@ export function runLifecycle({
567
570
  const processSummaryArtifact = shouldReadProcessSummary
568
571
  ? readProcessSummaryArtifact(resolvedProcessSummaryPath)
569
572
  : undefined;
573
+ const sourceCheckoutArtifact = fs.existsSync(resolvedSourceCheckoutPath)
574
+ ? JSON.parse(fs.readFileSync(resolvedSourceCheckoutPath, "utf8"))
575
+ : undefined;
570
576
  fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });
571
577
  const scanStartedAt = Date.now();
572
578
  const files = collectArtifactFiles(resolvedWorkspace, artifactPaths);
@@ -687,6 +693,7 @@ export function runLifecycle({
687
693
  artifactPaths,
688
694
  lifecycleObservability,
689
695
  processSummary: processSummaryArtifact?.summary,
696
+ sourceCheckout: sourceCheckoutArtifact,
690
697
  links: {
691
698
  artifactName,
692
699
  platformId,
@@ -700,6 +707,7 @@ export function runLifecycle({
700
707
  ...(relativeProcessSummaryPath ? { processSummary: relativeProcessSummaryPath } : {}),
701
708
  ...(processSummaryArtifact ? { diagnosticsProcessSummary: relativeDiagnosticsProcessSummaryPath } : {}),
702
709
  ...(processSummaryArtifact?.samplesPath ? { diagnosticsProcessSamples: relativeDiagnosticsProcessSamplesPath } : {}),
710
+ ...(sourceCheckoutArtifact ? { sourceCheckout: relativeDiagnosticsSourceCheckoutPath } : {}),
703
711
  },
704
712
  }),
705
713
  );
@@ -718,6 +726,9 @@ export function runLifecycle({
718
726
  });
719
727
  copyIfExists(resolvedSamplesPath, resolvedDiagnosticsProcessSamplesPath);
720
728
  }
729
+ if (sourceCheckoutArtifact) {
730
+ copyIfExists(resolvedSourceCheckoutPath, resolvedDiagnosticsSourceCheckoutPath);
731
+ }
721
732
  writeDiagnosticsSidecarManifest(resolvedDiagnosticsManifestPath, {
722
733
  workspace: resolvedWorkspace,
723
734
  artifactName,
@@ -728,6 +739,7 @@ export function runLifecycle({
728
739
  { kind: "events", filePath: resolvedDiagnosticsEventsPath, required: true },
729
740
  { kind: "process-summary", filePath: resolvedDiagnosticsProcessSummaryPath },
730
741
  { kind: "process-samples", filePath: resolvedDiagnosticsProcessSamplesPath },
742
+ { kind: "source-checkout", filePath: resolvedDiagnosticsSourceCheckoutPath },
731
743
  ],
732
744
  });
733
745
  console.log(`buildchain_manifest=${path.relative(resolvedWorkspace, resolvedManifestPath)}`);
@@ -633,14 +633,7 @@ function smokeUrlsForBinding({ binding, artifactPath, files }) {
633
633
  url: urlWithPath(binding.url, nestedPath),
634
634
  required: true,
635
635
  }]
636
- : [{
637
- kind: "nested",
638
- requestPath: "",
639
- url: "",
640
- required: true,
641
- missing: true,
642
- message: "no nested HTML route was found under this surface artifact prefix",
643
- }]),
636
+ : []),
644
637
  ];
645
638
  }
646
639