name: Deploy on: push: branches: [main] jobs: deploy: runs-on: vps-host steps: - uses: actions/checkout@v4.2.2 - name: Supply chain checks (production dependencies) run: | set -euo pipefail cd api npm ci npm audit --omit=dev --audit-level=high --json > /tmp/skymoney-api-audit.json || true node -e ' const fs = require("fs"); const report = JSON.parse(fs.readFileSync("/tmp/skymoney-api-audit.json", "utf8")); const vulnerabilities = report.vulnerabilities || {}; const allowlisted = new Set(["fast-jwt", "@fastify/jwt"]); const blockers = []; for (const [name, vuln] of Object.entries(vulnerabilities)) { const severity = String(vuln?.severity || "").toLowerCase(); if (severity !== "high" && severity !== "critical") continue; if (allowlisted.has(name)) continue; blockers.push({ name, severity }); } if (blockers.length > 0) { console.error("Blocking high/critical vulnerabilities found:"); for (const blocker of blockers) { console.error(` - ${blocker.name} (${blocker.severity})`); } process.exit(1); } const allowedPresent = Object.keys(vulnerabilities).filter((name) => allowlisted.has(name)); if (allowedPresent.length > 0) { console.warn("Allowed advisory exception(s) present:", allowedPresent.join(", ")); } else { console.log("No allowlisted API advisories present."); } ' cd ../web npm ci npm audit --omit=dev --audit-level=high - name: Build Web run: | cd web npm run build - name: Deploy with Docker Compose run: | set -euo pipefail # Fail fast if sudo requires interactive password in runner context sudo -n docker ps >/dev/null # Deploy directory APP_DIR=/opt/skymoney mkdir -p $APP_DIR # Sync repo to server (excluding node_modules, dist, etc) rsync -a --delete \ --exclude=node_modules \ --exclude=dist \ --exclude=.git \ --exclude=.gitea \ --exclude=backups \ --exclude=forensics \ --exclude=exporting \ ./ $APP_DIR/ # Copy built web to shared volume mkdir -p /var/www/skymoney/dist cp -r web/dist/* /var/www/skymoney/dist/ cd $APP_DIR # Validate migration target before touching containers export EXPECTED_PROD_DB_HOST="${EXPECTED_PROD_DB_HOST:-postgres}" export EXPECTED_PROD_DB_NAME="${EXPECTED_PROD_DB_NAME:-skymoney}" chmod +x ./scripts/validate-prod-db-target.sh ./scripts/guard-prod-volume.sh ./scripts/backup.sh bash ./scripts/validate-prod-db-target.sh PROD_DB_VOLUME_NAME="${PROD_DB_VOLUME_NAME:-skymoney_pgdata}" \ ALLOW_EMPTY_PROD_VOLUME="${ALLOW_EMPTY_PROD_VOLUME:-0}" \ PROD_VOLUME_GUARD_TIMEOUT_SEC="${PROD_VOLUME_GUARD_TIMEOUT_SEC:-20}" \ DOCKER_CMD="sudo -n docker" \ bash ./scripts/guard-prod-volume.sh # Build and start all services sudo -n docker-compose -p skymoney up -d --build # Wait for database to be ready sleep 10 # Mandatory pre-migration backup export EXPECTED_BACKUP_DB_HOST="${EXPECTED_BACKUP_DB_HOST:-127.0.0.1}" export EXPECTED_BACKUP_DB_NAME="${EXPECTED_BACKUP_DB_NAME:-skymoney}" BACKUP_ENFORCE_TARGET_CHECK=1 \ EXPECTED_BACKUP_DB_HOST="$EXPECTED_BACKUP_DB_HOST" \ EXPECTED_BACKUP_DB_NAME="$EXPECTED_BACKUP_DB_NAME" \ BACKUP_DIR=/opt/skymoney/backups \ bash ./scripts/backup.sh # Run Prisma migrations inside the API container sudo -n docker-compose -p skymoney exec -T api npx prisma migrate deploy - name: Reload Nginx run: sudo systemctl reload nginx