This commit is contained in:
parent
fb2f6079cc
commit
934c94cb5c
50 changed files with 10464 additions and 758 deletions
|
|
@ -27,11 +27,17 @@ jobs:
|
|||
# run inside this archlinux container — the runner execs them with
|
||||
# node. This shell step needs no node, so installing it here (before
|
||||
# Checkout) is enough.
|
||||
# ffmpeg is for ffprobe, which tools/fetch-media.sh uses to read the
|
||||
# pixel dimensions of each mirrored file. Those become the width/height
|
||||
# attributes that stop the posts page reflowing as 5 MB recordings
|
||||
# arrive, and tools/e2e.sh asserts they are present — so without this
|
||||
# package the deploy fails at the e2e gate rather than shipping a
|
||||
# janky page.
|
||||
pacman -Syu --noconfirm --needed \
|
||||
nodejs \
|
||||
clang lld libc++ \
|
||||
wasi-libc wasi-libc++ wasi-libc++abi wasi-compiler-rt \
|
||||
git curl tar rsync
|
||||
git curl tar rsync zstd gzip jq openssl ffmpeg gnupg
|
||||
# Container runs as root; workspace may be owned by another uid.
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
|
|
@ -63,6 +69,79 @@ jobs:
|
|||
restore-keys: |
|
||||
crafter-cache-${{ runner.os }}-
|
||||
|
||||
- name: Fetch ECB reference rates
|
||||
# Feeds the indicative national-currency line on order pages. Every
|
||||
# charge is in euros; this is display only, labelled with its date —
|
||||
# which is why build-time freshness is enough and no rate service is
|
||||
# ever called at page-view time. Exits 0 on failure: a stale rate
|
||||
# (or none — the page then shows only euros) must not fail a deploy.
|
||||
run: tools/fetch-rates.sh
|
||||
|
||||
- name: Fetch fediverse posts
|
||||
# Build-time, not run-time: the site embeds the owner's own posts and
|
||||
# links out for discussion, so there is no sync service and no runtime
|
||||
# dependency on the instance being up. The script leaves the committed
|
||||
# content/posts.json untouched and exits 0 on any failure, so a
|
||||
# fediverse outage cannot fail a deploy.
|
||||
run: tools/fetch-posts.sh
|
||||
|
||||
- name: Mirror post media
|
||||
# Downloads the images and screen recordings the posts carry and rewrites
|
||||
# content/posts.json to point at our own copies, so nothing the browser
|
||||
# loads is third-party — which is what keeps the privacy notice's
|
||||
# "everything comes from catcrafts.net" true.
|
||||
#
|
||||
# Content-addressed and incremental: a file already on the media mount is
|
||||
# never downloaded again. Writes straight into the mount so the copies
|
||||
# persist across deploys — they are NOT always reproducible, because a
|
||||
# source instance deleting a file leaves ours as the only one.
|
||||
run: |
|
||||
set -eu
|
||||
if [ -d /deploy-app ]; then
|
||||
mkdir -p /deploy-app/media
|
||||
tools/fetch-media.sh /deploy-app/media
|
||||
else
|
||||
echo "WARNING: /deploy-app not mounted; mirroring to a throwaway dir." >&2
|
||||
echo "Media will be re-downloaded on every build until the mount exists." >&2
|
||||
tools/fetch-media.sh media
|
||||
fi
|
||||
|
||||
- name: Build and test the backend
|
||||
id: srv
|
||||
# The server product builds Catcrafts.Shared for the host, which is the
|
||||
# only way to actually RUN the code that generates every byte of markup
|
||||
# the site emits. --selftest is a gate: if escaping or the JSON reader
|
||||
# regress, the deploy stops here rather than shipping broken pages.
|
||||
#
|
||||
# Same refuse-to-guess rule as the wasm bundle below: a variant
|
||||
# directory embeds a config hash, so more than one match means the tree
|
||||
# is ambiguous and picking the first would deploy an arbitrary build.
|
||||
run: |
|
||||
set -eux
|
||||
crafter-build -- --product=server
|
||||
matches=$(find bin -maxdepth 1 -type d -name 'Catcrafts.Server-*' | sort)
|
||||
count=$(printf '%s\n' "$matches" | grep -c . || true)
|
||||
if [ "$count" -ne 1 ]; then
|
||||
echo "Expected exactly one Catcrafts.Server-* directory, found $count:" >&2
|
||||
printf '%s\n' "$matches" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "srv=$matches" >> "$GITHUB_OUTPUT"
|
||||
"$matches/catcrafts-server" --selftest
|
||||
"$matches/catcrafts-server" --routes
|
||||
|
||||
- name: Generate sitemap and Atom feed
|
||||
# Both come from the same route table and Post model the pages use, so
|
||||
# they cannot drift from what the site serves. Generated BEFORE the wasm
|
||||
# build so cfg.files picks them up into the bundle.
|
||||
env:
|
||||
SRV: ${{ steps.srv.outputs.srv }}
|
||||
run: |
|
||||
set -eux
|
||||
"$SRV/catcrafts-server" --sitemap > sitemap.xml
|
||||
"$SRV/catcrafts-server" --feed > feed.xml
|
||||
head -n 4 sitemap.xml
|
||||
|
||||
- name: Build (wasm bundle)
|
||||
run: crafter-build
|
||||
|
||||
|
|
@ -70,16 +149,78 @@ jobs:
|
|||
id: out
|
||||
run: |
|
||||
set -eu
|
||||
dist=$(find bin -maxdepth 1 -type d -name 'Catcrafts.Net-wasm32-wasip1-*' | head -n1)
|
||||
if [ -z "$dist" ]; then
|
||||
# The directory name embeds a config hash, so glob for it. Any change
|
||||
# to compile/link flags produces a NEW hash, which is why we refuse to
|
||||
# guess when more than one variant is present rather than taking
|
||||
# whichever the filesystem happened to list first.
|
||||
matches=$(find bin -maxdepth 1 -type d -name 'Catcrafts.Net-wasm32-wasip1-*' | sort)
|
||||
count=$(printf '%s\n' "$matches" | grep -c . || true)
|
||||
if [ "$count" -eq 0 ]; then
|
||||
echo "No build output directory found under bin/" >&2
|
||||
ls -la bin || true
|
||||
exit 1
|
||||
fi
|
||||
if [ "$count" -gt 1 ]; then
|
||||
echo "Ambiguous build output — $count variant directories under bin/:" >&2
|
||||
printf '%s\n' "$matches" >&2
|
||||
echo "Refusing to guess which one to deploy. Clean bin/ and rebuild." >&2
|
||||
exit 1
|
||||
fi
|
||||
dist=$matches
|
||||
echo "dist=$dist" >> "$GITHUB_OUTPUT"
|
||||
echo "Built bundle: $dist"
|
||||
ls -la "$dist"
|
||||
|
||||
- name: Make the static shell depth-safe
|
||||
# Caddy serves this index.html directly when the backend is down, at
|
||||
# whatever URL was requested — including two-segment ones like
|
||||
# /demos/raytracer. Crafter.Build emits relative boot scripts and its
|
||||
# runtime.js fetches variants.json/files.json/the wasm relative to the
|
||||
# DOCUMENT, so at any depth the fallback loads nothing at all. The script
|
||||
# roots the tags and adds <base href="/">, and fails loudly rather than
|
||||
# silently no-opping. The SSR path handles itself; this is only the
|
||||
# backend-down fallback.
|
||||
env:
|
||||
DIST: ${{ steps.out.outputs.dist }}
|
||||
run: tools/fix-bundle-depth.sh "$DIST"
|
||||
|
||||
- name: End-to-end HTTP tests
|
||||
# Starts the freshly built server on a scratch port and exercises it over
|
||||
# real HTTP: status codes, redirects, headers, form submission, and the
|
||||
# no-JavaScript guarantee. --selftest covers the pure functions; only a
|
||||
# real request can show that /nope is a 404 rather than a soft 404, that
|
||||
# /projects contains its content with no <script> at all, and that a
|
||||
# rejected form comes back with the visitor's values still in it.
|
||||
#
|
||||
# Runs AFTER the wasm build, which is not cosmetic ordering. The server
|
||||
# discovers the bundle under bin/ and lifts its <script> tags from it, so
|
||||
# with no bundle present there is nothing to assert about wasm booting:
|
||||
# "/demos/raytracer loads the wasm" fails outright, and every
|
||||
# "ships no script" check passes vacuously because no page has scripts to
|
||||
# begin with. Building first is what makes both meaningful.
|
||||
#
|
||||
# A gate, not a report: a failure here stops the deploy.
|
||||
env:
|
||||
SRV: ${{ steps.srv.outputs.srv }}
|
||||
run: tools/e2e.sh "$SRV/catcrafts-server"
|
||||
|
||||
- name: Pre-compress static assets
|
||||
# Caddy's `precompressed zstd gzip` (see deploy/Caddyfile.example)
|
||||
# serves these siblings straight from disk instead of re-compressing
|
||||
# the ~700 KB wasm module on every request. Building them here also
|
||||
# buys a better ratio than on-the-fly encoding would spend CPU on.
|
||||
env:
|
||||
DIST: ${{ steps.out.outputs.dist }}
|
||||
run: |
|
||||
set -eu
|
||||
for f in "$DIST"/*.wasm "$DIST"/*.js "$DIST"/*.css "$DIST"/*.xml "$DIST"/*.svg; do
|
||||
[ -f "$f" ] || continue
|
||||
zstd -19 -q -f -k -- "$f"
|
||||
gzip -9 -f -k -- "$f"
|
||||
done
|
||||
echo "Compressed artifacts:"
|
||||
ls -la "$DIST"
|
||||
|
||||
- name: Deploy to web root
|
||||
# No SSH: the job already runs on the deploy box. The server's web root
|
||||
# is bind-mounted into this container at /deploy via the runner config
|
||||
|
|
@ -99,3 +240,55 @@ jobs:
|
|||
fi
|
||||
rsync -a --delete --exclude 'Caddyfile.coi' "$DIST"/ /deploy/
|
||||
echo "Deployed $DIST -> /deploy (host web root)"
|
||||
|
||||
- name: Guard against leaking runtime files into the web root
|
||||
# The web root is served by Caddy's file_server AND mirrored with
|
||||
# --delete, so anything runtime-owned that lands there is both published
|
||||
# and destroyed on the next push. Nothing does today; this is here so a
|
||||
# future cfg.files line cannot quietly change that once the shop has a
|
||||
# database and bank credentials.
|
||||
run: |
|
||||
set -eu
|
||||
bad=$(find /deploy -maxdepth 1 \( -name '*.db' -o -name '*.db-*' \
|
||||
-o -name '*.pem' -o -name '*.key' -o -name '*.env' \
|
||||
-o -name '.*' ! -name '.' \) -print 2>/dev/null || true)
|
||||
if [ -n "$bad" ]; then
|
||||
echo "ERROR: runtime-owned files found in the public web root:" >&2
|
||||
printf '%s\n' "$bad" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Web root contains no database, key or dotfile."
|
||||
|
||||
- name: Deploy backend
|
||||
# Second bind mount, separate from the web root on purpose: the backend
|
||||
# binary and its content must NOT be inside a directory Caddy serves and
|
||||
# rsync --delete mirrors. Add to the runner's container.options:
|
||||
# -v /srv/catcrafts-app:/deploy-app
|
||||
#
|
||||
# The restart is not done here. This job runs inside a container with no
|
||||
# access to the host's systemd, and the alternatives (host root for the
|
||||
# runner, or a polkit rule) grant CI far more than "restart one service"
|
||||
# needs. Instead the last step touches a marker file that
|
||||
# catcrafts-deploy.path watches — see deploy/catcrafts-deploy.path.
|
||||
# That unit re-runs --selftest against the new binary before cutting
|
||||
# over, so a broken deploy leaves the working server running.
|
||||
env:
|
||||
SRV: ${{ steps.srv.outputs.srv }}
|
||||
run: |
|
||||
set -eu
|
||||
if [ ! -d /deploy-app ]; then
|
||||
echo "ERROR: /deploy-app is not mounted into the runner container." >&2
|
||||
echo "Add '-v /srv/catcrafts-app:/deploy-app' to the runner's" >&2
|
||||
echo "container.options in config.yaml and restart the runner." >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "$SRV/catcrafts-server" /deploy-app/catcrafts-server.new
|
||||
mkdir -p /deploy-app/content
|
||||
# --delete on content/ is safe: it is regenerated every build. Note it
|
||||
# does NOT touch /deploy-app/media, which must survive.
|
||||
rsync -a --delete content/ /deploy-app/content/
|
||||
# Swap the binary into place atomically, so a request arriving mid-copy
|
||||
# never hits a truncated executable.
|
||||
mv -f /deploy-app/catcrafts-server.new /deploy-app/catcrafts-server
|
||||
date -u +%FT%TZ > /deploy-app/.deploy-stamp
|
||||
echo "Deployed backend -> /deploy-app; marker touched"
|
||||
|
|
|
|||
Loading…
Reference in a new issue