Conformance in CI: works locally, blocked on runner environment #7

Closed
opened 2026-08-19 06:49:30 +00:00 by yuris · 11 comments
Owner

Tier-3 testing: GitHub-Actions-style pipeline (Forgejo Actions) that:

  1. starts spacetime start + publishes test module
  2. builds example headless with bob
  3. runs two headless clients, asserts cache convergence, greps result
  • Forgejo Actions workflow
  • Headless dmengine exit-path investigation (print+grep fallback)
  • Weekly scheduled run vs spacetimedb-standalone:latest for drift detection
Tier-3 testing: GitHub-Actions-style pipeline (Forgejo Actions) that: 1. starts `spacetime start` + publishes test module 2. builds example headless with bob 3. runs two headless clients, asserts cache convergence, greps result - [ ] Forgejo Actions workflow - [ ] Headless dmengine exit-path investigation (print+grep fallback) - [ ] Weekly scheduled run vs spacetimedb-standalone:latest for drift detection
Author
Owner

Unit CI landed in 0a6602c — .forgejo/workflows/ci.yml runs stylua, the lua5.1 unit suite, and nix flake check on every push and PR (run #2 green).

Two things that bit during setup, worth knowing before the conformance job is written:

  1. test/.deps/json.lua is gitignored, so a fresh checkout has no JSON codec — the dev shells now symlink it from the store via vendorJson. Any CI step that runs the tests must go through a dev shell (or the check derivation), never bare lua.
  2. CI uses a lean .#ci shell (lua5_1 + stylua only). Do not point conformance at the default shell — it drags in jdk25, bob, and lua-language-server. The bob toolchain is needed for the headless-dmengine job, so that job should get its own shell attr rather than reusing default.

This issue now covers only the live-server tier: spacetime start + published module + two headless clients asserting cache convergence.

Unit CI landed in 0a6602c — `.forgejo/workflows/ci.yml` runs stylua, the lua5.1 unit suite, and `nix flake check` on every push and PR (run #2 green). Two things that bit during setup, worth knowing before the conformance job is written: 1. `test/.deps/json.lua` is gitignored, so a fresh checkout has no JSON codec — the dev shells now symlink it from the store via `vendorJson`. Any CI step that runs the tests must go through a dev shell (or the check derivation), never bare `lua`. 2. CI uses a lean `.#ci` shell (lua5_1 + stylua only). Do **not** point conformance at the default shell — it drags in jdk25, bob, and lua-language-server. The bob toolchain is needed for the headless-dmengine job, so that job should get its own shell attr rather than reusing `default`. This issue now covers only the live-server tier: `spacetime start` + published module + two headless clients asserting cache convergence.
yuris changed title from Conformance CI: run example against spacetime start to Conformance CI: live-server job (spacetime start + 2 headless clients) 2026-08-19 07:05:40 +00:00

Conformance job failed on run #5 (commit 3be6593db1):copying path '/nix/store/kq6bqbxkvblwxx4g89l63103jq72lxcd-roc-toolkit-0.4.0' from 'https://cache.nixos.org'... copying path '/nix/store/ahpzhy9kw4w3wnva40n8bi6qlhn9frsy-icu4c-78.3-dev' from 'https://cache.nixos.org'... copying path '/nix/store/28pl6r2nw05ai4fn9bvmdfaq2jb96l0g-libical-3.0.20' from 'https://cache.nixos.org'... copying path '/nix/store/w0p3046zxcqlybnhpz71xzivnxp30wy0-tinysparql-3.11.1' from 'https://cache.nixos.org'... copying path '/nix/store/kx9qcgaqkpchayz6a604zwq2cbj5d24i-ffmpeg-headless-9.0-lib' from 'https://cache.nixos.org'... copying path '/nix/store/wbv6jwxq9ay99jwx1sagxcgwrd10wk9f-bluez-5.87' from 'https://cache.nixos.org'... copying path '/nix/store/bfqsxlviikq7vlp36kasy5hhamlxlkd2-nodejs-slim-24.19.0' from 'https://cache.nixos.org'... copying path '/nix/store/mijaaxa13nv1h754xdic87f31z3mnbc6-gtk+3-3.24.52' from 'https://cache.nixos.org'... copying path '/nix/store/0a9l8lf8394msppsna27y58f8ljqyifn-openjdk-25.0.4+7' from 'https://cache.nixos.org'... copying path '/nix/store/hgcqpw6f8yg3cf0yii0p72kdp8334shf-nodejs-slim-24.19.0-corepack' from 'https://cache.nixos.org'... copying path '/nix/store/6x3bfz00nsp3qhcgg6wzvgfv67dhgy9j-nodejs-slim-24.19.0-npm' from 'https://cache.nixos.org'... building '/nix/store/px8hj7awj6ffpcamsaf95ywrmnn8n6aq-nodejs-24.19.0.drv'... copying path '/nix/store/3d1c302vw7kc8a5vknhmn34c0pd7zm6m-gcc-wrapper-15.3.0' from 'https://cache.nixos.org'... copying path '/nix/store/b4rc51gg4wxzia8xg834aphj83p5ss33-gstreamer-1.28.5' from 'https://cache.nixos.org'... copying path '/nix/store/4fgkln5dql87s2dlbahyh2cia78ivs2n-python3.14-packaging-26.2' from 'https://cache.nixos.org'... copying path '/nix/store/w60q4s3anfrlnn72841kw60fsjc0qmhk-stdenv-linux' from 'https://cache.nixos.org'... copying path '/nix/store/lfrjb7zz2g8mkm1g1jjg2hxwd1crryf6-glib-2.88.3-dev' from 'https://cache.nixos.org'... copying path '/nix/store/fdjdj3h57zi1g4x4pd8qplrflyz78883-gst-plugins-base-1.28.5' from 'https://cache.nixos.org'... copying path '/nix/store/n1cr847pvw2zppnsbl7s7cgpg4ibpypg-libcanberra-0.30' from 'https://cache.nixos.org'... building '/nix/store/4pdb4ygw35iiiik9sajriz7zr0cb8v4z-bob-run.drv'... copying path '/nix/store/ax8jrqcq50digb2xx9hlbi3xw70h9iqb-libcamera-0.7.0' from 'https://cache.nixos.org'... building '/nix/store/086v3sk8xdmcdqsyhq64xzvv2bxibbf7-fhsenv-ensure-gsettings-schemas-directory.drv'... copying path '/nix/store/h06qaqbc0llyr2vpqrj9ia2dpwgw120m-pipewire-1.6.8' from 'https://cache.nixos.org'... building '/nix/store/9c6fxh4pc12sp0ndk6bidn4g39si9n8b-bob-init.drv'... building '/nix/store/bpvg0sz2ibjll478jidnc7rvnx357pmn-defold-run.drv'... building '/nix/store/lvxy0iqd6am5zfqs9lp896rksjcb4dig-json.lua.drv'... copying path '/nix/store/bs3khmbhwlam0n9bj7n8nyj76kn0ki9a-openal-soft-1.24.3' from 'https://cache.nixos.org'... building '/nix/store/jzk32z8w4s92v5a8v8b3s6ixlxx8f3y2-defold-run-init.drv'... building '/nix/store/fbmh45jb2x4fhvi9612yxng6m30df85w-defold-run-fhsenv-rootfs.drv'... building '/nix/store/dbjc7m9hif2dd2kyxydydydxiafrq0l3-defold-run-bwrap.drv'... building '/nix/store/gs5qmm4s8igp91xj6l1rhpbn38z1za0j-defold-run.drv'... building '/nix/store/xxp6q54qv9h4s0rj01aqdsza8yifk6by-bob-fhsenv-rootfs.drv'... building '/nix/store/m3a2ml31jlz5jn92dvbaa7mkyxmnwp14-bob-bwrap.drv'... building '/nix/store/c2h8gdmvjzxkw8grlkxnnmyaxi1bxqvh-bob.drv'... building '/nix/store/g26kxq0yirdkwy5ams2kv5m1l4sxgmif-nix-shell-env.drv'... this path will be fetched (107.5 KiB download, 107.4 KiB unpacked): /nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man copying path '/nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man' from 'https://cache.nixos.org'... curl: (7) Failed to connect to 127.0.0.1:3000 after 0 ms: Could not connect to server [dev-server] server never became ready

Conformance job failed on run #5 (commit 3be6593db13eac17c1ce6e517745af485e1e5694):```copying path '/nix/store/kq6bqbxkvblwxx4g89l63103jq72lxcd-roc-toolkit-0.4.0' from 'https://cache.nixos.org'... copying path '/nix/store/ahpzhy9kw4w3wnva40n8bi6qlhn9frsy-icu4c-78.3-dev' from 'https://cache.nixos.org'... copying path '/nix/store/28pl6r2nw05ai4fn9bvmdfaq2jb96l0g-libical-3.0.20' from 'https://cache.nixos.org'... copying path '/nix/store/w0p3046zxcqlybnhpz71xzivnxp30wy0-tinysparql-3.11.1' from 'https://cache.nixos.org'... copying path '/nix/store/kx9qcgaqkpchayz6a604zwq2cbj5d24i-ffmpeg-headless-9.0-lib' from 'https://cache.nixos.org'... copying path '/nix/store/wbv6jwxq9ay99jwx1sagxcgwrd10wk9f-bluez-5.87' from 'https://cache.nixos.org'... copying path '/nix/store/bfqsxlviikq7vlp36kasy5hhamlxlkd2-nodejs-slim-24.19.0' from 'https://cache.nixos.org'... copying path '/nix/store/mijaaxa13nv1h754xdic87f31z3mnbc6-gtk+3-3.24.52' from 'https://cache.nixos.org'... copying path '/nix/store/0a9l8lf8394msppsna27y58f8ljqyifn-openjdk-25.0.4+7' from 'https://cache.nixos.org'... copying path '/nix/store/hgcqpw6f8yg3cf0yii0p72kdp8334shf-nodejs-slim-24.19.0-corepack' from 'https://cache.nixos.org'... copying path '/nix/store/6x3bfz00nsp3qhcgg6wzvgfv67dhgy9j-nodejs-slim-24.19.0-npm' from 'https://cache.nixos.org'... building '/nix/store/px8hj7awj6ffpcamsaf95ywrmnn8n6aq-nodejs-24.19.0.drv'... copying path '/nix/store/3d1c302vw7kc8a5vknhmn34c0pd7zm6m-gcc-wrapper-15.3.0' from 'https://cache.nixos.org'... copying path '/nix/store/b4rc51gg4wxzia8xg834aphj83p5ss33-gstreamer-1.28.5' from 'https://cache.nixos.org'... copying path '/nix/store/4fgkln5dql87s2dlbahyh2cia78ivs2n-python3.14-packaging-26.2' from 'https://cache.nixos.org'... copying path '/nix/store/w60q4s3anfrlnn72841kw60fsjc0qmhk-stdenv-linux' from 'https://cache.nixos.org'... copying path '/nix/store/lfrjb7zz2g8mkm1g1jjg2hxwd1crryf6-glib-2.88.3-dev' from 'https://cache.nixos.org'... copying path '/nix/store/fdjdj3h57zi1g4x4pd8qplrflyz78883-gst-plugins-base-1.28.5' from 'https://cache.nixos.org'... copying path '/nix/store/n1cr847pvw2zppnsbl7s7cgpg4ibpypg-libcanberra-0.30' from 'https://cache.nixos.org'... building '/nix/store/4pdb4ygw35iiiik9sajriz7zr0cb8v4z-bob-run.drv'... copying path '/nix/store/ax8jrqcq50digb2xx9hlbi3xw70h9iqb-libcamera-0.7.0' from 'https://cache.nixos.org'... building '/nix/store/086v3sk8xdmcdqsyhq64xzvv2bxibbf7-fhsenv-ensure-gsettings-schemas-directory.drv'... copying path '/nix/store/h06qaqbc0llyr2vpqrj9ia2dpwgw120m-pipewire-1.6.8' from 'https://cache.nixos.org'... building '/nix/store/9c6fxh4pc12sp0ndk6bidn4g39si9n8b-bob-init.drv'... building '/nix/store/bpvg0sz2ibjll478jidnc7rvnx357pmn-defold-run.drv'... building '/nix/store/lvxy0iqd6am5zfqs9lp896rksjcb4dig-json.lua.drv'... copying path '/nix/store/bs3khmbhwlam0n9bj7n8nyj76kn0ki9a-openal-soft-1.24.3' from 'https://cache.nixos.org'... building '/nix/store/jzk32z8w4s92v5a8v8b3s6ixlxx8f3y2-defold-run-init.drv'... building '/nix/store/fbmh45jb2x4fhvi9612yxng6m30df85w-defold-run-fhsenv-rootfs.drv'... building '/nix/store/dbjc7m9hif2dd2kyxydydydxiafrq0l3-defold-run-bwrap.drv'... building '/nix/store/gs5qmm4s8igp91xj6l1rhpbn38z1za0j-defold-run.drv'... building '/nix/store/xxp6q54qv9h4s0rj01aqdsza8yifk6by-bob-fhsenv-rootfs.drv'... building '/nix/store/m3a2ml31jlz5jn92dvbaa7mkyxmnwp14-bob-bwrap.drv'... building '/nix/store/c2h8gdmvjzxkw8grlkxnnmyaxi1bxqvh-bob.drv'... building '/nix/store/g26kxq0yirdkwy5ams2kv5m1l4sxgmif-nix-shell-env.drv'... this path will be fetched (107.5 KiB download, 107.4 KiB unpacked): /nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man copying path '/nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man' from 'https://cache.nixos.org'... curl: (7) Failed to connect to 127.0.0.1:3000 after 0 ms: Could not connect to server [dev-server] server never became ready```

Conformance failed on run #7 (8989aa8836).

Runner topology:

in-container: yes
docker client: 29.5.3-1
docker server: 29.4.3
hostname: 0a4ad57048bd
routes:

Tail:

no log captured — failed before the conformance step
Conformance failed on run #7 (8989aa88363f45decdd95bf4ebb397ff94e49e10). Runner topology: ``` in-container: yes docker client: 29.5.3-1 docker server: 29.4.3 hostname: 0a4ad57048bd routes: ``` Tail: ``` no log captured — failed before the conformance step ```

Conformance failed on run #8 (b30a36aa30).

Runner topology:

in-container: yes
docker client: 29.5.2-2
docker server: 29.4.3
hostname: c919cc9fa5ba
routes:

Tail:

no log captured — failed before the conformance step
Conformance failed on run #8 (b30a36aa30f24fad592d417302888ae2791d7ab4). Runner topology: ``` in-container: yes docker client: 29.5.2-2 docker server: 29.4.3 hostname: c919cc9fa5ba routes: ``` Tail: ``` no log captured — failed before the conformance step ```
Author
Owner

Status: works locally, unresolved on this runner — now manual

The engine conformance test is done and passing. From cold, in ~15s:

nix develop .#conformance --command bash tools/conformance.sh
...
DEBUG:SCRIPT: SMOKE: PASS cached row id=1 sender=headless
[conformance] PASS

It starts a real clockworklabs/spacetime:v2.8.2, publishes the chat module, builds the Defold project with bob --variant headless, runs the engine, and asserts the row round-tripped into the client cache. This is the only coverage for stdb/init.lua, and it immediately earned its keep by catching two engine-only bugs (binary-vs-text frames, and require("websocket") failing at build time).

What happened in CI

Five runs, each failing while unit gates stayed green. Findings, in order:

  1. Runner is containerised with a mounted docker socket (in-container: yes, client 29.5.x / server 29.4.3). Sibling containers publish ports on the host, so 127.0.0.1:3000 from the job is a dead end. Fixed by probing the container bridge IP, then by attaching the server to the job's own netns via --network container:$(hostname) (the job's hostname is its container id).
  2. issues: write was missing, so the self-report curl was rejected and swallowed by its || true. Fixed.
  3. Gating on a step output hid the real failure — when the diagnostic step aborted, conformance was skipped and the job failed with no log. Made unconditional.
  4. Suspected bubblewrap: defold-run was a buildFHSEnv, which needs user namespaces that unprivileged job containers usually deny. Replaced with defold-patch (patchelf the interpreter + rpath — no namespaces). Still failed.

Run #10 spent ~5 minutes before failing, so it got well past setup into the nix/bob work, then died without the reporter landing a comment.

Why it is now manual

This Forgejo build serves no job logs over the API (/actions/runs/{id}/jobs, /logs all 404), and the issue-comment channel only lands intermittently. Iterating blind at ~5 min per attempt against an environment I cannot inspect is a bad trade, and a permanently red job on every push is noise.

Conformance moved to .forgejo/workflows/conformance.yml: workflow_dispatch + weekly schedule, continue-on-error, and it reports outcome to this issue on every run (if: always()), including a topology block with userns count, whether unshare --user works, and free disk.

To close this issue

Trigger the workflow manually once and read the comment it posts. The topology block should say which of these it is:

  • unshare works: no → the runner denies user namespaces; nix builds needing sandboxing will struggle. Run the runner with --privileged, or give it a pre-built nix store.
  • low free disk → the run pulls a JDK, bob FHS env, node and the Defold engine; a small runner volume will fill.
  • something else entirely → the tail will finally say so.

Alternatively, accept it as a local pre-release gate. It is one command and it is documented in the README.

## Status: works locally, unresolved on this runner — now manual The engine conformance test is **done and passing**. From cold, in ~15s: ``` nix develop .#conformance --command bash tools/conformance.sh ... DEBUG:SCRIPT: SMOKE: PASS cached row id=1 sender=headless [conformance] PASS ``` It starts a real `clockworklabs/spacetime:v2.8.2`, publishes the chat module, builds the Defold project with `bob --variant headless`, runs the engine, and asserts the row round-tripped into the client cache. This is the only coverage for `stdb/init.lua`, and it immediately earned its keep by catching two engine-only bugs (binary-vs-text frames, and `require("websocket")` failing at build time). ## What happened in CI Five runs, each failing while unit gates stayed green. Findings, in order: 1. **Runner is containerised** with a mounted docker socket (`in-container: yes`, client 29.5.x / server 29.4.3). Sibling containers publish ports on the *host*, so `127.0.0.1:3000` from the job is a dead end. Fixed by probing the container bridge IP, then by attaching the server to the job's own netns via `--network container:$(hostname)` (the job's hostname is its container id). 2. **`issues: write` was missing**, so the self-report curl was rejected and swallowed by its `|| true`. Fixed. 3. **Gating on a step output hid the real failure** — when the diagnostic step aborted, conformance was skipped and the job failed with no log. Made unconditional. 4. **Suspected bubblewrap**: `defold-run` was a `buildFHSEnv`, which needs user namespaces that unprivileged job containers usually deny. Replaced with `defold-patch` (patchelf the interpreter + rpath — no namespaces). Still failed. Run #10 spent ~5 minutes before failing, so it got well past setup into the nix/bob work, then died without the reporter landing a comment. ## Why it is now manual This Forgejo build serves no job logs over the API (`/actions/runs/{id}/jobs`, `/logs` all 404), and the issue-comment channel only lands intermittently. Iterating blind at ~5 min per attempt against an environment I cannot inspect is a bad trade, and a permanently red job on every push is noise. Conformance moved to `.forgejo/workflows/conformance.yml`: `workflow_dispatch` + weekly schedule, `continue-on-error`, and it reports outcome to this issue on **every** run (`if: always()`), including a topology block with `userns` count, whether `unshare --user` works, and free disk. ## To close this issue Trigger the workflow manually once and read the comment it posts. The topology block should say which of these it is: - `unshare works: no` → the runner denies user namespaces; nix builds needing sandboxing will struggle. Run the runner with `--privileged`, or give it a pre-built nix store. - low `free disk` → the run pulls a JDK, bob FHS env, node and the Defold engine; a small runner volume will fill. - something else entirely → the tail will finally say so. Alternatively, accept it as a local pre-release gate. It is one command and it is documented in the README.
yuris changed title from Conformance CI: live-server job (spacetime start + 2 headless clients) to Conformance in CI: works locally, blocked on runner environment 2026-08-19 08:34:01 +00:00
Author
Owner

Runner config obtained — one hypothesis is dead, and the blocker is still observability

The runner is a code.forgejo.org/forgejo/runner:12 container on the orchestrator with the host docker socket mounted, spawning job containers as siblings on the host daemon:

labels:
  - ubuntu-latest:docker://catthehacker/ubuntu:act-22.04
container:
  docker_host: unix:///var/run/docker.sock
  options: --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add SYS_ADMIN

Finding 4 was wrong: user namespaces are not the problem

Those three options are exactly what bubblewrap needs. Reproduced locally with the same flags:

$ docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined \
    --cap-add SYS_ADMIN ubuntu:22.04 sh -c 'unshare --user true && echo YES'
max_user_namespaces: 111389
unshare --user: YES
unshare -Urm: YES

So the defold-run → defold-patch switch was not the fix it looked like (harmless, and still the better default), and unshare works: no is unlikely to be what the topology block reports. Caveat: max_user_namespaces is a host sysctl, so a the orchestrator worker with it set to 0 would still fail — the topology step prints it, which is why the log matters.

What is actually expensive: no persisted /nix

catthehacker/ubuntu:act-22.04 ships no nix, so every run installs it and then downloads the whole conformance closure — JDK 25, bob's FHS env, the Defold engine, node, esbuild, and now chromium. Roughly 2 GB, per run, discarded at the end. That matches run #10 burning ~5 minutes before dying, and makes disk exhaustion a live suspect on a small the orchestrator volume.

Nix is not the missing piece here: this pipeline already uses it, and that determinism is why the build got as far as it did. What broke is host and topology properties — capabilities, kernel sysctls, docker socket semantics, disk — and no flake pins those.

Fixed now: logs land somewhere readable

The reason five runs taught us so little is that this Forgejo serves no job logs over the API. The generic package registry is a different code path and does work — verified against this instance:

PUT /api/packages/yuris/generic/conformance-logs/probe/probe.log  ->  201
GET same URL -> content returned verbatim

conformance.yml now publishes topology.log and the full conformance.log there on if: always(), versioned by run number and attempt, and the issue comment links to it. Also fixed: the comment body was escaped with hand-rolled sed, which mangles any log containing a quote or backslash — that is most of them. It uses jq -Rs now, falling back to python3.

Two smaller things:

  • Install nix probed command -v nix, which is the wrong test once /nix is a persisted volume: the store is warm but the per-user profile is not, so it would reinstall on top of a perfectly good store. It now also checks /nix/var/nix/profiles/default/bin/nix.
  • The topology block reports /nix mounted: and free space on /nix separately, so a warm-store change can be confirmed rather than assumed.
  • tools/conformance.sh calls docker, but the flake never provided it — it worked locally only because the workstation has a CLI on PATH. pkgs.docker-client is in the .#conformance shell now, so the job no longer depends on the image shipping one. Verified: native conformance passes end to end through the new shell, both runs, identity persisted.

To actually close this

Runner-side changes are needed and they are not in this repo; filed as #18. The next manual run should now produce a readable log either way.

## Runner config obtained — one hypothesis is dead, and the blocker is still observability The runner is a `code.forgejo.org/forgejo/runner:12` container on the orchestrator with the host docker socket mounted, spawning job containers as **siblings** on the host daemon: ```yaml labels: - ubuntu-latest:docker://catthehacker/ubuntu:act-22.04 container: docker_host: unix:///var/run/docker.sock options: --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add SYS_ADMIN ``` ### Finding 4 was wrong: user namespaces are not the problem Those three options are exactly what bubblewrap needs. Reproduced locally with the same flags: ``` $ docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined \ --cap-add SYS_ADMIN ubuntu:22.04 sh -c 'unshare --user true && echo YES' max_user_namespaces: 111389 unshare --user: YES unshare -Urm: YES ``` So the `defold-run` → `defold-patch` switch was not the fix it looked like (harmless, and still the better default), and `unshare works: no` is unlikely to be what the topology block reports. Caveat: `max_user_namespaces` is a host sysctl, so a the orchestrator worker with it set to 0 would still fail — the topology step prints it, which is why the log matters. ### What is actually expensive: no persisted /nix `catthehacker/ubuntu:act-22.04` ships no nix, so every run installs it and then downloads the whole conformance closure — JDK 25, bob's FHS env, the Defold engine, node, esbuild, and now chromium. Roughly 2 GB, per run, discarded at the end. That matches run #10 burning ~5 minutes before dying, and makes disk exhaustion a live suspect on a small the orchestrator volume. Nix is not the missing piece here: **this pipeline already uses it**, and that determinism is why the build got as far as it did. What broke is host and topology properties — capabilities, kernel sysctls, docker socket semantics, disk — and no flake pins those. ### Fixed now: logs land somewhere readable The reason five runs taught us so little is that this Forgejo serves no job logs over the API. The **generic package registry** is a different code path and does work — verified against this instance: ``` PUT /api/packages/yuris/generic/conformance-logs/probe/probe.log -> 201 GET same URL -> content returned verbatim ``` `conformance.yml` now publishes `topology.log` and the full `conformance.log` there on `if: always()`, versioned by run number and attempt, and the issue comment links to it. Also fixed: the comment body was escaped with hand-rolled `sed`, which mangles any log containing a quote or backslash — that is most of them. It uses `jq -Rs` now, falling back to python3. Two smaller things: - `Install nix` probed `command -v nix`, which is the wrong test once /nix is a persisted volume: the store is warm but the per-user profile is not, so it would reinstall on top of a perfectly good store. It now also checks `/nix/var/nix/profiles/default/bin/nix`. - The topology block reports `/nix mounted:` and free space on `/nix` separately, so a warm-store change can be confirmed rather than assumed. - `tools/conformance.sh` calls `docker`, but the flake never provided it — it worked locally only because the workstation has a CLI on PATH. `pkgs.docker-client` is in the `.#conformance` shell now, so the job no longer depends on the image shipping one. Verified: native conformance passes end to end through the new shell, both runs, identity persisted. ### To actually close this Runner-side changes are needed and they are not in this repo; filed as #18. The next manual run should now produce a readable log either way.
Author
Owner

Docker is gone from conformance, which removes most of this issue's surface

Asked whether the test needs docker at all. It did not. It used it for three things — running the server, running the spacetime CLI to publish, and docker restart to force the reconnect phase — and all three collapse into a pinned binary.

nix/spacetime.nix packages the upstream 2.8.2 release tarball: two files needing only glibc, libstdc++ and zlib, so autoPatchelf is the entire derivation. The tests now need nothing but nix.

What that deletes from this investigation

Findings 1 and 4 are simply no longer reachable:

  • No docker socket, no sibling containers. The bridge-IP probing and --network container:$(hostname) namespace sharing are deleted. 127.0.0.1 always means this machine.
  • No user namespaces on the critical path. Already shown not to be the problem, now not involved either way.

What is left that can fail on the runner: nix setup cost and disk. That is #18.

Why not nixpkgs' spacetimedb

Checked both channels, since it would have been the lazier option:

Source Version Cached?
nixos-26.05 2.2.0 no
nixpkgs-unstable 2.7.0-hotfix1 no
pinned here 2.8.2 —

Both trail the version the fixtures were captured from, and meta.license.free = false (BSL 1.1) means Hydra never builds it, so it is structurally absent from cache.nixos.org rather than incidentally missing. Using it would mean compiling SpacetimeDB and librusty_v8 on every cold runner in order to arrive at the wrong server version.

Measured

Native tier, cold: 31s → 10s. HTML5 tier: 5s. Both pass:

[conformance] PASS (identity 0xc200d7a1…dfb0 persisted across a process restart)
[html5] SMOKE: PASS procedure round trip; failure surfaced as an error

Bugs found while doing it

  • yes y | spacetime publish reported a successful publish as a failure. Under set -o pipefail the pipeline returns yes's SIGPIPE (141). It only ever worked because the pipeline used to run inside docker run sh -c, where pipefail was not set. Uses the CLI's own --yes now.
  • An orphaned server produced a baffling 403. A ready port is not proof that our server is answering: if the port is already held, the child exits instantly while the stray keeps replying 200, and publish then targets a database owned by a different identity. start_server now checks its own child is alive and says so plainly.
  • The HTML5 harness swallowed a failed restart. It stopped the server, failed to start it, and let the client retry for 90s — so a broken PATH looked like a reconnect bug. Failures are loud now.

Side effect worth knowing

JWT signing keys are generated once and kept, so an identity token now survives a server restart. Every fresh docker container signed with new keys, which is what made cached tokens come back as mystery 401s.

tools/dev-server.sh grew stop for clean teardown; the server is still deliberately left running between runs, the way the container was.

## Docker is gone from conformance, which removes most of this issue's surface Asked whether the test needs docker at all. It did not. It used it for three things — running the server, running the `spacetime` CLI to publish, and `docker restart` to force the reconnect phase — and all three collapse into a pinned binary. `nix/spacetime.nix` packages the upstream 2.8.2 release tarball: two files needing only glibc, libstdc++ and zlib, so autoPatchelf is the entire derivation. The tests now need nothing but nix. ### What that deletes from this investigation Findings 1 and 4 are simply no longer reachable: - **No docker socket, no sibling containers.** The bridge-IP probing and `--network container:$(hostname)` namespace sharing are deleted. `127.0.0.1` always means this machine. - **No user namespaces on the critical path.** Already shown not to be the problem, now not involved either way. What is left that can fail on the runner: nix setup cost and disk. That is #18. ### Why not nixpkgs' spacetimedb Checked both channels, since it would have been the lazier option: | Source | Version | Cached? | |---|---|---| | `nixos-26.05` | 2.2.0 | no | | `nixpkgs-unstable` | 2.7.0-hotfix1 | no | | pinned here | **2.8.2** | — | Both trail the version the fixtures were captured from, and `meta.license.free = false` (BSL 1.1) means Hydra never builds it, so it is *structurally* absent from `cache.nixos.org` rather than incidentally missing. Using it would mean compiling SpacetimeDB and librusty_v8 on every cold runner in order to arrive at the wrong server version. ### Measured Native tier, cold: **31s → 10s**. HTML5 tier: 5s. Both pass: ``` [conformance] PASS (identity 0xc200d7a1…dfb0 persisted across a process restart) [html5] SMOKE: PASS procedure round trip; failure surfaced as an error ``` ### Bugs found while doing it - **`yes y | spacetime publish` reported a successful publish as a failure.** Under `set -o pipefail` the pipeline returns `yes`'s SIGPIPE (141). It only ever worked because the pipeline used to run inside `docker run sh -c`, where pipefail was not set. Uses the CLI's own `--yes` now. - **An orphaned server produced a baffling 403.** A ready port is not proof that *our* server is answering: if the port is already held, the child exits instantly while the stray keeps replying 200, and publish then targets a database owned by a different identity. `start_server` now checks its own child is alive and says so plainly. - **The HTML5 harness swallowed a failed restart.** It stopped the server, failed to start it, and let the client retry for 90s — so a broken PATH looked like a reconnect bug. Failures are loud now. ### Side effect worth knowing JWT signing keys are generated once and kept, so an identity token now survives a server restart. Every fresh docker container signed with new keys, which is what made cached tokens come back as mystery 401s. `tools/dev-server.sh` grew `stop` for clean teardown; the server is still deliberately left running between runs, the way the container was.

Conformance run #25: failure (a2cdbb23c5)

Full log: https://git.nokopia.com/api/packages/yuris/generic/conformance-logs/run-25-1/conformance.log

Topology:

in-container: yes
hostname: 
cpus: 4
kernel: 6.12.95+deb13-amd64
/nix mounted: 
free disk /: overlay          74G   31G   40G  44% /
free disk /nix: /dev/sda1        74G   31G   40G  44% /nix

Tail:

building '/nix/store/px8hj7awj6ffpcamsaf95ywrmnn8n6aq-nodejs-24.19.0.drv'...
building '/nix/store/19aslsfj0rd38lxx9s27cn8jbmi36aq7-spacetime-x86_64-unknown-linux-gnu.tar.gz.drv'...
building '/nix/store/xxp6q54qv9h4s0rj01aqdsza8yifk6by-bob-fhsenv-rootfs.drv'...
building '/nix/store/4pdb4ygw35iiiik9sajriz7zr0cb8v4z-bob-run.drv'...
building '/nix/store/dbjc7m9hif2dd2kyxydydydxiafrq0l3-defold-run-bwrap.drv'...
building '/nix/store/9c6fxh4pc12sp0ndk6bidn4g39si9n8b-bob-init.drv'...
building '/nix/store/gs5qmm4s8igp91xj6l1rhpbn38z1za0j-defold-run.drv'...
building '/nix/store/m3a2ml31jlz5jn92dvbaa7mkyxmnwp14-bob-bwrap.drv'...
building '/nix/store/22q266vibkgx6sswcq2yiwaw4ql5hxrv-spacetimedb-bin-2.8.2.drv'...
building '/nix/store/c2h8gdmvjzxkw8grlkxnnmyaxi1bxqvh-bob.drv'...
building '/nix/store/s5lsqy5gmhrpvb2wdfx9mwjcfgap67m6-nix-shell-env.drv'...
this path will be fetched (107.5 KiB download, 107.4 KiB unpacked):
  /nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man
copying path '/nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man' from 'https://cache.nixos.org'...
[dev-server] starting spacetimedb-standalone 2.8.2 at http://127.0.0.1:3000
[dev-server] generated JWT signing keys in /tmp/spacetimedb-defold/keys
[dev-server] minted identity

  dist/module.js  254.5kb

⚡ Done in 37ms
[dev-server] bundled module (260625 bytes)
(JS) Skipping build. Instead we are publishing dist/module.js
Uploading to http://127.0.0.1:3000 => http://127.0.0.1:3000
This will DESTROY the current chat module, and ALL corresponding data.
Skipping confirmation due to --yes
Publishing module...
Created new database with name: chat, identity: c200ab0cad4cca9eda0170c283b778d8f931b47fe25df5418665c9964f6e7138
[dev-server] regenerated test/fixtures/chat_schema.lua
[conformance] server up; engine will dial ws://127.0.0.1:3000/v1/database/chat/subscribe
[conformance] building headless bundle
 33% Working
100% Working
2026-08-19 13:27:07 SEVERE  Missing libraries folder. You need to run the 'resolve' command first! 
2026-08-19 13:27:07 SEVERE  com.dynamo.bob.Project.mount(Project.java:759) 
2026-08-19 13:27:07 SEVERE  com.dynamo.bob.Bob.setupProject(Bob.java:683) 
2026-08-19 13:27:07 SEVERE  com.dynamo.bob.Bob.invoke(Bob.java:858) 
2026-08-19 13:27:07 SEVERE  com.dynamo.bob.Bob.main(Bob.java:1065) 
2026-08-19 13:27:07 SEVERE  Missing libraries folder. You need to run the 'resolve' command first! 
[conformance] bob build failed
Conformance run #25: **failure** (a2cdbb23c5eb9d8fdfce810e8becd330017e60e1) Full log: https://git.nokopia.com/api/packages/yuris/generic/conformance-logs/run-25-1/conformance.log Topology: ``` in-container: yes hostname: cpus: 4 kernel: 6.12.95+deb13-amd64 /nix mounted: free disk /: overlay 74G 31G 40G 44% / free disk /nix: /dev/sda1 74G 31G 40G 44% /nix ``` Tail: ``` building '/nix/store/px8hj7awj6ffpcamsaf95ywrmnn8n6aq-nodejs-24.19.0.drv'... building '/nix/store/19aslsfj0rd38lxx9s27cn8jbmi36aq7-spacetime-x86_64-unknown-linux-gnu.tar.gz.drv'... building '/nix/store/xxp6q54qv9h4s0rj01aqdsza8yifk6by-bob-fhsenv-rootfs.drv'... building '/nix/store/4pdb4ygw35iiiik9sajriz7zr0cb8v4z-bob-run.drv'... building '/nix/store/dbjc7m9hif2dd2kyxydydydxiafrq0l3-defold-run-bwrap.drv'... building '/nix/store/9c6fxh4pc12sp0ndk6bidn4g39si9n8b-bob-init.drv'... building '/nix/store/gs5qmm4s8igp91xj6l1rhpbn38z1za0j-defold-run.drv'... building '/nix/store/m3a2ml31jlz5jn92dvbaa7mkyxmnwp14-bob-bwrap.drv'... building '/nix/store/22q266vibkgx6sswcq2yiwaw4ql5hxrv-spacetimedb-bin-2.8.2.drv'... building '/nix/store/c2h8gdmvjzxkw8grlkxnnmyaxi1bxqvh-bob.drv'... building '/nix/store/s5lsqy5gmhrpvb2wdfx9mwjcfgap67m6-nix-shell-env.drv'... this path will be fetched (107.5 KiB download, 107.4 KiB unpacked): /nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man copying path '/nix/store/mrn1i6hs3jaxj5h0ab5bi0jdqaa004nn-bash-interactive-5.3p15-man' from 'https://cache.nixos.org'... [dev-server] starting spacetimedb-standalone 2.8.2 at http://127.0.0.1:3000 [dev-server] generated JWT signing keys in /tmp/spacetimedb-defold/keys [dev-server] minted identity dist/module.js 254.5kb ⚡ Done in 37ms [dev-server] bundled module (260625 bytes) (JS) Skipping build. Instead we are publishing dist/module.js Uploading to http://127.0.0.1:3000 => http://127.0.0.1:3000 This will DESTROY the current chat module, and ALL corresponding data. Skipping confirmation due to --yes Publishing module... Created new database with name: chat, identity: c200ab0cad4cca9eda0170c283b778d8f931b47fe25df5418665c9964f6e7138 [dev-server] regenerated test/fixtures/chat_schema.lua [conformance] server up; engine will dial ws://127.0.0.1:3000/v1/database/chat/subscribe [conformance] building headless bundle 33% Working 100% Working 2026-08-19 13:27:07 SEVERE Missing libraries folder. You need to run the 'resolve' command first! 2026-08-19 13:27:07 SEVERE com.dynamo.bob.Project.mount(Project.java:759) 2026-08-19 13:27:07 SEVERE com.dynamo.bob.Bob.setupProject(Bob.java:683) 2026-08-19 13:27:07 SEVERE com.dynamo.bob.Bob.invoke(Bob.java:858) 2026-08-19 13:27:07 SEVERE com.dynamo.bob.Bob.main(Bob.java:1065) 2026-08-19 13:27:07 SEVERE Missing libraries folder. You need to run the 'resolve' command first! [conformance] bob build failed ```

Conformance run #28: success (a9ba07efbb)

Full log: https://git.nokopia.com/api/packages/yuris/generic/conformance-logs/run-28-1/conformance.log

Topology:

in-container: yes
hostname: 56a899588581
cpus: 4
kernel: 6.12.95+deb13-amd64
/nix mounted: /dev/sda1 /nix ext4 rw,relatime,discard,errors=remount-ro 0 0
free disk /: overlay          74G   36G   35G  51% /
free disk /nix: /dev/sda1        74G   36G   35G  51% /nix
nix store paths: 5498

Tail:

evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11'
evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11'
[dev-server] starting spacetimedb-standalone 2.8.2 at http://127.0.0.1:3000
[dev-server] generated JWT signing keys in /tmp/spacetimedb-defold/keys
[dev-server] minted identity

  dist/module.js  254.5kb

⚡ Done in 31ms
[dev-server] bundled module (260625 bytes)
(JS) Skipping build. Instead we are publishing dist/module.js
Uploading to http://127.0.0.1:3000 => http://127.0.0.1:3000
This will DESTROY the current chat module, and ALL corresponding data.
Skipping confirmation due to --yes
Publishing module...
Created new database with name: chat, identity: c2003f150c3f39d0a3581bf996a7cb8912ee4587112790a45745d5d490ece80e
[dev-server] regenerated test/fixtures/chat_schema.lua
[conformance] server up; engine will dial ws://127.0.0.1:3000/v1/database/chat/subscribe
[conformance] resolving dependencies and building headless bundle
[conformance] running headless client
[conformance] phase 1 passed — restarting the server to force a reconnect
[dev-server] restarting server (data preserved)
[dev-server] server back up at http://127.0.0.1:3000
DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe
DEBUG:SCRIPT: SMOKE: identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1
DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1
DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s)
DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored
DEBUG:SCRIPT: SMOKE: query set 1 subscribed
DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained
DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold"
DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error
[conformance] run 1 PASS (identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1)
[conformance] running headless client again without a token (identity persistence)
[dev-server] restarting server (data preserved)
[dev-server] server back up at http://127.0.0.1:3000
DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe
DEBUG:SCRIPT: SMOKE: identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1
DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1
DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s)
DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored
DEBUG:SCRIPT: SMOKE: query set 1 subscribed
DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained
DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold"
DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error
[conformance] PASS (identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1 persisted across a process restart)
Conformance run #28: **success** (a9ba07efbb588008ae72a20fdbe72a9256adff82) Full log: https://git.nokopia.com/api/packages/yuris/generic/conformance-logs/run-28-1/conformance.log Topology: ``` in-container: yes hostname: 56a899588581 cpus: 4 kernel: 6.12.95+deb13-amd64 /nix mounted: /dev/sda1 /nix ext4 rw,relatime,discard,errors=remount-ro 0 0 free disk /: overlay 74G 36G 35G 51% / free disk /nix: /dev/sda1 74G 36G 35G 51% /nix nix store paths: 5498 ``` Tail: ``` evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11' evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11' [dev-server] starting spacetimedb-standalone 2.8.2 at http://127.0.0.1:3000 [dev-server] generated JWT signing keys in /tmp/spacetimedb-defold/keys [dev-server] minted identity dist/module.js 254.5kb ⚡ Done in 31ms [dev-server] bundled module (260625 bytes) (JS) Skipping build. Instead we are publishing dist/module.js Uploading to http://127.0.0.1:3000 => http://127.0.0.1:3000 This will DESTROY the current chat module, and ALL corresponding data. Skipping confirmation due to --yes Publishing module... Created new database with name: chat, identity: c2003f150c3f39d0a3581bf996a7cb8912ee4587112790a45745d5d490ece80e [dev-server] regenerated test/fixtures/chat_schema.lua [conformance] server up; engine will dial ws://127.0.0.1:3000/v1/database/chat/subscribe [conformance] resolving dependencies and building headless bundle [conformance] running headless client [conformance] phase 1 passed — restarting the server to force a reconnect [dev-server] restarting server (data preserved) [dev-server] server back up at http://127.0.0.1:3000 DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe DEBUG:SCRIPT: SMOKE: identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1 DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1 DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s) DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored DEBUG:SCRIPT: SMOKE: query set 1 subscribed DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold" DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error [conformance] run 1 PASS (identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1) [conformance] running headless client again without a token (identity persistence) [dev-server] restarting server (data preserved) [dev-server] server back up at http://127.0.0.1:3000 DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe DEBUG:SCRIPT: SMOKE: identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1 DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1 DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s) DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored DEBUG:SCRIPT: SMOKE: query set 1 subscribed DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold" DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error [conformance] PASS (identity 0xc2000411acab105e62855e228fcf84c54c6d7890f39ea84f359bdd740bc9d5e1 persisted across a process restart) ```

Conformance run #29: success (640ce03dd0)

Logs:

upload of topology.log rejected (needs a user-scoped token)
upload of conformance.log rejected (needs a user-scoped token)

Topology:

in-container: yes
hostname: d6e3e58c28d9
cpus: 4
kernel: 6.12.95+deb13-amd64
/nix mounted: /dev/sda1 /nix ext4 rw,relatime,discard,errors=remount-ro 0 0
free disk /: overlay          74G   36G   35G  51% /
free disk /nix: /dev/sda1        74G   36G   35G  51% /nix
nix store paths: 5498

Tail:

evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11'
evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11'
[dev-server] starting spacetimedb-standalone 2.8.2 at http://127.0.0.1:3000
[dev-server] generated JWT signing keys in /tmp/spacetimedb-defold/keys
[dev-server] minted identity

  dist/module.js  254.5kb

⚡ Done in 33ms
[dev-server] bundled module (260625 bytes)
(JS) Skipping build. Instead we are publishing dist/module.js
Uploading to http://127.0.0.1:3000 => http://127.0.0.1:3000
This will DESTROY the current chat module, and ALL corresponding data.
Skipping confirmation due to --yes
Publishing module...
Created new database with name: chat, identity: c20030f9152dc276dcf11aa79ab1a6453f9a97ace592c7a3ec7c9b8b433e38ec
[dev-server] regenerated test/fixtures/chat_schema.lua
[conformance] server up; engine will dial ws://127.0.0.1:3000/v1/database/chat/subscribe
[conformance] resolving dependencies and building headless bundle
[conformance] running headless client
[conformance] phase 1 passed — restarting the server to force a reconnect
[dev-server] restarting server (data preserved)
[dev-server] server back up at http://127.0.0.1:3000
DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe
DEBUG:SCRIPT: SMOKE: identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022
DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1
DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s)
DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored
DEBUG:SCRIPT: SMOKE: query set 1 subscribed
DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained
DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold"
DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error
[conformance] run 1 PASS (identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022)
[conformance] running headless client again without a token (identity persistence)
[dev-server] restarting server (data preserved)
[dev-server] server back up at http://127.0.0.1:3000
DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe
DEBUG:SCRIPT: SMOKE: identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022
DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1
DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s
DEBUG:SCRIPT: SMOKE: server dropped us (code 0)
DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s)
DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored
DEBUG:SCRIPT: SMOKE: query set 1 subscribed
DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained
DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold"
DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error
[conformance] PASS (identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022 persisted across a process restart)
Conformance run #29: **success** (640ce03dd048edaab08480c40e8fa4420cb944a1) Logs: ``` upload of topology.log rejected (needs a user-scoped token) upload of conformance.log rejected (needs a user-scoped token) ``` Topology: ``` in-container: yes hostname: d6e3e58c28d9 cpus: 4 kernel: 6.12.95+deb13-amd64 /nix mounted: /dev/sda1 /nix ext4 rw,relatime,discard,errors=remount-ro 0 0 free disk /: overlay 74G 36G 35G 51% / free disk /nix: /dev/sda1 74G 36G 35G 51% /nix nix store paths: 5498 ``` Tail: ``` evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11' evaluation warning: The xorg package set has been deprecated, 'xorg.libX11' has been renamed to 'libx11' [dev-server] starting spacetimedb-standalone 2.8.2 at http://127.0.0.1:3000 [dev-server] generated JWT signing keys in /tmp/spacetimedb-defold/keys [dev-server] minted identity dist/module.js 254.5kb ⚡ Done in 33ms [dev-server] bundled module (260625 bytes) (JS) Skipping build. Instead we are publishing dist/module.js Uploading to http://127.0.0.1:3000 => http://127.0.0.1:3000 This will DESTROY the current chat module, and ALL corresponding data. Skipping confirmation due to --yes Publishing module... Created new database with name: chat, identity: c20030f9152dc276dcf11aa79ab1a6453f9a97ace592c7a3ec7c9b8b433e38ec [dev-server] regenerated test/fixtures/chat_schema.lua [conformance] server up; engine will dial ws://127.0.0.1:3000/v1/database/chat/subscribe [conformance] resolving dependencies and building headless bundle [conformance] running headless client [conformance] phase 1 passed — restarting the server to force a reconnect [dev-server] restarting server (data preserved) [dev-server] server back up at http://127.0.0.1:3000 DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe DEBUG:SCRIPT: SMOKE: identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022 DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1 DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s) DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored DEBUG:SCRIPT: SMOKE: query set 1 subscribed DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold" DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error [conformance] run 1 PASS (identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022) [conformance] running headless client again without a token (identity persistence) [dev-server] restarting server (data preserved) [dev-server] server back up at http://127.0.0.1:3000 DEBUG:SCRIPT: SMOKE: connecting to ws://127.0.0.1:3000/v1/database/chat/subscribe DEBUG:SCRIPT: SMOKE: identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022 DEBUG:SCRIPT: SMOKE: PHASE1 OK cached row id=1 DEBUG:SCRIPT: SMOKE: reconnecting attempt 1 in 1s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 2 in 2s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 3 in 4s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnecting attempt 4 in 5s DEBUG:SCRIPT: SMOKE: server dropped us (code 0) DEBUG:SCRIPT: SMOKE: reconnected, replayed 1 subscription(s) DEBUG:SCRIPT: SMOKE: PHASE2 OK survived restart; row id=1 restored DEBUG:SCRIPT: SMOKE: query set 1 subscribed DEBUG:SCRIPT: SMOKE: PHASE3 OK unsubscribed query set 1; cache drained DEBUG:SCRIPT: SMOKE: procedure returned "echo: from defold" DEBUG:SCRIPT: SMOKE: PASS procedure round trip; failure surfaced as an error [conformance] PASS (identity 0xc2003e60e445db3319697d225821d8dfe660c864b9a9ac36cbfc03af4a69a022 persisted across a process restart) ```
Author
Owner

Green. Closing.

Conformance run #29: **success** (640ce03)

/nix mounted: /dev/sda1 /nix ext4 rw,relatime,discard,errors=remount-ro
nix store paths: 5498

[conformance] PASS (identity 0xc2003e60…a022 persisted across a process restart)

Runs #28 and #29 both pass on the nix-latest runner, #29 in ~100s with a warm store. The full four-phase round trip runs in CI: connect, subscribe, reducer, server restart, subscription replay, query sets, procedures, and identity persistence across a process restart.

What was actually wrong

Ten runs of failure, and none of it was the original hypothesis. In order of discovery:

  1. The runner was containerised with a mounted docker socket, so a published port landed on the host and 127.0.0.1:3000 was a dead end. Fixed by bridge-IP probing, then netns sharing — then deleted entirely, because docker itself turned out to be unnecessary (the server and CLI are pinned release binaries now).
  2. issues: write was missing, so the self-report was swallowed.
  3. Gating on a step output hid the real failure.
  4. User namespaces were never the problem. --cap-add SYS_ADMIN with unconfined seccomp and apparmor is exactly what bubblewrap needs; reproducing those flags locally gives unshare --user → yes. The defold-run → defold-patch switch was not the fix it appeared to be.
  5. No log channel. This Forgejo serves no job logs over the API on 15.0.1 either — /actions/runs/*/jobs, /logs, /artifacts all 404. The self-report comment now carries a 120-line tail plus a topology block, and that is what finally made the next three failures diagnosable in one attempt each. The package-registry upload is rejected under the job token (generic packages are owner-scoped, the token is repo-scoped) and the comment now says so instead of linking a 404; wire CONFORMANCE_LOG_TOKEN if the full log is ever wanted.
  6. defold-patch omitted libstdc++ from the rpath. Invisible locally because the workstation's LD_LIBRARY_PATH happened to carry a gcc-lib path; env -i ldd said not found. Surfaced by running the suite in a minimal nix container.
  7. The runner image mattered more than the runner. ghcr.io/joschi/forgejo-nix sets ENTRYPOINT ["/bin/bash"], so the keep-alive command became /bin/bash tail -f /dev/null and the container died before any step ran — 88s, no logs. Official nixos/nix leaves Entrypoint null. It ships no node, so actions/checkout@v4 was replaced with a plain git clone; the workflow now needs only git and nix.
  8. valid_volumes defaults to [], which silently discards every bind mount (hostConfig.Binds = []string{} in sanitizeConfig). The --volume forgejo_nix_store:/nix was dropped without failing anything — it would have cost the entire speedup while looking like it worked.
  9. bob needs resolve before build. .internal/lib is gitignored, so a workstation always has it from an earlier build and a fresh checkout never does. Verified by deleting .internal and dist locally and running from that state.

Three of those nine (6, 8, 9) were only observable outside a developer machine. That is the actual value of this tier.

Runner configuration that works

On runner-node-b only, keeping the ubuntu labels so general capacity is unchanged:

runner:
  labels:
    - ubuntu-latest:docker://catthehacker/ubuntu:act-22.04
    - ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04
    - nix-latest:docker://nixos/nix@sha256:7a007c766426c1877758ddc5cb87a965ac131fc78c582ce0083d922d51ae945c
container:
  docker_host: unix:///var/run/docker.sock
  valid_volumes:
    - forgejo_nix_store
  options: >-
    --security-opt seccomp=unconfined --security-opt apparmor=unconfined
    --cap-add SYS_ADMIN --volume forgejo_nix_store:/nix

The label is confined to one runner deliberately: docker volumes are node-local, so the warm store only exists on the node that built it.

Remaining work is tracked in #18 (runner notes), #16 (HTML5 tier in CI) and #15 (upstream extension fix).

## Green. Closing. ``` Conformance run #29: **success** (640ce03) /nix mounted: /dev/sda1 /nix ext4 rw,relatime,discard,errors=remount-ro nix store paths: 5498 [conformance] PASS (identity 0xc2003e60…a022 persisted across a process restart) ``` Runs #28 and #29 both pass on the `nix-latest` runner, #29 in ~100s with a warm store. The full four-phase round trip runs in CI: connect, subscribe, reducer, server restart, subscription replay, query sets, procedures, and identity persistence across a process restart. ### What was actually wrong Ten runs of failure, and none of it was the original hypothesis. In order of discovery: 1. **The runner was containerised with a mounted docker socket**, so a published port landed on the host and `127.0.0.1:3000` was a dead end. Fixed by bridge-IP probing, then netns sharing — then deleted entirely, because docker itself turned out to be unnecessary (the server and CLI are pinned release binaries now). 2. **`issues: write` was missing**, so the self-report was swallowed. 3. **Gating on a step output hid the real failure.** 4. **User namespaces were never the problem.** `--cap-add SYS_ADMIN` with unconfined seccomp and apparmor is exactly what bubblewrap needs; reproducing those flags locally gives `unshare --user` → yes. The `defold-run` → `defold-patch` switch was not the fix it appeared to be. 5. **No log channel.** This Forgejo serves no job logs over the API on 15.0.1 either — `/actions/runs/*/jobs`, `/logs`, `/artifacts` all 404. The self-report comment now carries a 120-line tail plus a topology block, and that is what finally made the next three failures diagnosable in one attempt each. The package-registry upload is rejected under the job token (generic packages are owner-scoped, the token is repo-scoped) and the comment now says so instead of linking a 404; wire `CONFORMANCE_LOG_TOKEN` if the full log is ever wanted. 6. **`defold-patch` omitted libstdc++ from the rpath.** Invisible locally because the workstation's `LD_LIBRARY_PATH` happened to carry a gcc-lib path; `env -i ldd` said `not found`. Surfaced by running the suite in a minimal nix container. 7. **The runner image mattered more than the runner.** `ghcr.io/joschi/forgejo-nix` sets `ENTRYPOINT ["/bin/bash"]`, so the keep-alive command became `/bin/bash tail -f /dev/null` and the container died before any step ran — 88s, no logs. Official `nixos/nix` leaves `Entrypoint` null. It ships no node, so `actions/checkout@v4` was replaced with a plain `git clone`; the workflow now needs only git and nix. 8. **`valid_volumes` defaults to `[]`, which silently discards every bind mount** (`hostConfig.Binds = []string{}` in `sanitizeConfig`). The `--volume forgejo_nix_store:/nix` was dropped without failing anything — it would have cost the entire speedup while looking like it worked. 9. **`bob` needs `resolve` before `build`.** `.internal/lib` is gitignored, so a workstation always has it from an earlier build and a fresh checkout never does. Verified by deleting `.internal` and `dist` locally and running from that state. Three of those nine (6, 8, 9) were only observable outside a developer machine. That is the actual value of this tier. ### Runner configuration that works On runner-node-b only, keeping the ubuntu labels so general capacity is unchanged: ```yaml runner: labels: - ubuntu-latest:docker://catthehacker/ubuntu:act-22.04 - ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04 - nix-latest:docker://nixos/nix@sha256:7a007c766426c1877758ddc5cb87a965ac131fc78c582ce0083d922d51ae945c container: docker_host: unix:///var/run/docker.sock valid_volumes: - forgejo_nix_store options: >- --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add SYS_ADMIN --volume forgejo_nix_store:/nix ``` The label is confined to one runner deliberately: docker volumes are node-local, so the warm store only exists on the node that built it. Remaining work is tracked in #18 (runner notes), #16 (HTML5 tier in CI) and #15 (upstream extension fix).
yuris closed this issue 2026-08-19 13:37:19 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/spacetimedb-defold#7
No description provided.