@kungfu-tech/buildchain 2.8.15 → 2.8.16-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.
- package/README.md +19 -11
- package/bin/buildchain.mjs +150 -0
- package/dist/site/buildchain-contract.json +83 -11
- package/dist/site/buildchain-site.json +121 -20
- package/dist/site/cli-registry.json +5 -0
- package/dist/site/kfd-claims.json +85 -3
- package/dist/site/manual-registry.json +18 -3
- package/dist/site/node-api-registry.json +28 -6
- package/dist/site/page-registry.json +88 -11
- package/dist/site/release-model.json +8 -0
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +23 -7
- package/docs/MAP.md +8 -0
- package/docs/cli.md +40 -3
- package/docs/homebrew.md +116 -0
- package/docs/readme-badges.md +124 -0
- package/docs/web-surface-deployments.md +46 -8
- package/package.json +3 -1
- package/packages/core/README.md +12 -0
- package/packages/core/buildchain-config.js +2 -2
- package/packages/core/buildchain-contract.js +56 -0
- package/packages/core/buildchain-kfd-claims.js +37 -0
- package/packages/core/homebrew.js +414 -0
- package/packages/core/index.js +21 -0
- package/packages/core/readme-badges.js +450 -0
- package/packages/core/release-passport.js +24 -9
- package/scripts/check-inventory.mjs +69 -0
- package/scripts/generate-site-bundle.mjs +12 -0
- package/scripts/web-surface-core.mjs +177 -5
|
@@ -213,6 +213,23 @@ function joinS3Key(...parts) {
|
|
|
213
213
|
return normalizeS3Key(parts.filter(Boolean).join("/"));
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
function joinUrlPath(...parts) {
|
|
217
|
+
const raw = parts.join("/");
|
|
218
|
+
const normalized = normalizeS3Key(raw);
|
|
219
|
+
if (!normalized) {
|
|
220
|
+
return "/";
|
|
221
|
+
}
|
|
222
|
+
return raw.endsWith("/") ? `/${normalized}/` : `/${normalized}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function urlWithPath(baseUrl, requestPath) {
|
|
226
|
+
const url = new URL(baseUrl);
|
|
227
|
+
url.pathname = joinUrlPath(requestPath);
|
|
228
|
+
url.search = "";
|
|
229
|
+
url.hash = "";
|
|
230
|
+
return url.toString();
|
|
231
|
+
}
|
|
232
|
+
|
|
216
233
|
function s3Uri(bucket, key = "") {
|
|
217
234
|
if (!bucket) {
|
|
218
235
|
throw new Error("aws-s3-cloudfront adapter requires a bucket or target");
|
|
@@ -239,6 +256,19 @@ function viewerWildcardPath(binding) {
|
|
|
239
256
|
}
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
function surfaceArtifactPrefix(binding) {
|
|
260
|
+
return normalizeS3Key(binding.sourcePath);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function surfaceArtifactRootFor({ artifactRoot, binding }) {
|
|
264
|
+
const prefix = normalizeS3Key(binding.artifactPathPrefix || surfaceArtifactPrefix(binding));
|
|
265
|
+
const root = prefix ? path.join(artifactRoot, prefix) : artifactRoot;
|
|
266
|
+
if (fs.existsSync(root)) {
|
|
267
|
+
return root;
|
|
268
|
+
}
|
|
269
|
+
return artifactRoot;
|
|
270
|
+
}
|
|
271
|
+
|
|
242
272
|
function syncStaticArtifactArgs({ artifactRoot, bucket, objectPrefix }) {
|
|
243
273
|
const args = ["s3", "sync", artifactRoot, s3Uri(bucket, objectPrefix), "--delete"];
|
|
244
274
|
if (!objectPrefix) {
|
|
@@ -528,6 +558,10 @@ function resolveSurfaceBindings({ config, channelName, alias, deployConfig }) {
|
|
|
528
558
|
alias,
|
|
529
559
|
url,
|
|
530
560
|
sourcePath: normalizeSurfacePath(surface.path),
|
|
561
|
+
artifactPathPrefix: surfaceArtifactPrefix({ sourcePath: surface.path }),
|
|
562
|
+
viewerPathPrefix: "/",
|
|
563
|
+
directoryIndex: "index.html",
|
|
564
|
+
directoryIndexResolution: true,
|
|
531
565
|
canonicalUrl: surface.productionUrl || (channelName === "production" ? url : ""),
|
|
532
566
|
pathOnly: Boolean(surface.pathOnly),
|
|
533
567
|
bucket,
|
|
@@ -544,6 +578,91 @@ function resolveSurfaceBindings({ config, channelName, alias, deployConfig }) {
|
|
|
544
578
|
}));
|
|
545
579
|
}
|
|
546
580
|
|
|
581
|
+
function relativeArtifactPath({ artifactPath, filePath }) {
|
|
582
|
+
const normalizedArtifact = normalizeS3Key(artifactPath);
|
|
583
|
+
const normalizedFile = normalizeS3Key(filePath);
|
|
584
|
+
if (!normalizedArtifact) {
|
|
585
|
+
return normalizedFile;
|
|
586
|
+
}
|
|
587
|
+
return normalizedFile === normalizedArtifact
|
|
588
|
+
? ""
|
|
589
|
+
: normalizedFile.startsWith(`${normalizedArtifact}/`)
|
|
590
|
+
? normalizedFile.slice(normalizedArtifact.length + 1)
|
|
591
|
+
: normalizedFile;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function requestPathFromArtifactPath({ binding, artifactPath, filePath }) {
|
|
595
|
+
const relative = relativeArtifactPath({ artifactPath, filePath });
|
|
596
|
+
const prefix = normalizeS3Key(binding.artifactPathPrefix || surfaceArtifactPrefix(binding));
|
|
597
|
+
if (prefix && relative !== prefix && !relative.startsWith(`${prefix}/`)) {
|
|
598
|
+
return "";
|
|
599
|
+
}
|
|
600
|
+
const surfaceRelative = prefix
|
|
601
|
+
? relative.slice(prefix.length).replace(/^\/+/, "")
|
|
602
|
+
: relative;
|
|
603
|
+
if (!surfaceRelative || surfaceRelative === "index.html") {
|
|
604
|
+
return "/";
|
|
605
|
+
}
|
|
606
|
+
if (surfaceRelative.endsWith("/index.html")) {
|
|
607
|
+
const directoryPath = normalizeS3Key(surfaceRelative.slice(0, -"index.html".length));
|
|
608
|
+
return directoryPath ? `/${directoryPath}/` : "/";
|
|
609
|
+
}
|
|
610
|
+
return joinUrlPath(surfaceRelative);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function smokeUrlsForBinding({ binding, artifactPath, files }) {
|
|
614
|
+
const rootUrl = urlWithPath(binding.url, "/");
|
|
615
|
+
const candidates = (files || [])
|
|
616
|
+
.map((file) => requestPathFromArtifactPath({ binding, artifactPath, filePath: file.path }))
|
|
617
|
+
.filter(Boolean)
|
|
618
|
+
.filter((requestPath) => requestPath !== "/")
|
|
619
|
+
.filter((requestPath) => requestPath.endsWith("/") || requestPath.endsWith(".html"))
|
|
620
|
+
.sort();
|
|
621
|
+
const nestedPath = candidates[0] || "";
|
|
622
|
+
return [
|
|
623
|
+
{
|
|
624
|
+
kind: "root",
|
|
625
|
+
requestPath: "/",
|
|
626
|
+
url: rootUrl,
|
|
627
|
+
required: true,
|
|
628
|
+
},
|
|
629
|
+
...(nestedPath
|
|
630
|
+
? [{
|
|
631
|
+
kind: "nested",
|
|
632
|
+
requestPath: nestedPath,
|
|
633
|
+
url: urlWithPath(binding.url, nestedPath),
|
|
634
|
+
required: true,
|
|
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
|
+
}]),
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function withSurfaceRoutingEvidence(bindings, { artifactPath, files }) {
|
|
648
|
+
return bindings.map((binding) => ({
|
|
649
|
+
...binding,
|
|
650
|
+
artifactPathPrefix: normalizeS3Key(binding.artifactPathPrefix || surfaceArtifactPrefix(binding)),
|
|
651
|
+
viewerPathPrefix: binding.viewerPathPrefix || "/",
|
|
652
|
+
directoryIndex: binding.directoryIndex || "index.html",
|
|
653
|
+
directoryIndexResolution: binding.directoryIndexResolution !== false,
|
|
654
|
+
routing: {
|
|
655
|
+
contract: "kungfu-buildchain-web-surface-path-prefix-rewrite",
|
|
656
|
+
viewerPathPrefix: binding.viewerPathPrefix || "/",
|
|
657
|
+
artifactPathPrefix: normalizeS3Key(binding.artifactPathPrefix || surfaceArtifactPrefix(binding)),
|
|
658
|
+
objectPrefix: binding.objectPrefix,
|
|
659
|
+
directoryIndex: binding.directoryIndex || "index.html",
|
|
660
|
+
directoryIndexResolution: binding.directoryIndexResolution !== false,
|
|
661
|
+
},
|
|
662
|
+
smokeUrls: smokeUrlsForBinding({ binding, artifactPath, files }),
|
|
663
|
+
}));
|
|
664
|
+
}
|
|
665
|
+
|
|
547
666
|
export function validateWebSurfaceProject(cwd = process.cwd()) {
|
|
548
667
|
const summary = validateBuildchainConfig(cwd, {
|
|
549
668
|
requireConfig: true,
|
|
@@ -706,6 +825,11 @@ export function planWebSurfaceDeploy({
|
|
|
706
825
|
rollbackLimitations,
|
|
707
826
|
deployedAt,
|
|
708
827
|
});
|
|
828
|
+
const surfaceBindings = withSurfaceRoutingEvidence(manifest.surfaceBindings, {
|
|
829
|
+
artifactPath: artifactPath || deployConfig.artifactPath || ".",
|
|
830
|
+
files: resolvedArtifact.files,
|
|
831
|
+
});
|
|
832
|
+
manifest.surfaceBindings = surfaceBindings;
|
|
709
833
|
return {
|
|
710
834
|
schemaVersion: 1,
|
|
711
835
|
contract: "kungfu-buildchain-web-surface-deploy-plan",
|
|
@@ -714,14 +838,14 @@ export function planWebSurfaceDeploy({
|
|
|
714
838
|
channel,
|
|
715
839
|
alias,
|
|
716
840
|
url: manifest.url,
|
|
717
|
-
urls: Object.fromEntries(
|
|
841
|
+
urls: Object.fromEntries(surfaceBindings.map((binding) => [binding.surface, binding.url])),
|
|
718
842
|
artifact: {
|
|
719
843
|
path: artifactPath || deployConfig.artifactPath || ".",
|
|
720
844
|
hash: resolvedArtifact.artifactHash,
|
|
721
845
|
files: resolvedArtifact.files,
|
|
722
846
|
},
|
|
723
847
|
manifest,
|
|
724
|
-
surfaceBindings
|
|
848
|
+
surfaceBindings,
|
|
725
849
|
steps: planAdapterSteps(deployConfig.adapter, deployConfig, manifest),
|
|
726
850
|
};
|
|
727
851
|
}
|
|
@@ -763,7 +887,11 @@ export function applyWebSurfaceDeploy({
|
|
|
763
887
|
}
|
|
764
888
|
const bucket = deployConfig.bucket || deployConfig.target || "";
|
|
765
889
|
const artifactRoot = path.resolve(cwd, resolvedPlan.artifact.path);
|
|
766
|
-
const bindings = resolvedPlan.manifest.surfaceBindings || []
|
|
890
|
+
const bindings = withSurfaceRoutingEvidence(resolvedPlan.manifest.surfaceBindings || [], {
|
|
891
|
+
artifactPath: resolvedPlan.artifact.path,
|
|
892
|
+
files: resolvedPlan.artifact.files || [],
|
|
893
|
+
});
|
|
894
|
+
resolvedPlan.manifest.surfaceBindings = bindings;
|
|
767
895
|
if (!dryRun) {
|
|
768
896
|
for (const binding of bindings) {
|
|
769
897
|
assertConcreteAwsDeployConfig({
|
|
@@ -1103,7 +1231,45 @@ export async function checkWebSurfaceHealth({
|
|
|
1103
1231
|
});
|
|
1104
1232
|
}
|
|
1105
1233
|
|
|
1106
|
-
|
|
1234
|
+
const smokeTargets = bindings.length > 0
|
|
1235
|
+
? bindings.flatMap((binding) => {
|
|
1236
|
+
const smokeUrls = Array.isArray(binding.smokeUrls) && binding.smokeUrls.length > 0
|
|
1237
|
+
? binding.smokeUrls
|
|
1238
|
+
: [{ kind: "root", requestPath: "/", url: binding.url || urls[binding.surface] || "", required: true }];
|
|
1239
|
+
return smokeUrls.map((smoke) => ({
|
|
1240
|
+
surface: binding.surface,
|
|
1241
|
+
kind: smoke.kind || "root",
|
|
1242
|
+
requestPath: smoke.requestPath || "",
|
|
1243
|
+
url: smoke.url || "",
|
|
1244
|
+
required: smoke.required !== false,
|
|
1245
|
+
missing: Boolean(smoke.missing),
|
|
1246
|
+
message: smoke.message || "",
|
|
1247
|
+
}));
|
|
1248
|
+
})
|
|
1249
|
+
: Object.entries(urls).map(([surface, url]) => ({
|
|
1250
|
+
surface,
|
|
1251
|
+
kind: "root",
|
|
1252
|
+
requestPath: "/",
|
|
1253
|
+
url,
|
|
1254
|
+
required: true,
|
|
1255
|
+
}));
|
|
1256
|
+
|
|
1257
|
+
for (const target of smokeTargets) {
|
|
1258
|
+
const { surface, url } = target;
|
|
1259
|
+
if (!url || target.missing) {
|
|
1260
|
+
checks.push({
|
|
1261
|
+
surface,
|
|
1262
|
+
kind: target.kind,
|
|
1263
|
+
requestPath: target.requestPath,
|
|
1264
|
+
url: url || "",
|
|
1265
|
+
status: "fail",
|
|
1266
|
+
httpStatus: 0,
|
|
1267
|
+
finalUrl: "",
|
|
1268
|
+
noindexHeader: false,
|
|
1269
|
+
message: target.message || "web-surface smoke URL is missing",
|
|
1270
|
+
});
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1107
1273
|
try {
|
|
1108
1274
|
const response = await fetchImpl(url, { redirect: "follow" });
|
|
1109
1275
|
const expectedNoindex = Boolean(config.channels?.[channel]?.noindex);
|
|
@@ -1117,6 +1283,8 @@ export async function checkWebSurfaceHealth({
|
|
|
1117
1283
|
const noindexOk = channel !== "production" || expectedNoindex || !noindex;
|
|
1118
1284
|
checks.push({
|
|
1119
1285
|
surface,
|
|
1286
|
+
kind: target.kind,
|
|
1287
|
+
requestPath: target.requestPath,
|
|
1120
1288
|
url,
|
|
1121
1289
|
status: statusOk && noindexOk ? "pass" : "fail",
|
|
1122
1290
|
httpStatus: response.status,
|
|
@@ -1131,6 +1299,8 @@ export async function checkWebSurfaceHealth({
|
|
|
1131
1299
|
} catch (error) {
|
|
1132
1300
|
checks.push({
|
|
1133
1301
|
surface,
|
|
1302
|
+
kind: target.kind,
|
|
1303
|
+
requestPath: target.requestPath,
|
|
1134
1304
|
url,
|
|
1135
1305
|
status: "fail",
|
|
1136
1306
|
httpStatus: 0,
|
|
@@ -1173,12 +1343,14 @@ function deployBindingOperations({ artifactRoot, deployConfig, manifest, binding
|
|
|
1173
1343
|
const effectiveDeploy = surfaceDeployConfig(deployConfig, binding.surface);
|
|
1174
1344
|
const bucket = binding.bucket || effectiveDeploy.bucket || effectiveDeploy.target || "";
|
|
1175
1345
|
const distribution = binding.distributionId || effectiveDeploy.cloudfront_distribution || effectiveDeploy.distribution || "";
|
|
1346
|
+
const surfaceArtifactRoot = surfaceArtifactRootFor({ artifactRoot, binding });
|
|
1176
1347
|
const operations = [
|
|
1177
1348
|
{
|
|
1178
1349
|
action: "sync-static-artifact",
|
|
1179
1350
|
surface: binding.surface,
|
|
1180
1351
|
command: "aws",
|
|
1181
|
-
args: syncStaticArtifactArgs({ artifactRoot, bucket, objectPrefix: binding.objectPrefix }),
|
|
1352
|
+
args: syncStaticArtifactArgs({ artifactRoot: surfaceArtifactRoot, bucket, objectPrefix: binding.objectPrefix }),
|
|
1353
|
+
routing: binding.routing,
|
|
1182
1354
|
},
|
|
1183
1355
|
{
|
|
1184
1356
|
action: "write-deployment-manifest",
|