The parallel per-platform builds shared one uv cache mount, so they
queued on uv's cache lock. When uv.lock changed, the emulated armv7
sync held it long enough for the others to hit uv's 300 s lock timeout
and fail publish-images.
Persist uv's cache of source-built wheels (asyncpg, greenlet, psycopg2,
cffi, ...) across CI runs via actions/cache + buildkit-cache-dance, so a
lockfile or base-image change no longer recompiles everything under QEMU
(~11 min -> ~3 min). Move the greenlet patchelf step into the cached
dependency layer so source-only builds skip an apt-get under QEMU, and
drop python3-dev, which only pulled in Debian's unrelated python3.11.
Brings the branch up to 0.25.0 and picks up cosign image signing, the
armv7/arm64 smoke tests, the armv7 greenlet/libstdc++ fix, the
PUID/PGID and Kubernetes SPOOLMAN_PORT entrypoint guards, and the
externaldb unwritable-path fix.
The only conflict was publish-images' needs list, where both sides had
added a job: test-frontend-v2 here, test-multiarch-smoke on master.
Resolved as the union, so nothing publishes until every test job has
passed. The rest of the graph is unchanged, and the new smoke tests
inherit client_v2 coverage for free -- build-arm64 and build-armv7
already download the client_v2 artifact, so booting those images to
/health now also proves the default client is present in them.
Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
greenlet ships no 32-bit ARM wheel, so on armv7 it is compiled from source.
setuptools links the C++ extension with gcc, which leaves libstdc++ out of the
.so's NEEDED list. The extension then fails to import with an undefined
libstdc++ typeinfo symbol (_ZTVN10__cxxabiv120__si_class_type_infoE), which
takes the whole app down during the startup DB migration -- the container
crashloops on any armv7 host such as a 32-bit Raspberry Pi.
Add libstdc++ to the extension's NEEDED list with patchelf so the dynamic
loader pulls it in. amd64 and arm64 use a correctly linked prebuilt wheel and
are untouched.
Verified by patching the shipped 0.24.0 armv7 image's greenlet .so the same
way: `alembic upgrade head` then runs to completion instead of crashing.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Build the new Svelte client (client_v2) in CI, bake it into the Docker
image alongside the legacy React client, and serve it by default. Set
SPOOLMAN_LEGACY_CLIENT=TRUE to fall back to the old client.
SinglePageApplication is generalized to serve either client: the React
client keeps its index.html + base-path rewriting, while the Svelte
client uses a 200.html SPA fallback, serves prerendered per-route pages,
and serves relative-path assets verbatim. The tweaked-HTML store is moved
off self.html, which StaticFiles uses as its HTML-mode flag.
The Playwright frontend suite targets the React DOM, so it's pinned to
the legacy client via SPOOLMAN_LEGACY_CLIENT for now.
Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
Brings in the latest #798 commit (388082f) that our earlier merge missed: it
drops --chown from the builder COPYs (matching the Podman build fix already on
master), removes the redundant `usermod -G users app`, and simplifies the
entrypoint to re-exec itself via gosu when root instead of duplicating the
uvicorn invocation. Dockerfile comment conflict resolved in favour of the
Podman-explaining comment.
Brings in jpiccari's unprivileged-Docker support (rootless entrypoint that only
uses gosu when running as root and only remaps app UID/GID when they differ, plus
dropping the redundant `groupmod -g 1000 users` in the runner stage).
Combines cleanly with the Podman build/test enablement already on master
(builder-stage --chown removal, postgres healthcheck, run.py engine selection),
giving full rootless build + run + test support.
Running tests_integration against rootless Podman surfaced three issues; none
affect the Docker/CI path:
- run.py: add a SPOOLMAN_CONTAINER_ENGINE env var (defaults to "docker") so the
build/compose commands can be driven by podman instead.
- Dockerfile: drop `--chown=app:app` from the builder-stage COPYs. The "app"
user only exists in the runner stage; Docker/BuildKit tolerated the dangling
reference but Podman rejects it. Final ownership is unchanged — it is set when
the files are copied into the runner stage.
- docker-compose-postgres.yml: add a pg_isready healthcheck and gate spoolman on
`condition: service_healthy`, matching the mariadb/cockroachdb compose files.
Without it spoolman raced postgres' initdb and died on connection-refused
before running migrations.
Verified: full 4-DB suite (sqlite/postgres/mariadb/cockroachdb) green via
`SPOOLMAN_CONTAINER_ENGINE=podman uv run poe itest` — 223 tests each.
This change enables the container to run in unprivileged mode (without
--privileged or additional capabilities) by making permission drops
conditional:
- Changed users group GID from 1000 to 1001 to avoid conflicts with
the app user's primary group (UID 1000)
- Only modify user/group IDs when PUID/PGID environment variables differ
from the default value of 1000, avoiding unnecessary privilege
requirements
- Skip su-exec entirely when running as the default user, since changing
user context requires additional capabilities that unprivileged
containers don't have
This allows the container to work in restricted environments like
Kubernetes with security contexts or rootless Docker while maintaining
backward compatibility for users who customize PUID/PGID.
Fixes Donkie#791
This proved to be extremely slow on emulated hardware to build e.g. the linux/arm/v7 image.
Since the built client is architecture agnostic, it's instead now built on the native hardware and inserted into the docker image.
Had to remove the VOLUME definition in the dockerfile for this, otherwise the data dir would always be mounted by an anonymous volume if the user didn't mount it correctly, so the python script wouldn't be able to detect it.