Skip to content

Merge pull request #89 from voltdatalab/fix/vote-date-column #134

Merge pull request #89 from voltdatalab/fix/vote-date-column

Merge pull request #89 from voltdatalab/fix/vote-date-column #134

Workflow file for this run

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'],
});