@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.
- package/README.md +4 -4
- package/dist/site/agent-index.json +2 -1
- package/dist/site/artifact-schemas.json +1 -0
- package/dist/site/badge-endpoint-registry.json +659 -0
- package/dist/site/badges/v1/buildchain-release-passport/aligned.json +14 -0
- package/dist/site/badges/v1/buildchain-release-passport/declared.json +14 -0
- package/dist/site/badges/v1/buildchain-release-passport/downgraded.json +14 -0
- package/dist/site/badges/v1/buildchain-release-passport/draft.json +14 -0
- package/dist/site/badges/v1/buildchain-release-passport/failed.json +14 -0
- package/dist/site/badges/v1/buildchain-release-passport/missing.json +14 -0
- package/dist/site/badges/v1/buildchain-release-passport/passed.json +14 -0
- package/dist/site/badges/v1/buildchain-release-passport/planned.json +14 -0
- package/dist/site/badges/v1/kfd-1/aligned.json +14 -0
- package/dist/site/badges/v1/kfd-1/declared.json +14 -0
- package/dist/site/badges/v1/kfd-1/downgraded.json +14 -0
- package/dist/site/badges/v1/kfd-1/draft.json +14 -0
- package/dist/site/badges/v1/kfd-1/failed.json +14 -0
- package/dist/site/badges/v1/kfd-1/missing.json +14 -0
- package/dist/site/badges/v1/kfd-1/passed.json +14 -0
- package/dist/site/badges/v1/kfd-1/planned.json +14 -0
- package/dist/site/badges/v1/kfd-2/aligned.json +14 -0
- package/dist/site/badges/v1/kfd-2/declared.json +14 -0
- package/dist/site/badges/v1/kfd-2/downgraded.json +14 -0
- package/dist/site/badges/v1/kfd-2/draft.json +14 -0
- package/dist/site/badges/v1/kfd-2/failed.json +14 -0
- package/dist/site/badges/v1/kfd-2/missing.json +14 -0
- package/dist/site/badges/v1/kfd-2/passed.json +14 -0
- package/dist/site/badges/v1/kfd-2/planned.json +14 -0
- package/dist/site/badges/v1/kfd-3/aligned.json +14 -0
- package/dist/site/badges/v1/kfd-3/declared.json +14 -0
- package/dist/site/badges/v1/kfd-3/downgraded.json +14 -0
- package/dist/site/badges/v1/kfd-3/draft.json +14 -0
- package/dist/site/badges/v1/kfd-3/failed.json +14 -0
- package/dist/site/badges/v1/kfd-3/missing.json +14 -0
- package/dist/site/badges/v1/kfd-3/passed.json +14 -0
- package/dist/site/badges/v1/kfd-3/planned.json +14 -0
- package/dist/site/buildchain-contract.json +6 -6
- package/dist/site/buildchain-site.json +25 -19
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +5 -5
- package/dist/site/page-registry.json +15 -10
- package/dist/site/site-manifest.json +9 -8
- package/docs/MAP.md +3 -0
- package/docs/readme-badges.md +48 -7
- package/docs/reusable-build-surface.md +49 -0
- package/docs/web-surface-deployments.md +7 -5
- package/package.json +1 -1
- package/packages/core/diagnostics.js +16 -0
- package/packages/core/index.js +3 -0
- package/packages/core/readme-badges.js +235 -15
- package/scripts/check-inventory.mjs +22 -1
- package/scripts/generate-site-bundle.mjs +17 -0
- package/scripts/locked-source-checkout.mjs +386 -0
- package/scripts/run-lifecycle-core.mjs +12 -0
- 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
|
-
|
|
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
|
|