feat(ci): Passo 5 — Build Once / Promote / Deploy no build.yml
- build.yml dividido em 2 jobs por ref:
• build (só push na main): builda 1x e publica por COMMIT (:<sha> + :latest), sem APP_VERSION.
• promote (só tag vX.Y.Z): re-tagueia :<sha> -> :vX.Y.Z via 'docker buildx imagetools create'
(SEM rebuild), valida integridade (imagem do commit existe; digest :versao == :commit;
front+back do mesmo commit) e deploya na VPS1. Falha se o commit não foi pushado.
- Versão semântica nasce da TAG e é injetada no deploy em runtime (compose.prod).
- Removidos frontend/constants.ts e update-version.js (fim do APP_VERSION manual).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+65
-34
@@ -1,24 +1,28 @@
|
|||||||
name: build-and-push
|
name: build-and-promote
|
||||||
|
|
||||||
# Etapa 5: a cada push na main (ou tag vX.Y.Z), builda as imagens com o carimbo
|
# Passo 5 — Build Once → Promote → Deploy:
|
||||||
# de versão e envia ao Container Registry do Gitea. NÃO faz deploy (Etapa 6 separada).
|
# • 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 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.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# ───────────────────────── BUILD (só em push de branch) ─────────────────────────
|
||||||
build:
|
build:
|
||||||
# Roda direto no host VPS4 (runner registrado com label de host + acesso ao Docker).
|
if: startsWith(github.ref, 'refs/heads/')
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Calcular versão/commit/data
|
- name: Vars (commit + data)
|
||||||
id: vars
|
id: vars
|
||||||
run: |
|
run: |
|
||||||
echo "version=$(grep -oE 'V[0-9]+\.[0-9]+\.[0-9]+' frontend/constants.ts | head -1 | tr 'V' 'v')" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
echo "build_time=$(date -u +'%Y-%m-%d %H:%M UTC')" >> "$GITHUB_OUTPUT"
|
echo "build_time=$(date -u +'%Y-%m-%d %H:%M UTC')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
@@ -27,65 +31,92 @@ jobs:
|
|||||||
if [ -n "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
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 git.clube67.com -u ruicesar --password-stdin
|
||||||
else
|
else
|
||||||
echo "REGISTRY_TOKEN não definido — usando a credencial já presente no host (~/.docker/config.json)."
|
echo "REGISTRY_TOKEN ausente — usando a credencial já presente no host."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build (compose, com carimbo de versão)
|
- name: Build (carimba commit/data; versão vem da tag no deploy, não aqui)
|
||||||
env:
|
env:
|
||||||
# Fixa o nome do projeto p/ a imagem buildada bater com o passo de tag
|
|
||||||
# (em modo host o diretório do checkout muda o project name por padrão).
|
|
||||||
COMPOSE_PROJECT_NAME: scoreodontocom
|
COMPOSE_PROJECT_NAME: scoreodontocom
|
||||||
APP_VERSION: ${{ steps.vars.outputs.version }}
|
|
||||||
GIT_COMMIT: ${{ steps.vars.outputs.commit }}
|
GIT_COMMIT: ${{ steps.vars.outputs.commit }}
|
||||||
BUILD_TIME: ${{ steps.vars.outputs.build_time }}
|
BUILD_TIME: ${{ steps.vars.outputs.build_time }}
|
||||||
APP_ENV: PROD
|
APP_ENV: PROD
|
||||||
run: docker compose build scoreodonto-backend scoreodonto-frontend
|
run: docker compose build scoreodonto-backend scoreodonto-frontend
|
||||||
|
|
||||||
- name: Tag + push (com retry p/ 499 transitório)
|
- name: Publicar por COMMIT (:<sha> + :latest), com retry p/ 499
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
V="${{ steps.vars.outputs.version }}"
|
REG=git.clube67.com/ruicesar
|
||||||
C="${{ steps.vars.outputs.commit }}"
|
C="${{ steps.vars.outputs.commit }}"
|
||||||
for app in backend frontend; do
|
for app in backend frontend; do
|
||||||
SRC="scoreodontocom-scoreodonto-${app}:latest"
|
SRC="scoreodontocom-scoreodonto-${app}:latest"
|
||||||
DST="git.clube67.com/ruicesar/scoreodonto-${app}"
|
DST="${REG}/scoreodonto-${app}"
|
||||||
docker tag "$SRC" "$DST:$V"
|
|
||||||
docker tag "$SRC" "$DST:$C"
|
docker tag "$SRC" "$DST:$C"
|
||||||
docker tag "$SRC" "$DST:latest"
|
docker tag "$SRC" "$DST:latest"
|
||||||
for tag in "$V" "$C" "latest"; do
|
for tag in "$C" "latest"; do
|
||||||
n=0
|
n=0
|
||||||
until docker push "$DST:$tag"; do
|
until docker push "$DST:$tag"; do
|
||||||
n=$((n+1)); [ $n -ge 4 ] && { echo "push falhou após 4 tentativas: $DST:$tag"; exit 1; }
|
n=$((n+1)); [ $n -ge 4 ] && { echo "push falhou: $DST:$tag"; exit 1; }
|
||||||
echo "retry $n (499 transitório) em $DST:$tag"; sleep 3
|
echo "retry $n (499 transitório) em $DST:$tag"; sleep 3
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
echo "Publicado: scoreodonto-{backend,frontend}:$C (+ :latest). Push NÃO deploya."
|
||||||
|
|
||||||
- name: Resumo
|
# ─────────────────────── PROMOTE (só em tag vX.Y.Z) ───────────────────────
|
||||||
run: |
|
promote:
|
||||||
echo "Imagens publicadas:"
|
|
||||||
echo " git.clube67.com/ruicesar/scoreodonto-backend:${{ steps.vars.outputs.version }}"
|
|
||||||
echo " git.clube67.com/ruicesar/scoreodonto-frontend:${{ steps.vars.outputs.version }}"
|
|
||||||
echo "Deploy só acontece em TAG (job deploy)."
|
|
||||||
|
|
||||||
# Deploy automático SÓ em tags vX.Y.Z (a tag = 'aprovado para produção').
|
|
||||||
# Push na main NÃO deploya — só builda. Etapa 6.
|
|
||||||
deploy:
|
|
||||||
needs: build
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout (na tag)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Promover a tag para produção (VPS1)
|
- 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: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
TAG="${GITHUB_REF_NAME}"
|
REG=git.clube67.com/ruicesar
|
||||||
echo "Promovendo $TAG para a produção (VPS1)..."
|
C="${{ steps.vars.outputs.commit }}"
|
||||||
# Garante que o mecanismo de deploy está atualizado na VPS1
|
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."
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
REG=git.clube67.com/ruicesar
|
||||||
|
C="${{ steps.vars.outputs.commit }}"; V="${{ steps.vars.outputs.version }}"
|
||||||
|
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}}')
|
||||||
|
[ "$dC" = "$dV" ] || { echo "ERRO: digest divergiu em $app ($V=$dV != $C=$dC)"; exit 1; }
|
||||||
|
echo "$app: $V -> $C (digest $dV)"
|
||||||
|
done
|
||||||
|
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)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
V="${{ steps.vars.outputs.version }}"
|
||||||
scp -o StrictHostKeyChecking=accept-new \
|
scp -o StrictHostKeyChecking=accept-new \
|
||||||
docker-compose.prod.yml deploy-prod.sh \
|
docker-compose.prod.yml deploy-prod.sh \
|
||||||
deploy@10.99.0.1:/home/deploy/stack/scoreodonto.com/
|
deploy@10.99.0.1:/home/deploy/stack/scoreodonto.com/
|
||||||
ssh -o StrictHostKeyChecking=accept-new deploy@10.99.0.1 \
|
ssh -o StrictHostKeyChecking=accept-new deploy@10.99.0.1 \
|
||||||
"/home/deploy/stack/scoreodonto.com/deploy-prod.sh $TAG"
|
"/home/deploy/stack/scoreodonto.com/deploy-prod.sh $V"
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
// LEGADO (Passo 4): a APLICAÇÃO não usa mais este valor — a versão exibida vem do backend
|
|
||||||
// (/api/version) em runtime via useAppVersion(). Permanece apenas porque o PIPELINE ainda o lê
|
|
||||||
// (update-version.js + .gitea/workflows/build.yml). Será eliminado no Passo 5, quando a versão
|
|
||||||
// passar a nascer da TAG git. Não referenciar no código do frontend.
|
|
||||||
export const APP_VERSION = 'V1.0.15';
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Increments the patch number in frontend/constants.ts before every build.
|
|
||||||
// Called via: node ../update-version.js (from inside frontend/)
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const constantsPath = path.join(__dirname, 'frontend', 'constants.ts');
|
|
||||||
const content = fs.readFileSync(constantsPath, 'utf8');
|
|
||||||
|
|
||||||
const match = content.match(/APP_VERSION\s*=\s*['"]V(\d+)\.(\d+)\.(\d+)['"]/);
|
|
||||||
if (!match) {
|
|
||||||
console.error('[update-version] Could not parse APP_VERSION in constants.ts');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, major, minor, patch] = match;
|
|
||||||
const next = `V${major}.${minor}.${parseInt(patch, 10) + 1}`;
|
|
||||||
const updated = content.replace(
|
|
||||||
/APP_VERSION\s*=\s*['"]V\d+\.\d+\.\d+['"]/,
|
|
||||||
`APP_VERSION = '${next}'`
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(constantsPath, updated, 'utf8');
|
|
||||||
console.log(`[update-version] ${match[0].match(/V\d+\.\d+\.\d+/)[0]} → ${next}`);
|
|
||||||
Reference in New Issue
Block a user