Run 533, the first on the Flatpak toolchain, installed the Flatpak, built
the server and passed all 28 suites, then failed building the wasm bundle:
Crafter.Graphics master has used the Shader `defines` and `variant` fields
since 2026-10-06, and the `stable` Flatpak is the 2.1.0 release from three
days before that. Only Crafter.Build master has them, so the toolchain is
its `beta` branch for now, which is also what the rolling `latest` tarball
this job used to download was: a build of master.
The branch is one variable at the top of the job, the way imsd's package
job pins its own (imsd stays on stable; it has no Crafter.Graphics). Flip
it back to //stable once a Crafter.Build release carries the fields. The
wrapper scripts expand the ref at write time, and the install step prints
them along with the Flatpak's branch and commit so a run's log says which
toolchain it used.
Verified locally with beta 2.1.0-10-g4af9280: the wasm bundle builds and
the server suite passes, except ShouldServePostPages, which needs the media
mirror on the deploy mount that only exists in CI.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Crafter.Build's CI publishes only the Flatpak since 2026-10-02, so the
rolling `latest` tarball the Install crafter-build step downloaded is gone
and the 2026-10-12 deploy failed with a 404 before building anything.
Take the route imsd's package job takes. The job runs on `flatpak`, a
privileged runner registered to this repository alone
(forgejo-runner-catcrafts on the box, same arrangement as imsd's), with the
installed Flatpak and its Sdk kept on a docker volume between runs. The
container installs nothing that compiles any more: clang, lld, libc++, the
WASI sysroot, cmake, perl, gpg and OpenSSL all come with the Flatpak and its
Sdk. `crafter-build` is a wrapper around `flatpak run`, `crafter-run` runs a
built binary inside the sandbox, cast is installed under /opt where the
sandbox can see it, and the cache path is the Flatpak's own.
The server binary links the Flatpak's libc++, libc++abi and libunwind, which
are newer than the host's, so the deploy ships them next to the binary the
way it already ships libmsquic; the rpath finds them there. They need at
most GLIBC_2.38, the host has 2.41.
ShouldMatchBankTransfers passed braced lists straight into a std::span
parameter. That compiled only because libc++ 22 had the C++26
span(initializer_list) constructor; the libc++ 23 in the Flatpak does not,
so the lists now go through a vector like the rest of the file.
deploy/README.md documents the runner, the sandbox's private /tmp and how
to run a built binary where the host lacks a matching libc++.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A 167 MB screen recording uploaded to catbox.moe hit fetch-media.sh's 64 MB
MAX_BYTES, so curl refused it, the entry kept its third-party URL, and e2e
failed "/posts media origin" on a file we had on disk the whole time. Raising
the cap would have papered over it: the mirror step still depends on someone
else's server being up, fast, and still holding the file.
Invert it. tools/publish-media.sh uploads a recording to the media mount under
its content hash BEFORE the post exists and prints the URL to post, and
fetch-media.sh adopts an own-origin URL by rewriting it to /media/<hash> with no
request at all — no size cap, no third party in the build.
Also here, because publishing exposed them:
* Rotation. Phones record 1920x1080 and attach a display matrix rather than
rotating pixels, so an untouched file reports landscape while playing
portrait and width/height reserve exactly the wrong box. publish-media.sh
bakes rotation into the frames; fetch-media.sh swaps the dimensions on a
quarter-turn matrix for anything mirrored straight from a phone.
* Posters. pict-rs will not thumbnail AV1, so a self-hosted video usually
arrives with no poster. publish-media.sh uploads <hash>.poster.webp beside
the video and fetch-media.sh falls back to it — a real thumbnail still wins.
* The workflow comment claiming a file on the mount is never downloaded again
was wrong: the name is the hash of the bytes, so third-party media is
re-fetched every build and only the write is skipped.
Transcoding to AV1 is what makes self-hosting cheap: that clip was 78 s of a
dark room at 17 Mbps, and denoise + AV1 gives the same picture in 15 MB. Note
<video> carries a single src with no fallback, so AV1 excludes Safari < 17;
--raw skips the transcode when that matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
actions/checkout and actions/cache are executed with node inside the
archlinux:latest job container, which has no node by default. Install it
in the first (shell) step, before Checkout, mirroring the Crafter.Build CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- .forgejo/workflows/deploy.yaml: build the wasm bundle on the arch-latest
Docker runner and rsync it into the bind-mounted web root (/deploy) on
push to master
- deploy/Caddyfile.example: reference site block with the cross-origin
isolation headers the WASM runtime requires
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>