From caf541d75006d3a8ea5a75003a0d31b7e344a288 Mon Sep 17 00:00:00 2001 From: MatheusAlves96 Date: Tue, 21 Apr 2026 00:49:05 -0300 Subject: [PATCH] ci: use dind container for build, alpine+ssh for deploy, fix runner label --- .forgejo/workflows/deploy.yml | 235 ++++++++++++++++------------------ 1 file changed, 113 insertions(+), 122 deletions(-) diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index c875fe5..ce2fb1c 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -12,55 +12,55 @@ env: jobs: # ──────────────────────────────────────────────────────────────────────────── - # 1. BUILD & PUSH IMAGES + # 1. BUILD & PUSH IMAGES (Docker-in-Docker) # ──────────────────────────────────────────────────────────────────────────── build: name: Build & Push Docker Images - runs-on: self-hosted + runs-on: docker + container: + image: docker:27-dind + env: + DOCKER_TLS_CERTDIR: "" + options: --privileged steps: + - name: Install git + run: apk add --no-cache git + - name: Checkout uses: actions/checkout@v4 - name: Set image tag id: tag - run: echo "IMAGE_TAG=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT + run: echo "IMAGE_TAG=$(echo $GITHUB_SHA | cut -c1-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: Log in to registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login ${{ vars.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin - 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 + run: | + TAG=${{ steps.tag.outputs.IMAGE_TAG }} + docker build \ + -f backend/Dockerfile.prod \ + -t ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${TAG} \ + -t ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest \ + ./backend + docker push ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:${TAG} + docker push ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}:latest - 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 + run: | + TAG=${{ steps.tag.outputs.IMAGE_TAG }} + docker build \ + -f frontend/Dockerfile.prod \ + --build-arg VITE_API_URL=https://${{ vars.DOMAIN }}/api \ + -t ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:${TAG} \ + -t ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:latest \ + ./frontend + docker push ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:${TAG} + docker push ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}:latest outputs: image_tag: ${{ steps.tag.outputs.IMAGE_TAG }} @@ -70,134 +70,125 @@ jobs: # ──────────────────────────────────────────────────────────────────────────── deploy: name: Deploy via SSH - runs-on: self-hosted + runs-on: docker needs: build + container: + image: alpine:3.19 steps: + - name: Install SSH tools + run: apk add --no-cache openssh-client + - name: Checkout uses: actions/checkout@v4 - name: Setup SSH key run: | mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + printf '%s\n' "${{ 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 + ssh-keyscan -p ${{ vars.SSH_PORT }} ${{ vars.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - - name: Copy docker-compose.prod.yml to server + - name: Copy compose file to server run: | - scp -i ~/.ssh/deploy_key \ - -P ${{ vars.SSH_PORT || '22' }} \ + scp -i ~/.ssh/deploy_key -P ${{ vars.SSH_PORT }} \ docker-compose.prod.yml \ ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }}:/opt/saas-imobiliaria/docker-compose.prod.yml - - name: Deploy on server (Docker Swarm stack) + - name: Deploy Swarm stack on server + env: + IMAGE_TAG: ${{ needs.build.outputs.image_tag }} run: | - ssh -i ~/.ssh/deploy_key \ - -p ${{ vars.SSH_PORT || '22' }} \ + ssh -i ~/.ssh/deploy_key -p ${{ vars.SSH_PORT }} \ ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} \ - bash -s << ENDSSH + "set -e + mkdir -p /opt/saas-imobiliaria + cd /opt/saas-imobiliaria - set -e + echo '${{ secrets.REGISTRY_PASSWORD }}' | \ + docker login ${{ vars.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin - DEPLOY_DIR="/opt/saas-imobiliaria" - mkdir -p "\$DEPLOY_DIR" - cd "\$DEPLOY_DIR" + docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-backend:${IMAGE_TAG} + docker pull ${{ vars.REGISTRY }}/saas-imobiliaria-frontend:${IMAGE_TAG} - # Log in to registry - echo "${{ secrets.REGISTRY_PASSWORD }}" | \ - docker login ${{ vars.REGISTRY }} \ - -u ${{ secrets.REGISTRY_USER }} \ - --password-stdin + IMAGE_TAG=${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 - # 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 + sleep 15 + docker stack services saas-imobiliaria" # ──────────────────────────────────────────────────────────────────────────── - # 3. HEALTH CHECK — valida HTTPS + endpoints críticos + # 3. HEALTH CHECK # ──────────────────────────────────────────────────────────────────────────── healthcheck: name: Validate HTTPS & Endpoints - runs-on: self-hosted - needs: deploy + runs-on: docker + needs: [build, deploy] + container: + image: alpine:3.19 steps: - - name: Wait for containers to stabilize - run: sleep 30 + - name: Install tools + run: apk add --no-cache curl openssl - - 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: Wait for stack to stabilize + run: sleep 40 - - name: Check HTTP → HTTPS redirect + - name: Frontend HTTPS 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) + S=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "https://${{ vars.DOMAIN }}") + echo "Frontend: $S" + [ "$S" = "200" ] || (echo "❌ Frontend falhou ($S)" && exit 1) - - name: Check backend /health + - name: HTTP → HTTPS redirect 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) + L=$(curl -s -o /dev/null -w "%{redirect_url}" --max-time 10 "http://${{ vars.DOMAIN }}") + echo "Redirect: $L" + echo "$L" | grep -q "https://" || (echo "❌ Redirect ausente" && exit 1) - - name: Check TLS certificate validity + - name: Backend /api/health run: | - EXPIRY=$(echo | openssl s_client \ - -connect ${{ vars.DOMAIN }}:443 \ + R=$(curl -s --max-time 15 "https://${{ vars.DOMAIN }}/api/health") + S=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "https://${{ vars.DOMAIN }}/api/health") + echo "Health: $S → $R" + [ "$S" = "200" ] || (echo "❌ Health falhou ($S)" && exit 1) + echo "$R" | grep -q '"db": "ok"' || (echo "❌ DB não conectado" && exit 1) + + - name: 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) + echo "Expira: $EXPIRY" + DAYS=$(( ($(date -d "$EXPIRY" +%s) - $(date +%s)) / 86400 )) + echo "Dias restantes: $DAYS" + [ "$DAYS" -gt 7 ] || (echo "❌ Cert expira em $DAYS dias" && exit 1) - - name: Check API properties endpoint + - name: GET /api/properties 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) + S=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 "https://${{ vars.DOMAIN }}/api/properties?limit=1") + echo "Properties: $S" + [ "$S" = "200" ] || (echo "❌ Properties falhou ($S)" && exit 1) - - name: Check deployed version matches image tag + - name: Version tag matches 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) + R=$(curl -s --max-time 10 "https://${{ vars.DOMAIN }}/api/version") + echo "Version: $R" + echo "$R" | grep -q "${{ needs.build.outputs.image_tag }}" \ + || (echo "❌ Tag não confere — esperado ${{ needs.build.outputs.image_tag }}" && exit 1) - - name: All checks passed + - name: Summary run: | - echo "✅ Deploy validado com sucesso!" - echo " → https://${{ vars.DOMAIN }} está no ar" + echo "✅ Todos os checks passaram!" + echo " → https://${{ vars.DOMAIN }} está no ar com tag ${{ needs.build.outputs.image_tag }}"