Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d7870811 | |||
| 0a83c0f515 |
+43
-13
@@ -6,6 +6,10 @@ name: build-and-promote
|
||||
# valida integridade e deploya na VPS1.
|
||||
# A versão semântica NÃO entra no build: nasce da tag e é injetada no deploy em runtime
|
||||
# (docker-compose.prod.yml). Não há mais APP_VERSION/constants.ts.
|
||||
#
|
||||
# REGISTRY PELA VPN (10.99.0.3:3000). O proxy HTTPS de git.clube67.com corta uploads em
|
||||
# ~60 s a ~0,75 MB/s: blobs acima de ~45 MB não sobem (o maior daqui tem 39,5 MB — passava
|
||||
# raspando). Mesmo Gitea e mesmo storage; só muda o caminho de rede.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -29,7 +33,7 @@ jobs:
|
||||
- name: Login no registry
|
||||
run: |
|
||||
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login 10.99.0.3:3000 -u ruicesar --password-stdin
|
||||
else
|
||||
echo "REGISTRY_TOKEN ausente — usando a credencial já presente no host."
|
||||
fi
|
||||
@@ -48,7 +52,7 @@ jobs:
|
||||
- name: Publicar por COMMIT (:<sha> + :latest), com retry p/ 499
|
||||
run: |
|
||||
set -e
|
||||
REG=git.clube67.com/ruicesar
|
||||
REG=10.99.0.3:3000/ruicesar
|
||||
C="${{ steps.vars.outputs.commit }}"
|
||||
for app in backend frontend; do
|
||||
SRC="scoreodontocom-scoreodonto-${app}:latest"
|
||||
@@ -82,36 +86,62 @@ jobs:
|
||||
- name: Login no registry
|
||||
run: |
|
||||
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.clube67.com -u ruicesar --password-stdin
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login 10.99.0.3:3000 -u ruicesar --password-stdin
|
||||
fi
|
||||
|
||||
- name: Credencial do registry (reusa o docker login do runner)
|
||||
run: |
|
||||
set -e
|
||||
# `secrets.REGISTRY_TOKEN` não está definido neste repo; o `docker login` acima
|
||||
# cai na credencial do host. O curl abaixo precisa dela explicitamente.
|
||||
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||
AUTH=$(printf 'ruicesar:%s' "${{ secrets.REGISTRY_TOKEN }}" | base64 -w0)
|
||||
else
|
||||
AUTH=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.docker/config.json')))['auths']['10.99.0.3:3000']['auth'])")
|
||||
fi
|
||||
[ -n "$AUTH" ] || { echo "ERRO: sem credencial (rode: docker login 10.99.0.3:3000)"; exit 1; }
|
||||
echo "::add-mask::$AUTH"
|
||||
echo "REG_AUTH=$AUTH" >> "$GITHUB_ENV"
|
||||
echo "ACCEPT=application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Integridade — a imagem do commit existe? (nunca buildar na tag)
|
||||
run: |
|
||||
set -e
|
||||
REG=git.clube67.com/ruicesar
|
||||
C="${{ steps.vars.outputs.commit }}"
|
||||
for app in backend frontend; do
|
||||
if ! docker buildx imagetools inspect "${REG}/scoreodonto-${app}:$C" >/dev/null 2>&1; then
|
||||
echo "ERRO: ${REG}/scoreodonto-${app}:$C não existe no registry."
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Basic ${REG_AUTH}" \
|
||||
-H "Accept: ${ACCEPT}" "http://10.99.0.3:3000/v2/ruicesar/scoreodonto-${app}/manifests/$C")
|
||||
[ "$code" = "200" ] || {
|
||||
echo "ERRO: scoreodonto-${app}:$C não existe no registry (HTTP $code)."
|
||||
echo " Faça 'git push origin main' (que builda o commit $C) ANTES de criar a tag."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
done
|
||||
echo "OK: backend:$C e frontend:$C presentes — mesmo commit $C (front+back atômicos)."
|
||||
|
||||
- name: Promover — re-tag commit → versão (SEM rebuild) + conferir digest
|
||||
- name: Promover — copiar manifest 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 }}"
|
||||
A="Authorization: Basic ${REG_AUTH}"
|
||||
# `docker buildx imagetools` não fala com registry HTTP (e a versão instalada não
|
||||
# tem --insecure), então copiamos o manifest pela API v2. Byte a byte ⇒ mesmo digest.
|
||||
for app in backend frontend; do
|
||||
DST="${REG}/scoreodonto-${app}"
|
||||
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}}')
|
||||
REPO="ruicesar/scoreodonto-${app}"
|
||||
CT=$(curl -s -H "$A" -H "Accept: ${ACCEPT}" \
|
||||
-o manifest.json -w '%{content_type}' "http://10.99.0.3:3000/v2/${REPO}/manifests/$C")
|
||||
dC=$(curl -s -H "$A" -H "Accept: ${ACCEPT}" -o /dev/null -D - \
|
||||
"http://10.99.0.3:3000/v2/${REPO}/manifests/$C" | tr -d '\r' \
|
||||
| awk '/^[Dd]ocker-[Cc]ontent-[Dd]igest:/{print $2}')
|
||||
curl -sf -H "$A" -X PUT -H "Content-Type: ${CT}" \
|
||||
--data-binary @manifest.json "http://10.99.0.3:3000/v2/${REPO}/manifests/$V" >/dev/null
|
||||
dV=$(curl -s -H "$A" -H "Accept: ${ACCEPT}" -o /dev/null -D - \
|
||||
"http://10.99.0.3:3000/v2/${REPO}/manifests/$V" | tr -d '\r' \
|
||||
| awk '/^[Dd]ocker-[Cc]ontent-[Dd]igest:/{print $2}')
|
||||
[ "$dC" = "$dV" ] || { echo "ERRO: digest divergiu em $app ($V=$dV != $C=$dC)"; exit 1; }
|
||||
echo "$app: $V -> $C (digest $dV)"
|
||||
done
|
||||
rm -f manifest.json
|
||||
echo "Promovido sem rebuild: a imagem :$V E a imagem :$C (mesmo artefato)."
|
||||
|
||||
- name: Deploy na VPS1 (pull :versao + up --no-build; versao injetada em runtime)
|
||||
|
||||
+15
-2
@@ -30,8 +30,21 @@ echo "⚠️ Recomendado: backup do banco de PRODUÇÃO antes (migrações roda
|
||||
echo " ssh deploy@10.99.0.3 'pg_dump -Fc scoreodonto -f ~/backups/scoreodonto_\$(date +%Y%m%d_%H%M).dump'"
|
||||
echo
|
||||
|
||||
echo "[1/3] pull das imagens $TAG ..."
|
||||
docker compose $FILES pull scoreodonto-backend scoreodonto-frontend
|
||||
echo "[1/3] pull das imagens $TAG (registry pela VPN) ..."
|
||||
# Registry = 10.99.0.3:3000 (WireGuard). Se o pull falhar (VPN fora do ar), exigimos
|
||||
# a imagem local COM A TAG EXATA — nunca caímos para :latest.
|
||||
if ! docker compose $FILES pull scoreodonto-backend scoreodonto-frontend 2>/dev/null; then
|
||||
echo " pull indisponível — conferindo imagens locais com a tag $TAG ..."
|
||||
for app in backend frontend; do
|
||||
img="10.99.0.3:3000/ruicesar/scoreodonto-${app}:${TAG}"
|
||||
docker image inspect "$img" >/dev/null 2>&1 || {
|
||||
echo "ERRO: $img não existe nem no registry nem localmente."
|
||||
echo " Confira a VPN (ping 10.99.0.3)."
|
||||
exit 1
|
||||
}
|
||||
echo " ok: $img (local)"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "[2/3] subindo (sem build) ..."
|
||||
docker compose $FILES up -d --no-build scoreodonto-backend scoreodonto-frontend nginx
|
||||
|
||||
+12
-3
@@ -12,14 +12,23 @@
|
||||
# MESMA imagem pode ser promovida sob qualquer tag sem rebuild — pré-requisito do
|
||||
# "build once, promote the same artifact". O frontend não precisa de versão aqui: é estático
|
||||
# e lê tudo de /api/version (backend) em runtime.
|
||||
#
|
||||
# REGISTRY PELA VPN (10.99.0.3:3000), não por git.clube67.com: o proxy HTTPS corta
|
||||
# uploads em ~60 s e a banda por ele é ~0,75 MB/s, então blobs acima de ~45 MB não
|
||||
# publicam (o maior daqui tem 39,5 MB — passava raspando). Mesmo Gitea, mesmo
|
||||
# repositório, mesmas tags: só o caminho de rede muda, e o rollback para tags
|
||||
# antigas continua funcionando. Exige `insecure-registries: ["10.99.0.3:3000"]`
|
||||
# no daemon da VPS1 e do runner.
|
||||
services:
|
||||
scoreodonto-backend:
|
||||
image: git.clube67.com/ruicesar/scoreodonto-backend:${SCOREODONTO_TAG:-latest}
|
||||
# `:?` (e não `:-latest`): sem SCOREODONTO_TAG o deploy ABORTA, em vez de subir
|
||||
# silenciosamente uma imagem indeterminada em produção.
|
||||
image: "10.99.0.3:3000/ruicesar/scoreodonto-backend:${SCOREODONTO_TAG:?defina SCOREODONTO_TAG}"
|
||||
# Mescla com o environment do compose base (por chave). Sobrescreve o APP_VERSION/APP_ENV
|
||||
# que vieram embutidos na imagem pelo build, fixando a versão = tag e o ambiente = PROD.
|
||||
environment:
|
||||
- APP_VERSION=${SCOREODONTO_TAG:-dev}
|
||||
- APP_VERSION=${SCOREODONTO_TAG}
|
||||
- APP_ENV=PROD
|
||||
|
||||
scoreodonto-frontend:
|
||||
image: git.clube67.com/ruicesar/scoreodonto-frontend:${SCOREODONTO_TAG:-latest}
|
||||
image: "10.99.0.3:3000/ruicesar/scoreodonto-frontend:${SCOREODONTO_TAG:?defina SCOREODONTO_TAG}"
|
||||
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Sincroniza CONFIGURAÇÃO + CONTAS do scoreodonto de DEV → PROD (upsert seguro).
|
||||
#
|
||||
# DEV : scoreodonto_dev (pgdev, VPS4) — fonte da verdade da config
|
||||
# PROD: scoreodonto (10.99.0.3, VPS3) — 1.750 pacientes reais; NÃO destruir
|
||||
#
|
||||
# Método: para cada tabela do escopo, carrega os dados de dev numa tabela staging
|
||||
# em prod e faz INSERT ... ON CONFLICT (pk) DO UPDATE. Isso INSERE o que falta e
|
||||
# ATUALIZA o que mudou, mas NUNCA apaga registros que só existam em prod.
|
||||
#
|
||||
# NÃO toca em: pacientes, agendamentos, financeiro, tratamentos, orçamentos, etc.
|
||||
# NÃO migra sessões de WhatsApp (fora do escopo por decisão do dono).
|
||||
#
|
||||
# ./sync-config-dev-para-prod.sh → DRY-RUN (mostra o que mudaria, ROLLBACK)
|
||||
# ./sync-config-dev-para-prod.sh --apply → aplica de verdade (faz backup antes)
|
||||
#
|
||||
# Requisitos: rodar na VPS4 (acesso ao pgdev local e ao Postgres da VPS3 pela VPN).
|
||||
# A senha do prod vem de scoreodonto.com/.env na VPS1 (SCOREO_DB_PASS) — passe via
|
||||
# env SCOREO_DB_PASS=... ou o script tenta buscá-la por SSH.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
APPLY="${1:-}"
|
||||
DEV_CT="pgdev"
|
||||
DEV_DB="scoreodonto_dev"
|
||||
DEV_USER="devadmin"
|
||||
PROD_HOST="10.99.0.3"
|
||||
PROD_DB="scoreodonto"
|
||||
PROD_USER="scoreodonto_user"
|
||||
|
||||
# Ordem importa (FK): pais antes dos filhos.
|
||||
# usuarios, clinicas → vinculos, clinicas_horarios, pessoa_comunicacao, feriados
|
||||
TABLES=(usuarios clinicas feriados vinculos clinicas_horarios pessoa_comunicacao)
|
||||
declare -A PK=(
|
||||
[usuarios]="id" [clinicas]="id" [vinculos]="id"
|
||||
[clinicas_horarios]="id" [feriados]="id"
|
||||
[pessoa_comunicacao]="usuario_id,clinica_id"
|
||||
)
|
||||
|
||||
: "${SCOREO_DB_PASS:=$(ssh -o BatchMode=yes deploy@10.99.0.1 'grep -oP "SCOREO_DB_PASS=\K.*" /home/deploy/stack/scoreodonto.com/.env' 2>/dev/null | tr -d '\r\n')}"
|
||||
[ -n "$SCOREO_DB_PASS" ] || { echo "ERRO: SCOREO_DB_PASS não definida"; exit 1; }
|
||||
|
||||
dev_psql() { docker exec -i "$DEV_CT" psql -U "$DEV_USER" -d "$DEV_DB" "$@"; }
|
||||
prod_psql() { docker run --rm -i --network host -e PGPASSWORD="$SCOREO_DB_PASS" \
|
||||
postgis/postgis:18-3.6 psql -h "$PROD_HOST" -U "$PROD_USER" -d "$PROD_DB" "$@"; }
|
||||
|
||||
# Monta o SQL de upsert de uma tabela: cria staging, carrega dados de dev via COPY,
|
||||
# faz INSERT ... ON CONFLICT DO UPDATE, e reporta quantas linhas entraram/mudaram.
|
||||
emit_table_sql() {
|
||||
local t="$1" pk="${PK[$t]}"
|
||||
local cols; cols=$(dev_psql -tAc "select string_agg(quote_ident(column_name),',' order by ordinal_position) from information_schema.columns where table_name='$t' and table_schema='public'")
|
||||
local setclause; setclause=$(dev_psql -tAc "select string_agg(quote_ident(column_name)||'=EXCLUDED.'||quote_ident(column_name),',' order by ordinal_position) from information_schema.columns where table_name='$t' and table_schema='public' and column_name <> all(string_to_array('$pk',','))")
|
||||
|
||||
echo "CREATE TEMP TABLE stg_$t (LIKE public.$t INCLUDING DEFAULTS) ON COMMIT DROP;"
|
||||
echo "\\copy stg_$t ($cols) FROM STDIN"
|
||||
dev_psql -c "\\copy (SELECT $cols FROM public.$t) TO STDOUT"
|
||||
echo "\\."
|
||||
echo "WITH ins AS ("
|
||||
echo " INSERT INTO public.$t ($cols) SELECT $cols FROM stg_$t"
|
||||
echo " ON CONFLICT ($pk) DO UPDATE SET $setclause"
|
||||
echo " RETURNING xmax=0 AS inserted"
|
||||
echo ") SELECT '$t' AS tabela, count(*) FILTER (WHERE inserted) AS inseridos, count(*) FILTER (WHERE NOT inserted) AS atualizados FROM ins;"
|
||||
}
|
||||
|
||||
build_sql() {
|
||||
# A ordem de TABLES respeita as FKs (pais antes de filhos), então não é preciso
|
||||
# desligar triggers/FK (o que exigiria superuser). Tudo numa transação: erro = rollback.
|
||||
echo "BEGIN;"
|
||||
for t in "${TABLES[@]}"; do emit_table_sql "$t"; done
|
||||
if [ "$APPLY" = "--apply" ]; then echo "COMMIT;"; else echo "ROLLBACK;"; fi
|
||||
}
|
||||
|
||||
if [ "$APPLY" = "--apply" ]; then
|
||||
TS=$(date +%Y%m%d_%H%M)
|
||||
echo "→ Backup do prod (tabelas do escopo) antes de aplicar..."
|
||||
prod_psql -c "\\copy (select 1) to /dev/null" >/dev/null # valida conexão
|
||||
BK="/home/deploy/backups/scoreodonto_config_pre-sync_${TS}.sql"
|
||||
ssh -o BatchMode=yes deploy@10.99.0.1 "PGPASSWORD='$SCOREO_DB_PASS' pg_dump -h $PROD_HOST -U $PROD_USER -d $PROD_DB --data-only $(printf ' -t public.%s' "${TABLES[@]}") | gzip > ${BK}.gz && echo ' backup: ${BK}.gz'" 2>/dev/null \
|
||||
|| echo " (aviso: backup remoto falhou — verifique antes de prosseguir)"
|
||||
echo "→ Aplicando (COMMIT)..."
|
||||
else
|
||||
echo "→ DRY-RUN (nada será gravado; ao final: ROLLBACK)"
|
||||
fi
|
||||
|
||||
build_sql | prod_psql -v ON_ERROR_STOP=1
|
||||
Reference in New Issue
Block a user