site build: arc as index (CSP-clean node assembler), healthz endpoint, ship webps in-repo
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
node_modules/
|
||||
*.log
|
||||
assets/work/
|
||||
dist/
|
||||
/dist/
|
||||
pilot/course-1.html
|
||||
pilot/arc.html
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -1,70 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assemble dist/ — the CSP-clean static build served at learn.bztmon.com.
|
||||
#
|
||||
# The artifact build inlines GSAP and the app JS because the artifact CSP blocks every
|
||||
# external request. nginx has no such constraint, so here they stay SEPARATE files:
|
||||
# `script-src 'self'` holds with no 'unsafe-inline' exception, and the two GSAP bundles
|
||||
# (116KB, unchanged between deploys) become independently cacheable.
|
||||
# The site's face is the delivery arc (index.html); scripts stay SEPARATE files so
|
||||
# `script-src 'self'` holds with no 'unsafe-inline' exception. Assembly logic lives
|
||||
# in pilot/assemble-arc.mjs (node — the build image carries no python).
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
trap 'printf "build failed at line %s\n" "$LINENO" >&2' ERR
|
||||
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd -- "$here"
|
||||
|
||||
dist="$here/dist"
|
||||
src_markup="ec2-part1.html"
|
||||
src_app="ec2-part2.html"
|
||||
|
||||
for f in "$src_markup" "$src_app" gsap.min.js st.min.js; do
|
||||
for f in pilot/arc.tpl.html pilot/manifest-gitops.json pilot/manifest-supply.json \
|
||||
pilot/manifest-helm.json gsap.min.js st.min.js \
|
||||
assets/dist/course-VI-gitops.webp assets/dist/course-VII-supply-chain.webp \
|
||||
assets/dist/course-VIII-helm-press.webp assets/dist/appendix-dependency-ledger.webp; do
|
||||
[[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; }
|
||||
done
|
||||
|
||||
dist="$here/dist"
|
||||
rm -rf -- "$dist"
|
||||
mkdir -p -- "$dist"
|
||||
|
||||
# --- app.js: strip the single <script> wrapper part2 carries -----------------
|
||||
# part2 is exactly one <script>…</script> block; served standalone it must be raw JS.
|
||||
sed -e '1{/^[[:space:]]*<script>[[:space:]]*$/d}' \
|
||||
-e '${/^[[:space:]]*<\/script>[[:space:]]*$/d}' \
|
||||
-- "$src_app" > "$dist/app.js"
|
||||
|
||||
if grep -qiE '</?script' -- "$dist/app.js"; then
|
||||
printf 'app.js still contains a script tag — the wrapper strip did not match\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- index.html: markup + external script refs -------------------------------
|
||||
{
|
||||
cat -- "$src_markup"
|
||||
cat <<'HTML'
|
||||
|
||||
<script src="/gsap.min.js"></script>
|
||||
<script src="/st.min.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
} > "$dist/index.html"
|
||||
node pilot/assemble-arc.mjs "$dist"
|
||||
|
||||
# No inline JS may survive into the served HTML, or the CSP silently kills the page.
|
||||
# Every <script> opening tag must carry a src=. (grep -E has no lookahead — list the
|
||||
# tags, then assert none of them lacks src.)
|
||||
if grep -oiE '<script[^>]*>' -- "$dist/index.html" | grep -qivE 'src='; then
|
||||
printf 'index.html contains an inline <script> — CSP script-src self would block it\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0644 -- gsap.min.js st.min.js "$dist/"
|
||||
|
||||
# --- scene assets ------------------------------------------------------------
|
||||
if [[ -d assets/dist ]]; then
|
||||
mkdir -p -- "$dist/assets"
|
||||
find assets/dist -type f -print0 | while IFS= read -r -d '' f; do
|
||||
install -m 0644 -- "$f" "$dist/assets/$(basename -- "$f")"
|
||||
done
|
||||
fi
|
||||
|
||||
printf 'built %s\n' "$dist"
|
||||
find "$dist" -type f -printf ' %-22f %8s bytes\n' | sort
|
||||
find "$dist" -type f -printf ' %-32P %8s bytes\n' | sort
|
||||
|
||||
@@ -36,6 +36,12 @@ server {
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
# Probe endpoint - no file IO, no cache headers, invisible to the CDN.
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
return 200 "ok";
|
||||
}
|
||||
|
||||
# ---- Routing -------------------------------------------------------------
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
// Site build for the delivery-arc page (CSP-clean: script-src 'self', no inline JS).
|
||||
// Reads pilot/arc.tpl.html, splits the app <script> out to arc.js, injects the scene
|
||||
// manifests, points heroes at /assets/ files, and wraps a full HTML document.
|
||||
// Runs on bare node (the build image has no python). Artifact build = assemble-arc.py.
|
||||
import { readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const DIST = process.argv[2] || join(ROOT, "dist");
|
||||
|
||||
const man = t => JSON.stringify(JSON.parse(readFileSync(join(ROOT, "pilot", `manifest-${t}.json`), "utf8")));
|
||||
|
||||
let tpl = readFileSync(join(ROOT, "pilot/arc.tpl.html"), "utf8");
|
||||
|
||||
// split the app script (the last <script> block) out of the template
|
||||
const i = tpl.lastIndexOf("<script>");
|
||||
const j = tpl.lastIndexOf("</script>");
|
||||
if (i < 0 || j < i) throw new Error("app script block not found");
|
||||
let app = tpl.slice(i + 8, j)
|
||||
.replace("__MAN_GITOPS__", man("gitops"))
|
||||
.replace("__MAN_SUPPLY__", man("supply"))
|
||||
.replace("__MAN_HELM__", man("helm"));
|
||||
let body = tpl.slice(0, i) + tpl.slice(j + 9);
|
||||
|
||||
const heroes = {
|
||||
__HERO_GITOPS__: "course-VI-gitops.webp",
|
||||
__HERO_SUPPLY__: "course-VII-supply-chain.webp",
|
||||
__HERO_HELM__: "course-VIII-helm-press.webp",
|
||||
__HERO_APPENDIX__: "appendix-dependency-ledger.webp",
|
||||
};
|
||||
for (const [ph, file] of Object.entries(heroes)) body = body.replace(ph, "/assets/" + file);
|
||||
if (body.includes("__HERO_") || app.includes("__MAN_")) throw new Error("unfilled placeholder");
|
||||
|
||||
const favicon = "data:image/svg+xml," + encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#070b14"/><rect x="6" y="4" width="14" height="7" rx="1.5" fill="#3fbaf5"/><rect x="11" y="13" width="14" height="7" rx="1.5" fill="#7dd0f9"/><rect x="6" y="22" width="14" height="7" rx="1.5" fill="#e879f9"/></svg>`);
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>The Exploded Cluster</title>
|
||||
<meta name="description" content="Interactive teardown of the toolchain around Kubernetes and OpenShift: GitOps, the image supply chain, and Helm - every scene one generated hero, sliced and scrubbed.">
|
||||
<meta name="theme-color" content="#070b14">
|
||||
<link rel="icon" href="${favicon}">
|
||||
</head>
|
||||
<body>
|
||||
${body}
|
||||
<script src="/gsap.min.js"></script>
|
||||
<script src="/st.min.js"></script>
|
||||
<script src="/arc.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
mkdirSync(join(DIST, "assets"), { recursive: true });
|
||||
writeFileSync(join(DIST, "index.html"), html);
|
||||
writeFileSync(join(DIST, "arc.js"), app);
|
||||
copyFileSync(join(ROOT, "gsap.min.js"), join(DIST, "gsap.min.js"));
|
||||
copyFileSync(join(ROOT, "st.min.js"), join(DIST, "st.min.js"));
|
||||
for (const f of Object.values(heroes)) copyFileSync(join(ROOT, "assets/dist", f), join(DIST, "assets", f));
|
||||
|
||||
// CSP gate: no inline <script> may survive into the served HTML
|
||||
const inline = (html.match(/<script(?![^>]*src=)[^>]*>/gi) || []).length;
|
||||
if (inline) throw new Error("inline <script> in index.html - CSP would block it");
|
||||
console.log(`site build -> ${DIST}: index.html ${(html.length / 1024).toFixed(0)}KB + arc.js + 2 bundles + ${Object.keys(heroes).length} heroes`);
|
||||
Reference in New Issue
Block a user