Merge pull request #89 from voltdatalab/fix/vote-date-column #134
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Mamute Politico | |
| # Roda em push em main (deploy) e em PRs (so validate, sem deploy). | |
| # Deploy depende de TODOS os checks passarem — gate obrigatorio pre-prod. | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| # Serializa execucao por branch: 2 pushes seguidos em main nao disparam deploys | |
| # concorrentes (rsync race + container thrashing). PRs cancelam runs anteriores | |
| # da mesma PR pra economizar minutos de runner. | |
| concurrency: | |
| group: deploy-prd-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| # Permissoes minimas: leitura de codigo + escrita em issues (pra notificar falha). | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| ui-validate: | |
| name: UI — build, tests, bundle smoke | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: ui/package-lock.json | |
| - name: Install deps | |
| working-directory: ui | |
| run: npm ci | |
| - name: Tests (vitest) | |
| working-directory: ui | |
| run: npm run test:coverage | |
| - name: Upload coverage | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ui-coverage | |
| path: ui/coverage/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Build (vite + tsc) | |
| working-directory: ui | |
| run: npm run build | |
| env: | |
| # Default vazio = mesmo comportamento de prod (fallback same-origin). | |
| VITE_BASE_URL: '' | |
| - name: Bundle smoke — sem URLs loopback hardcoded + presenca esperada | |
| run: bash scripts/check_bundle_no_local_urls.sh ui/dist/assets | |
| - name: Upload bundle (pra job e2e) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ui-dist | |
| path: ui/dist/ | |
| retention-days: 1 | |
| api-smoke: | |
| name: API — pytest smoke | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| cache: pip | |
| cache-dependency-path: api/requirements-dev.txt | |
| - name: Install deps | |
| working-directory: api | |
| run: pip install -r requirements-dev.txt | |
| - name: Pytest | |
| run: pytest api/tests/ -v | |
| contract-check: | |
| name: Contract UI ↔ API | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Verifica que toda chamada da UI tem rota+metodo na API | |
| run: python3 scripts/check_ui_api_contract.py | |
| compose-validate: | |
| name: Compose — sintaxe e refs | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: docker compose config (sintaxe + variaveis) | |
| working-directory: environments/production | |
| env: | |
| # Mock vars exigidas pelo compose pra ele nao avisar — sao so pra validar | |
| # sintaxe, nao pra deploy. Valores reais ficam no .env do servidor. | |
| PUBLIC_URL: 'https://example.com' | |
| GHOST_DB_PASSWORD: 'placeholder' | |
| CADDY_HTTP_PORT: '80' | |
| MAILGUN_SMTP_USER: 'placeholder' | |
| MAILGUN_SMTP_PASS: 'placeholder' | |
| run: docker compose config -q | |
| migration-smoke: | |
| name: Migrations — alembic upgrade head | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 8 | |
| services: | |
| postgres: | |
| image: pgvector/pgvector:pg16 | |
| env: | |
| POSTGRES_USER: mamute_test | |
| POSTGRES_PASSWORD: mamute_test | |
| POSTGRES_DB: mamute_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U mamute_test -d mamute_test" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| cache: pip | |
| cache-dependency-path: mamute_scrappers/requirements.txt | |
| - name: Install deps | |
| working-directory: mamute_scrappers | |
| run: pip install -r requirements.txt | |
| - name: Habilita extensao pgvector | |
| env: | |
| PGPASSWORD: mamute_test | |
| run: psql -h localhost -U mamute_test -d mamute_test -c "CREATE EXTENSION IF NOT EXISTS vector;" | |
| - name: Alembic upgrade head | |
| working-directory: mamute_scrappers | |
| env: | |
| DATABASE_URL: postgresql+psycopg2://mamute_test:mamute_test@localhost:5432/mamute_test | |
| run: alembic upgrade head | |
| e2e: | |
| name: E2E — Playwright smoke | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [ui-validate] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: npm | |
| cache-dependency-path: ui/package-lock.json | |
| - name: Install deps | |
| working-directory: ui | |
| run: npm ci | |
| - name: Restaurar bundle do job ui-validate | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ui-dist | |
| path: ui/dist/ | |
| - name: Install Playwright browser | |
| working-directory: ui | |
| run: npx playwright install --with-deps chromium | |
| - name: Run E2E | |
| working-directory: ui | |
| run: npx playwright test | |
| - name: Upload report (em falha) | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: ui/playwright-report/ | |
| retention-days: 14 | |
| deploy: | |
| name: Deploy producao | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # So roda em push em main + se TODOS os validates passaram. PRs nao deployam. | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| needs: [ui-validate, api-smoke, contract-check, compose-validate, migration-smoke, e2e] | |
| steps: | |
| - name: Checkout arquivos | |
| uses: actions/checkout@v4 | |
| - name: Ajusta permissões | |
| run: sudo chown -R root:root . | |
| - name: Copiando arquivos via SSH | |
| # Pinado em tag estavel (ex-main era trunk movel). | |
| uses: easingthemes/ssh-deploy@v5.1.0 | |
| with: | |
| SSH_PRIVATE_KEY: ${{ secrets.KEY_GH_ACTIONS }} | |
| ARGS: "-rltgoDzvO" | |
| SOURCE: "./" | |
| REMOTE_HOST: ${{ secrets.HOST_PRD }} | |
| REMOTE_USER: "deploy" | |
| TARGET: "/var/www/mamute/" | |
| - name: Rebuild e restart dos containers Docker | |
| uses: appleboy/ssh-action@v1.2.0 | |
| with: | |
| host: ${{ secrets.HOST_PRD }} | |
| username: deploy | |
| key: ${{ secrets.KEY_GH_ACTIONS }} | |
| script_stop: true | |
| script: | | |
| cd /var/www/mamute/environments/production | |
| bash up.sh | |
| docker image prune -f | |
| - name: Aplica migrations (alembic upgrade head) | |
| uses: appleboy/ssh-action@v1.2.0 | |
| with: | |
| host: ${{ secrets.HOST_PRD }} | |
| username: deploy | |
| key: ${{ secrets.KEY_GH_ACTIONS }} | |
| script_stop: true | |
| # Roda alembic dentro do container scrappers, onde alembic.ini + migrations | |
| # vivem e a env do DB ja esta wired. Idempotente: se ja esta no head, e no-op. | |
| # script_stop garante que falha aqui interrompe o deploy e dispara o handler | |
| # de issue automatico — evita API/UI servirem com schema desalinhado do codigo. | |
| # As migrations em si sao validadas no job migration-smoke (gate de PR); | |
| # este passo so aplica em prod o que ja foi testado. | |
| script: | | |
| set -euo pipefail | |
| CONTAINER=prod-mamute-politico-scrappers | |
| # docker compose up -d ja retorna com os containers em "started", mas | |
| # guard extra contra race (ate ~30s) ao bater no exec logo apos. | |
| for i in $(seq 1 6); do | |
| if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" = "true" ]; then | |
| break | |
| fi | |
| echo " aguardando $CONTAINER ficar Running... ($i/6)" | |
| sleep 5 | |
| done | |
| run_alembic() { | |
| # -e SQLALCHEMY_ECHO=0 silencia o ruido do .env do container. | |
| docker exec -e SQLALCHEMY_ECHO=0 "$CONTAINER" \ | |
| sh -lc "cd /app/mamute_scrappers && alembic $*" | |
| } | |
| echo "::group::alembic current (antes)" | |
| run_alembic current | |
| echo "::endgroup::" | |
| echo "::group::alembic upgrade head" | |
| run_alembic upgrade head | |
| echo "::endgroup::" | |
| echo "::group::alembic current (depois)" | |
| run_alembic current | |
| echo "::endgroup::" | |
| - name: Health check pos-deploy | |
| uses: appleboy/ssh-action@v1.2.0 | |
| with: | |
| host: ${{ secrets.HOST_PRD }} | |
| username: deploy | |
| key: ${{ secrets.KEY_GH_ACTIONS }} | |
| script_stop: true | |
| # Cada endpoint: tenta ate 12x (60s) com 5s entre tentativas. | |
| # Curl com -o /dev/null + -w '%{http_code}' so checa status code. | |
| # | |
| # NOTA DE PORTA: nginx do host serve :80 (vhosts por hostname); | |
| # Caddy docker faz roteamento /api/* /app/* /ghost/* via :${CADDY_HTTP_PORT}. | |
| # Pra health check direto sem TLS/DNS, batemos no Caddy docker. | |
| # Bug anterior: usavamos :80 (sem hostname) e cai em nginx default = 404. | |
| script: | | |
| set -euo pipefail | |
| # Le CADDY_HTTP_PORT do .env de prod. Fallback 8800 (valor atual) | |
| # so como rede de seguranca — fonte de verdade e o .env. | |
| ENV_FILE=/var/www/mamute/environments/production/.env | |
| if [ -f "$ENV_FILE" ]; then | |
| CADDY_HTTP_PORT=$(grep -E '^CADDY_HTTP_PORT=' "$ENV_FILE" | head -1 | cut -d= -f2- | tr -d '"' | tr -d "'") | |
| fi | |
| : "${CADDY_HTTP_PORT:=8800}" | |
| BASE="http://127.0.0.1:${CADDY_HTTP_PORT}" | |
| echo "Health check: $BASE" | |
| check() { | |
| local url=$1 | |
| local label=$2 | |
| local expected=${3:-200} | |
| for i in $(seq 1 12); do | |
| code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url" || echo "000") | |
| if [ "$code" = "$expected" ]; then | |
| echo "✓ $label ($url) → $code" | |
| return 0 | |
| fi | |
| echo " attempt $i/12: $label → $code (esperando $expected)" | |
| sleep 5 | |
| done | |
| echo "✗ $label ($url) nunca respondeu $expected" | |
| return 1 | |
| } | |
| check "$BASE/api/" "API root" 200 | |
| check "$BASE/app/" "UI SPA" 200 | |
| check "$BASE/ghost/" "Ghost" 200 | |
| - name: Notificar falha (cria issue) | |
| if: failure() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const sha = context.sha.substring(0, 7); | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `🚨 Deploy producao falhou em ${sha}`, | |
| body: [ | |
| `Run: ${runUrl}`, | |
| `Commit: ${context.sha}`, | |
| `Branch: ${context.ref}`, | |
| `Actor: @${context.actor}`, | |
| ``, | |
| `Triggered by: \`${context.eventName}\``, | |
| ``, | |
| `Investigar logs do run acima. Estado provavel do servidor:`, | |
| `- Containers podem estar parcialmente atualizados (rsync rodou, mas up.sh ou health falhou).`, | |
| `- SSH em correiosabia e \`docker ps\` + \`docker logs\` nos containers afetados.`, | |
| ``, | |
| `Auto-criada por .github/workflows/deploy-prd.yml.`, | |
| ].join('\n'), | |
| labels: ['deploy-failure', 'urgent'], | |
| }); |