mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
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).
687 lines
29 KiB
Makefile
687 lines
29 KiB
Makefile
default:
|
||
@just --list
|
||
|
||
# XAMXAM Justfile
|
||
|
||
# ============================================================================
|
||
# Development
|
||
# ============================================================================
|
||
|
||
[group('dev')]
|
||
setup:
|
||
@bash scripts/setup-dev.sh
|
||
|
||
# One-shot provisioning for a fresh clone: app/.env + APP_KEY (idempotent, never
|
||
# overwrites an existing key), composer deps, npm deps, DB schema + migrations,
|
||
# and a first-admin-password check. Prefer this over the stale `setup` recipe.
|
||
[group('dev')]
|
||
provision:
|
||
@bash scripts/provision.sh
|
||
|
||
# One-shot build of all frontend assets (run before `dev` if sources changed)
|
||
[group('dev')]
|
||
dev-build:
|
||
@node scripts/build.mjs --quiet
|
||
|
||
# Watch CSS/JS sources and rebuild on change.
|
||
# Run in a separate terminal alongside `just dev`.
|
||
[group('dev')]
|
||
dev-watch:
|
||
@npx chokidar \
|
||
"app/public/assets/css/**/*.css" \
|
||
"app/public/assets/js/app/**/*.js" \
|
||
--initial \
|
||
-c "node scripts/build.mjs --quiet"
|
||
|
||
[group('dev')]
|
||
dev: dev-build migrate
|
||
@xdg-open http://127.0.0.1:8000 &
|
||
@xdg-open http://127.0.0.1:8000/admin/ &
|
||
@npx chokidar \
|
||
"app/public/assets/css/**/*.css" \
|
||
"app/public/assets/js/app/**/*.js" \
|
||
-c "node scripts/build.mjs --quiet" &
|
||
@sleep 0.5
|
||
@php \
|
||
-d upload_max_filesize=8192M \
|
||
-d post_max_size=8704M \
|
||
-d memory_limit=512M \
|
||
-d max_execution_time=600 \
|
||
-d max_input_time=600 \
|
||
-S 127.0.0.1:8000 -t app/public/ app/router.php 2>&1 \
|
||
| stdbuf -oL grep -Ev '(Accepted|Closing|live-reload\.php|assets/|favicon)'
|
||
|
||
[group('dev')]
|
||
stop:
|
||
@pkill -f "php -S 127.0.0.1:8000" 2>/dev/null; \
|
||
pkill -f "chokidar.*assets/(css|js)" 2>/dev/null; \
|
||
echo "stopped"
|
||
|
||
[group('dev')]
|
||
logs:
|
||
@tail -n 20 error.log 2>/dev/null || echo "no error log"
|
||
|
||
# ============================================================================
|
||
# Build (JS/CSS bundling & minification)
|
||
# ============================================================================
|
||
|
||
[group('build')]
|
||
build:
|
||
@node scripts/build.mjs
|
||
|
||
[group('build')]
|
||
build-css:
|
||
@node scripts/build-css.mjs
|
||
|
||
[group('build')]
|
||
build-js:
|
||
@node scripts/build-js.mjs
|
||
|
||
# Diagnostic: report unused CSS symbols per bundle (rebuilds then reports).
|
||
# Does NOT strip anything. Run after changing templates/JS to refresh the corpus.
|
||
[group('build')]
|
||
css-report:
|
||
@node scripts/build-css.mjs
|
||
@node scripts/css-unused-report.mjs
|
||
|
||
[group('build')]
|
||
build-install:
|
||
@npm ci
|
||
|
||
[group('build')]
|
||
build-lint:
|
||
@just lint-css
|
||
@just lint-js
|
||
|
||
[group('build')]
|
||
build-check:
|
||
@echo "Checking if build output is up to date…"
|
||
@node scripts/check-build.mjs
|
||
|
||
# ============================================================================
|
||
# Deploy
|
||
# ============================================================================
|
||
|
||
[group('deploy')]
|
||
deploy: build deploy-code deploy-nginx deploy-deps deploy-migrate
|
||
@just deploy-env
|
||
@just deploy-verify-permissions
|
||
@echo ""
|
||
@echo "ℹ️ First deploy? Also run: just deploy-all-first"
|
||
@echo ""
|
||
|
||
[group('deploy')]
|
||
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 -az --info=progress2 --delete \
|
||
--exclude-from=.rsync-exclude \
|
||
app/ xamxam:/var/www/xamxam/
|
||
|
||
[group('deploy')]
|
||
deploy-deps:
|
||
# Sync composer.json + composer.lock to server, then run composer install
|
||
# (only if composer.lock checksum changed — skip expensive install otherwise)
|
||
rsync -v composer.json composer.lock xamxam:/var/www/xamxam/
|
||
ssh xamxam 'cd /var/www/xamxam && \
|
||
sed -i "s|\"app/src/\"|\"src/\"|" composer.json && \
|
||
if [ ! -f vendor/.composer-lock-checksum ] || \
|
||
[ "$(sha256sum composer.lock | cut -d" " -f1)" != "$(cat vendor/.composer-lock-checksum)" ]; then \
|
||
echo "→ composer.lock changed, installing dependencies…"; \
|
||
composer install --no-dev --no-interaction --optimize-autoloader && \
|
||
sha256sum composer.lock | cut -d" " -f1 > vendor/.composer-lock-checksum; \
|
||
else \
|
||
echo "→ composer.lock unchanged, dumping autoloader (new classes may exist)…"; \
|
||
composer dump-autoload --optimize --no-interaction; \
|
||
fi'
|
||
|
||
[group('deploy')]
|
||
deploy-migrate:
|
||
# Run pending DB migrations (creates DB from schema if missing, idempotent)
|
||
rsync -v scripts/migrate.sh xamxam:/tmp/migrate.sh
|
||
ssh xamxam "cd /var/www/xamxam && REPO_ROOT=/var/www/xamxam bash /tmp/migrate.sh"
|
||
ssh xamxam "rm -f /tmp/migrate.sh"
|
||
|
||
[group('deploy')]
|
||
deploy-cover-webp dry_run='':
|
||
# Backfill small WebP cover thumbnails on the server (idempotent).
|
||
# Dry-run by default; pass --no-dry-run to actually generate. Runs as the
|
||
# SSH user (member of the xamxam group, dirs are setgid group-writable) so
|
||
# no sudo is needed. Deploy as www-data if group-write ever changes.
|
||
rsync -v scripts/generate-cover-webp.php xamxam:/tmp/generate-cover-webp.php
|
||
ssh xamxam "cd /var/www/xamxam && php /tmp/generate-cover-webp.php {{dry_run}}"
|
||
ssh xamxam "rm -f /tmp/generate-cover-webp.php"
|
||
|
||
[group('deploy')]
|
||
deploy-env:
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
# Upload app/.env only if it exists locally; never overwrites a remote .env that already has APP_KEY.
|
||
if [ ! -f app/.env ]; then
|
||
echo "WARNING: app/.env not found locally — skipping."
|
||
exit 0
|
||
fi
|
||
if ssh xamxam '[ -f /var/www/xamxam/.env ]'; then
|
||
echo "Remote .env already exists — skipping to avoid overwriting key."
|
||
echo "Run 'just reencrypt-password' if you rotated APP_KEY."
|
||
else
|
||
rsync -v --progress app/.env xamxam:/var/www/xamxam/.env
|
||
ssh -t xamxam "sudo chmod 640 /var/www/xamxam/.env && sudo chown www-data:xamxam /var/www/xamxam/.env"
|
||
echo ".env uploaded."
|
||
fi
|
||
|
||
[group('deploy')]
|
||
reencrypt-password new_key_b64="":
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
# Re-encrypt the SMTP password in the remote DB after rotating APP_KEY.
|
||
# Usage:
|
||
# 1. Generate a new key: php -r "echo base64_encode(random_bytes(32));"
|
||
# 2. Run: just reencrypt-password <new_base64_key>
|
||
# 3. Update app/.env locally with the new key, then run: just deploy-env
|
||
if [ -z "{{new_key_b64}}" ]; then
|
||
echo "Usage: just reencrypt-password <new_base64_key>"
|
||
echo "Generate a key: php -r \"echo base64_encode(random_bytes(32));\""
|
||
exit 1
|
||
fi
|
||
# Run the re-encryption script on the server using the current key (from remote .env)
|
||
# and the supplied new key.
|
||
ssh xamxam "php /var/www/xamxam/scripts/reencrypt-smtp-password.php '{{new_key_b64}}' /var/www/xamxam/storage/xamxam.db"
|
||
|
||
[group('deploy')]
|
||
deploy-db:
|
||
@ssh xamxam '[ ! -f /var/www/xamxam/storage/xamxam.db ]' || (echo "ERROR: remote database already exists. Remove it manually if you intend to overwrite." && exit 1)
|
||
rsync -v --progress app/storage/xamxam.db xamxam:/var/www/xamxam/storage/xamxam.db
|
||
ssh xamxam "chown www-data:xamxam /var/www/xamxam/storage/xamxam.db && chmod 660 /var/www/xamxam/storage/xamxam.db"
|
||
|
||
[group('deploy')]
|
||
deploy-verify-permissions:
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
APP_DIR="/var/www/xamxam"
|
||
WEB_USER="www-data"
|
||
APP_GROUP="xamxam"
|
||
ERRORS=0
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
NC='\033[0m'
|
||
|
||
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
|
||
err() { printf "${RED}✗${NC} %s\n" "$*" >&2; ERRORS=$((ERRORS + 1)); }
|
||
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
|
||
|
||
printf "🔍 Verifying permissions on %s…\n\n" "$APP_DIR"
|
||
|
||
# ── Ownership ──────────────────────────────────────────────────────────────────
|
||
echo "── Ownership ───────────────────────────────────"
|
||
while IFS= read -r line; do
|
||
owner=$(echo "$line" | awk '{print $1}')
|
||
group=$(echo "$line" | awk '{print $2}')
|
||
path=$(echo "$line" | awk '{print $NF}')
|
||
if [ "$owner" != "$WEB_USER" ] || [ "$group" != "$APP_GROUP" ]; then
|
||
err "$path → $owner:$group (expected $WEB_USER:$APP_GROUP)"
|
||
else
|
||
ok "$path → $owner:$group"
|
||
fi
|
||
done < <(ssh xamxam "stat -c '%U %G %n' $APP_DIR $APP_DIR/app $APP_DIR/storage $APP_DIR/var 2>/dev/null")
|
||
|
||
# ── Key directories: 2775 ─────────────────────────────────────────────────────
|
||
echo "── Directory permissions (expected 2775) ───────"
|
||
while IFS= read -r line; do
|
||
perms=$(echo "$line" | awk '{print $1}')
|
||
path=$(echo "$line" | awk '{print $NF}')
|
||
if [ "$perms" != "drwxrwsr-x" ]; then
|
||
err "$path → $perms (expected drwxrwsr-x / 2775)"
|
||
else
|
||
ok "$path → $perms"
|
||
fi
|
||
done < <(ssh xamxam "find $APP_DIR -maxdepth 2 -type d -exec stat -c '%A %n' {} \\; 2>/dev/null | sort")
|
||
|
||
# ── Key files: 664 ────────────────────────────────────────────────────────────
|
||
echo "── File permissions (expected 664 / 660) ───────"
|
||
# Spot-check a few critical files
|
||
while IFS= read -r path; do
|
||
perms=$(ssh xamxam "stat -c '%a %U %G' '$path' 2>/dev/null" || echo "MISSING")
|
||
if [ "$perms" = "MISSING" ]; then
|
||
err "$path → FILE MISSING"
|
||
else
|
||
perm_num=$(echo "$perms" | awk '{print $1}')
|
||
owner=$(echo "$perms" | awk '{print $2}')
|
||
group=$(echo "$perms" | awk '{print $3}')
|
||
case "$path" in
|
||
*/storage/xamxam.db|*/storage/*.db)
|
||
expected_perm="660" ;;
|
||
*)
|
||
expected_perm="664" ;;
|
||
esac
|
||
if [ "$perm_num" != "$expected_perm" ]; then
|
||
err "$path → $perm_num ($owner:$group), expected $expected_perm $WEB_USER:$APP_GROUP"
|
||
elif [ "$owner" != "$WEB_USER" ]; then
|
||
err "$path → owner $owner, expected $WEB_USER (perm $perm_num OK)"
|
||
else
|
||
ok "$path → $perm_num $owner:$group"
|
||
fi
|
||
fi
|
||
done < <(printf '%s\n' \
|
||
"$APP_DIR/storage/xamxam.db")
|
||
|
||
# ── var/ subdirectories must be writable ──────────────────────────────────────
|
||
echo "── var/ writability ────────────────────────────"
|
||
for subdir in cache logs tmp; do
|
||
if ssh xamxam "[ -w /var/www/xamxam/var/$subdir ]"; then
|
||
ok "var/$subdir → writable"
|
||
else
|
||
err "var/$subdir → NOT WRITABLE"
|
||
fi
|
||
done
|
||
|
||
# ── storage/cache/rate_limit writable ─────────────────────────────────────────
|
||
if ssh xamxam "[ -w /var/www/xamxam/storage/cache/rate_limit ]"; then
|
||
ok "storage/cache/rate_limit → writable"
|
||
else
|
||
err "storage/cache/rate_limit → NOT WRITABLE"
|
||
fi
|
||
|
||
# ── /var/log/xamxam writable (app log dir — NullHandler if missing) ─────────
|
||
if ssh xamxam "[ -d /var/log/xamxam ] && [ -w /var/log/xamxam ]"; then
|
||
ok "/var/log/xamxam → writable"
|
||
else
|
||
err "/var/log/xamxam → MISSING or NOT WRITABLE (run: sudo bash /tmp/deploy-server.sh)"
|
||
fi
|
||
|
||
# ── .env must be 640 ──────────────────────────────────────────────────────────
|
||
env_perm=$(ssh xamxam "stat -c '%a' /var/www/xamxam/.env 2>/dev/null" || echo "")
|
||
if [ "$env_perm" = "640" ]; then
|
||
ok ".env → 640"
|
||
elif [ -z "$env_perm" ]; then
|
||
warn ".env → MISSING"
|
||
else
|
||
err ".env → $env_perm (expected 640)"
|
||
fi
|
||
|
||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||
echo ""
|
||
if [ "$ERRORS" -eq 0 ]; then
|
||
printf "${GREEN}✅ All permissions OK${NC}\n"
|
||
else
|
||
printf "${RED}❌ %d permission error(s) found${NC}\n" "$ERRORS"
|
||
printf "${YELLOW}Fix with: sudo bash /tmp/deploy-server.sh${NC}\n"
|
||
exit 1
|
||
fi
|
||
|
||
[group('deploy')]
|
||
deploy-nginx:
|
||
# Upload nginx config to the server, test it, and reload.
|
||
# Uses the scripts/deploy-server.sh helper that handles the nginx
|
||
# config installation and reload (steps 2-4).
|
||
@echo "📋 Deploying nginx configuration…"
|
||
rsync -v nginx/xamxam.conf xamxam:/tmp/xamxam.conf
|
||
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
|
||
ssh -t xamxam "sudo DEPLOY_USER=\$USER bash /tmp/deploy-server.sh"
|
||
ssh xamxam "rm -f /tmp/deploy-server.sh /tmp/xamxam.conf"
|
||
|
||
[group('deploy')]
|
||
deploy-script script_name:
|
||
# Generic script deployer (e.g., just deploy-script setup-server)
|
||
rsync -v scripts/{{script_name}}.sh xamxam:/tmp/{{script_name}}.sh
|
||
@echo ""
|
||
@echo "Script uploaded. SSH into the server and run:"
|
||
@echo ""
|
||
@echo " sudo DEPLOY_USER=\$USER bash /tmp/{{script_name}}.sh"
|
||
@echo ""
|
||
|
||
[group('deploy')]
|
||
migrate-log-names apply="":
|
||
# Rename pre-existing logs to the xamxam-{service}-{date}.log convention.
|
||
# Dry-run by default; pass apply="--apply" to actually rename on the server.
|
||
# Requires sudo — renames files under /var/log and /var/www/xamxam/storage/logs.
|
||
rsync -v scripts/migrate-log-names.sh xamxam:/tmp/migrate-log-names.sh
|
||
ssh -t xamxam "sudo bash /tmp/migrate-log-names.sh {{apply}}"
|
||
ssh xamxam "rm -f /tmp/migrate-log-names.sh"
|
||
|
||
[group('deploy')]
|
||
deploy-backup-script:
|
||
# Upload backup-sqlite.sh to /usr/local/bin on the server (requires sudo)
|
||
# Run once after initial deploy or when the backup script changes.
|
||
@echo "📋 Deploying backup script…"
|
||
rsync -v scripts/backup-sqlite.sh xamxam:/tmp/backup-sqlite.sh
|
||
ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/backup-sqlite.sh /usr/local/bin/backup-sqlite.sh && rm -f /tmp/backup-sqlite.sh"
|
||
@echo "✅ backup-sqlite.sh installed to /usr/local/bin/"
|
||
@echo "📋 Deploying backup watchdog…"
|
||
rsync -v scripts/backup-watchdog.php xamxam:/tmp/backup-watchdog.php
|
||
ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/backup-watchdog.php /usr/local/bin/backup-watchdog.php && rm -f /tmp/backup-watchdog.php"
|
||
@echo "✅ backup-watchdog.php installed to /usr/local/bin/"
|
||
@echo "📋 Deploying Nextcloud sync…"
|
||
rsync -v scripts/nextcloud-sync.php xamxam:/tmp/nextcloud-sync.php
|
||
ssh -t xamxam "sudo install -o root -g root -m 755 /tmp/nextcloud-sync.php /usr/local/bin/nextcloud-sync.php && rm -f /tmp/nextcloud-sync.php"
|
||
@echo "✅ nextcloud-sync.php installed to /usr/local/bin/"
|
||
|
||
[group('deploy')]
|
||
deploy-backup-cron:
|
||
# Install cron jobs for hourly (30d retention) and daily (90d) backups.
|
||
# Uses /etc/cron.d/xamxam-backup (system cron format: minute hour dom month dow user command)
|
||
# Creates backup directory and log file on the server.
|
||
@echo "📋 Installing backup cron jobs…"
|
||
rsync -v deploy/xamxam-backup.cron xamxam:/tmp/xamxam-backup.cron
|
||
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-backup.cron /etc/cron.d/xamxam-backup && rm -f /tmp/xamxam-backup.cron"
|
||
ssh -t xamxam "sudo mkdir -p /var/backups/xamxam && sudo chown www-data:xamxam /var/backups/xamxam && sudo chmod 775 /var/backups/xamxam"
|
||
ssh -t xamxam "sudo touch /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-backup-\$(date +%Y-%m-%d).log"
|
||
@echo "✅ Cron jobs installed."
|
||
@echo " Cron file: /etc/cron.d/xamxam-backup"
|
||
@echo " Backup dir: /var/backups/xamxam"
|
||
@echo " Log file: /var/log/xamxam-backup-\$(date +%Y-%m-%d).log"
|
||
@echo ""
|
||
@echo "Verify with: just deploy-check-backup-log"
|
||
|
||
[group('deploy')]
|
||
deploy-backup: deploy-backup-script deploy-backup-cron
|
||
# One-shot: deploy backup script + install cron jobs + set up directories.
|
||
|
||
[group('deploy')]
|
||
deploy-check-backup-log sync='':
|
||
# Tail the backup log, then optionally run the Nextcloud sync manually.
|
||
# Usage: `just deploy-check-backup-log` (tail only) or `just deploy-check-backup-log 1` (tail + sync).
|
||
ssh xamxam "tail -20 /var/log/xamxam-backup-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
|
||
# Accept both `1` and `sync=1` for convenience; anything non-empty other than
|
||
# the literal "sync=1" positional string is also treated as a truthy request.
|
||
@case "{{sync}}" in \
|
||
1|sync|sync=1) \
|
||
echo "===== Running manual Nextcloud sync ====="; \
|
||
ssh -t xamxam "sudo -u www-data php /usr/local/bin/nextcloud-sync.php; echo exit=\$?";; \
|
||
''|0|no) \
|
||
echo "(sync skipped — pass 1 to also run it)";; \
|
||
*) \
|
||
echo "===== Running manual Nextcloud sync ====="; \
|
||
ssh -t xamxam "sudo -u www-data php /usr/local/bin/nextcloud-sync.php; echo exit=\$?";; \
|
||
esac
|
||
|
||
[group('deploy')]
|
||
deploy-list-backups:
|
||
# List all existing backups on the server (most recent last).
|
||
ssh xamxam "ls -lth /var/backups/xamxam/ 2>/dev/null || echo 'No backups yet.'"
|
||
|
||
[group('deploy')]
|
||
deploy-cleanup-cron:
|
||
# Install cron job for orphaned draft cleanup (every 2 days, 7-day threshold).
|
||
# Creates /etc/cron.d/xamxam-cleanup and log file on the server.
|
||
@echo "📋 Installing draft cleanup cron job…"
|
||
rsync -v scripts/cleanup-drafts.php xamxam:/tmp/cleanup-drafts.php
|
||
ssh xamxam "chmod 755 /tmp/cleanup-drafts.php"
|
||
rsync -v deploy/xamxam-cleanup.cron xamxam:/tmp/xamxam-cleanup.cron
|
||
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-cleanup.cron /etc/cron.d/xamxam-cleanup && rm -f /tmp/xamxam-cleanup.cron"
|
||
ssh -t xamxam "sudo touch /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log"
|
||
@echo "✅ Cleanup cron installed."
|
||
@echo " Cron file: /etc/cron.d/xamxam-cleanup"
|
||
@echo " Script: /tmp/cleanup-drafts.php"
|
||
@echo " Log file: /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log"
|
||
@echo ""
|
||
@echo "Verify with: just deploy-check-cleanup-log"
|
||
|
||
[group('deploy')]
|
||
deploy-check-cleanup-log:
|
||
ssh xamxam "tail -20 /var/log/xamxam-cleanup-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
|
||
|
||
deploy-tmp-cleanup-cron:
|
||
# Install cron job for abandoned-upload garbage collection (hourly, 2h threshold).
|
||
# Creates /etc/cron.d/xamxam-tmp-cleanup and log file on the server.
|
||
@echo "📋 Installing abandoned-upload cleanup cron job…"
|
||
rsync -v scripts/cleanup-tmp-uploads.php xamxam:/tmp/cleanup-tmp-uploads.php
|
||
ssh xamxam "chmod 755 /tmp/cleanup-tmp-uploads.php"
|
||
rsync -v deploy/xamxam-tmp-cleanup.cron xamxam:/tmp/xamxam-tmp-cleanup.cron
|
||
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-tmp-cleanup.cron /etc/cron.d/xamxam-tmp-cleanup && rm -f /tmp/xamxam-tmp-cleanup.cron"
|
||
ssh -t xamxam "sudo touch /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log"
|
||
@echo "✅ Abandoned-upload cleanup cron installed."
|
||
@echo " Cron file: /etc/cron.d/xamxam-tmp-cleanup"
|
||
@echo " Script: /tmp/cleanup-tmp-uploads.php"
|
||
@echo " Log file: /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log"
|
||
@echo ""
|
||
@echo "Verify with: just deploy-check-tmp-cleanup-log"
|
||
|
||
[group('deploy')]
|
||
deploy-check-tmp-cleanup-log:
|
||
ssh xamxam "tail -20 /var/log/xamxam-tmp-cleanup-\$(date +%Y-%m-%d).log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
|
||
|
||
[group('deploy')]
|
||
deploy-logrotate:
|
||
# Install /etc/logrotate.d/xamxam for app + nginx + cron logs.
|
||
@echo "📋 Installing logrotate config…"
|
||
rsync -v deploy/xamxam-logrotate xamxam:/tmp/xamxam-logrotate
|
||
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-logrotate /etc/logrotate.d/xamxam && sudo logrotate -d /etc/logrotate.d/xamxam && rm -f /tmp/xamxam-logrotate"
|
||
@echo "✅ logrotate config installed and validated (dry-run)."
|
||
@echo " Config: /etc/logrotate.d/xamxam"
|
||
|
||
[group('deploy')]
|
||
test-restore remote_gz_path:
|
||
# Test-restore a production backup snapshot to a local temp DB and verify.
|
||
# Usage: just test-restore /var/backups/xamxam/db-2026-05-11T14-00-00.db.gz
|
||
@scp xamxam:'{{remote_gz_path}}' /tmp/xamxam-restore-test.db.gz
|
||
@gunzip -c /tmp/xamxam-restore-test.db.gz > /tmp/xamxam-restore-test.db
|
||
@echo "Tables in snapshot:"
|
||
@sqlite3 /tmp/xamxam-restore-test.db ".tables"
|
||
@echo ""
|
||
@echo "Thesis count:"
|
||
@sqlite3 /tmp/xamxam-restore-test.db "SELECT COUNT(*) FROM theses WHERE deleted_at IS NULL;"
|
||
@echo ""
|
||
@echo "✅ Snapshot is valid. Remove temp files:"
|
||
@echo " rm /tmp/xamxam-restore-test.db /tmp/xamxam-restore-test.db.gz"
|
||
@rm -f /tmp/xamxam-restore-test.db.gz
|
||
|
||
[group('deploy')]
|
||
trigger-backup:
|
||
# Manually trigger the backup script on the server now (doesn't wait for cron).
|
||
ssh -t xamxam "sudo -u www-data /usr/local/bin/backup-sqlite.sh"
|
||
|
||
[group('deploy')]
|
||
deploy-migrate-storage dry_run='' target_host='xamxam':
|
||
# Run the storage path migration on the remote server.
|
||
# Usage:
|
||
# just deploy-migrate-storage # apply migration
|
||
# just deploy-migrate-storage --dry-run # dry-run only
|
||
rsync -v scripts/migrate-storage-paths.php {{target_host}}:/var/www/xamxam/migrate-storage-paths.php
|
||
ssh {{target_host}} 'cd /var/www/xamxam && php migrate-storage-paths.php {{dry_run}}'
|
||
ssh {{target_host}} 'rm -f /var/www/xamxam/migrate-storage-paths.php'
|
||
|
||
[group('deploy')]
|
||
deploy-all-first: deploy deploy-backup deploy-cleanup-cron deploy-tmp-cleanup-cron deploy-logrotate
|
||
# One-shot: full initial deploy including backup and cleanup cron jobs.
|
||
|
||
# One-shot remote provisioning for a fresh xamxam server.
|
||
# Chains: server .env/APP_KEY (idempotent, never overwrites an existing key) →
|
||
# full deploy → nginx → backup + cleanup + logrotate, then prints the
|
||
# first-admin and service-credential follow-ups.
|
||
[group('deploy')]
|
||
provision-server:
|
||
@bash scripts/provision-server-env.sh xamxam
|
||
@just deploy
|
||
@just deploy-backup
|
||
@just deploy-cleanup-cron
|
||
@just deploy-logrotate
|
||
@echo ""
|
||
@echo "✅ Server provisioning complete."
|
||
@echo ""
|
||
@echo "Next, from the server's /admin/account:"
|
||
@echo " 1. Set the admin password (fresh DB starts unauthenticated)"
|
||
@echo " 2. Configure SMTP + PeerTube credentials (stored encrypted in the DB)"
|
||
@echo " and verify Nextcloud sync in /admin"
|
||
|
||
# ============================================================================
|
||
# Testing
|
||
# ============================================================================
|
||
|
||
[group('test')]
|
||
test:
|
||
# Run all PHPUnit tests
|
||
@vendor/bin/phpunit tests/phpunit/
|
||
|
||
[group('test')]
|
||
test-coverage:
|
||
# Generate HTML coverage report in coverage/
|
||
@vendor/bin/phpunit --coverage-html coverage/ tests/phpunit/
|
||
|
||
[group('test')]
|
||
smoke-password-reset:
|
||
# End-to-end smoke test for the password-reset flow (throwaway DB, no email).
|
||
@php scripts/smoke-test-password-reset.php
|
||
|
||
[group('test')]
|
||
lint-php:
|
||
# Static analysis (phpstan) + coding standards (php-cs-fixer)
|
||
@vendor/bin/phpstan analyse --memory-limit=512M
|
||
@vendor/bin/php-cs-fixer check --no-interaction
|
||
|
||
[group('test')]
|
||
lint-css:
|
||
# Lint CSS with biome
|
||
@npx biome lint app/public/assets/css/
|
||
|
||
[group('test')]
|
||
lint-js:
|
||
# Lint JS/build scripts with biome
|
||
@npx biome lint app/public/assets/js/app/ scripts/
|
||
|
||
[group('test')]
|
||
lint:
|
||
# Run all linters
|
||
@just lint-php
|
||
@just lint-css
|
||
@just lint-js
|
||
|
||
[group('test')]
|
||
fix:
|
||
# Auto-fix: biome (JS/CSS formatting + lint) + php-cs-fixer (PHP coding standards)
|
||
@npx biome check --write --unsafe app/public/assets/css/ app/public/assets/js/app/ scripts/
|
||
@vendor/bin/php-cs-fixer fix --no-interaction
|
||
|
||
# ============================================================================
|
||
# Database
|
||
# ============================================================================
|
||
|
||
[group('database')]
|
||
migrate:
|
||
@echo "Running migrations…"
|
||
@bash scripts/migrate.sh
|
||
|
||
[group('database')]
|
||
init-db:
|
||
@sqlite3 app/storage/xamxam.db < app/storage/schema.sql
|
||
@sqlite3 app/storage/xamxam.db "SELECT COUNT(*) || ' tables' FROM sqlite_master WHERE type='table';"
|
||
|
||
[group('database')]
|
||
reset-db:
|
||
@rm -f app/storage/xamxam.db
|
||
@just init-db
|
||
|
||
[group('database')]
|
||
query:
|
||
@sqlite3 app/storage/xamxam.db
|
||
|
||
[group('database')]
|
||
backup:
|
||
@sqlite3 app/storage/xamxam.db .dump > app/storage/backup_$(date +%Y%m%d_%H%M%S).sql
|
||
|
||
[group('database')]
|
||
fix-finality-types:
|
||
# Rename finality types from old forms to canonical names
|
||
# Approfondi → Approfondie, Didactique → Enseignement, Spécialisé → Spécialisée
|
||
@php scripts/fix-finality-types.php
|
||
|
||
[group('database')]
|
||
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
|
||
# ============================================================================
|
||
|
||
[group('utils')]
|
||
creds-test:
|
||
# Probe the SMTP + PeerTube credentials stored in the DB (gum UI).
|
||
# --instance / --channel to override the stored PeerTube values
|
||
# --show-pwd to reveal the decrypted password (interactive confirm)
|
||
@bash scripts/creds-test.sh
|
||
|
||
[group('utils')]
|
||
app-token:
|
||
# Probe whether a long-lived PeerTube app token is obtainable.
|
||
# --instance https://videos.erg.be --client <id> --secret <secret>
|
||
@bash scripts/app-token.sh
|
||
|
||
[group('utils')]
|
||
sso-diagnose:
|
||
# Verify every claim in docs/peertube-sso-incident.md (DNS, IdP discovery,
|
||
# PeerTube OAuth, SMTP AUTH, header propagation) and append to sso-diagnose.log.
|
||
# --instance <url> --idp <host> --smtp <host:port> --db <path> --log <path>
|
||
@bash scripts/sso-diagnose.sh
|
||
|
||
[group('utils')]
|
||
clean:
|
||
@rm -f app/error.log
|
||
@rm -rf app/storage/cache/rate_limit/*
|
||
@rm -f /tmp/xamxam-*.log /tmp/xamxam-*.pid
|
||
|
||
[group('utils')]
|
||
cleanup-drafts dry_run='':
|
||
# List (dry-run) or delete orphaned draft theses older than 7 days (168h).
|
||
# Pass --no-dry-run to actually delete. Override the age with OLDER_THAN_HOURS:
|
||
# just cleanup-drafts --no-dry-run
|
||
# OLDER_THAN_HOURS=24 just cleanup-drafts
|
||
@php scripts/cleanup-drafts.php {{dry_run}}
|
||
|
||
cleanup-tmp-uploads dry_run='':
|
||
# List (dry-run) or delete abandoned FilePond uploads (>2h or missing session).
|
||
# Pass --no-dry-run to actually delete. Override the age with
|
||
# TMP_UPLOAD_MAX_AGE_SECONDS:
|
||
# just cleanup-tmp-uploads --no-dry-run
|
||
@php scripts/cleanup-tmp-uploads.php {{dry_run}}
|