ci: add forgejo actions pipeline with traefik labels and https health checks
Some checks are pending
CI/CD → Portainer / Build & Push Docker Images (push) Waiting to run
CI/CD → Portainer / Deploy to Portainer (push) Blocked by required conditions
CI/CD → Portainer / Validate HTTPS & Endpoints (push) Blocked by required conditions

This commit is contained in:
MatheusAlves96 2026-04-21 00:06:22 -03:00
parent e6cb06255b
commit dcd18a07e6
7 changed files with 407 additions and 0 deletions

View file

@ -0,0 +1,63 @@
# CI/CD Pipeline — SaaS Imobiliária
## Fluxo
```
push main → Build images → Push registry → Deploy Portainer → Health checks HTTPS
```
## Configurar no Forgejo (Settings → Secrets & Variables)
### Secrets (valores sensíveis)
| Secret | Descrição |
|--------|-----------|
| `REGISTRY_USER` | Usuário do registry (ex: `gitadmin`) |
| `REGISTRY_PASSWORD` | Senha ou token do registry |
| `PORTAINER_WEBHOOK_URL` | URL do webhook do stack no Portainer |
| `POSTGRES_DB` | Nome do banco de dados |
| `POSTGRES_USER` | Usuário do PostgreSQL |
| `POSTGRES_PASSWORD` | Senha do PostgreSQL |
| `SECRET_KEY` | Flask SECRET_KEY |
| `JWT_SECRET_KEY` | Chave JWT (mín. 32 chars) |
### Variables (valores não-sensíveis)
| Variable | Exemplo |
|----------|---------|
| `REGISTRY` | `git.matheussouza.com.br/gitadmin` |
| `DOMAIN` | `imobiliaria.matheussouza.com.br` |
## Configurar no Portainer
1. Crie um **Stack** com o arquivo `docker-compose.prod.yml`
2. Ative o **webhook** do stack (Stack → Webhooks → Enable)
3. Copie a URL do webhook → cole em `PORTAINER_WEBHOOK_URL`
4. Certifique-se que a rede `traefik-public` existe:
```
docker network create traefik-public
```
## Traefik — pré-requisitos
O Traefik deve estar rodando com:
- Entrypoint `web` (porta 80)
- Entrypoint `websecure` (porta 443)
- CertResolver `letsencrypt` configurado
- Conectado à rede `traefik-public`
Exemplo mínimo de configuração do Traefik:
```yaml
# traefik.yml
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: seu@email.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
```

View file

@ -0,0 +1,160 @@
name: CI/CD → Portainer
on:
push:
branches:
- main
env:
REGISTRY: ${{ vars.REGISTRY }} # ex: git.matheussouza.com.br/gitadmin
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
# ── Backend ──────────────────────────────────────────────────────────────
- 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
# ── Frontend ─────────────────────────────────────────────────────────────
- 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 PORTAINER WEBHOOK
# ────────────────────────────────────────────────────────────────────────────
deploy:
name: Deploy to Portainer
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Trigger Portainer stack deploy
run: |
curl -s -o /dev/null -w "%{http_code}" \
--fail \
-X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{
"env": [
{"name": "IMAGE_TAG", "value": "${{ needs.build.outputs.image_tag }}"},
{"name": "REGISTRY", "value": "${{ vars.REGISTRY }}"},
{"name": "DOMAIN", "value": "${{ vars.DOMAIN }}"},
{"name": "POSTGRES_DB", "value": "${{ secrets.POSTGRES_DB }}"},
{"name": "POSTGRES_USER", "value": "${{ secrets.POSTGRES_USER }}"},
{"name": "POSTGRES_PASSWORD", "value": "${{ secrets.POSTGRES_PASSWORD }}"},
{"name": "SECRET_KEY", "value": "${{ secrets.SECRET_KEY }}"},
{"name": "JWT_SECRET_KEY", "value": "${{ secrets.JWT_SECRET_KEY }}"}
]
}'
# ────────────────────────────────────────────────────────────────────────────
# 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"
[ "$STATUS" = "200" ] || (echo "Frontend HTTPS failed ($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 to: $LOCATION"
echo "$LOCATION" | grep -q "https://" || (echo "HTTP redirect missing" && 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"
[ "$STATUS" = "200" ] || (echo "Backend health check failed ($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 "Certificate expires: $EXPIRY"
# Fail if cert expires in less than 7 days
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
echo "Days until expiry: $DAYS_LEFT"
[ "$DAYS_LEFT" -gt 7 ] || (echo "Certificate expires in $DAYS_LEFT days!" && 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"
[ "$STATUS" = "200" ] || (echo "Properties API failed ($STATUS)" && exit 1)
- name: Deployment validated successfully
run: |
echo "✅ All checks passed!"
echo " → https://${{ vars.DOMAIN }} is live and healthy"