Add a test environment that simulates a blank Debian trixie box (systemd + sshd container) and routes the project's real 📦 Linting CSS + JS (biome)… Checked 71 files in 140ms. No fixes applied. 📦 Building CSS bundles… 🎨 Building CSS bundles… ✓ base.min.css (22,317 bytes) ✓ admin.min.css (55,602 bytes) ✓ form.min.css (41,855 bytes) ✓ public.min.css (4,310 bytes) ✓ tfe.min.css (9,089 bytes) ✓ repertoire.min.css (13,166 bytes) ✓ content-page.min.css (3,683 bytes) ✓ not-found.min.css (672 bytes) ✓ system.min.css (7,408 bytes) ✓ file-access.min.css (3,733 bytes) ✓ form-base.min.css (19,110 bytes) ✓ partage-form.min.css (37,229 bytes) ✅ CSS bundles done — 218,174 bytes total 📦 Building JS bundles… 📦 Building JS bundles… ✓ admin.min.js (60,911 bytes) ✓ public.min.js (22,022 bytes) ✓ form.min.js (41,547 bytes) ✓ partage.min.js (42,441 bytes) ✅ JS bundles done ✅ Build complete 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=461/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=458/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=411/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=411/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=401/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=360/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=325/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=319/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=309/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=302/469) 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=280/469) 700 0% 0.00kB/s 0:00:00 55,602 1% 52.36MB/s 0:00:00 (xfr#1, to-chk=265/469) 55,602 1% 52.36MB/s 0:00:00 (xfr#1, to-chk=250/469) 116,513 2% 110.45MB/s 0:00:00 (xfr#2, to-chk=264/469) 138,830 2% 131.73MB/s 0:00:00 (xfr#3, to-chk=263/469) 142,513 2% 135.24MB/s 0:00:00 (xfr#4, to-chk=262/469) 146,246 2% 138.80MB/s 0:00:00 (xfr#5, to-chk=261/469) 165,356 2% 157.03MB/s 0:00:00 (xfr#6, to-chk=260/469) 207,211 3% 196.94MB/s 0:00:00 (xfr#7, to-chk=259/469) 248,758 4% 236.57MB/s 0:00:00 (xfr#8, to-chk=258/469) 249,430 4% 237.21MB/s 0:00:00 (xfr#9, to-chk=257/469) 286,659 5% 272.71MB/s 0:00:00 (xfr#10, to-chk=256/469) 329,100 5% 313.19MB/s 0:00:00 (xfr#11, to-chk=255/469) 333,410 6% 317.30MB/s 0:00:00 (xfr#12, to-chk=254/469) 355,432 6% 338.30MB/s 0:00:00 (xfr#13, to-chk=253/469) 368,598 6% 350.85MB/s 0:00:00 (xfr#14, to-chk=252/469) 376,006 6% 357.92MB/s 0:00:00 (xfr#15, to-chk=251/469) 385,095 6% 366.59MB/s 0:00:00 (xfr#16, to-chk=250/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=250/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=235/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=230/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=193/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=192/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=189/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=156/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=156/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=147/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=135/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=134/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=129/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=120/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=93/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=78/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=77/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=71/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=71/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=64/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=43/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=39/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=34/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=33/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=25/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=8/469) 385,095 6% 2.96MB/s 0:00:00 (xfr#16, to-chk=0/469) 📋 Deploying nginx configuration… xamxam.conf sent 145 bytes received 125 bytes 540.00 bytes/sec total size is 10,012 speedup is 37.08 deploy-server.sh sent 1,089 bytes received 107 bytes 797.33 bytes/sec total size is 8,519 speedup is 7.12 recipes to it via safe ssh/rsync shims, so setup scripts can be validated without touching production. Includes provision-server-packages.sh, setup.sh/teardown.sh, server Dockerfile + helper, and the rendered ssh config. Validated end-to-end against the box: - provisioning (apt nginx, php8.4-fpm, composer), - scripts/setup-server.sh (group/user/dir bootstrap), - just deploy-code (transfer; surfaced the deploy-code --chown regression).
XAMXAM test environment — a fresh Debian box you can deploy to
test-env/ spins up a throwaway Debian trixie systemd container that behaves
like a brand-new server with nothing preinstalled, then drives the project's
real setup scripts and just deploy recipes against it over SSH — without
ever touching the production host.
This is how you verify the setup scripts actually work before trusting them on real hardware.
How it works
HOST (you) podman network
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ just test-env-run … │ ssh / │ server = Debian trixie │
│ uses test-env/bin/* shims │ rsync │ systemd + sshd :22022 │
│ (ssh/rsync route `xamxam` │───────▶│ "fresh box" │
│ → 127.0.0.1:22022) │ └──────────────────────────────┘
└─────────────────────────────┘
- The
justrecipes run unmodified on your host. All they know is an SSH host namedxamxam. Normally that alias points at production; with the test env it points at the container. - Two tiny shims,
test-env/bin/sshandtest-env/bin/rsync, are put onPATHonly insidetest-env-run. They force the test SSH config (test-env/ssh/config), so everyssh xamxam …/rsync … xamxam:/…hits the container. Your real~/.ssh/configis never touched. - Safety: the shims always force the test transport. A caller-supplied
rsync -eis replaced, and when none is given the shim injects its own so rsync can never fall back to the host's real config (which mapsxamxam→ PRODUCTION). - The systemd container runs nginx + php-fpm as real services, so
systemctl,nginx -t, socket paths etc. all behave like a real box.
What it tests
| Step | File | Does |
|---|---|---|
| Stack install | test-env/scripts/provision-server-packages.sh |
apt-get nginx, php8.4-fpm, php-cli/curl/sqlite3/mbstring/xml, composer, sqlite3, rsync, git, just |
| Server setup | scripts/setup-server.sh (real) |
xamxam group, deploy+www-data membership, /var/www/xamxam, 2775/664 perms, cache/log/backup dirs |
| Deploy | just deploy (real) |
build, deploy-code, deploy-nginx (→ scripts/deploy-server.sh), deploy-deps, deploy-migrate, deploy-env, deploy-verify-permissions |
| Nginx config | nginx/xamxam.conf (real) |
installed via deploy-nginx, validated with nginx -t, reloaded |
| First provision | scripts/provision-server-env.sh (real) |
server-side .env / APP_KEY |
| Remote provisioning | just provision-server (real) |
chains env + deploy + backup + logrotate |
Usage
# 1. Boot the fresh box (builds image, starts container, renders ssh config)
just test-env-up
# 2. Install the stack on the box (apt-get)
just test-env-provision
# 3. Run the real one-shot server setup (group/user/dir bootstrap)
just test-env-setup-server
# 4. Run the real deploy chain, pointed at the box
just test-env-run # == just deploy
# or individual recipes:
just test-env-run recipe=deploy-nginx
just test-env-run recipe=provision-server-env
just test-env-run recipe=deploy-db
# 5. Health check
just test-env-status
# 6. Interactive shell
podman compose -f test-env/compose.yaml exec server bash
# Tear down
just test-env-teardown # + -- --keys to also delete the SSH keypair
SSH to the box directly
ssh -p 22022 -i test-env/keys/xamxam-test_ed25519 deploy@127.0.0.1
The deploy user has passwordless sudo so the recipes' sudo … calls run
unattended (the realistic option, matching that provision-server-env uses a
non-TTY sudo tee). To force sudo to prompt instead, remove
/etc/sudoers.d/deploy inside the container:
podman compose -f test-env/compose.yaml exec server rm /etc/sudoers.d/deploy
…but note some recipes (e.g. provision-server-env) rely on non-TTY sudo, so
passwordless is the supported default.
How the justfile is wired
test-env-run recipe='deploy': # run any recipe against the box
@PATH="$(cd test-env && pwd)/bin:$PATH" just {{recipe}}
Only the test-env-* recipes prepend the shims; a plain just deploy still
targets production as before.
Caveat: recipes using ssh -t need a real terminal
Recipes that call ssh -t xamxam "sudo …" (e.g. deploy-nginx) require a
pseudo-terminal. When you run just test-env-run from a normal interactive
terminal you SSH through the shim fine. But if just is driven from a
non-TTY context (a script, CI, or agent), ssh -t cannot allocate a pty and
sudo refuses with “a terminal is required”. Always drive these recipes from
an interactive shell.
Generated / ignored
test-env/keys/— throwaway SSH keypair (gitignored).test-env/ssh/config— rendered fromconfig.template, never committed.
Finding: deploy-code lost --chown=www-data:xamxam
First run of just deploy against a fresh box reproduces a real regression
in the working-copy justfile (pre-existing uncommitted refactor that switched
deploy-code to rsync -az … --exclude-from=.rsync-exclude):
rsync: [receiver] mkstemp "/var/www/xamxam/storage/.xamxam.sqlite…" failed: Permission denied
rsync error: code 23
Every deployed file/dir ends up deploy:deploy / drwxr-xr-x instead of
www-data:xamxam / group-writable, so php-fpm (www-data) cannot write
storage/. The recipe dropped --chown="www-data:xamxam" that the previous
version had. Restore it to fix fresh deploys.