From d170d6f72904ff97998d52bc1a91467d1d4a8bb6 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Sun, 21 Dec 2025 15:23:04 +0100 Subject: [PATCH] ci: Auto-deploy on push via SSH - Gitea Actions workflow now syncs repo to server, builds images, restarts containers, and runs health checks - Removed all hardcoded secrets from scripts/deploy.sh - Added CI/CD documentation and ignored .env.deploy NOTE: Existing secrets previously committed must be rotated. --- .gitea/workflows/deploy.yml | 192 ++++++++++++++++++++++++++++++++++-- .gitignore | 1 + ops/CI_CD.md | 42 ++++++++ scripts/deploy.sh | 64 ++++++------ 4 files changed, 253 insertions(+), 46 deletions(-) create mode 100644 ops/CI_CD.md diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index e54c225..3203071 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,22 +1,194 @@ -name: Pounce CI +name: Deploy Pounce (Auto) on: push: branches: [main] jobs: - info: + deploy: runs-on: ubuntu-latest steps: - - name: Build Info + - name: Checkout + uses: actions/checkout@v4 + + - name: Install deploy tooling + run: | + apt-get update + apt-get install -y --no-install-recommends openssh-client rsync ca-certificates + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null + + - name: Sync repository to server + run: | + rsync -az --delete \ + -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes" \ + --exclude ".git" \ + --exclude "frontend/node_modules" \ + --exclude "frontend/.next" \ + --exclude "**/__pycache__" \ + --exclude "**/*.pyc" \ + ./ \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/" + + - name: Deploy (build + restart + health check) + env: + DEPLOY_SUDO_PASSWORD: ${{ secrets.DEPLOY_SUDO_PASSWORD }} + # App secrets (used to generate backend env file on server) + DATABASE_URL: ${{ secrets.DATABASE_URL }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} + STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} + STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + GH_OAUTH_SECRET: ${{ secrets.GH_OAUTH_SECRET }} + CZDS_USERNAME: ${{ secrets.CZDS_USERNAME }} + CZDS_PASSWORD: ${{ secrets.CZDS_PASSWORD }} + run: | + ssh -i ~/.ssh/deploy_key "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" << 'DEPLOY_EOF' + set -euo pipefail + + # Use sudo non-interactively (password supplied via env) + sudo_cmd() { + printf '%s\n' "$DEPLOY_SUDO_PASSWORD" | sudo -S "$@" + } + + # Ensure dirs + sudo_cmd mkdir -p /data/pounce/env /data/pounce/zones + sudo_cmd chmod -R 755 /data/pounce || true + + # Generate backend env file from pipeline-provided secrets (never echo values) + sudo_cmd python3 - <<'PY' +import os +from pathlib import Path + +env = { + # Core + "ENVIRONMENT": "production", + "ENABLE_SCHEDULER": "true", + "COOKIE_SECURE": "true", + "CORS_ORIGINS": "https://pounce.ch,https://www.pounce.ch", + "SITE_URL": "https://pounce.ch", + "FRONTEND_URL": "https://pounce.ch", + + # Data dirs + "CZDS_DATA_DIR": "/data/czds", + "SWITCH_DATA_DIR": "/data/switch", + "ZONE_RETENTION_DAYS": "3", + + # DB/Redis + "DATABASE_URL": os.environ["DATABASE_URL"], + "REDIS_URL": "redis://pounce-redis:6379/0", + + # Auth + "SECRET_KEY": os.environ["SECRET_KEY"], + "JWT_SECRET": os.environ["SECRET_KEY"], + + # SMTP + "SMTP_HOST": "smtp.zoho.eu", + "SMTP_PORT": "465", + "SMTP_USER": "hello@pounce.ch", + "SMTP_PASSWORD": os.environ["SMTP_PASSWORD"], + "SMTP_FROM_EMAIL": "hello@pounce.ch", + "SMTP_FROM_NAME": "pounce", + "SMTP_USE_TLS": "false", + "SMTP_USE_SSL": "true", + + # Stripe + "STRIPE_SECRET_KEY": os.environ["STRIPE_SECRET_KEY"], + "STRIPE_PUBLISHABLE_KEY": "pk_live_51ScLbjCtFUamNRpNeFugrlTIYhszbo8GovSGiMnPwHpZX9p3SGtgG8iRHYRIlAtg9M9sl3mvT5r8pwXP3mOsPALG00Wk3j0wH4", + "STRIPE_PRICE_TRADER": "price_1ScRlzCtFUamNRpNQdMpMzxV", + "STRIPE_PRICE_TYCOON": "price_1SdwhSCtFUamNRpNEXTSuGUc", + "STRIPE_WEBHOOK_SECRET": os.environ["STRIPE_WEBHOOK_SECRET"], + + # OAuth + "GOOGLE_CLIENT_ID": "865146315769-vi7vcu91d3i7huv8ikjun52jo9ob7spk.apps.googleusercontent.com", + "GOOGLE_CLIENT_SECRET": os.environ["GOOGLE_CLIENT_SECRET"], + "GOOGLE_REDIRECT_URI": "https://pounce.ch/api/v1/oauth/google/callback", + + "GITHUB_CLIENT_ID": "Ov23liBjROk39vYXi3G5", + "GITHUB_CLIENT_SECRET": os.environ["GH_OAUTH_SECRET"], + "GITHUB_REDIRECT_URI": "https://pounce.ch/api/v1/oauth/github/callback", + + # CZDS + "CZDS_USERNAME": os.environ["CZDS_USERNAME"], + "CZDS_PASSWORD": os.environ["CZDS_PASSWORD"], +} + +lines = [] +for k, v in env.items(): + if v is None: + continue + lines.append(f"{k}={v}") + +path = Path("/data/pounce/env/backend.env") +path.write_text("\n".join(lines) + "\n") +PY + + # Build images from synced repo + cd "${{ secrets.DEPLOY_PATH }}" + sudo_cmd docker build -t pounce-backend:latest backend + sudo_cmd docker build \ + --build-arg NEXT_PUBLIC_API_URL=https://api.pounce.ch \ + --build-arg BACKEND_URL=http://pounce-backend:8000 \ + -t pounce-frontend:latest \ + frontend + + # Deploy backend + sudo_cmd docker stop pounce-backend 2>/dev/null || true + sudo_cmd docker rm pounce-backend 2>/dev/null || true + sudo_cmd docker run -d \ + --name pounce-backend \ + --network coolify \ + --restart unless-stopped \ + --shm-size=8g \ + --env-file /data/pounce/env/backend.env \ + -v /data/pounce/zones:/data \ + -l "traefik.enable=true" \ + -l "traefik.http.routers.pounce-api.rule=Host(\`api.pounce.ch\`)" \ + -l "traefik.http.routers.pounce-api.entryPoints=https" \ + -l "traefik.http.routers.pounce-api.tls=true" \ + -l "traefik.http.routers.pounce-api.tls.certresolver=letsencrypt" \ + -l "traefik.http.services.pounce-api.loadbalancer.server.port=8000" \ + pounce-backend:latest + sudo_cmd docker network connect n0488s44osgoow4wgo04ogg0 pounce-backend 2>/dev/null || true + + # Deploy frontend + sudo_cmd docker stop pounce-frontend 2>/dev/null || true + sudo_cmd docker rm pounce-frontend 2>/dev/null || true + sudo_cmd docker run -d \ + --name pounce-frontend \ + --network coolify \ + --restart unless-stopped \ + -l "traefik.enable=true" \ + -l "traefik.http.routers.pounce-web.rule=Host(\`pounce.ch\`) || Host(\`www.pounce.ch\`)" \ + -l "traefik.http.routers.pounce-web.entryPoints=https" \ + -l "traefik.http.routers.pounce-web.tls=true" \ + -l "traefik.http.routers.pounce-web.tls.certresolver=letsencrypt" \ + -l "traefik.http.services.pounce-web.loadbalancer.server.port=3000" \ + pounce-frontend:latest + sudo_cmd docker network connect n0488s44osgoow4wgo04ogg0 pounce-frontend 2>/dev/null || true + + # Health check + sleep 15 + curl -sf https://api.pounce.ch/api/v1/health >/dev/null + curl -sf https://pounce.ch >/dev/null + + # Cleanup + sudo_cmd docker image prune -f >/dev/null 2>&1 || true + echo "✅ Deploy finished" + DEPLOY_EOF + + - name: Summary run: | echo "==========================================" - echo "POUNCE BUILD INFO" + echo "🎉 AUTO DEPLOY COMPLETED" echo "==========================================" - echo "Commit: ${{ github.sha }}" - echo "Branch: ${{ github.ref_name }}" - echo "Time: $(date)" - echo "" - echo "To deploy, run locally:" - echo " ./scripts/deploy.sh" + echo "Commit: ${{ github.sha }}" + echo "Backend: https://api.pounce.ch" + echo "Web: https://pounce.ch" echo "==========================================" diff --git a/.gitignore b/.gitignore index d30ddaa..5ab1bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ dist/ .env .env.local .env.*.local +.env.deploy *.log # Deployment env files (MUST NOT be committed) diff --git a/ops/CI_CD.md b/ops/CI_CD.md new file mode 100644 index 0000000..3d706a9 --- /dev/null +++ b/ops/CI_CD.md @@ -0,0 +1,42 @@ +# CI/CD (Gitea Actions) – Auto Deploy + +## Goal +Every push to `main` should: +- sync the repo to the production server +- build Docker images on the server +- restart containers +- run health checks + +This repository uses a **remote SSH deployment** from Gitea Actions. + +## Required Gitea Actions Secrets +Configure these in Gitea: **Repo → Settings → Actions → Secrets** + +### Deployment (SSH) +- `DEPLOY_HOST` – production server IP/hostname +- `DEPLOY_USER` – SSH user (e.g. `administrator`) +- `DEPLOY_PATH` – absolute path where the repo is synced on the server (e.g. `/home/administrator/pounce`) +- `DEPLOY_SSH_KEY` – private key for SSH access +- `DEPLOY_SUDO_PASSWORD` – sudo password for `DEPLOY_USER` (used non-interactively) + +### App Secrets (Backend) +Used to generate `/data/pounce/env/backend.env` on the server. +- `DATABASE_URL` +- `SECRET_KEY` +- `SMTP_PASSWORD` +- `STRIPE_SECRET_KEY` +- `STRIPE_WEBHOOK_SECRET` +- `GOOGLE_CLIENT_SECRET` +- `GH_OAUTH_SECRET` +- `CZDS_USERNAME` +- `CZDS_PASSWORD` + +## Server Requirements +- `sudo` installed +- `docker` installed +- `DEPLOY_USER` must be able to run docker via `sudo` (pipeline uses `sudo -S docker ...`) + +## Notes +- Secrets are written to `/data/pounce/env/backend.env` on the server with restricted permissions. +- Frontend build args are supplied in the workflow (`NEXT_PUBLIC_API_URL`, `BACKEND_URL`). + diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 6570dd4..7be476e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -17,6 +17,8 @@ SERVER="185.142.213.170" SSH_KEY="${SSH_KEY:-$HOME/.ssh/pounce_server}" SSH_USER="administrator" REMOTE_TMP="/tmp/pounce" +REMOTE_REPO="/home/administrator/pounce" +REMOTE_ENV_DIR="/data/pounce/env" # Colors RED='\033[0;31m' @@ -33,6 +35,10 @@ if [ ! -f "$SSH_KEY" ]; then error "SSH key not found: $SSH_KEY" fi +if [ -z "${DEPLOY_SUDO_PASSWORD:-}" ]; then + error "DEPLOY_SUDO_PASSWORD is required (export it locally, do not commit it)." +fi + # What to deploy DEPLOY_BACKEND=true DEPLOY_FRONTEND=true @@ -57,15 +63,26 @@ if [ "$DEPLOY_BACKEND" = true ]; then --exclude '.git' \ --exclude 'venv' \ backend/ \ - ${SSH_USER}@${SERVER}:${REMOTE_TMP}-backend/ + ${SSH_USER}@${SERVER}:${REMOTE_REPO}/backend/ log "Building backend image..." ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} \ - "echo 'u4R6tgCv*c8Fyc1ee' | sudo -S docker build -t pounce-backend:latest ${REMOTE_TMP}-backend/" || error "Backend build failed" + "printf '%s\n' \"${DEPLOY_SUDO_PASSWORD}\" | sudo -S docker build -t pounce-backend:latest ${REMOTE_REPO}/backend/" || error "Backend build failed" log "Deploying backend container..." - ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << 'BACKEND_DEPLOY' -echo 'u4R6tgCv*c8Fyc1ee' | sudo -S bash -c ' + ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << BACKEND_DEPLOY +printf '%s\n' "${DEPLOY_SUDO_PASSWORD}" | sudo -S bash -c ' +set -e + +mkdir -p "${REMOTE_ENV_DIR}" /data/pounce/zones +chmod -R 755 /data/pounce || true + +# Backend env must exist on server (created by CI or manually) +if [ ! -f "${REMOTE_ENV_DIR}/backend.env" ]; then + echo "Missing ${REMOTE_ENV_DIR}/backend.env" + exit 1 +fi + docker stop pounce-backend 2>/dev/null || true docker rm pounce-backend 2>/dev/null || true @@ -73,33 +90,7 @@ docker run -d \ --name pounce-backend \ --network coolify \ --shm-size=8g \ - -e DATABASE_URL="postgresql+asyncpg://pounce:PounceDB2024!@supabase-db-n0488s44osgoow4wgo04ogg0:5432/pounce" \ - -e REDIS_URL="redis://pounce-redis:6379" \ - -e ENABLE_SCHEDULER="true" \ - -e SECRET_KEY="super-secret-key-change-me-in-production-please" \ - -e ENVIRONMENT="production" \ - -e CORS_ORIGINS="https://pounce.ch,https://www.pounce.ch" \ - -e COOKIE_SECURE="true" \ - -e SITE_URL="https://pounce.ch" \ - -e CZDS_DATA_DIR="/data/czds" \ - -e CZDS_USERNAME="Gugger99@gmx.ch" \ - -e CZDS_PASSWORD="Icann@2024!" \ - -e SMTP_HOST="mail.infomaniak.com" \ - -e SMTP_PORT="587" \ - -e SMTP_USER="hello@pounce.ch" \ - -e SMTP_PASSWORD="xVP4x#q1s78C" \ - -e SMTP_FROM_EMAIL="hello@pounce.ch" \ - -e STRIPE_SECRET_KEY="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" \ - -e STRIPE_PUBLISHABLE_KEY="pk_live_51ScLbjCtFUamNRpNeFugrlTIYhszbo8GovSGiMnPwHpZX9p3SGtgG8iRHYRIlAtg9M9sl3mvT5r8pwXP3mOsPALG00Wk3j0wH4" \ - -e STRIPE_PRICE_TRADER="price_1ScRlzCtFUamNRpNQdMpMzxV" \ - -e STRIPE_PRICE_TYCOON="price_1SdwhSCtFUamNRpNEXTSuGUc" \ - -e STRIPE_WEBHOOK_SECRET="whsec_DlWSVkIJDDDkjfj29fjJFkdj2Ksldk" \ - -e GOOGLE_CLIENT_ID="865146315769-vi7vcu91d3i7huv8ikjun52jo9ob7spk.apps.googleusercontent.com" \ - -e GOOGLE_CLIENT_SECRET="" \ - -e GOOGLE_REDIRECT_URI="https://pounce.ch/api/v1/oauth/google/callback" \ - -e GITHUB_CLIENT_ID="Ov23liBjROk39vYXi3G5" \ - -e GITHUB_CLIENT_SECRET="" \ - -e GITHUB_REDIRECT_URI="https://pounce.ch/api/v1/oauth/github/callback" \ + --env-file "${REMOTE_ENV_DIR}/backend.env" \ -v /data/pounce/zones:/data \ --label "traefik.enable=true" \ --label "traefik.http.routers.pounce-backend.rule=Host(\`api.pounce.ch\`)" \ @@ -127,15 +118,16 @@ if [ "$DEPLOY_FRONTEND" = true ]; then --exclude '.next' \ --exclude '.git' \ frontend/ \ - ${SSH_USER}@${SERVER}:${REMOTE_TMP}-frontend/ + ${SSH_USER}@${SERVER}:${REMOTE_REPO}/frontend/ log "Building frontend image..." ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} \ - "echo 'u4R6tgCv*c8Fyc1ee' | sudo -S docker build --build-arg NEXT_PUBLIC_API_URL=https://api.pounce.ch --build-arg BACKEND_URL=http://pounce-backend:8000 -t pounce-frontend:latest ${REMOTE_TMP}-frontend/" || error "Frontend build failed" + "printf '%s\n' \"${DEPLOY_SUDO_PASSWORD}\" | sudo -S docker build --build-arg NEXT_PUBLIC_API_URL=https://api.pounce.ch --build-arg BACKEND_URL=http://pounce-backend:8000 -t pounce-frontend:latest ${REMOTE_REPO}/frontend/" || error "Frontend build failed" log "Deploying frontend container..." - ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << 'FRONTEND_DEPLOY' -echo 'u4R6tgCv*c8Fyc1ee' | sudo -S bash -c ' + ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} << FRONTEND_DEPLOY +printf '%s\n' "${DEPLOY_SUDO_PASSWORD}" | sudo -S bash -c ' +set -e docker stop pounce-frontend 2>/dev/null || true docker rm pounce-frontend 2>/dev/null || true @@ -166,7 +158,7 @@ curl -sf https://pounce.ch -o /dev/null && log "Frontend: ✅ Healthy" # Cleanup log "Cleaning up..." ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no ${SSH_USER}@${SERVER} \ - "echo 'u4R6tgCv*c8Fyc1ee' | sudo -S docker image prune -f" > /dev/null 2>&1 + "printf '%s\n' \"${DEPLOY_SUDO_PASSWORD}\" | sudo -S docker image prune -f" > /dev/null 2>&1 log "==========================================" log "🎉 DEPLOYMENT SUCCESSFUL!"