feat(deploy): padroniza rx p/ Build Once->Promote->Deploy (CI registry, compose.prod, badge/health)
- .gitea/workflows/build.yml: push->build rx-scoreodonto:<sha> no registry; tag->promote+deploy - docker-compose.prod.yml: image do registry, APP_VERSION/GIT_COMMIT em runtime - deploy-prod.sh: VPS1 pull + up --no-build (antigo vira deploy-prod-legado-webhook.sh) - /api/health usa APP_VERSION (tag) com fallback version.txt; badge no canto - webhook 9003 aposentado (gatilho agora e CI/tag) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
name: build-and-promote
|
||||||
|
|
||||||
|
# Build Once → Promote → Deploy (modelo da stack — ver Documentos-Unicos/deploy):
|
||||||
|
# • push na main → job BUILD: builda 1x e publica por COMMIT (:<sha> + :latest). NÃO deploya.
|
||||||
|
# • tag vX.Y.Z → job PROMOTE: re-tagueia a imagem do commit como :vX.Y.Z (SEM rebuild),
|
||||||
|
# valida digest e deploya na VPS1 (pull + up --no-build).
|
||||||
|
# A versão semântica NÃO entra no build: nasce da tag e é injetada em runtime (docker-compose.prod.yml).
|
||||||
|
# O React (Vite) é buildado como step ANTES do docker build (o Dockerfile atual espera o dist/ pronto).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ───────────────────────── BUILD (só em push de branch) ─────────────────────────
|
||||||
|
build:
|
||||||
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Vars (commit + data)
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "build_time=$(date -u +'%Y-%m-%d %H:%M UTC')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Login no registry
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
|
||||||
|
else
|
||||||
|
echo "REGISTRY_TOKEN ausente — usando a credencial já presente no host."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build do React (Vite) — gera dental-client/dist/ antes do docker build
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cp dental-server/version.txt dental-server/dental-client/public/version.txt
|
||||||
|
cd dental-server/dental-client
|
||||||
|
NODE_ENV=development npm install --include=dev --prefer-offline
|
||||||
|
npm run build
|
||||||
|
ls dist/index.html >/dev/null && echo "OK: dist/ pronto"
|
||||||
|
|
||||||
|
- name: Build da imagem (inclui dental-client/dist/)
|
||||||
|
env:
|
||||||
|
COMPOSE_PROJECT_NAME: rxscoreodontocom
|
||||||
|
run: docker compose -f docker-compose.yml build app
|
||||||
|
|
||||||
|
- name: Publicar por COMMIT (:<sha> + :latest), com retry p/ 499
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
REG=git.clube67.com/ruicesar
|
||||||
|
C="${{ steps.vars.outputs.commit }}"
|
||||||
|
SRC="rxscoreodontocom-app:latest"
|
||||||
|
DST="${REG}/rx-scoreodonto"
|
||||||
|
docker tag "$SRC" "$DST:$C"
|
||||||
|
docker tag "$SRC" "$DST:latest"
|
||||||
|
for tag in "$C" "latest"; do
|
||||||
|
n=0
|
||||||
|
until docker push "$DST:$tag"; do
|
||||||
|
n=$((n+1)); [ $n -ge 4 ] && { echo "push falhou: $DST:$tag"; exit 1; }
|
||||||
|
echo "retry $n (499 transitório) em $DST:$tag"; sleep 3
|
||||||
|
done
|
||||||
|
done
|
||||||
|
echo "Publicado: rx-scoreodonto:$C (+ :latest). Push NÃO deploya."
|
||||||
|
|
||||||
|
# ─────────────────────── PROMOTE (só em tag vX.Y.Z) ───────────────────────
|
||||||
|
promote:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Checkout (na tag)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Vars (tag + commit)
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Login no registry
|
||||||
|
run: |
|
||||||
|
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Integridade — a imagem do commit existe? (nunca buildar na tag)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
REG=git.clube67.com/ruicesar
|
||||||
|
C="${{ steps.vars.outputs.commit }}"
|
||||||
|
if ! docker buildx imagetools inspect "${REG}/rx-scoreodonto:$C" >/dev/null 2>&1; then
|
||||||
|
echo "ERRO: ${REG}/rx-scoreodonto:$C não existe no registry."
|
||||||
|
echo " Faça 'git push origin main' (que builda o commit $C) ANTES de criar a tag."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: rx-scoreodonto:$C presente no registry."
|
||||||
|
|
||||||
|
- name: Promover — re-tag commit → versão (SEM rebuild) + conferir digest
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
REG=git.clube67.com/ruicesar
|
||||||
|
C="${{ steps.vars.outputs.commit }}"; V="${{ steps.vars.outputs.version }}"
|
||||||
|
DST="${REG}/rx-scoreodonto"
|
||||||
|
docker buildx imagetools create -t "$DST:$V" "$DST:$C"
|
||||||
|
dC=$(docker buildx imagetools inspect "$DST:$C" --format '{{.Manifest.Digest}}')
|
||||||
|
dV=$(docker buildx imagetools inspect "$DST:$V" --format '{{.Manifest.Digest}}')
|
||||||
|
[ "$dC" = "$dV" ] || { echo "ERRO: digest divergiu ($V=$dV != $C=$dC)"; exit 1; }
|
||||||
|
echo "Promovido sem rebuild: :$V e :$C apontam para o mesmo artefato (digest $dV)."
|
||||||
|
|
||||||
|
- name: Deploy na VPS1 (pull :versao + up --no-build; versao/commit injetados em runtime)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
V="${{ steps.vars.outputs.version }}"; C="${{ steps.vars.outputs.commit }}"
|
||||||
|
scp -o StrictHostKeyChecking=accept-new \
|
||||||
|
docker-compose.yml docker-compose.prod.yml deploy-prod.sh \
|
||||||
|
deploy@10.99.0.1:/home/deploy/stack/rx.scoreodonto.com/
|
||||||
|
ssh -o StrictHostKeyChecking=accept-new deploy@10.99.0.1 \
|
||||||
|
"/home/deploy/stack/rx.scoreodonto.com/deploy-prod.sh $V $C"
|
||||||
@@ -1 +1 @@
|
|||||||
2.1.76
|
2.1.77
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { SocketProvider } from './contexts/SocketContext';
|
import { SocketProvider } from './contexts/SocketContext';
|
||||||
@@ -23,8 +24,24 @@ import UsersPage from './pages/UsersPage';
|
|||||||
import SyncPage from './pages/SyncPage';
|
import SyncPage from './pages/SyncPage';
|
||||||
import ApiDocsPage from './pages/ApiDocsPage';
|
import ApiDocsPage from './pages/ApiDocsPage';
|
||||||
|
|
||||||
|
// Versão fixa no canto inferior direito (padrão da stack — lê /api/health; não bloqueia cliques).
|
||||||
|
function VersionBadge() {
|
||||||
|
const [ver, setVer] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/health').then(r => (r.ok ? r.json() : null))
|
||||||
|
.then(v => { if (v && v.version) setVer(v.version); }).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
if (!ver) return null;
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'fixed', bottom: 6, right: 8, zIndex: 40, pointerEvents: 'none', userSelect: 'none', fontSize: 10, fontFamily: 'monospace', color: 'rgba(120,120,120,0.7)' }}>
|
||||||
|
v{ver}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
@@ -145,5 +162,7 @@ export default function App() {
|
|||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
<VersionBadge />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
// Em dev, faz proxy da API para o backend Express
|
// Em dev, o vite serve o frontend e faz proxy da API para o backend (container rx_dev_api).
|
||||||
server: {
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
strictPort: true,
|
||||||
cors: true,
|
cors: true,
|
||||||
|
allowedHosts: ['dev.rx.scoreodonto.com'],
|
||||||
hmr: {
|
hmr: {
|
||||||
host: 'dev.rx.scoreodonto.com',
|
host: 'dev.rx.scoreodonto.com',
|
||||||
clientPort: 443,
|
clientPort: 443,
|
||||||
@@ -15,7 +19,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:3000',
|
target: 'http://rx_dev_api:3000',
|
||||||
configure: (proxy) => {
|
configure: (proxy) => {
|
||||||
proxy.on('proxyRes', (proxyRes) => {
|
proxy.on('proxyRes', (proxyRes) => {
|
||||||
proxyRes.headers['access-control-allow-origin'] = '*';
|
proxyRes.headers['access-control-allow-origin'] = '*';
|
||||||
@@ -23,15 +27,14 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'/socket.io': {
|
'/socket.io': {
|
||||||
target: 'http://127.0.0.1:3000',
|
target: 'http://rx_dev_api:3000',
|
||||||
ws: true
|
ws: true
|
||||||
},
|
},
|
||||||
'/uploads': 'http://127.0.0.1:3000',
|
'/uploads': 'http://rx_dev_api:3000',
|
||||||
'/updates': 'http://127.0.0.1:3000'
|
'/updates': 'http://rx_dev_api:3000'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
// O output vai para o diretório que o Express vai servir
|
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
emptyOutDir: true
|
emptyOutDir: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,40 @@ app.get('/api/redis/ping', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Health check padronizado (status do backend + banco + cache). Padrão da stack:
|
||||||
|
// ver git.clube67.com/ruicesar/Documentos-Unicos → deploy/versionamento.md
|
||||||
|
app.get('/api/health', async (req, res) => {
|
||||||
|
const checks = {};
|
||||||
|
try {
|
||||||
|
const t = Date.now();
|
||||||
|
await db.get('SELECT 1');
|
||||||
|
checks.database = { status: 'ok', latency_ms: Date.now() - t };
|
||||||
|
} catch {
|
||||||
|
checks.database = { status: 'down' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const client = redis.getClient();
|
||||||
|
if (client) { const t = Date.now(); await client.ping(); checks.cache = { status: 'ok', latency_ms: Date.now() - t }; }
|
||||||
|
else checks.cache = { status: 'disabled' };
|
||||||
|
} catch {
|
||||||
|
checks.cache = { status: 'down' };
|
||||||
|
}
|
||||||
|
// Em PROD a versão vem da TAG (APP_VERSION, injetada em runtime pelo deploy);
|
||||||
|
// em DEV cai para version.txt (arquivo da imagem). Padrão "build once, promote".
|
||||||
|
let version = (process.env.APP_VERSION && process.env.APP_VERSION !== 'dev') ? process.env.APP_VERSION : 'dev';
|
||||||
|
if (version === 'dev') { try { version = (await fs.readFile(path.join(__dirname, 'version.txt'), 'utf8')).trim(); } catch {} }
|
||||||
|
const dbOk = checks.database.status === 'ok';
|
||||||
|
const status = !dbOk ? 'down' : (checks.cache.status === 'down' ? 'degraded' : 'ok');
|
||||||
|
res.status(dbOk ? 200 : 503).json({
|
||||||
|
status,
|
||||||
|
version,
|
||||||
|
commit: process.env.GIT_COMMIT || 'dev',
|
||||||
|
environment: (process.env.APP_ENV || 'DEV').toUpperCase(),
|
||||||
|
uptime_s: Math.round(process.uptime()),
|
||||||
|
checks,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Verificar se o nome do computador/cliente já existe
|
// Verificar se o nome do computador/cliente já existe
|
||||||
app.get('/api/clients/check-name', async (req, res) => {
|
app.get('/api/clients/check-name', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.1.76
|
2.1.77
|
||||||
|
|||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ─── deploy-prod.sh (rx.scoreodonto.com) ──────────────────────────────────────
|
||||||
|
# GitOps: VPS4 (Builder) → VPS1 (Produção)
|
||||||
|
# Modelo A: build na VPS4 (inclui build React + Docker) → imagem pronta → VPS1
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROD_VPS="10.99.0.1"
|
||||||
|
PROD_USER="deploy"
|
||||||
|
PROD_PATH="/home/deploy/stack/rx.scoreodonto.com"
|
||||||
|
LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log"
|
||||||
|
LOG_TAG="[rx.scoreodonto.com]"
|
||||||
|
DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock"
|
||||||
|
|
||||||
|
LOCK_FILE="/tmp/deploy-rx-scoreodonto.lock"
|
||||||
|
if [ -f "$LOCK_FILE" ]; then
|
||||||
|
echo "🚨 Deploy já em andamento!" | tee -a "$LOG_FILE"
|
||||||
|
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..."
|
||||||
|
|
||||||
|
# 1. GitOps: fonte de verdade é o Gitea (VPS3)
|
||||||
|
cd "$PROD_PATH"
|
||||||
|
log "📦 git fetch + reset --hard origin/main"
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
|
# 2. Bump de versão (sincroniza version.txt)
|
||||||
|
log "🔢 Incrementando versão..."
|
||||||
|
node update-version.js
|
||||||
|
|
||||||
|
# 3. Build do frontend React (Vite) — gera dental-server/dental-client/dist/
|
||||||
|
# DEVE ocorrer ANTES do docker compose build para que o dist/ seja copiado na imagem
|
||||||
|
log "⚛️ Build do React (Vite)..."
|
||||||
|
cd "$PROD_PATH/dental-server/dental-client"
|
||||||
|
cp "$PROD_PATH/dental-server/version.txt" public/version.txt
|
||||||
|
# --include=dev: o listener roda com NODE_ENV=production, que por padrão pula
|
||||||
|
# devDependencies — mas o Vite é devDependency e é necessário para o build.
|
||||||
|
NODE_ENV=development npm install --include=dev --prefer-offline
|
||||||
|
npm run build
|
||||||
|
cd "$PROD_PATH"
|
||||||
|
log "✅ Build React concluído — dist/ pronto"
|
||||||
|
|
||||||
|
# 4. Build da imagem Docker na VPS4 (inclui dental-client/dist/)
|
||||||
|
log "🐳 docker compose build na VPS4..."
|
||||||
|
docker compose -f "$PROD_PATH/docker-compose.yml" build app
|
||||||
|
|
||||||
|
# 5. Exportar imagem para arquivo
|
||||||
|
log "💾 Exportando imagem para arquivo..."
|
||||||
|
IMAGE_NAME="rxscoreodontocom-app:latest"
|
||||||
|
IMAGE_TAR="/tmp/rxscoreodontocom-app.tar.gz"
|
||||||
|
docker save "$IMAGE_NAME" | gzip > "$IMAGE_TAR"
|
||||||
|
|
||||||
|
# 6. Enviar docker-compose.yml + imagem + versão para VPS1
|
||||||
|
log "🚚 rsync docker-compose + imagem + versão → VPS1..."
|
||||||
|
rsync -avz \
|
||||||
|
"$PROD_PATH/docker-compose.yml" \
|
||||||
|
"$PROD_PATH/dental-server/version.txt" \
|
||||||
|
"$IMAGE_TAR" \
|
||||||
|
"${PROD_USER}@${PROD_VPS}:${PROD_PATH}/"
|
||||||
|
|
||||||
|
# 7. Carregar imagem e subir container na VPS1
|
||||||
|
log "🚀 Load imagem + docker compose up na VPS1..."
|
||||||
|
ssh -o BatchMode=yes -o StrictHostKeyChecking=no "${PROD_USER}@${PROD_VPS}" "
|
||||||
|
set -e
|
||||||
|
cd ${PROD_PATH}
|
||||||
|
export DOCKER_HOST=${DOCKER_HOST_REMOTE}
|
||||||
|
docker load < rxscoreodontocom-app.tar.gz
|
||||||
|
docker compose up -d --no-deps app
|
||||||
|
rm -f rxscoreodontocom-app.tar.gz
|
||||||
|
"
|
||||||
|
|
||||||
|
# 8. Commit do bump de versão de volta ao Gitea (sem loop — listener ignora)
|
||||||
|
# package.json NÃO é incluído: preserva cache do npm install no Docker
|
||||||
|
log "💾 git commit + push do bump de versão..."
|
||||||
|
git add dental-server/version.txt
|
||||||
|
git commit -m "chore(deploy): bump version" || true
|
||||||
|
git push origin main || true
|
||||||
|
|
||||||
|
log "✅ Deploy concluído sem downtime!"
|
||||||
+40
-76
@@ -1,85 +1,49 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# ─── deploy-prod.sh (rx.scoreodonto.com) ──────────────────────────────────────
|
# =============================================================================
|
||||||
# GitOps: VPS4 (Builder) → VPS1 (Produção)
|
# Deploy por TAG na PRODUÇÃO (VPS1) — rx.scoreodonto.com (modelo build-once/promote)
|
||||||
# Modelo A: build na VPS4 (inclui build React + Docker) → imagem pronta → VPS1
|
# Puxa a imagem versionada do registry do Gitea e sobe, SEM build.
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
#
|
||||||
set -e
|
# Uso (NA VPS1): ./deploy-prod.sh v2.2.0 <commit>
|
||||||
|
# Rollback: ./deploy-prod.sh <tag-anterior> <commit-anterior>
|
||||||
|
#
|
||||||
|
# Pré-requisitos (uma vez): docker login git.clube67.com (token read:package).
|
||||||
|
# Substitui o modelo antigo (webhook 9003 → build na VPS4 → save/rsync/load).
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
PROD_VPS="10.99.0.1"
|
TAG="${1:-}"
|
||||||
PROD_USER="deploy"
|
COMMIT="${2:-dev}"
|
||||||
PROD_PATH="/home/deploy/stack/rx.scoreodonto.com"
|
if [ -z "$TAG" ]; then
|
||||||
LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log"
|
echo "uso: ./deploy-prod.sh <vX.Y.Z> [commit] (ex.: ./deploy-prod.sh v2.2.0 ab12cd3)"
|
||||||
LOG_TAG="[rx.scoreodonto.com]"
|
echo "tags disponíveis: git.clube67.com → Packages → rx-scoreodonto"
|
||||||
DOCKER_HOST_REMOTE="unix:///run/user/1000/docker.sock"
|
|
||||||
|
|
||||||
LOCK_FILE="/tmp/deploy-rx-scoreodonto.lock"
|
|
||||||
if [ -f "$LOCK_FILE" ]; then
|
|
||||||
echo "🚨 Deploy já em andamento!" | tee -a "$LOG_FILE"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"; }
|
export DOCKER_HOST=unix:///run/user/1000/docker.sock
|
||||||
|
export RX_TAG="$TAG"
|
||||||
|
export RX_COMMIT="$COMMIT"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
FILES="-f docker-compose.yml -f docker-compose.prod.yml"
|
||||||
|
|
||||||
log "🚀 Iniciando deploy..."
|
echo "──────────────────────────────────────────────────────────────"
|
||||||
|
echo " Deploy rx.scoreodonto.com → PRODUÇÃO tag: $TAG commit: $COMMIT"
|
||||||
|
echo " Versão exibida (/api/health → badge) injetada em runtime = $TAG"
|
||||||
|
echo " Volumes preservados: uploads/ processed/ data/ dental-server/updates/ (auto-update dos clients)"
|
||||||
|
echo "──────────────────────────────────────────────────────────────"
|
||||||
|
|
||||||
# 1. GitOps: fonte de verdade é o Gitea (VPS3)
|
echo "[1/3] pull da imagem $TAG ..."
|
||||||
cd "$PROD_PATH"
|
docker compose $FILES pull app
|
||||||
log "📦 git fetch + reset --hard origin/main"
|
|
||||||
git fetch origin main
|
|
||||||
git reset --hard origin/main
|
|
||||||
|
|
||||||
# 2. Bump de versão (sincroniza version.txt)
|
echo "[2/3] subindo (sem build) ..."
|
||||||
log "🔢 Incrementando versão..."
|
docker compose $FILES up -d --no-build app
|
||||||
node update-version.js
|
|
||||||
|
|
||||||
# 3. Build do frontend React (Vite) — gera dental-server/dental-client/dist/
|
echo "[3/3] verificação (aguardando boot) ..."
|
||||||
# DEVE ocorrer ANTES do docker compose build para que o dist/ seja copiado na imagem
|
sleep 12
|
||||||
log "⚛️ Build do React (Vite)..."
|
echo -n " health no ar: "
|
||||||
cd "$PROD_PATH/dental-server/dental-client"
|
curl -s --max-time 10 https://rx.scoreodonto.com/api/health || echo "(ainda subindo — confira em alguns segundos)"
|
||||||
cp "$PROD_PATH/dental-server/version.txt" public/version.txt
|
echo
|
||||||
# --include=dev: o listener roda com NODE_ENV=production, que por padrão pula
|
docker logs rx_scoreodonto_app --tail 5 2>&1 | grep -iE 'VERSION|listen|porta|ready' || true
|
||||||
# devDependencies — mas o Vite é devDependency e é necessário para o build.
|
|
||||||
NODE_ENV=development npm install --include=dev --prefer-offline
|
|
||||||
npm run build
|
|
||||||
cd "$PROD_PATH"
|
|
||||||
log "✅ Build React concluído — dist/ pronto"
|
|
||||||
|
|
||||||
# 4. Build da imagem Docker na VPS4 (inclui dental-client/dist/)
|
echo
|
||||||
log "🐳 docker compose build na VPS4..."
|
echo "✅ Deploy concluído (tag $TAG)."
|
||||||
docker compose -f "$PROD_PATH/docker-compose.yml" build app
|
echo " Rollback: ./deploy-prod.sh <tag-anterior> <commit-anterior>"
|
||||||
|
|
||||||
# 5. Exportar imagem para arquivo
|
|
||||||
log "💾 Exportando imagem para arquivo..."
|
|
||||||
IMAGE_NAME="rxscoreodontocom-app:latest"
|
|
||||||
IMAGE_TAR="/tmp/rxscoreodontocom-app.tar.gz"
|
|
||||||
docker save "$IMAGE_NAME" | gzip > "$IMAGE_TAR"
|
|
||||||
|
|
||||||
# 6. Enviar docker-compose.yml + imagem + versão para VPS1
|
|
||||||
log "🚚 rsync docker-compose + imagem + versão → VPS1..."
|
|
||||||
rsync -avz \
|
|
||||||
"$PROD_PATH/docker-compose.yml" \
|
|
||||||
"$PROD_PATH/dental-server/version.txt" \
|
|
||||||
"$IMAGE_TAR" \
|
|
||||||
"${PROD_USER}@${PROD_VPS}:${PROD_PATH}/"
|
|
||||||
|
|
||||||
# 7. Carregar imagem e subir container na VPS1
|
|
||||||
log "🚀 Load imagem + docker compose up na VPS1..."
|
|
||||||
ssh -o BatchMode=yes -o StrictHostKeyChecking=no "${PROD_USER}@${PROD_VPS}" "
|
|
||||||
set -e
|
|
||||||
cd ${PROD_PATH}
|
|
||||||
export DOCKER_HOST=${DOCKER_HOST_REMOTE}
|
|
||||||
docker load < rxscoreodontocom-app.tar.gz
|
|
||||||
docker compose up -d --no-deps app
|
|
||||||
rm -f rxscoreodontocom-app.tar.gz
|
|
||||||
"
|
|
||||||
|
|
||||||
# 8. Commit do bump de versão de volta ao Gitea (sem loop — listener ignora)
|
|
||||||
# package.json NÃO é incluído: preserva cache do npm install no Docker
|
|
||||||
log "💾 git commit + push do bump de versão..."
|
|
||||||
git add dental-server/version.txt
|
|
||||||
git commit -m "chore(deploy): bump version" || true
|
|
||||||
git push origin main || true
|
|
||||||
|
|
||||||
log "✅ Deploy concluído sem downtime!"
|
|
||||||
|
|||||||
+12
-2
@@ -1,10 +1,11 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
|
# DEV do rx (VPS4). Antes usava network_mode:host, que no Docker rootless não expõe na
|
||||||
|
# rede real → Traefik (VPS1) não alcançava o vite → 502. Agora: bridge + porta publicada.
|
||||||
services:
|
services:
|
||||||
dev-api:
|
dev-api:
|
||||||
image: node:20
|
image: node:20
|
||||||
container_name: rx_dev_api
|
container_name: rx_dev_api
|
||||||
network_mode: "host"
|
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
volumes:
|
volumes:
|
||||||
- ./dental-server:/app
|
- ./dental-server:/app
|
||||||
@@ -13,16 +14,25 @@ services:
|
|||||||
sh -c "npm install && npm run dev"
|
sh -c "npm install && npm run dev"
|
||||||
env_file:
|
env_file:
|
||||||
- ./dental-server/.env
|
- ./dental-server/.env
|
||||||
|
networks:
|
||||||
|
- rxdev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
dev-client:
|
dev-client:
|
||||||
image: node:20
|
image: node:20
|
||||||
container_name: rx_dev_client
|
container_name: rx_dev_client
|
||||||
network_mode: "host"
|
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
volumes:
|
volumes:
|
||||||
- ./dental-server/dental-client:/app
|
- ./dental-server/dental-client:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
command: >
|
command: >
|
||||||
sh -c "npm install && npx vite --host 0.0.0.0 --port 5173"
|
sh -c "npm install && npx vite --host 0.0.0.0 --port 5173"
|
||||||
|
ports:
|
||||||
|
- "5173:5173" # publicado na VPS4 → alvo do Traefik (dev.rx → 10.99.0.4:5173)
|
||||||
|
networks:
|
||||||
|
- rxdev # mesma rede do api → o proxy do vite alcança rx_dev_api:3000
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rxdev:
|
||||||
|
driver: bridge
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Override de PRODUÇÃO (VPS1): roda a IMAGEM VERSIONADA do registry em vez de buildar.
|
||||||
|
# A produção não builda — só consome a imagem pronta publicada pela CI (build once).
|
||||||
|
#
|
||||||
|
# Uso (na VPS1), via deploy-prod.sh <tag> <commit>, que exporta RX_TAG/RX_COMMIT e faz:
|
||||||
|
# docker compose -f docker-compose.yml -f docker-compose.prod.yml pull app
|
||||||
|
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-build app
|
||||||
|
# O --no-build garante que o build: do compose base seja ignorado (usa a imagem puxada).
|
||||||
|
#
|
||||||
|
# VERSÃO EM RUNTIME: a versão semântica exibida (badge → /api/health) vem da TAG promovida,
|
||||||
|
# injetada via `environment` abaixo — NÃO do build. Assim a MESMA imagem pode ser promovida
|
||||||
|
# sob qualquer tag sem rebuild (pré-requisito do "build once, promote the same artifact").
|
||||||
|
# O `commit` também é injetado em runtime (vem do CI que buildou a imagem do commit).
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: git.clube67.com/ruicesar/rx-scoreodonto:${RX_TAG:-latest}
|
||||||
|
environment:
|
||||||
|
- APP_VERSION=${RX_TAG:-dev}
|
||||||
|
- APP_ENV=PROD
|
||||||
|
- GIT_COMMIT=${RX_COMMIT:-dev}
|
||||||
Reference in New Issue
Block a user