Compare commits

..

1 Commits

Author SHA1 Message Date
VPS 4 Builder 0a83c0f515 build: registry pela VPN (10.99.0.3:3000) + fail-fast na tag
build-and-promote / build (push) Has been skipped
build-and-promote / promote (push) Successful in 1m40s
O proxy HTTPS de git.clube67.com corta uploads em ~60 s, a ~0,75 MB/s: blobs
acima de ~45 MB não publicam (o Gitea recebe o corpo truncado e responde 500).
O maior blob daqui tem 39,5 MB — publicava raspando, e qualquer dependência nova
quebraria o deploy. Pelo WireGuard são ~2,4 MB/s, sem corte.

Mesmo Gitea, mesmo repositório, mesmas tags: só muda o caminho de rede. Verificado
na VPS1 que `10.99.0.3:3000/ruicesar/scoreodonto-backend:v1.0.39` entrega o mesmo
image id que o container em execução — o rollback para tags antigas segue válido.

- docker-compose.prod.yml / deploy-prod.sh / workflow: registry passa a ser
  10.99.0.3:3000. Requer `insecure-registries` no daemon da VPS1 e do runner
  (já aplicado; a porta 3000 não é exposta à internet, só pelo túnel).
- promote: `buildx imagetools` não fala com registry HTTP, então o manifest é
  copiado pela API v2 do registry. Byte a byte ⇒ digest idêntico, que continua
  sendo conferido antes de deployar.
- promote: autentica com o `docker login` do runner. `secrets.REGISTRY_TOKEN` não
  existe neste repo — o `docker login` mascarava isso; o curl não.
- docker-compose.prod.yml: `${SCOREODONTO_TAG:?}` no lugar de `:-latest`. Sem a
  tag o deploy agora ABORTA, em vez de subir uma imagem indeterminada em produção.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:46:21 +02:00
3 changed files with 70 additions and 18 deletions
+43 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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}"