name: CI/CD → Deploy via SSH on: push: branches: - main env: REGISTRY: ${{ vars.REGISTRY }} BACKEND_IMAGE: saas-imobiliaria-backend FRONTEND_IMAGE: saas-imobiliaria-frontend jobs: # ──────────────────────────────────────────────────────────────────────────── # 1. BUILD & PUSH IMAGES # ──────────────────────────────────────────────────────────────────────────── build: name: Build & Push Docker Images runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set image tag id: tag run: echo "IMAGE_TAG=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT - name: Log in to Forgejo Container Registry uses: docker/login-action@v3 with: registry: ${{ vars.REGISTRY }} username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASSWORD }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build & push backend uses: docker/build-push-action@v5 with: context: ./backend file: ./backend/Dockerfile.prod push: true tags: | ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${{ steps.tag.outputs.IMAGE_TAG }} ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:cache cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:cache,mode=max - name: Build & push frontend uses: docker/build-push-action@v5 with: context: ./frontend file: ./frontend/Dockerfile.prod push: true build-args: | VITE_API_URL=https://${{ vars.DOMAIN }}/api tags: | ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:${{ steps.tag.outputs.IMAGE_TAG }} ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:latest cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:cache cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:cache,mode=max outputs: image_tag: ${{ steps.tag.outputs.IMAGE_TAG }} # ──────────────────────────────────────────────────────────────────────────── # 2. DEPLOY VIA SSH # ──────────────────────────────────────────────────────────────────────────── deploy: name: Deploy via SSH runs-on: ubuntu-latest needs: build steps: - name: Checkout uses: actions/checkout@v4 - name: Setup SSH key run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -p ${{ vars.SSH_PORT || '22' }} ${{ vars.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - name: Copy docker-compose.prod.yml to server run: | scp -i ~/.ssh/deploy_key \ -P ${{ vars.SSH_PORT || '22' }} \ docker-compose.prod.yml \ ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}:/opt/saas-imobiliaria/docker-compose.prod.yml - name: Deploy on server (Docker Swarm stack) run: | ssh -i ~/.ssh/deploy_key \ -p ${{ vars.SSH_PORT || '22' }} \ ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} \ bash -s << ENDSSH set -e DEPLOY_DIR="/opt/saas-imobiliaria" mkdir -p "\$DEPLOY_DIR" cd "\$DEPLOY_DIR" # Log in to registry echo "${{ secrets.REGISTRY_PASSWORD }}" | \ docker login ${{ vars.REGISTRY }} \ -u ${{ secrets.REGISTRY_USER }} \ --password-stdin # Pull images explicitly so swarm has them cached docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-backend:${{ needs.build.outputs.image_tag }} docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-frontend:${{ needs.build.outputs.image_tag }} # Deploy stack with env vars inline IMAGE_TAG=${{ needs.build.outputs.image_tag }} \ REGISTRY=${{ vars.REGISTRY }} \ DOMAIN=${{ vars.DOMAIN }} \ POSTGRES_DB=${{ secrets.POSTGRES_DB }} \ POSTGRES_USER=${{ secrets.POSTGRES_USER }} \ POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} \ SECRET_KEY=${{ secrets.SECRET_KEY }} \ JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} \ docker stack deploy \ --compose-file docker-compose.prod.yml \ --with-registry-auth \ --prune \ saas-imobiliaria echo "Stack deployed: ${{ needs.build.outputs.image_tag }}" # Wait for services to converge sleep 10 docker stack services saas-imobiliaria ENDSSH # ──────────────────────────────────────────────────────────────────────────── # 3. HEALTH CHECK — valida HTTPS + endpoints críticos # ──────────────────────────────────────────────────────────────────────────── healthcheck: name: Validate HTTPS & Endpoints runs-on: ubuntu-latest needs: deploy steps: - name: Wait for containers to stabilize run: sleep 30 - name: Check frontend HTTPS run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ --max-time 15 "https://${{ vars.DOMAIN }}") echo "Frontend: $STATUS" [ "$STATUS" = "200" ] || (echo "❌ Frontend HTTPS falhou ($STATUS)" && exit 1) - name: Check HTTP → HTTPS redirect run: | LOCATION=$(curl -s -o /dev/null -w "%{redirect_url}" \ --max-time 10 "http://${{ vars.DOMAIN }}") echo "Redirect: $LOCATION" echo "$LOCATION" | grep -q "https://" || (echo "❌ Redirect HTTP→HTTPS ausente" && exit 1) - name: Check backend /health run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ --max-time 15 "https://${{ vars.DOMAIN }}/api/health") echo "Backend health: $STATUS" [ "$STATUS" = "200" ] || (echo "❌ Backend health check falhou ($STATUS)" && exit 1) - name: Check TLS certificate validity run: | EXPIRY=$(echo | openssl s_client \ -connect ${{ vars.DOMAIN }}:443 \ -servername ${{ vars.DOMAIN }} 2>/dev/null \ | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) echo "Certificado expira: $EXPIRY" EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s) NOW_EPOCH=$(date +%s) DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 )) echo "Dias restantes: $DAYS_LEFT" [ "$DAYS_LEFT" -gt 7 ] || (echo "❌ Certificado expira em $DAYS_LEFT dias!" && exit 1) - name: Check API properties endpoint run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ --max-time 15 "https://${{ vars.DOMAIN }}/api/properties?limit=1") echo "Properties API: $STATUS" [ "$STATUS" = "200" ] || (echo "❌ Properties API falhou ($STATUS)" && exit 1) - name: Check deployed version matches image tag run: | RESPONSE=$(curl -s --max-time 10 "https://${{ vars.DOMAIN }}/api/version") echo "Version response: $RESPONSE" echo "$RESPONSE" | grep -q "${{ needs.build.outputs.image_tag }}" \ || (echo "❌ Version tag não confere — esperado: ${{ needs.build.outputs.image_tag }}" && exit 1) - name: All checks passed run: | echo "✅ Deploy validado com sucesso!" echo " → https://${{ vars.DOMAIN }} está no ar"