Files
xamxam/justfile

388 lines
16 KiB
Makefile
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
default:
@just --list
# XAMXAM Justfile
# ============================================================================
# Development
# ============================================================================
[group('dev')]
setup:
@bash scripts/setup-dev.sh
[group('dev')]
serve: migrate
@xdg-open http://127.0.0.1:8000 &
@xdg-open http://127.0.0.1:8000/admin/ &
@php \
-d upload_max_filesize=512M \
-d post_max_size=520M \
-d memory_limit=256M \
-d max_execution_time=300 \
-d max_input_time=300 \
-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 && echo "stopped" || echo "no server running"
[group('dev')]
logs:
@tail -n 20 error.log 2>/dev/null || echo "no error log"
# ============================================================================
# Deploy
# ============================================================================
[group('deploy')]
deploy:
# Main deploy (code + assets) then run any pending DB migrations
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/theses' \
--exclude 'storage/covers' \
--exclude 'storage/backup_*' \
--exclude 'storage/cache/*' \
--exclude 'storage/maintenance.flag' \
--exclude 'storage/fixtures' \
--exclude 'storage/docs' \
--exclude 'var/' \
app/ xamxam:/var/www/xamxam/
# Upload deploy-server.sh for post-deploy permission fix
rsync -v scripts/deploy-server.sh xamxam:/tmp/deploy-server.sh
ssh -t xamxam "sudo bash /tmp/deploy-server.sh"
ssh xamxam "rm -f /tmp/deploy-server.sh"
ssh xamxam "mkdir -p /var/www/xamxam/var/{cache,logs,tmp}"
ssh xamxam "cd /var/www/xamxam && php -r 'if (!file_exists(\"/var/www/xamxam/storage/xamxam.db\")) { \$db = new PDO(\"sqlite:/var/www/xamxam/storage/xamxam.db\"); \$db->exec(file_get_contents(\"/var/www/xamxam/storage/schema.sql\")); echo \"Database created from schema.\\n\"; } else { echo \"Database already exists.\\n\"; }'"
# Run pending migrations
ssh xamxam "cd /var/www/xamxam && bash scripts/migrate.sh"
# Deploy nginx configuration
@just deploy-nginx
# Sync .env separately (excluded above to avoid accidental overwrite on subsequent deploys)
@just deploy-env
@just deploy-verify-permissions
@echo ""
@echo " First deploy? Also run: just deploy-backup"
@echo ""
[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 xamxam "chmod 640 /var/www/xamxam/.env && 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 $3}')
group=$(echo "$line" | awk '{print $4}')
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/.env" \
"$APP_DIR/app/router.php" \
"$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
# ── .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')]
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/"
[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:www-data /var/backups/xamxam && sudo chmod 755 /var/backups/xamxam"
ssh -t xamxam "sudo touch /var/log/sqlite-backup.log && sudo chown www-data:www-data /var/log/sqlite-backup.log && sudo chmod 644 /var/log/sqlite-backup.log"
@echo "✅ Cron jobs installed."
@echo " Cron file: /etc/cron.d/xamxam-backup"
@echo " Backup dir: /var/backups/xamxam"
@echo " Log file: /var/log/sqlite-backup.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:
ssh xamxam "tail -20 /var/log/sqlite-backup.log 2>/dev/null || echo '(log file empty or missing — will be created on first cron run)'"
[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')]
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"
# ============================================================================
# Testing
# ============================================================================
[group('test')]
test:
# Run all tests. To run a subset, use:
# php app/tests/Unit/DatabaseTest.php
# php app/tests/Integration/SearchTest.php
@php app/tests/run-tests.php
[group('test')]
lint-biome:
@biome lint app/public/assets/js/
[group('test')]
phpstan:
@vendor/bin/phpstan analyse --memory-limit=512M
[group('test')]
cs-check:
@vendor/bin/php-cs-fixer check --no-interaction
[group('test')]
cs-fix:
@vendor/bin/php-cs-fixer fix --no-interaction
[group('test')]
syntax:
@find app/ -name '*.php' -exec php -l {} \; 2>/dev/null | grep -v 'No syntax errors' || true
@echo '✅ Syntax OK'
# ============================================================================
# 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')]
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
# ============================================================================
# Utils
# ============================================================================
[group('utils')]
clean:
@rm -f app/error.log
@rm -rf app/storage/cache/rate_limit/*
@rm -f /tmp/xamxam-*.log /tmp/xamxam-*.pid