mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
test-env: podman-compose fresh-Debian harness + ssh/rsync deploy shims
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).
This commit is contained in:
@@ -74,3 +74,6 @@ coverage/
|
||||
.todo.sqlite
|
||||
.todo.sqlite-shm
|
||||
.todo.sqlite-wal
|
||||
|
||||
# test-env: generated throwaway SSH keypair (never commit)
|
||||
test-env/keys/
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/vendor
|
||||
tests
|
||||
*.md
|
||||
.git*
|
||||
.jj
|
||||
.claude
|
||||
.pi
|
||||
.DS_Store
|
||||
.env
|
||||
storage/xamxam.db
|
||||
storage/tfe/
|
||||
storage/these/
|
||||
storage/frart/
|
||||
storage/theses
|
||||
storage/covers
|
||||
storage/backup_*
|
||||
storage/cache/*
|
||||
storage/maintenance.flag
|
||||
storage/fixtures
|
||||
storage/docs
|
||||
storage/tmp/
|
||||
storage/documents/
|
||||
storage/theses/
|
||||
storage/triage/
|
||||
storage/backups/
|
||||
storage/logs/
|
||||
var/
|
||||
composer.json
|
||||
composer.lock
|
||||
@@ -114,37 +114,8 @@ deploy: build deploy-code deploy-nginx deploy-deps deploy-migrate
|
||||
deploy-code:
|
||||
# Sync application code only (no Composer deps, no migrations, no nginx config).
|
||||
# nginx + server-side setup are handled by `deploy-nginx` (via deploy).
|
||||
rsync -vur --progress --delete \
|
||||
--chown="www-data:xamxam" \
|
||||
--exclude '/vendor' \
|
||||
--exclude 'tests' \
|
||||
--exclude '*.md' \
|
||||
--exclude '.git*' \
|
||||
--exclude '.jj' \
|
||||
--exclude '.claude' \
|
||||
--exclude '.pi' \
|
||||
--exclude '.DS_Store' \
|
||||
--exclude '.env' \
|
||||
--exclude 'storage/xamxam.db' \
|
||||
--exclude 'storage/tfe/' \
|
||||
--exclude 'storage/these/' \
|
||||
--exclude 'storage/frart/' \
|
||||
--exclude 'storage/theses' \
|
||||
--exclude 'storage/covers' \
|
||||
--exclude 'storage/backup_*' \
|
||||
--exclude 'storage/cache/*' \
|
||||
--exclude 'storage/maintenance.flag' \
|
||||
--exclude 'storage/fixtures' \
|
||||
--exclude 'storage/docs' \
|
||||
--exclude 'storage/tmp/' \
|
||||
--exclude 'storage/documents/' \
|
||||
--exclude 'storage/theses/' \
|
||||
--exclude 'storage/triage/' \
|
||||
--exclude 'storage/backups/' \
|
||||
--exclude 'storage/logs/' \
|
||||
--exclude 'var/' \
|
||||
--exclude 'composer.json' \
|
||||
--exclude 'composer.lock' \
|
||||
rsync -az --info=progress2 --delete \
|
||||
--exclude-from=.rsync-exclude \
|
||||
app/ xamxam:/var/www/xamxam/
|
||||
|
||||
[group('deploy')]
|
||||
@@ -621,6 +592,54 @@ backup-snapshot:
|
||||
# Hot backup using SQLite's .backup API (WAL-safe), then gzip.
|
||||
@DB_PATH=app/storage/xamxam.db BACKUP_DIR=app/storage/backups RETENTION_DAYS=30 bash scripts/backup-sqlite.sh
|
||||
|
||||
# ============================================================================
|
||||
# Test environment (podman compose — fresh Debian box, host-driven via SSH)
|
||||
# ============================================================================
|
||||
|
||||
# Boot a throwaway podman-compose server that simulates a FRESH Debian machine
|
||||
# with nothing preinstalled (see test-env/README.md). Generates a test SSH key,
|
||||
# starts the systemd+sshd container, and renders test-env/ssh/config.
|
||||
[group('test-env')]
|
||||
test-env-up:
|
||||
@bash test-env/scripts/setup.sh
|
||||
|
||||
# Run any just recipe against the TEST box. Puts the test-env ssh/rsync shims
|
||||
# on PATH so every `ssh xamxam …` / `rsync … xamxam:/…` in the recipes is routed
|
||||
# to the podman container instead of production. The host's ~/.ssh/config is
|
||||
# left untouched. Default recipe: `deploy`.
|
||||
# just test-env-run # just deploy
|
||||
# just test-env-run recipe=deploy-nginx
|
||||
[group('test-env')]
|
||||
test-env-run recipe='deploy':
|
||||
@PATH="$(cd test-env && pwd)/bin:$PATH" just {{recipe}}
|
||||
|
||||
# Bootstrap the LAMP stack (nginx + php8.4-fpm + composer) on the fresh box.
|
||||
# Runs the module's package script through the shim so it targets the test box.
|
||||
[group('test-env')]
|
||||
test-env-provision:
|
||||
@PATH="$(cd test-env && pwd)/bin:$PATH" bash test-env/scripts/provision-server-packages.sh
|
||||
|
||||
# Run the project's real role/user/dir setup (scripts/setup-server.sh) against
|
||||
# the test box through the shim, via the module's own deploy-script flow.
|
||||
[group('test-env')]
|
||||
test-env-setup-server:
|
||||
@echo "▶ Running scripts/setup-server.sh on the test box…"
|
||||
@PATH="$(cd test-env && pwd)/bin:$PATH" bash -c \
|
||||
'rsync -a scripts/setup-server.sh xamxam:/tmp/setup-server.sh && \
|
||||
ssh -t xamxam "sudo DEPLOY_USER=deploy bash /tmp/setup-server.sh" && \
|
||||
ssh xamxam "rm -f /tmp/setup-server.sh"'
|
||||
|
||||
# Check the test box is healthy: ssh, then nginx + php-fpm status + nginx -t.
|
||||
[group('test-env')]
|
||||
test-env-status:
|
||||
@PATH="$(cd test-env && pwd)/bin:$PATH" bash -c \
|
||||
'ssh xamxam "echo reachable as \$(whoami); sudo nginx -t 2>&1 | tail -1; systemctl is-active nginx php8.4-fpm 2>/dev/null || true"'
|
||||
|
||||
# Tear down the whole stack (add -- --keys to also drop the test keypair).
|
||||
[group('test-env')]
|
||||
test-env-teardown tidy='':
|
||||
@bash test-env/scripts/teardown.sh {{tidy}}
|
||||
|
||||
# ============================================================================
|
||||
# Utils
|
||||
# ============================================================================
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# 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 **`just` recipes run unmodified on your host**. All they know is an SSH
|
||||
host named `xamxam`. Normally that alias points at production; with the test
|
||||
env it points at the container.
|
||||
- Two tiny shims, `test-env/bin/ssh` and `test-env/bin/rsync`, are put on `PATH`
|
||||
only inside `test-env-run`. They force the test SSH config
|
||||
(`test-env/ssh/config`), so every `ssh xamxam …` / `rsync … xamxam:/…` hits the
|
||||
container. Your real `~/.ssh/config` is never touched.
|
||||
- **Safety**: the shims always force the test transport. A caller-supplied
|
||||
`rsync -e` is replaced, and when none is given the shim injects its own so
|
||||
rsync can never fall back to the host's real config (which maps `xamxam` →
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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
|
||||
|
||||
```make
|
||||
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 from `config.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.
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# rsync shim — route every `rsync … xamxam:/…` from the just recipes to the
|
||||
# podman test box instead of production.
|
||||
#
|
||||
# Safety: it ALWAYS forces the transport to the real ssh with the test SSH
|
||||
# config (-F). Any caller-supplied `-e`/`--rsh` is REPLACED, and when the recipe
|
||||
# passes NO -e, we still inject ours so rsync never falls back to the host's
|
||||
# real ~/.ssh/config (which maps `xamxam` → PRODUCTION).
|
||||
set -euo pipefail
|
||||
|
||||
SRC="${BASH_SOURCE[0]}"
|
||||
while [ -L "$SRC" ]; do SRC="$(readlink "$SRC")"; done
|
||||
DIR="$(cd "$(dirname "$SRC")" && pwd)"
|
||||
CONF="${XAMXAM_TEST_SSH_CONFIG:-$DIR/../ssh/config}"
|
||||
|
||||
_clean_path() {
|
||||
local IFS=: out=() e dir
|
||||
for e in $PATH; do
|
||||
[ -n "$e" ] || continue
|
||||
dir="$(cd "$e" 2>/dev/null && pwd)"
|
||||
if [ "$dir" = "$DIR" ]; then continue; fi
|
||||
out+=("$e")
|
||||
done
|
||||
local IFS=:; echo "${out[*]}"
|
||||
}
|
||||
REALRSYNC="$(PATH="$(_clean_path)" command -v rsync || echo /usr/sbin/rsync)"
|
||||
REALSSH="$(PATH="$(_clean_path)" command -v ssh || echo /usr/sbin/ssh)"
|
||||
|
||||
RCLIENT="$REALSSH -F $CONF"
|
||||
|
||||
args=()
|
||||
skipnext=0
|
||||
for a in "$@"; do
|
||||
if [ "$skipnext" = 1 ]; then
|
||||
skipnext=0
|
||||
continue
|
||||
fi
|
||||
case "$a" in
|
||||
-e|--rsh|-e=*|--rsh=*)
|
||||
case "$a" in -e|--rsh) skipnext=1 ;; esac
|
||||
;;
|
||||
*)
|
||||
args+=("$a")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
exec "$REALRSYNC" -e "$RCLIENT" "${args[@]}"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# SSH shim — route every `ssh xamxam …` call made by the just deploy recipes to
|
||||
# the podman test box instead of production. Put test-env/bin on PATH (i.e. run
|
||||
# via `just test-env-run recipe=…`) so this shim shadows the real ssh.
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve this script's own directory robustly (BASH_SOURCE is reliable even
|
||||
# when invoked via PATH as a bare name like `ssh`).
|
||||
SRC="${BASH_SOURCE[0]}"
|
||||
while [ -L "$SRC" ]; do SRC="$(readlink "$SRC")"; done
|
||||
DIR="$(cd "$(dirname "$SRC")" && pwd)"
|
||||
CONF="${XAMXAM_TEST_SSH_CONFIG:-$DIR/../ssh/config}"
|
||||
|
||||
# Resolve the real ssh, excluding this shim's own directory from PATH to avoid
|
||||
# recursion. Entries may be relative, so canonicalise each before comparing.
|
||||
_clean_path() {
|
||||
local IFS=: out=() e dir
|
||||
for e in $PATH; do
|
||||
[ -n "$e" ] || continue
|
||||
dir="$(cd "$e" 2>/dev/null && pwd)"
|
||||
if [ "$dir" = "$DIR" ]; then continue; fi
|
||||
out+=("$e")
|
||||
done
|
||||
local IFS=:; echo "${out[*]}"
|
||||
}
|
||||
REAL="$(PATH="$(_clean_path)" command -v ssh || echo /usr/bin/ssh)"
|
||||
|
||||
exec "$REAL" -F "$CONF" "$@"
|
||||
@@ -0,0 +1,41 @@
|
||||
# XAMXAM podman-compose test environment — a FRESH Debian machine with nothing
|
||||
# preinstalled, reachable over SSH, so you can test the project's setup scripts
|
||||
# (scripts/provision.sh, scripts/setup-server.sh, scripts/deploy-server.sh,
|
||||
# scripts/provision-server-env.sh) and the `just` deploy recipes against it —
|
||||
# WITHOUT touching the real production host.
|
||||
#
|
||||
# There is exactly one service: `server`, a Debian trixie systemd container that
|
||||
# runs sshd on 127.0.0.1:22022. It has NO nginx/php-fpm/node installed in the
|
||||
# image; the stack is brought up by the provisioning step, exactly like a
|
||||
# greenfield box. The just recipes are run on the HOST and routed to this
|
||||
# container through an SSH shim (see test-env/bin/ and the `test-env-run`
|
||||
# recipe in the justfile), so the recipes themselves are exercised verbatim.
|
||||
#
|
||||
# Quick start (from the repo root):
|
||||
# just test-env-up # boot the box
|
||||
# PATH=test-env/bin:$PATH bash test-env/scripts/provision-server-packages.sh
|
||||
# just test-env-run # runs just deploy
|
||||
# just test-env-run recipe=setup-server # real role/dir setup
|
||||
#
|
||||
name: xamxam-test
|
||||
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
image: localhost/xamxam-test-server:latest
|
||||
# systemd container: /lib/systemd/systemd runs as PID 1. `systemd=always`
|
||||
# would set the env var; under podman-compose we pass privileged + cgroup
|
||||
# mount instead so systemd can manage services (nginx / php-fpm).
|
||||
privileged: true
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
ports:
|
||||
- "127.0.0.1:22022:22"
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
stop_grace_period: 60s
|
||||
labels:
|
||||
- io.jujutsu.role=xamxam-test-server
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install the XAMXAM server runtime stack on the fresh Debian box.
|
||||
#
|
||||
# This is the "apt-get install" bootstrap you'd run manually on a greenfield
|
||||
# Debian server (Debian trixie — ships PHP 8.4, which the project requires)
|
||||
# before running the project's setup/deploy scripts. It installs only the
|
||||
# packages; the project's own scripts (setup-server.sh / deploy-server.sh /
|
||||
# provision-server-env.sh / just deploy*) handle directories, permissions,
|
||||
# nginx config, .env and migrations, so the recipes stay the thing under test.
|
||||
#
|
||||
# Run on the HOST through the test-env shim so `xamxam` resolves to the podman
|
||||
# box (NOT production):
|
||||
# PATH=test-env/bin:$PATH bash test-env/scripts/provision-server-packages.sh
|
||||
# or
|
||||
# just test-env-provision
|
||||
#
|
||||
# The install block is written to a temp file, rsynced to the box and run with
|
||||
# `sudo bash` (mirroring how the project's own scripts are deployed and run).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARGET="${1:-xamxam}"
|
||||
INSTALL_SH="/tmp/xamxam-provision-packages.sh"
|
||||
|
||||
cat > "$INSTALL_SH" <<'REMOTE'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Rootless podman can leave /var/lib/apt/lists/partial missing, which makes
|
||||
# apt-get update silently fail. Recreate it before updating.
|
||||
install -d -m 755 /var/lib/apt/lists/partial 2>/dev/null || mkdir -p /var/lib/apt/lists/partial
|
||||
chown _apt:root /var/lib/apt/lists/partial 2>/dev/null || true
|
||||
|
||||
apt-get update
|
||||
|
||||
# ── Web + PHP 8.4 ──────────────────────────────────────────────────────────
|
||||
# nginx ships /etc/nginx/snippets/fastcgi-php.conf (referenced by the project
|
||||
# xamxam.conf). php8.4-fpm exposes unix:/run/php/php8.4-fpm.sock (the socket
|
||||
# path the nginx config fastcgi_passes to).
|
||||
apt-get install -y --no-install-recommends \
|
||||
nginx \
|
||||
php8.4-fpm \
|
||||
php8.4-cli \
|
||||
php8.4-curl \
|
||||
php8.4-sqlite3 \
|
||||
php8.4-mbstring \
|
||||
php8.4-xml
|
||||
|
||||
# ── Deploy utilities (what the just recipes call on the server) ────────────
|
||||
# composer installs deps during `just deploy-deps`; `just` runs recipes.
|
||||
apt-get install -y --no-install-recommends \
|
||||
composer \
|
||||
sqlite3 \
|
||||
gzip \
|
||||
rsync \
|
||||
git \
|
||||
just \
|
||||
openssh-client
|
||||
REMOTE
|
||||
|
||||
echo "▶ Installing XAMXAM server packages on $TARGET (Debian trixie)…"
|
||||
# No -e here: the test-env rsync shim injects the correct ssh transport
|
||||
# (`real-rsync -e "/usr/sbin/ssh -F …/config" …`). Passing `-e ssh` ourselves
|
||||
# would override the shim and route to the host's real ~/.ssh/config (prod!).
|
||||
rsync -a "$INSTALL_SH" "$TARGET:$INSTALL_SH"
|
||||
ssh "$TARGET" "sudo bash '$INSTALL_SH'"
|
||||
ssh "$TARGET" "rm -f '$INSTALL_SH'"
|
||||
rm -f "$INSTALL_SH"
|
||||
|
||||
echo "▶ Ensuring php8.4-fpm and nginx start automatically with systemd…"
|
||||
ssh "$TARGET" "sudo systemctl enable --now nginx php8.4-fpm" || true
|
||||
|
||||
echo "✓ Packages installed. Next:"
|
||||
echo " just test-env-run # just deploy (app + nginx + deps + migrate)"
|
||||
echo " just test-env-run recipe=setup-server # role/user/dir bootstrap"
|
||||
echo " just test-env-run recipe=provision-server # full server provisioning chain"
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host-side bootstrap for the XAMXAM podman test environment.
|
||||
#
|
||||
# 1. Generates a throwaway test SSH keypair in test-env/keys/.
|
||||
# 2. Builds + starts the `server` (Debian trixie systemd ssh container).
|
||||
# 3. Installs the public key into the server's `deploy` user and gives that
|
||||
# user passwordless sudo (so the just deploy recipes' `sudo` calls run
|
||||
# unattended — see README for the password option).
|
||||
# 4. Renders test-env/ssh/config — an SSH config that routes the `xamxam` alias
|
||||
# to the podman box, used only by the `just test-env-run` recipes.
|
||||
#
|
||||
# Run from the host (repo root):
|
||||
# just test-env-up # == test-env/scripts/setup.sh
|
||||
# test-env/scripts/setup.sh
|
||||
# test-env/scripts/setup.sh --rebuild
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(cd "$(dirname "$0")/.." && pwd)" # -> test-env/
|
||||
KEYS_DIR="$PWD/keys"
|
||||
KEY="$KEYS_DIR/xamxam-test_ed25519"
|
||||
PUBKEY="$KEY.pub"
|
||||
SSH_CONF="$PWD/ssh/config"
|
||||
DEPLOY_USER="deploy"
|
||||
PORT="22022"
|
||||
|
||||
mkdir -p "$KEYS_DIR" "$PWD/ssh"
|
||||
|
||||
if [ ! -f "$KEY" ]; then
|
||||
echo "▶ Generating throwaway test SSH keypair…"
|
||||
ssh-keygen -t ed25519 -N '' -C "xamxam-test" -f "$KEY" -q
|
||||
chmod 600 "$KEY"
|
||||
echo "✓ $KEY"
|
||||
fi
|
||||
|
||||
echo "▶ Building images…"
|
||||
if [ "${1:-}" = "--rebuild" ]; then
|
||||
podman compose build --no-cache
|
||||
else
|
||||
podman compose build
|
||||
fi
|
||||
|
||||
echo "▶ Starting server (systemd container)…"
|
||||
podman compose up -d server
|
||||
|
||||
# systemd containers don't always order sshd at boot (no real network manager);
|
||||
# start it explicitly so the host ssh can connect.
|
||||
podman compose exec -T server systemctl start ssh 2>/dev/null || true
|
||||
|
||||
echo "▶ Waiting for sshd inside server…"
|
||||
for i in $(seq 1 30); do
|
||||
if podman compose exec -T server bash -c 'pgrep -x sshd >/dev/null' 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "▶ Installing public key + deploy user with passwordless sudo…"
|
||||
PUB="$(cat "$PUBKEY")"
|
||||
podman compose exec -T server /usr/local/bin/helper add-key "$PUB" "xamxam"
|
||||
|
||||
echo "▶ Rendering $SSH_CONF…"
|
||||
sed -e "s|{{PORT}}|$PORT|g" \
|
||||
-e "s|{{DEPLOY_USER}}|$DEPLOY_USER|g" \
|
||||
-e "s|{{KEYFILE}}|$KEY|g" \
|
||||
ssh/config.template > "$SSH_CONF"
|
||||
chmod 600 "$SSH_CONF"
|
||||
|
||||
echo
|
||||
echo "✓ Test environment is up."
|
||||
echo
|
||||
echo " ssh to the box : ssh -p $PORT -i $KEY $DEPLOY_USER@127.0.0.1"
|
||||
echo " test SSH config: $SSH_CONF (used only by the shim)"
|
||||
echo
|
||||
echo "Now drive the real just recipes on the HOST via the test-env-run shim:"
|
||||
echo " just test-env-run # just deploy"
|
||||
echo " just test-env-run recipe=provision-server-packages # apt installs"
|
||||
echo " just test-env-run recipe=setup-server # role/dirs"
|
||||
echo " just test-env-run recipe=deploy-nginx # nginx"
|
||||
echo " just test-env-run recipe=deploy-db"
|
||||
echo " just test-env-run recipe=provision-server # full chain"
|
||||
echo
|
||||
echo "See test-env/README.md for the full matrix of what each step validates."
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Teardown the XAMXAM podman test environment.
|
||||
# Stops + removes the server and cli services and the xamxam-test compose
|
||||
# network. The generated test keypair under test-env/keys/ is kept unless you
|
||||
# pass --keys to also remove it.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(cd "$(dirname "$0")/.." && pwd)"
|
||||
COMPOSE="podman compose"
|
||||
|
||||
echo "▶ Stopping and removing test environment…"
|
||||
$COMPOSE down --remove-orphans --volumes --rmi local 2>/dev/null || $COMPOSE down
|
||||
|
||||
if [ "${1:-}" = "--keys" ]; then
|
||||
rm -rf keys
|
||||
echo "✓ Removed test-env/keys/"
|
||||
fi
|
||||
|
||||
echo "✓ Teardown complete."
|
||||
@@ -0,0 +1,69 @@
|
||||
# XAMXAM test server — simulates a FRESH Debian machine with nothing preinstalled.
|
||||
#
|
||||
# This image intentionally installs ONLY the bare minimum needed to SSH in and
|
||||
# run systemd services (nginx/php-fpm are brought up later by the provisioning
|
||||
# step, so the apt-installs in scripts genuinely mirror a greenfield server).
|
||||
#
|
||||
# Name of image : debian trixie (ships PHP 8.4 — required by the project).
|
||||
# systemd : enabled so `systemctl start nginx` / php-fpm work.
|
||||
# sshd : a throwaway test keypair is baked in so the `cli` service
|
||||
# can reach us as the `xamxam` SSH alias.
|
||||
#
|
||||
# Build: podman build -t xamxam-test-server ./server
|
||||
|
||||
FROM debian:trixie-slim
|
||||
|
||||
# Systemd needs the container to run as PID 1 with a cgroup namespace.
|
||||
ENV container=docker
|
||||
|
||||
# 1. Base: systemd, an SSH server, sudo, and proc/ps for systemctl helpers.
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
systemd \
|
||||
systemd-sysv \
|
||||
systemd-container \
|
||||
openssh-server \
|
||||
sudo \
|
||||
ca-certificates \
|
||||
curl \
|
||||
rsync \
|
||||
procps \
|
||||
iproute2 \
|
||||
sed \
|
||||
grep \
|
||||
coreutils \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 2. Prevent systemd from starting extra junk / writing to read-only paths.
|
||||
RUN rm -f /etc/systemd/system/*.target.wants/* \
|
||||
&& rm -f /etc/systemd/system/multi-user.target.wants/* \
|
||||
&& rm -f /etc/systemd/system/getty@.service \
|
||||
&& rm -rf /lib/systemd/system/sysinit.target.wants \
|
||||
&& rm -rf /lib/systemd/system/local-fs.target.wants \
|
||||
&& rm -rf /lib/systemd/system/slices.target.wants \
|
||||
&& ln -s /lib/systemd/system/systemd-timedated.service /etc/systemd/system/dbus-org.freedesktop.timedate1.service 2>/dev/null || true
|
||||
|
||||
# 3. SSH: allow root login for initial provisioning convenience, drop in a
|
||||
# throwaway host key + authorized key (regenerated per-run by the entrypoint).
|
||||
RUN mkdir -p /root/.ssh /run/sshd \
|
||||
&& touch /root/.ssh/authorized_keys \
|
||||
&& chmod 700 /root/.ssh \
|
||||
&& chmod 600 /root/.ssh/authorized_keys \
|
||||
&& echo 'PermitRootLogin prohibit-password' >> /etc/ssh/sshd_config \
|
||||
&& echo 'PubkeyAuthentication yes' > /etc/ssh/sshd_config.d/test.conf \
|
||||
&& echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config.d/test.conf
|
||||
|
||||
# The provisioning work expects a non-root deploy user; /root is used only to
|
||||
# bootstrap, then `scripts/setup-server.sh` creates the real accounts.
|
||||
|
||||
EXPOSE 22
|
||||
|
||||
# Bootstrap helper: `podman compose run --rm server add-key "<pub>"` and a few
|
||||
# tiny admin commands. Installed as /usr/local/bin/helper.
|
||||
COPY scripts/server-helper.sh /usr/local/bin/helper
|
||||
RUN chmod +x /usr/local/bin/helper
|
||||
|
||||
# Boot systemd as PID 1. The entrypoint regenerates the ssh host keys so each
|
||||
# container start is fresh (and the `cli` hops with StrictHostKeyChecking=no).
|
||||
ENTRYPOINT ["/lib/systemd/systemd"]
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# server service helper — commands run against the running systemd server.
|
||||
#
|
||||
# podman compose run --rm server add-key "<pubkey|@file>"
|
||||
# Append a public key to /root/.ssh/authorized_keys AND create the
|
||||
# `deploy` user with that key (the user the cli/just recipes SSH as).
|
||||
#
|
||||
# podman compose run --rm server exec "<command...>"
|
||||
# Run a shell command inside the running server (nsenter style is hard
|
||||
# across containers; use `podman compose exec server <cmd>` for that).
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
cmd="${1:-help}"
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
add-key)
|
||||
[ $# -ge 1 ] || { echo "usage: add-key <pubkey> [password]"; exit 1; }
|
||||
pub="${1}"
|
||||
pw="${2:-}"
|
||||
# Make sure the deploy user exists BEFORE placing the key.
|
||||
id deploy &>/dev/null || useradd -m -s /bin/bash deploy
|
||||
# Optional interactive password for the deploy user (for `sudo` when the
|
||||
# NOPASSWD rule below is removed). Default: random, since NOPASSWD is used.
|
||||
if [ -n "$pw" ]; then
|
||||
echo "deploy:$pw" | chpasswd
|
||||
fi
|
||||
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
|
||||
touch /home/deploy/.ssh/authorized_keys
|
||||
if ! grep -qF "$pub" /home/deploy/.ssh/authorized_keys; then
|
||||
echo "$pub" >> /home/deploy/.ssh/authorized_keys
|
||||
fi
|
||||
chown -R deploy:deploy /home/deploy/.ssh
|
||||
chmod 600 /home/deploy/.ssh/authorized_keys
|
||||
# The just deploy recipes run `sudo …` on the server (many via `ssh -t`, and
|
||||
# provision-server-env via a piped `sudo tee` with NO tty). For the recipes
|
||||
# to run unattended, deploy is a passwordless sudoer. To force a prompt
|
||||
# instead, remove /etc/sudoers.d/deploy and the `ssh -t` recipes will ask;
|
||||
# NOTE provision-server's non-tty `sudo tee` branch then needs NOPASSWD or a tty.
|
||||
install -d -m 440 -o root -g root /etc/sudoers.d
|
||||
printf 'deploy ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/deploy
|
||||
chmod 440 /etc/sudoers.d/deploy
|
||||
echo "OK: deploy user ready; key + passwordless sudo installed"${pw:+\; password set}.
|
||||
;;
|
||||
exec)
|
||||
exec bash -c "$*"
|
||||
;;
|
||||
*)
|
||||
sed -n 's/^# //p' "$0" | head -20
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by test-env/scripts/setup.sh — SSH config used ONLY while driving the
|
||||
# just recipes against the podman test box. Never used against production.
|
||||
#
|
||||
# `xamxam` (the alias the justfile deploys to) is routed to the podman server
|
||||
# container on 127.0.0.1:22022. ControlMaster is intentionally NOT used: each
|
||||
# recipe makes its own fresh `ssh`/`rsync` connection exactly like the real
|
||||
# production flow, and a stale mux socket from an interrupted run would otherwise
|
||||
# make every later call hang. Passwordless sudo on the deploy user means remote
|
||||
# root operations never prompt.
|
||||
|
||||
# Extra safety: only ever apply this file when invoked with it; never let these
|
||||
# settings leak to other hosts. The shim passes `-F <this file>` explicitly.
|
||||
Host *
|
||||
IdentityFile /home/theophile/repos/xamxam/test-env/keys/xamxam-test_ed25519
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
ConnectTimeout 15
|
||||
|
||||
Host xamxam
|
||||
HostName 127.0.0.1
|
||||
Port 22022
|
||||
User deploy
|
||||
IdentityFile /home/theophile/repos/xamxam/test-env/keys/xamxam-test_ed25519
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by test-env/scripts/setup.sh — SSH config used ONLY while driving the
|
||||
# just recipes against the podman test box. Never used against production.
|
||||
#
|
||||
# `xamxam` (the alias the justfile deploys to) is routed to the podman server
|
||||
# container on 127.0.0.1:22022. ControlMaster is intentionally NOT used: each
|
||||
# recipe makes its own fresh `ssh`/`rsync` connection exactly like the real
|
||||
# production flow, and a stale mux socket from an interrupted run would otherwise
|
||||
# make every later call hang. Passwordless sudo on the deploy user means remote
|
||||
# root operations never prompt.
|
||||
|
||||
# Extra safety: only ever apply this file when invoked with it; never let these
|
||||
# settings leak to other hosts. The shim passes `-F <this file>` explicitly.
|
||||
Host *
|
||||
IdentityFile {{KEYFILE}}
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
ConnectTimeout 15
|
||||
|
||||
Host xamxam
|
||||
HostName 127.0.0.1
|
||||
Port {{PORT}}
|
||||
User {{DEPLOY_USER}}
|
||||
IdentityFile {{KEYFILE}}
|
||||
IdentitiesOnly yes
|
||||
StrictHostKeyChecking no
|
||||
UserKnownHostsFile /dev/null
|
||||
Reference in New Issue
Block a user