name: Deploy on: push: branches: [master] workflow_dispatch: # One deploy at a time; if you push twice quickly, cancel the older run so the # newest commit is what lands on the server. concurrency: group: deploy cancel-in-progress: true jobs: build-deploy: # `flatpak` is a privileged runner registered to this repository alone # (/etc/forgejo-runner/config-catcrafts.yaml on the box, label # flatpak:docker://archlinux:latest): crafter-build is the Crafter.Build # Flatpak, and bubblewrap inside the job container needs the privilege. # The same runner carries the two deploy bind mounts (/deploy, /deploy-app) # and a docker volume at /var/lib/flatpak that keeps the installed Flatpak # and its Sdk between runs. Same route as imsd's package job; see # deploy/README.md "Runner configuration". runs-on: flatpak env: # Which crafter-build. `stable` is the branch Crafter.Build publishes # from its release tags, `beta` follows every push to its master (the # same thing the rolling `latest` tarball this job used to download # was). This site tracks Crafter.Graphics master, which since 2026-10-06 # uses the Shader `defines`/`variant` fields that only Crafter.Build # master has: the 2.1.0 release predates them by three days, and run # 533 failed on exactly that. Flip to //stable once a Crafter.Build # release carries them. imsd pins stable; it has no Crafter.Graphics. CRAFTER_REF: net.catcrafts.CrafterBuild//beta steps: - name: Check live payment test credentials # ShouldSettleEurcOnTestnet is a mandatory deploy gate, so a missing # secret fails HERE, before any build time is spent — never as a silent # skip. The secret lives in Forgejo: repo Settings -> Actions -> # Secrets. See deploy/README.md "Live payment suites in CI" for what it # is and how to keep the testnet wallet funded. # # There is ONE live suite now, not two. The bank half used to be a # hosted provider with a test-mode API, and that gate died with the # account on 2026-08-20. Its replacement is a transfer to our own # account, which has no counterpart to call and therefore no credential # to check: ShouldSettleBankTransfers drives the real rail with no # network at all and runs unconditionally with the other black-box # suites. Losing a live gate here is a fact about the rail, not a # weakening of the standard. env: EURC_E2E_PRIVATE_KEY: ${{ secrets.EURC_E2E_PRIVATE_KEY }} run: | set -eu if [ -z "${EURC_E2E_PRIVATE_KEY:-}" ]; then echo "ERROR: EURC_E2E_PRIVATE_KEY secret is not set (an Ethereum Sepolia key holding testnet EURC + gas)." >&2 exit 1 fi echo "Live payment test credentials are present." - name: Install build dependencies run: | # Same keyring bootstrap the Crafter.Build CI does: the slim # archlinux:latest image ships without a populated pacman keyring # or local master key. pacman-key --init pacman-key --populate archlinux pacman -Sy --noconfirm --needed archlinux-keyring # nodejs is required for the JS-based actions (checkout, cache) to # 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/ffprobe are both used by tools/fetch-media.sh. ffprobe reads # the pixel dimensions of each mirrored file, which become the # width/height attributes that stop the posts page reflowing as 5 MB # recordings arrive. ffmpeg transcodes each still image into the AVIF # and PNG renditions the page serves it between. The # ShouldServePostPages suite asserts both are present — so without # this package the deploy fails at the test gate rather than # shipping a janky page. # # Nothing that compiles is installed here. clang, lld, libc++ and the # WASI sysroot come with the crafter-build Flatpak (next step), and # its Sdk has the cmake, make and perl that build msquic and quictls # (Crafter.Network's ExternalBuilder::CMake dependency), the OpenSSL # the server links, and the gpg the suites call: the suites run # inside the sandbox, so what this container has is beside the point. pacman -Syu --noconfirm --needed \ nodejs flatpak \ git curl tar rsync zstd gzip jq ffmpeg # Container runs as root; workspace may be owned by another uid. git config --global --add safe.directory '*' - name: Install crafter-build # crafter-build is the Crafter.Build Flatpak, the route imsd's package # job takes: it carries its own clang, lld, libc++ and WASI sysroot, and # its Sdk (org.freedesktop.Sdk, from Flathub) has the cmake, make, perl, # git, gpg and OpenSSL the build and the suites need. The branch is # CRAFTER_REF at the top of the job. Crafter.Build's CI publishes the # Flatpak and nothing else since 2026-10-02: the rolling `latest` # release's crafter-build-linux-x86_64-v2.tar.gz this step used to # download is gone (404), which is what failed the deploy on # 2026-10-12. # # /var/lib/flatpak is a docker volume the runner mounts, so the Sdk and # the Flatpak (about 4 GB on disk) are downloaded once; `install` is a # no-op after that and `update` moves the installed branch forward. # The install is retried: a flaky download must not fail a deploy # before any build time is spent. run: | set -eu flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo for i in 1 2 3; do flatpak install -y --noninteractive catcrafts "$CRAFTER_REF" && break if [ "$i" -eq 3 ]; then echo "ERROR: installing the crafter-build Flatpak failed 3 times." >&2 exit 1 fi echo "install failed (attempt $i/3), retrying in 15s..." >&2 sleep 15 done flatpak update -y --noninteractive # The Flatpak under the name the rest of this file uses. Inside the # sandbox PATH is /app/bin:/usr/bin (the Flatpak's tools, the Sdk's); # /opt/foundry/bin is appended because the suites run INSIDE the # sandbox and ShouldSettleEurcOnTestnet shells out to cast, which # the next step installs there: the sandbox sees the host's /opt but # not its /usr/local. cat > /usr/local/bin/crafter-build < # ` is how the steps below call catcrafts-server. The host # itself gets those libraries shipped next to the binary, see # "Deploy backend". cat > /usr/local/bin/crafter-run <&2 echo "Media will be re-downloaded on every build until the mount exists." >&2 dir=media fi tools/fetch-media.sh "$dir" # Handed to the test step (E2E_MEDIA_DIR) so ShouldServePostPages # checks the files where they actually are. It used to assume # ./media and reported every referenced file as missing here, which # reads as a broken site and means a wrong path. echo "dir=$dir" >> "$GITHUB_OUTPUT" - 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. `crafter-build test` is the gate: if escaping or the # JSON reader regress, the deploy stops here rather than shipping # broken pages. One suite per tests//main.cpp — the unit suites # replaced the binary's --selftest flag, the Should{ServeRoutes,…} # black-box suites replaced tools/e2e.sh: each spawns the freshly # built server on its own scratch port with the fake payment rails and # exercises it over real HTTP. ShouldBootWasmAtDepth self-skips here — # no wasm bundle exists yet — and re-runs after the wasm build below. # # E2E_MEDIA_DIR points ShouldServePostPages at the mirror mount, where # the media files actually are. # # EURC_E2E_PRIVATE_KEY feeds the live crypto suite, which runs as part # of this same gate: a deploy that cannot settle a testnet EURC # transfer does not ship. If it fails at `cast send`, the testnet # wallet is empty — see deploy/README.md "Live payment suites in CI" # for the faucets. The bank rail's own suite needs no secret; it drives # the real rail with a credits file instead of a provider. # # 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. # # The binary is called through crafter-run: it links the Flatpak's # libc++, which this container does not have. env: E2E_MEDIA_DIR: ${{ steps.media.outputs.dir }} EURC_E2E_PRIVATE_KEY: ${{ secrets.EURC_E2E_PRIVATE_KEY }} run: | set -eux crafter-build --product=server crafter-build test --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" crafter-run "$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 crafter-run "$SRV/catcrafts-server" --sitemap > sitemap.xml crafter-run "$SRV/catcrafts-server" --feed > feed.xml head -n 4 sitemap.xml - name: Build (wasm bundle) run: crafter-build - name: Locate build output id: out run: | set -eu # 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 , 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: Wasm boot and script-shape checks # The bundle-sensitive slice of the black-box suites, re-run now that # the wasm bundle exists under bin/. The server discovers the bundle # and lifts its