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.
This commit is contained in:
@ -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 "=========================================="
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,6 +26,7 @@ dist/
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.deploy
|
||||
*.log
|
||||
|
||||
# Deployment env files (MUST NOT be committed)
|
||||
|
||||
42
ops/CI_CD.md
Normal file
42
ops/CI_CD.md
Normal file
@ -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`).
|
||||
|
||||
@ -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!"
|
||||
|
||||
Reference in New Issue
Block a user