feat: exemplos e documentacao da automacao de deploy GitOps (v1.0.0)
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
# Nunca commitar segredos nem dependências
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!config.example.env
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.next/
|
||||||
|
*.log
|
||||||
@@ -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.
|
||||||
@@ -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 ] ◄───────────────────┘
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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/<projeto>.env
|
||||||
|
nano /home/deploy/stack/webhook-listener/<projeto>.env
|
||||||
|
# PORT=9003
|
||||||
|
# WEBHOOK_TOKEN=<token-forte>
|
||||||
|
# DEPLOY_SCRIPT=/home/deploy/stack/<projeto>/deploy-prod-onhost.sh
|
||||||
|
# GITEA_TOKEN=<token-da-API-do-gitea>
|
||||||
|
|
||||||
|
# 2. Instalar a unit systemd
|
||||||
|
sudo cp webhook-listener/webhook-listener.service /etc/systemd/system/webhook-<projeto>.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now webhook-<projeto>
|
||||||
|
|
||||||
|
# 3. Conferir
|
||||||
|
systemctl status webhook-<projeto>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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=<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`).
|
||||||
@@ -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.
|
||||||
@@ -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/<projeto>"
|
||||||
|
PROD_VPS="10.99.0.1"
|
||||||
|
PROD_USER="deploy"
|
||||||
|
LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log"
|
||||||
|
LOG_TAG="[<projeto>]"
|
||||||
|
DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock" # Docker rootless na VPS1
|
||||||
|
|
||||||
|
# Trava de concorrência
|
||||||
|
LOCK_FILE="/tmp/deploy-<projeto>.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!"
|
||||||
@@ -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/<projeto>"
|
||||||
|
PROD_VPS="10.99.0.1"
|
||||||
|
PROD_USER="deploy"
|
||||||
|
LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log"
|
||||||
|
LOG_TAG="[<projeto>]"
|
||||||
|
DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock"
|
||||||
|
SERVICE="app" # nome do serviço no docker-compose.yml
|
||||||
|
|
||||||
|
LOCK_FILE="/tmp/deploy-<projeto>.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!"
|
||||||
@@ -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/<projeto>"
|
||||||
|
PROD_VPS="10.99.0.1"
|
||||||
|
PROD_USER="deploy"
|
||||||
|
LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log"
|
||||||
|
LOG_TAG="[<projeto>-PROD]"
|
||||||
|
DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock"
|
||||||
|
|
||||||
|
LOCK_FILE="/tmp/deploy-<projeto>-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!"
|
||||||
@@ -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=<WEBHOOK_TOKEN>
|
||||||
|
|
||||||
|
# Script de deploy que será disparado a cada push válido
|
||||||
|
DEPLOY_SCRIPT=/home/deploy/stack/<projeto>/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=<GITEA_API_TOKEN>
|
||||||
@@ -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 || '<WEBHOOK_TOKEN>';
|
||||||
|
const DEPLOY_SCRIPT = process.env.DEPLOY_SCRIPT || '/home/deploy/stack/<projeto>/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 || '<GITEA_API_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`));
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# /etc/systemd/system/webhook-<projeto>.service (EXEMPLO)
|
||||||
|
# Habilite com: sudo systemctl daemon-reload && sudo systemctl enable --now webhook-<projeto>
|
||||||
|
[Unit]
|
||||||
|
Description=GitOps Webhook Listener (<projeto>)
|
||||||
|
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/<projeto>.env
|
||||||
|
ExecStart=/usr/bin/node listener.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=syslog
|
||||||
|
StandardError=syslog
|
||||||
|
SyslogIdentifier=webhook-<projeto>
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user