commit 928afe8697630b4297544e7b649d441388c55d23 Author: VPS 4 Deploy Agent Date: Sat May 30 14:41:27 2026 +0200 feat: exemplos e documentacao da automacao de deploy GitOps (v1.0.0) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4e2ac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Nunca commitar segredos nem dependências +.env +*.env +!config.example.env +node_modules/ +dist/ +.next/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bd8c81 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# 🚀 Deploy-Automático — Modelo GitOps (Gitea + Multi-VPS) + +Repositório de **referência/exemplo** da nossa automação de deploy contínuo baseada em +**Gitea Webhooks** + **listener na VPS de build** + **Docker** na VPS de produção. + +> ⚠️ **Importante:** todos os scripts aqui são **exemplos sanitizados**. Tokens, secrets, +> senhas de banco, JWT e IPs sensíveis foram substituídos por **placeholders** (`<...>`). +> Nunca commite segredos reais — use variáveis de ambiente / arquivos `.env` fora do Git. + +--- + +## 🗺️ Arquitetura (papéis dos servidores) + +| Servidor | IP (VPN) | Função | +|---|---|---| +| **VPS 1 — App** | `10.99.0.1` | Execução em **produção** (containers Docker) | +| **VPS 3 — Data/Git** | `10.99.0.3` | **Gitea** + PostgreSQL | +| **VPS 4 — Builder** | `10.99.0.4` | Desenvolvimento, **build** e disparo do deploy | + +``` +Dev (VPS4) ──git push──► Gitea (VPS3) ──webhook──► listener (VPS4:porta) + │ + ▼ + deploy-*.sh (VPS4) + │ + ┌────────────────────┴────────────────────┐ + ▼ ▼ + [Modelo A] build na VPS4 + [Modelo B] rsync do fonte + + rsync de artefatos → VPS1 build na VPS1 (docker compose + → docker compose restart up -d --build) +``` + +--- + +## 📦 O que tem aqui + +``` +. +├── webhook-listener/ +│ ├── listener.js # Listener genérico do webhook do Gitea (parametrizável por ENV) +│ ├── webhook-listener.service # Unit systemd de exemplo +│ └── config.example.env # Variáveis de ambiente do listener +├── scripts/ +│ ├── deploy-prod-builder.sh # Modelo A: build na VPS4 → envia artefato → VPS1 restart +│ ├── deploy-to-prod.sh # Promoção manual Staging(VPS4) → Produção(VPS1) +│ └── deploy-prod-onhost.sh # Modelo B: rsync do fonte → build na VPS1 +└── docs/ + ├── 01-arquitetura-vps.md + ├── 02-fluxo-gitops.md + ├── 03-configurar-webhook-gitea.md + └── 04-seguranca-deploy.md +``` + +## 🔑 Conceitos-chave da automação + +- **Um listener por projeto**, cada um numa **porta** própria (ex.: 9000, 9002, 9003), apontando + para o `deploy-*.sh` daquele projeto. +- O listener **valida um token**, tem **trava de concorrência**, **ignora commits de bump de versão** + (`chore(deploy): bump version`) para evitar loop de CI, e **reporta o status do commit ao Gitea**. +- **GitOps**: o script de deploy roda `git fetch && git reset --hard origin/main` — a fonte de verdade + é sempre o Gitea, nunca arquivos soltos na VPS de build. + +Veja `docs/` para o passo a passo completo. diff --git a/docs/01-arquitetura-vps.md b/docs/01-arquitetura-vps.md new file mode 100644 index 0000000..0a9aca3 --- /dev/null +++ b/docs/01-arquitetura-vps.md @@ -0,0 +1,24 @@ +# 01 — Arquitetura Multi-VPS + +| Servidor | IP (VPN WireGuard) | Função principal | Papel no deploy | +|---|---|---|---| +| **VPS 1 — App** | `10.99.0.1` | Produção (containers Docker) | Recebe os arquivos da VPS 4 e roda os containers | +| **VPS 3 — Data/Git** | `10.99.0.3` | **Gitea** + PostgreSQL | Repositório central + banco | +| **VPS 4 — Builder** | `10.99.0.4` | Dev / build | Roda o listener e os scripts de deploy | + +## Princípios + +1. **A fonte de verdade é o Gitea (VPS3).** O deploy sempre faz `git reset --hard origin/main` + antes de buildar — nunca confia em arquivos soltos na VPS de build. +2. **Produção não recebe edição manual.** O `rsync --delete` sobrescreve tudo no próximo deploy. +3. **Docker rootless na VPS 1.** Sempre usar `DOCKER_HOST=unix:///run/user/1000/docker.sock`. + O daemon root fica desabilitado de propósito (ver `04-seguranca-deploy.md`). +4. **Comunicação entre VPS é pela VPN** (`10.99.0.x`), via chave SSH do usuário `deploy`. + +``` + [ VPS 4: Builder ] ──git push──► [ VPS 3: Gitea/DB ] + │ ▲ + (rsync artefato/fonte) (git fetch/reset) + ▼ │ + [ VPS 1: Produção ] ◄───────────────────┘ +``` diff --git a/docs/02-fluxo-gitops.md b/docs/02-fluxo-gitops.md new file mode 100644 index 0000000..cdb74f3 --- /dev/null +++ b/docs/02-fluxo-gitops.md @@ -0,0 +1,32 @@ +# 02 — Fluxo GitOps (do push ao container no ar) + +```mermaid +sequenceDiagram + participant Dev as Dev (VPS4) + participant Gitea as Gitea (VPS3) + participant Listener as Listener (VPS4:porta) + participant Build as deploy-*.sh (VPS4) + participant Prod as Docker (VPS1) + + Dev->>Gitea: git push origin main + Gitea->>Listener: POST /webhook?token=... + Listener->>Listener: valida token + trava de concorrência + Listener->>Gitea: status = pending + Listener->>Build: executa deploy-*.sh (background) + Build->>Gitea: git fetch & reset --hard + Build->>Build: build / version bump + Build->>Prod: rsync (artefato ou fonte) + Build->>Prod: ssh docker compose up -d [--build] + Build->>Gitea: git push (chore(deploy): bump version) + Note over Listener: commit de bump é IGNORADO (anti-loop) + Build-->>Listener: exit 0 + Listener->>Gitea: status = success +``` + +## Pontos importantes do listener + +- **Resposta 202 imediata**: evita timeout do webhook do Gitea; o build roda em background. +- **Trava `isBuilding`**: retorna `429` se já houver build rodando (sem deploys concorrentes). +- **Anti-loop de CI**: commits cuja mensagem casa com `^chore\(deploy\): bump version` são + **ignorados** — senão o `git push` do bump dispararia um novo deploy infinitamente. +- **Status no Gitea**: `pending` → `success`/`failure`, aparecendo o ✔/✘ ao lado do commit. diff --git a/docs/03-configurar-webhook-gitea.md b/docs/03-configurar-webhook-gitea.md new file mode 100644 index 0000000..413d5e3 --- /dev/null +++ b/docs/03-configurar-webhook-gitea.md @@ -0,0 +1,52 @@ +# 03 — Configurar o Webhook no Gitea + Listener + +## A) Subir o listener na VPS 4 (um por projeto, porta única) + +```bash +# 1. Copiar o exemplo e ajustar variáveis +cp webhook-listener/config.example.env /home/deploy/stack/webhook-listener/.env +nano /home/deploy/stack/webhook-listener/.env +# PORT=9003 +# WEBHOOK_TOKEN= +# DEPLOY_SCRIPT=/home/deploy/stack//deploy-prod-onhost.sh +# GITEA_TOKEN= + +# 2. Instalar a unit systemd +sudo cp webhook-listener/webhook-listener.service /etc/systemd/system/webhook-.service +sudo systemctl daemon-reload +sudo systemctl enable --now webhook- + +# 3. Conferir +systemctl status webhook- +``` + +## B) Criar o webhook no Gitea (VPS 3) + +No repositório → **Settings → Webhooks → Add Webhook → Gitea**: + +| Campo | Valor | +|---|---| +| Target URL | `http://10.99.0.4:9003/webhook?token=` | +| HTTP Method | `POST` | +| Content Type | `application/json` | +| Trigger | *Push events* (branch `main`) | +| Branch filter | `main` | + +> O `GITEA_TOKEN` (para reportar status) é gerado em **Settings → Applications → Generate Token** +> com escopo `repo:status` (ou `write:repository`). + +## C) Testar + +```bash +# Push de teste dispara o deploy: +git commit --allow-empty -m "test: dispara webhook" && git push origin main + +# Acompanhar: +tail -f /home/deploy/stack/webhook-listener/deploy.log +``` + +## Pré-requisitos + +- Chave SSH do usuário `deploy` (VPS4) com acesso **sem senha** à VPS 1 (`ssh deploy@10.99.0.1 echo ok`). +- Chave SSH do `deploy` com permissão de **push** no repositório do Gitea. +- Porta do listener liberada apenas na rede VPN (`10.99.0.0/24`). diff --git a/docs/04-seguranca-deploy.md b/docs/04-seguranca-deploy.md new file mode 100644 index 0000000..2c93438 --- /dev/null +++ b/docs/04-seguranca-deploy.md @@ -0,0 +1,32 @@ +# 04 — Segurança do Deploy + +## Segredos +- **Nunca** commitar tokens, senhas, JWT ou secrets. Use `.env` (no `.gitignore`) e + `EnvironmentFile=` no systemd. +- Tokens distintos por projeto: **WEBHOOK_TOKEN** (Gitea → listener) e **GITEA_TOKEN** (listener → API). +- Rotacionar tokens periodicamente. + +## SSH +- Comunicação VPS↔VPS só pela VPN (`10.99.0.x`). +- Chave do usuário `deploy` sem passphrase para automação, com acesso restrito (produção + push no Gitea). +- Usar `-o BatchMode=yes` nos scripts (falha rápido em vez de pedir senha). + +## Docker rootless na VPS 1 (⚠️ crítico) +- A VPS 1 roda **Docker rootless**. O `docker.service` (root) fica **desabilitado de propósito**. +- Sempre exportar `DOCKER_HOST=unix:///run/user/1000/docker.sock` nos comandos remotos. +- Se o daemon root for reativado e `deploy` estiver no grupo `docker`, um `docker compose up` sem + `DOCKER_HOST` cria containers **root paralelos**, invisíveis ao `docker ps` rootless e ao Traefik, + e pode prender portas (`docker-proxy` zumbi). + +```bash +# Estado correto da VPS 1: +sudo systemctl status docker # deve estar: inactive (dead) +groups deploy # NÃO deve conter "docker" +DOCKER_HOST=unix:///run/user/1000/docker.sock docker ps +``` + +## Boas práticas +- **Commite antes de buildar**: o deploy faz `git reset --hard` e descarta mudanças locais. +- **Nunca edite arquivos direto na VPS 1**: serão sobrescritos pelo `rsync --delete`. +- Trava de concorrência (lock file / `isBuilding`) sempre ativa. +- Acompanhar o `deploy.log` e o status do commit no Gitea após cada deploy. diff --git a/scripts/deploy-prod-builder.sh b/scripts/deploy-prod-builder.sh new file mode 100644 index 0000000..0bdd2a8 --- /dev/null +++ b/scripts/deploy-prod-builder.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# ─── deploy-prod-builder.sh (EXEMPLO — Modelo A: Builder-to-Host) ───────────── +# Build na VPS 4 (Builder) e envio dos ARTEFATOS COMPILADOS para a VPS 1. +# A VPS 1 NÃO compila nada — só recebe o build pronto e reinicia os containers. +# Indicado para frontends pesados (Next.js/Vite) onde compilar em produção é caro. +# ───────────────────────────────────────────────────────────────────────────── +set -e + +PROJECT_PATH="/home/deploy/stack/" +PROD_VPS="10.99.0.1" +PROD_USER="deploy" +LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log" +LOG_TAG="[]" +DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock" # Docker rootless na VPS1 + +# Trava de concorrência +LOCK_FILE="/tmp/deploy-.lock" +if [ -f "$LOCK_FILE" ]; then echo "🚨 Deploy já em andamento!"; exit 1; fi +touch "$LOCK_FILE"; trap 'rm -f "$LOCK_FILE"' EXIT + +log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] ${LOG_TAG} $1" | tee -a "$LOG_FILE"; } + +log "🚀 Iniciando build & deploy (Modelo A)..." + +# 1. GitOps: a fonte de verdade é o Gitea (VPS3) +cd "$PROJECT_PATH" +log "📦 git fetch + reset --hard origin/main" +git fetch origin main +git reset --hard origin/main + +# 2. Incrementar versão (grava version.json / deploys.json) +log "🔢 Incrementando versão..." +node update-version.js + +# 3. Compilar na VPS 4 +log "⚙️ Build backend..." +( cd backend && npm ci ) +log "⚙️ Build frontend..." +( cd frontend && npm ci && npm run build ) + +# 4. Enviar SOMENTE artefatos compilados para a VPS 1 (rsync) +log "🚚 rsync de artefatos -> VPS1..." +rsync -avz --delete --exclude '.env' --exclude 'node_modules/.cache' \ + "$PROJECT_PATH/backend/" "${PROD_USER}@${PROD_VPS}:${PROJECT_PATH}/backend/" +rsync -avz --delete \ + "$PROJECT_PATH/frontend/dist/" "${PROD_USER}@${PROD_VPS}:${PROJECT_PATH}/frontend/dist/" +rsync -avz \ + "$PROJECT_PATH/docker-compose.yml" "${PROD_USER}@${PROD_VPS}:${PROJECT_PATH}/" + +# 5. Reiniciar containers na VPS 1 (sem build) +log "🔄 Restart de containers na VPS1..." +ssh -o BatchMode=yes "${PROD_USER}@${PROD_VPS}" \ + "cd ${PROJECT_PATH} && DOCKER_HOST=${DOCKER_HOST_REMOTE} docker compose up -d" + +# 6. Commit do bump de volta ao Gitea (o listener ignora este commit -> sem loop) +log "💾 git commit + push do bump de versão..." +git add version.json deploys.json 2>/dev/null || true +git commit -m "chore(deploy): bump version" || true +git push origin main || true + +log "✅ Deploy (Modelo A) concluído com sucesso!" diff --git a/scripts/deploy-prod-onhost.sh b/scripts/deploy-prod-onhost.sh new file mode 100644 index 0000000..79124c5 --- /dev/null +++ b/scripts/deploy-prod-onhost.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# ─── deploy-prod-onhost.sh (EXEMPLO — Modelo B: build no host de produção) ──── +# Envia o CÓDIGO-FONTE para a VPS 1 e a própria VPS 1 builda a imagem Docker. +# Mais simples; indicado quando o build é leve (ex.: app Node servindo estáticos). +# Faz swap do container com quase zero downtime (--no-deps). +# ───────────────────────────────────────────────────────────────────────────── +set -e + +PROJECT_PATH="/home/deploy/stack/" +PROD_VPS="10.99.0.1" +PROD_USER="deploy" +LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log" +LOG_TAG="[]" +DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock" +SERVICE="app" # nome do serviço no docker-compose.yml + +LOCK_FILE="/tmp/deploy-.lock" +if [ -f "$LOCK_FILE" ]; then echo "🚨 Deploy já em andamento!"; exit 1; fi +touch "$LOCK_FILE"; trap 'rm -f "$LOCK_FILE"' EXIT + +log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] ${LOG_TAG} $1" | tee -a "$LOG_FILE"; } + +log "🚀 Iniciando deploy (Modelo B — build no host)..." + +# 1. GitOps na VPS 4 +cd "$PROJECT_PATH" +log "📦 git fetch + reset --hard origin/main" +git fetch origin main +git reset --hard origin/main + +# 2. Enviar o FONTE para a VPS 1 (sem node_modules, sem dados/segredos) +log "🚚 rsync do código-fonte -> VPS1..." +rsync -avz --delete \ + --exclude '.git' --exclude '.env' --exclude 'node_modules' \ + --exclude 'uploads' --exclude 'processed' --exclude 'data' \ + "$PROJECT_PATH/" "${PROD_USER}@${PROD_VPS}:${PROJECT_PATH}/" + +# 3. Build + swap do container NA VPS 1 +log "🐳 build + up (no-deps) na VPS1..." +ssh -o BatchMode=yes -o StrictHostKeyChecking=no "${PROD_USER}@${PROD_VPS}" " + set -e + cd ${PROJECT_PATH} + export DOCKER_HOST=${DOCKER_HOST_REMOTE} + docker compose build ${SERVICE} + docker compose up -d --no-deps ${SERVICE} +" + +log "✅ Deploy (Modelo B) concluído sem downtime!" diff --git a/scripts/deploy-to-prod.sh b/scripts/deploy-to-prod.sh new file mode 100644 index 0000000..587ac93 --- /dev/null +++ b/scripts/deploy-to-prod.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# ─── deploy-to-prod.sh (EXEMPLO — Promoção manual Staging -> Produção) ──────── +# Usado quando há um ambiente de STAGING na VPS 4 (docker-compose-staging.yml) e +# se deseja promover a build já validada para a PRODUÇÃO (VPS 1). +# ───────────────────────────────────────────────────────────────────────────── +set -e + +PROJECT_PATH="/home/deploy/stack/" +PROD_VPS="10.99.0.1" +PROD_USER="deploy" +LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log" +LOG_TAG="[-PROD]" +DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock" + +LOCK_FILE="/tmp/deploy--release.lock" +if [ -f "$LOCK_FILE" ]; then echo "🚨 Promoção já em andamento!"; exit 1; fi +touch "$LOCK_FILE"; trap 'rm -f "$LOCK_FILE"' EXIT + +log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] ${LOG_TAG} $1" | tee -a "$LOG_FILE"; } + +log "🚀 Promovendo Staging (VPS4) -> Produção (VPS1)..." + +# 1. Backend (fonte/artefato já validado em staging) +rsync -avz --delete --exclude '.env' --exclude 'node_modules/.cache' \ + "$PROJECT_PATH/backend/" "${PROD_USER}@${PROD_VPS}:${PROJECT_PATH}/backend/" + +# 2. Frontend (build compilada) +rsync -avz --delete \ + "$PROJECT_PATH/frontend/dist/" "${PROD_USER}@${PROD_VPS}:${PROJECT_PATH}/frontend/dist/" + +# 3. Infra (compose + configs) +rsync -avz \ + "$PROJECT_PATH/docker-compose.yml" "$PROJECT_PATH/nginx" \ + "${PROD_USER}@${PROD_VPS}:${PROJECT_PATH}/" + +# 4. Subir em produção +log "🔄 docker compose up -d na VPS1..." +ssh -o BatchMode=yes "${PROD_USER}@${PROD_VPS}" \ + "cd ${PROJECT_PATH} && DOCKER_HOST=${DOCKER_HOST_REMOTE} docker compose up -d" + +log "✅ Promoção para produção concluída!" diff --git a/webhook-listener/config.example.env b/webhook-listener/config.example.env new file mode 100644 index 0000000..3892fbb --- /dev/null +++ b/webhook-listener/config.example.env @@ -0,0 +1,18 @@ +# config.example.env — variáveis do listener (copie para .env e NÃO commite o .env) +# Use uma porta única por projeto. + +PORT=9000 +HOST=10.99.0.4 + +# Token combinado entre o webhook do Gitea e este listener (?token=...) +WEBHOOK_TOKEN= + +# Script de deploy que será disparado a cada push válido +DEPLOY_SCRIPT=/home/deploy/stack//deploy-prod-builder.sh + +# Log compartilhado dos deploys +LOG_FILE=/home/deploy/stack/webhook-listener/deploy.log + +# API do Gitea (VPS3) para reportar status do commit +GITEA_API_URL=http://10.99.0.3:3000/api/v1 +GITEA_TOKEN= diff --git a/webhook-listener/listener.js b/webhook-listener/listener.js new file mode 100644 index 0000000..38436c9 --- /dev/null +++ b/webhook-listener/listener.js @@ -0,0 +1,125 @@ +/** + * listener.js — Webhook listener genérico para deploy via Gitea (EXEMPLO SANITIZADO) + * -------------------------------------------------------------------------------- + * Recebe o webhook de "push" do Gitea, valida o token, dispara o script de deploy + * em background e reporta o status do commit de volta ao Gitea (pending/success/failure). + * + * Parametrizável por variáveis de ambiente (ver config.example.env): + * PORT, HOST, WEBHOOK_TOKEN, DEPLOY_SCRIPT, LOG_FILE, GITEA_API_URL, GITEA_TOKEN + * + * Rode um listener por projeto, cada um numa porta diferente. + */ +const http = require('http'); +const { exec } = require('child_process'); +const fs = require('fs'); + +const PORT = process.env.PORT || 9000; +const HOST = process.env.HOST || '10.99.0.4'; +const WEBHOOK_TOKEN = process.env.WEBHOOK_TOKEN || ''; +const DEPLOY_SCRIPT = process.env.DEPLOY_SCRIPT || '/home/deploy/stack//deploy-prod-builder.sh'; +const LOG_FILE = process.env.LOG_FILE || '/home/deploy/stack/webhook-listener/deploy.log'; +const GITEA_API_URL = process.env.GITEA_API_URL || 'http://10.99.0.3:3000/api/v1'; +const GITEA_TOKEN = process.env.GITEA_TOKEN || ''; + +function log(message) { + const line = '[' + new Date().toISOString() + '] ' + message + '\n'; + console.log(message); + try { fs.appendFileSync(LOG_FILE, line, 'utf8'); } catch (_) {} +} + +// Atualiza o status do commit no Gitea (mostra o ✔/✘ na interface) +function updateCommitStatus(owner, repo, sha, state, description) { + if (!owner || !repo || !sha) return; + const postData = JSON.stringify({ + state, description, + target_url: 'http://' + HOST + ':' + PORT, + context: 'continuous-integration/webhook' + }); + const url = new URL(`${GITEA_API_URL}/repos/${owner}/${repo}/statuses/${sha}`); + const req = http.request({ + hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'token ' + GITEA_TOKEN, + 'Content-Length': Buffer.byteLength(postData) + } + }, (res) => { res.on('data', () => {}); res.on('end', () => log(`Gitea Status (${state}): HTTP ${res.statusCode}`)); }); + req.on('error', (e) => log(`Erro API Gitea: ${e.message}`)); + req.write(postData); req.end(); +} + +let isBuilding = false; +const VERSION_BUMP_RE = /^chore\(deploy\):\s*bump version/i; + +const server = http.createServer((req, res) => { + const urlPath = req.url.split('?')[0]; + if (req.method !== 'POST' || urlPath !== '/webhook') { + res.writeHead(404, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ error: 'Rota nao encontrada' })); + } + + // Token via header (x-gitea-token) ou query string (?token=...) + let tokenQuery = null; + const qIndex = req.url.indexOf('?'); + if (qIndex !== -1) { + for (const pair of req.url.substring(qIndex + 1).split('&')) { + const [k, v] = pair.split('='); + if (decodeURIComponent(k) === 'token') { tokenQuery = decodeURIComponent(v || ''); break; } + } + } + const token = req.headers['x-gitea-token'] || tokenQuery; + if (!token || token !== WEBHOOK_TOKEN) { + log('Acesso nao autorizado: token invalido/ausente.'); + res.writeHead(401, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ error: 'Nao autorizado' })); + } + + if (isBuilding) { + log('Build ja em andamento — requisicao ignorada (anti-concorrencia).'); + res.writeHead(429, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ error: 'Build em andamento' })); + } + + let body = ''; + req.on('data', (chunk) => { body += chunk.toString(); }); + req.on('end', () => { + let payload = {}; + try { payload = JSON.parse(body); } catch (_) {} + + const commits = payload.commits || []; + const headMsg = (payload.head_commit || {}).message || ''; + const messages = commits.map((c) => c.message || '').concat(headMsg).filter((m) => m.trim() !== ''); + + const owner = payload.repository && payload.repository.owner ? payload.repository.owner.login : null; + const repo = payload.repository ? payload.repository.name : null; + const sha = payload.after || (payload.head_commit ? payload.head_commit.id : null); + + // Ignora commits de bump de versao -> evita loop de CI (o deploy faz git push do bump) + if (messages.length > 0 && messages.every((m) => VERSION_BUMP_RE.test(m))) { + log('Commit de bump de versao -> deploy ignorado.'); + updateCommitStatus(owner, repo, sha, 'success', 'Ignorado: bump version'); + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ status: 'skipped', reason: 'version-bump' })); + } + + // Responde imediatamente (202) para nao estourar timeout do Gitea + res.writeHead(202, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok', message: 'Deploy iniciado em background' })); + + isBuilding = true; + log('Iniciando ' + DEPLOY_SCRIPT + ' ...'); + updateCommitStatus(owner, repo, sha, 'pending', 'Deploy em andamento (VPS4)'); + + exec(DEPLOY_SCRIPT, (error) => { + isBuilding = false; + if (error) { + log('Erro no deploy: ' + error.message); + return updateCommitStatus(owner, repo, sha, 'failure', 'Falha no deploy (VPS4)'); + } + log('Deploy finalizado com sucesso.'); + updateCommitStatus(owner, repo, sha, 'success', 'Deploy concluido (VPS4)'); + }); + }); +}); + +server.listen(PORT, HOST, () => log(`Webhook listener ativo em http://${HOST}:${PORT}/webhook`)); diff --git a/webhook-listener/webhook-listener.service b/webhook-listener/webhook-listener.service new file mode 100644 index 0000000..eef3a18 --- /dev/null +++ b/webhook-listener/webhook-listener.service @@ -0,0 +1,22 @@ +# /etc/systemd/system/webhook-.service (EXEMPLO) +# Habilite com: sudo systemctl daemon-reload && sudo systemctl enable --now webhook- +[Unit] +Description=GitOps Webhook Listener () +After=network.target + +[Service] +Type=simple +User=deploy +Group=deploy +WorkingDirectory=/home/deploy/stack/webhook-listener +# Carregue as variáveis a partir do .env (não commitar o .env real) +EnvironmentFile=/home/deploy/stack/webhook-listener/.env +ExecStart=/usr/bin/node listener.js +Restart=always +RestartSec=5 +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=webhook- + +[Install] +WantedBy=multi-user.target