rewrite
Some checks failed
Deploy / build-deploy (push) Failing after 4m56s

This commit is contained in:
Jorijn van der Graaf 2026-08-05 04:18:37 +02:00
commit 934c94cb5c
50 changed files with 10464 additions and 758 deletions

204
tools/dev.sh Executable file
View file

@ -0,0 +1,204 @@
#!/bin/sh
# Run the whole site locally, in the same shape as production.
#
# tools/dev.sh build both products and serve on :8080
# tools/dev.sh --no-build use whatever is already in bin/
#
# Why this exists: catcrafts-server serves PAGES only. Static assets — the wasm
# module, styles.css, the JS bridges — are Caddy's job in production, so running
# the server on its own gives you correct HTML with no stylesheet, which looks
# broken and isn't. This starts both and puts Caddy in front, so what you see
# locally is what the deployed site does, including the reverse-proxy split and
# the scoped cross-origin headers.
#
# Ctrl-C stops both.
set -eu
BUILD=1
[ "${1:-}" = "--no-build" ] && BUILD=0
PORT="${DEV_PORT:-8080}"
BACKEND_PORT="${DEV_BACKEND_PORT:-8081}"
WORK="$(mktemp -d)"
# Refuse to start if either port is taken.
#
# Without this, a leftover instance from an earlier run keeps serving: the new
# backend fails to bind, the old Caddy carries on proxying to the OLD binary, and
# the site looks like the build did not take effect. That has wasted real time
# twice — the symptom (stale content) points at the build, not at a process.
port_busy() {
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | grep -qE "[:.]$1 "
else
curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$1/" 2>/dev/null
fi
}
for _p in "$PORT" "$BACKEND_PORT"; do
if port_busy "$_p"; then
echo "dev: port $_p is already in use — another instance is probably still running." >&2
echo "dev: find it with: ss -ltnp | grep -E ':(8080|8081) '" >&2
echo "dev: then kill those PIDs, or set DEV_PORT / DEV_BACKEND_PORT." >&2
exit 1
fi
done
cleanup() {
[ -n "${SRV_PID:-}" ] && kill "$SRV_PID" 2>/dev/null || true
[ -n "${CADDY_PID:-}" ] && kill "$CADDY_PID" 2>/dev/null || true
rm -rf "$WORK"
}
trap cleanup EXIT INT TERM
# Exactly one match or fail loudly. A variant directory name embeds a config
# hash, so two matches means the tree holds artifacts from two different
# configurations and picking either would be a coin flip — this has caused real
# confusion (testing a stale binary and believing the result).
onedir() {
_m=$(find bin -maxdepth 1 -type d -name "$1" 2>/dev/null | sort)
_n=$(printf '%s\n' "$_m" | grep -c . || true)
if [ "$_n" -ne 1 ]; then
echo "dev: expected exactly one $1 directory under bin/, found $_n" >&2
[ "$_n" -gt 1 ] && printf '%s\n' "$_m" >&2
echo "dev: run 'rm -rf bin' and try again" >&2
exit 1
fi
printf '%s' "$_m"
}
if [ "$BUILD" = 1 ]; then
echo "dev: building the server product..."
crafter-build --local -- --product=server >"$WORK/build-server.log" 2>&1 \
|| { echo "dev: server build failed:" >&2; tail -20 "$WORK/build-server.log" >&2; exit 1; }
SRV=$(onedir 'Catcrafts.Server-*')
# sitemap.xml and feed.xml are generated from the same route table and Post
# model the pages use, and the wasm build copies them into the bundle — so
# they have to exist before it runs.
"$SRV/catcrafts-server" --sitemap > sitemap.xml
"$SRV/catcrafts-server" --feed > feed.xml
echo "dev: building the wasm bundle..."
crafter-build --local >"$WORK/build-web.log" 2>&1 \
|| { echo "dev: wasm build failed:" >&2; tail -20 "$WORK/build-web.log" >&2; exit 1; }
fi
SRV=$(onedir 'Catcrafts.Server-*')
WEB=$(onedir 'Catcrafts.Net-*')
# Makes the static shell survive being served at a deep URL. Run unconditionally,
# not just after a build: --no-build may be pointing at a bundle someone produced
# with a bare crafter-build, and the script is idempotent.
./tools/fix-bundle-depth.sh "$WEB" >/dev/null
ABS_WEB="$(cd "$WEB" && pwd)"
# Mirrored post media. Absent is fine — the pages render, the media 404s — so
# this does not block running the site before fetch-media.sh has been run.
mkdir -p media
PWD_MEDIA="$(cd media && pwd)"
# Mirrors deploy/Caddyfile.example: static assets from disk, everything else
# proxied to the backend, cross-origin isolation only on the paths that boot the
# wasm module.
cat > "$WORK/Caddyfile" <<EOF
:$PORT {
root * $ABS_WEB
encode zstd gzip
@isolated path /demos/* /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js /catcrafts-head.js /files.json /variants.json /*.wgsl
header @isolated {
Cross-Origin-Opener-Policy "same-origin"
Cross-Origin-Embedder-Policy "require-corp"
Cross-Origin-Resource-Policy "same-origin"
}
@static path /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js /catcrafts-head.js /files.json /variants.json /styles.css /favicon.svg /robots.txt /*.wgsl /*.jpg /posts.json /rates.json
handle @static {
header Cache-Control "no-store"
file_server
}
handle_path /media/* {
root * $PWD_MEDIA
header Cache-Control "no-store"
file_server
}
handle {
reverse_proxy 127.0.0.1:$BACKEND_PORT
}
}
EOF
# Rail selection for dev:
# * a repo-root .env (gitignored, never committed) is sourced if present —
# put MOLLIE_API_KEY=test_… there to point dev at Mollie's real test mode;
# * DEV_RAIL=fake|mollie overrides the automatic choice;
# * default with no key is the fake rail: full order lifecycle, no network.
# "Pay" an order with: touch $WORK/orders.jsonl.fake-paid
#
# A live_ key is refused outright. Dev creates throwaway orders; pointing them
# at real money collection is never what anyone meant.
if [ -f .env ]; then
set -a; . ./.env; set +a
fi
RAIL="${DEV_RAIL:-}"
if [ -z "$RAIL" ]; then
RAIL=fake
[ -n "${MOLLIE_API_KEY:-}" ] && RAIL=mollie
fi
if [ "$RAIL" = mollie ]; then
case "${MOLLIE_API_KEY:-}" in
test_*) echo "dev: payments via Mollie TEST mode" ;;
live_*) echo "dev: refusing to run dev against a LIVE Mollie key." >&2
echo "dev: live keys belong in /etc/catcrafts/payments.env on the server." >&2
exit 1 ;;
*) echo "dev: MOLLIE_API_KEY is not a test_ or live_ key" >&2; exit 1 ;;
esac
fi
"$SRV/catcrafts-server" --serve "$BACKEND_PORT" \
--orders="$WORK/orders.jsonl" --rail="$RAIL" \
--redirect-base="http://localhost:$PORT" >"$WORK/server.log" 2>&1 &
SRV_PID=$!
caddy run --config "$WORK/Caddyfile" --adapter caddyfile >"$WORK/caddy.log" 2>&1 &
CADDY_PID=$!
# Wait for the front door rather than sleeping a fixed amount.
i=0
while [ "$i" -lt 100 ]; do
curl -s -o /dev/null "http://127.0.0.1:$PORT/api/healthz" 2>/dev/null && break
i=$((i + 1)); sleep 0.1
done
if [ "$i" -ge 100 ]; then
echo "dev: did not come up. server log:" >&2; cat "$WORK/server.log" >&2
echo "dev: caddy log:" >&2; cat "$WORK/caddy.log" >&2
exit 1
fi
cat <<EOF
catcrafts.net is running: http://localhost:$PORT
/ home
/shop product list
/shop/fp6-pmos product page + checkout (fake payment rail)
/projects the Crafter suite
/posts fediverse posts
/demos demo list; /demos/raytracer loads the wasm
/legal/privacy privacy notice
/feed.xml Atom feed
/media/* mirrored post media (run tools/fetch-media.sh to populate)
Orders from this session go to a temp file and are discarded on exit.
Payment rail: $RAIL$([ "$RAIL" = fake ] && printf '%s' " — simulate a customer paying with:
touch $WORK/orders.jsonl.fake-paid")
Ctrl-C to stop.
EOF
# Surface backend output as it happens — this is where a render error shows up.
tail -f "$WORK/server.log" &
wait "$SRV_PID"