Files
rkm-conectasus-ci-sandbox/.gitea/workflows/build-api-image.yml
Antonio Lopes dos Santos afec302c3e
All checks were successful
build-api-image / build (push) Successful in 51s
init commit
2026-08-04 16:52:02 -03:00

454 lines
20 KiB
YAML

name: build-api-image
on:
push:
branches:
# Branch = ambiente. Um merge aceito aqui constroi e implanta no ambiente
# correspondente (mapeamento no step de metadados).
- develop # -> teste
- 'release-*' # -> homologacao
- feat_CI_CD_TEST # -> teste (branch de validacao da propria esteira)
tags:
- '2.*'
jobs:
build:
runs-on: ubuntu-latest
env:
REGISTRY: ${{ vars.REGISTRY_HOST || '192.168.0.41:3001' }}
IMAGE_NAME: ${{ vars.API_IMAGE_NAME || 'rkm/conectasus-api' }}
BASE_IMAGE: ${{ vars.API_BASE_IMAGE || 'tirkm/v2-application:6' }}
# O contexto e a raiz do application: o build precisa enxergar api/ e
# frontend/ ao mesmo tempo, entao Dockerfile e docker/ vivem na raiz.
CONTEXT_DIR: ${{ vars.CONTEXT_DIR || '.' }}
DOCKERFILE: ${{ vars.DOCKERFILE || 'Dockerfile' }}
# Subdiretorio da API, usado nas checagens que leem arquivos do repo
# (composer.json, migrations) — nao confundir com o contexto do build.
API_DIR: ${{ vars.API_DIR || 'api' }}
PUSH_LATEST: ${{ vars.PUSH_LATEST || 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Definir metadados do build
id: meta
run: |
set -euo pipefail
if [ "${GITHUB_REF}" != "${GITHUB_REF#refs/tags/}" ]; then
IS_TAG=true
else
IS_TAG=false
fi
VERSION=$(echo "${GITHUB_REF_NAME}" | tr '/' '-')
IMAGE="${REGISTRY}/${IMAGE_NAME}"
# Branch -> ambiente. A tag de release constroi a imagem, mas nao
# implanta nem aplica migrations: ambos exigem acao manual.
case "${GITHUB_REF_NAME}" in
develop|feat_CI_CD_TEST) AMBIENTE=teste ;;
release-*) AMBIENTE=homolog ;;
*) AMBIENTE="" ;;
esac
[ "${IS_TAG}" = "true" ] && AMBIENTE=""
# Uma imagem serve todos os ambientes: a config do frontend e resolvida
# em runtime (config.js montado no deploy), nao gravada no bundle.
# Toda imagem tambem recebe uma tag pelo commit, para rastreabilidade
# e retencao. Tags de release sempre constroem uma imagem nova a
# partir do commit marcado: a tag e o marco formal do artefato.
SHA_TAG="sha-$(echo "${GITHUB_SHA}" | cut -c1-8)"
echo "ambiente=${AMBIENTE}" >> "$GITHUB_OUTPUT"
echo "sha_tag=${SHA_TAG}" >> "$GITHUB_OUTPUT"
echo "tag_sha=${IMAGE}:${SHA_TAG}" >> "$GITHUB_OUTPUT"
echo "is_tag=${IS_TAG}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
echo "tag_version=${IMAGE}:${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag_latest=${IMAGE}:latest" >> "$GITHUB_OUTPUT"
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
echo "Evento : $([ "${IS_TAG}" = "true" ] && echo tag || echo branch)"
echo "Versao : ${VERSION}"
echo "Imagem : ${IMAGE}:${VERSION}"
echo "Base : ${BASE_IMAGE}"
- name: Conferir versao do composer.json
run: |
set -euo pipefail
if [ "${{ steps.meta.outputs.is_tag }}" != "true" ]; then
echo "Build de branch; comparacao com composer.json nao se aplica."
exit 0
fi
DECLARED=$(grep -m1 '"version"' "${API_DIR}/composer.json" \
| sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
TAG="${{ steps.meta.outputs.version }}"
if [ "${DECLARED}" != "${TAG}" ]; then
echo "::warning::composer.json declara versao '${DECLARED}' mas a tag e '${TAG}'. Build segue com a tag."
else
echo "composer.json e tag batem: ${TAG}"
fi
- name: Avaliar reversibilidade das migrations
id: rollback
run: |
set -euo pipefail
PREV=$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || true)
if [ -z "${PREV}" ]; then
echo "Nenhuma tag anterior encontrada; analise de rollback ignorada."
echo "safe=unknown" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Comparando ${PREV} -> ${GITHUB_REF_NAME}"
NOVAS=$(git diff --name-only --diff-filter=A "${PREV}" "${GITHUB_REF_NAME}" \
-- "${API_DIR}/database/migrations" || true)
if [ -z "${NOVAS}" ]; then
echo "Nenhuma migration nova neste release."
echo "safe=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Migrations novas:"
echo "${NOVAS}" | sed 's/^/ /'
DESTRUTIVAS=""
while IFS= read -r f; do
[ -f "${f}" ] || continue
UP=$(sed '/function[[:space:]]*down[[:space:]]*(/,$d' "${f}")
if echo "${UP}" | grep -qE 'dropColumn|renameColumn|dropIfExists|DROP TABLE|DROP COLUMN'; then
DESTRUTIVAS="${DESTRUTIVAS}${f}"$'\n'
fi
done <<< "${NOVAS}"
{
echo "### Reversibilidade"
echo ""
echo "Comparado com \`${PREV}\`."
} >> "$GITHUB_STEP_SUMMARY"
if [ -z "${DESTRUTIVAS}" ]; then
echo "Todas as migrations novas sao aditivas."
echo "safe=true" >> "$GITHUB_OUTPUT"
{
echo ""
echo "Migrations novas sao **aditivas**. Rollback da imagem para \`${PREV}\` e seguro mesmo com o schema ja migrado."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "safe=false" >> "$GITHUB_OUTPUT"
echo "${DESTRUTIVAS}" | while IFS= read -r f; do
[ -n "${f}" ] && echo "::warning file=${f}::Migration destrutiva no up(): rollback para ${PREV} nao sera seguro apos aplicar."
done
{
echo ""
echo "**Atencao: este release contem migrations destrutivas.**"
echo ""
echo "Depois de aplicar a migration, voltar a imagem para \`${PREV}\` NAO restaura o funcionamento:"
echo "o codigo antigo vai referenciar colunas/tabelas que deixaram de existir. Faca backup do banco"
echo "antes de migrar, ou planeje a correcao como hotfix para frente."
echo ""
echo "| Migration |"
echo "| --- |"
echo "${DESTRUTIVAS}" | while IFS= read -r f; do
[ -n "${f}" ] && echo "| \`${f}\` |"
done
} >> "$GITHUB_STEP_SUMMARY"
- name: Login no Docker Hub (imagem base)
env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
set -euo pipefail
if [ -z "${DOCKERHUB_TOKEN:-}" ]; then
echo "DOCKERHUB_TOKEN nao definido; assumindo imagem base publica ou ja em cache."
exit 0
fi
echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USER}" --password-stdin
- name: Login no registry do Gitea
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
REGISTRY_USER: ${{ vars.REGISTRY_USER }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
USER="${REGISTRY_USER:-$ACTOR}"
TOKEN="${REGISTRY_TOKEN:-}"
if [ -z "${TOKEN}" ]; then
echo "REGISTRY_TOKEN ausente; usando o GITEA_TOKEN do job."
TOKEN="${GITEA_TOKEN}"
USER="${ACTOR}"
fi
echo "${TOKEN}" | docker login "${REGISTRY}" -u "${USER}" --password-stdin
- name: Build da imagem
run: |
set -euo pipefail
docker build \
--file "${DOCKERFILE}" \
--build-arg BASE_IMAGE="${BASE_IMAGE}" \
--build-arg APP_VERSION="${{ steps.meta.outputs.version }}" \
--build-arg VCS_REF="${GITHUB_SHA}" \
--build-arg BUILD_DATE="${{ steps.meta.outputs.build_date }}" \
--build-arg SOURCE_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
--tag "${{ steps.meta.outputs.tag_version }}" \
--tag "${{ steps.meta.outputs.tag_sha }}" \
"${CONTEXT_DIR}"
- name: Smoke test da imagem
run: |
set -euo pipefail
IMG="${{ steps.meta.outputs.tag_version }}"
echo "--> aplicacao deve bootar com config presente"
docker run --rm --entrypoint bash "${IMG}" -c '
set -e
cd /var/www/api
cp .env.example .env
cp connections.json.example connections.json
php artisan --version
test -f bootstrap/cache/routes-v7.php || test -f bootstrap/cache/routes.php
test -f bootstrap/cache/packages.php
'
echo "--> imagem nao pode carregar credenciais"
if docker run --rm --entrypoint bash "${IMG}" -c 'test -f /var/www/api/.env || test -f /var/www/api/connections.json'; then
echo "::error::.env ou connections.json foram parar dentro da imagem"
exit 1
fi
echo "--> entrypoint deve recusar subir sem config"
if docker run --rm "${IMG}" true 2>/dev/null; then
echo "::error::entrypoint subiu sem .env/connections.json"
exit 1
fi
echo "--> frontend deve estar publicado no dist"
docker run --rm --entrypoint bash "${IMG}" -c '
set -e
test -f /var/www/frontend/dist/index.html
test -d /var/www/frontend/dist/js
test -d /var/www/frontend/dist/css
'
echo "--> imagem NAO pode ter URL de ambiente gravada no bundle"
# O ponto da config em runtime e a imagem ser neutra. Se alguem voltar
# a ler process.env.VUE_APP_* num componente, o webpack grava o valor
# no bundle e a imagem silenciosamente vira especifica de um ambiente —
# o que quebra a promessa de "o que foi homologado e o que vai a prod".
# O index.html precisa carregar o config.js antes do bundle.
docker run --rm --entrypoint bash "${IMG}" -c '
set -e
test -f /var/www/frontend/dist/config.js
grep -q "__APP_CONFIG__" /var/www/frontend/dist/config.js
grep -q "config.js" /var/www/frontend/dist/index.html
'
echo "--> apache deve servir api na :80 e frontend na :8080"
docker run --rm --entrypoint bash "${IMG}" -c '
set -e
apache2ctl configtest 2>&1 | grep -q "Syntax OK"
apache2ctl -S 2>/dev/null | grep -q "^\*:80 "
apache2ctl -S 2>/dev/null | grep -q "^\*:8080 "
'
# As checagens acima usam --entrypoint bash, que nunca executa o CMD.
# As de baixo sobem o container de verdade: sao as unicas que pegam
# defeito de supervisord/apache que so aparece no boot real.
#
# O job roda com o docker.sock do host, entao bind mount de arquivo
# criado aqui dentro nao chegaria ao daemon (o caminho nao existe no
# host). Por isso a config entra via 'docker cp' num container criado
# e ainda nao iniciado.
TMP=$(mktemp -d)
docker run --rm --entrypoint bash "${IMG}" -c 'cat /var/www/api/.env.example' > "${TMP}/.env"
docker run --rm --entrypoint bash "${IMG}" -c 'cat /var/www/api/connections.json.example' > "${TMP}/connections.json"
sed -i "s|^APP_KEY=.*|APP_KEY=base64:$(head -c 32 /dev/urandom | base64)|" "${TMP}/.env"
printf 'window.__APP_CONFIG__ = { ROOT_API: "http://smoke-test", DNS: {}, ENV: "" }\n' > "${TMP}/config.js"
chmod 644 "${TMP}/.env" "${TMP}/connections.json" "${TMP}/config.js"
echo "--> entrypoint deve recusar subir sem a config de runtime do frontend"
# Sem o mount, o app subiria apontando para lugar nenhum em vez de
# falhar — o silencio e que e perigoso aqui.
CID_SEM=$(docker create "${IMG}")
docker cp "${TMP}/.env" "${CID_SEM}:/var/www/api/.env"
docker cp "${TMP}/connections.json" "${CID_SEM}:/var/www/api/connections.json"
docker start "${CID_SEM}" >/dev/null 2>&1 || true
sleep 3
if [ "$(docker inspect -f '{{.State.Running}}' "${CID_SEM}")" = "true" ]; then
echo "::error::container subiu sem /var/www/frontend/config.js montado"
docker rm -f "${CID_SEM}" >/dev/null 2>&1 || true
exit 1
fi
docker rm -f "${CID_SEM}" >/dev/null 2>&1 || true
echo "--> container deve subir com config e permanecer de pe"
CID=$(docker create "${IMG}")
cleanup() { docker rm -f "${CID}" >/dev/null 2>&1 || true; rm -rf "${TMP}"; }
trap cleanup EXIT
docker cp "${TMP}/.env" "${CID}:/var/www/api/.env"
docker cp "${TMP}/connections.json" "${CID}:/var/www/api/connections.json"
docker cp "${TMP}/config.js" "${CID}:/var/www/frontend/config.js"
docker start "${CID}" >/dev/null
for _ in $(seq 1 15); do
docker exec "${CID}" curl -s -o /dev/null http://localhost:8080/ 2>/dev/null && break
sleep 2
done
if [ "$(docker inspect -f '{{.State.Running}}' "${CID}")" != "true" ]; then
echo "::error::container nao permaneceu de pe (exit=$(docker inspect -f '{{.State.ExitCode}}' "${CID}"))"
docker logs "${CID}" 2>&1 | tail -20
exit 1
fi
CODE_API=$(docker exec "${CID}" curl -s -o /dev/null -w '%{http_code}' http://localhost:80/)
CODE_SPA=$(docker exec "${CID}" curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/)
CODE_ROTA=$(docker exec "${CID}" curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/rota/interna)
echo " api :80 -> ${CODE_API} | spa :8080 -> ${CODE_SPA} | spa /rota -> ${CODE_ROTA}"
# A API nao tem rota "/", entao 404 e resposta valida; o que nao pode e
# 000 (sem resposta). Ja o SPA precisa de 200 na raiz e tambem na rota
# interna — este segundo cobre o FallbackResource do mode: 'history'.
[ "${CODE_API}" != "000" ] || { echo "::error::apache nao respondeu na :80"; exit 1; }
[ "${CODE_SPA}" = "200" ] || { echo "::error::frontend nao respondeu 200 na :8080"; exit 1; }
[ "${CODE_ROTA}" = "200" ] || { echo "::error::FallbackResource do SPA nao esta ativo"; exit 1; }
echo "--> config.js servido deve ser o montado, e sem cache"
# Fecha o ciclo: nao basta o entrypoint copiar, o Apache precisa servir
# o arquivo instalado. E o no-store evita que o browser continue com a
# URL do ambiente anterior depois de uma troca.
SERVIDO=$(docker exec "${CID}" curl -s http://localhost:8080/config.js)
echo "${SERVIDO}" | grep -q 'smoke-test' \
|| { echo "::error::config.js servido nao e o montado: ${SERVIDO}"; exit 1; }
docker exec "${CID}" curl -sI http://localhost:8080/config.js | grep -qi 'cache-control: *no-store' \
|| { echo "::error::config.js servido sem Cache-Control no-store (mod_headers habilitado?)"; exit 1; }
echo "Smoke test OK"
- name: Push da imagem
run: |
set -euo pipefail
docker push "${{ steps.meta.outputs.tag_version }}"
docker push "${{ steps.meta.outputs.tag_sha }}"
if [ "${{ steps.meta.outputs.is_tag }}" != "true" ]; then
echo "Build de branch: tag 'latest' preservada."
exit 0
fi
if [ "${PUSH_LATEST}" = "true" ]; then
docker tag "${{ steps.meta.outputs.tag_version }}" "${{ steps.meta.outputs.tag_latest }}"
docker push "${{ steps.meta.outputs.tag_latest }}"
else
echo "PUSH_LATEST=${PUSH_LATEST}: tag 'latest' nao publicada."
fi
- name: Deploy no ambiente da branch
# Branch = ambiente: develop -> teste e release-* -> homolog.
# Tags e master nao fazem deploy por este workflow.
#
# Fica aqui, e nao num workflow proprio disparado por push, porque dois
# workflows no mesmo evento sao enfileirados em ordem arbitraria — o
# deploy poderia rodar antes de a imagem existir. Aqui a ordem e
# garantida e a imagem acabou de ser publicada.
#
# Precede a Limpeza de proposito: ela faz docker logout, e o deploy
# ainda precisa do login para o pull.
if: ${{ steps.meta.outputs.ambiente != '' && vars.AUTO_DEPLOY != 'false' }}
env:
AMBIENTE: ${{ steps.meta.outputs.ambiente }}
SSH_HOST: ${{ vars.TEST_SSH_HOST || '192.168.0.44' }}
SSH_PORT: ${{ vars.TEST_SSH_PORT || '22' }}
SSH_USER: ${{ vars.SSH_USER_TEST || secrets.SSH_USER_TEST }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY_TEST }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS_TEST }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REGISTRY_USER: ${{ vars.REGISTRY_USER }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
# O Gitea publica a tag em minusculas; a branch pode ter maiusculas.
TAG=$(echo "${{ steps.meta.outputs.version }}" | tr '[:upper:]' '[:lower:]')
if [ "${AMBIENTE}" = "teste" ]; then
echo "Deploy de teste usa SSH para ${SSH_HOST}:${SSH_PORT}."
chmod +x docker/deploy/deploy-over-ssh.sh
./docker/deploy/deploy-over-ssh.sh "${AMBIENTE}" "${TAG}"
else
chmod +x docker/deploy/deploy.sh
./docker/deploy/deploy.sh "${AMBIENTE}" "${TAG}"
fi
- name: Migrations automaticas do ambiente de teste
# Migrations nunca acompanham homolog/prod automaticamente. Em teste,
# a lista continua explicita para nao atingir conexoes tecnicas do
# connections.json. As duas variables sao opt-in deliberado.
if: ${{ steps.meta.outputs.ambiente == 'teste' && vars.AUTO_DEPLOY != 'false' && vars.AUTO_MIGRATE_TEST == 'true' }}
env:
TAG_VERSION: ${{ steps.meta.outputs.version }}
MUNICIPIOS: ${{ vars.MIGRATION_CONNECTIONS_TEST }}
run: |
set -euo pipefail
if [ -z "${MUNICIPIOS:-}" ]; then
echo "::error::AUTO_MIGRATE_TEST=true exige a variable MIGRATION_CONNECTIONS_TEST com a lista explicita de municipios."
exit 1
fi
# O Gitea normaliza a tag de branch para minusculas no registry.
TAG=$(echo "${TAG_VERSION}" | tr '[:upper:]' '[:lower:]')
chmod +x docker/deploy/migrate.sh
./docker/deploy/migrate.sh teste "${TAG}" "${MUNICIPIOS}"
- name: Resumo
if: always()
run: |
{
echo "### Imagem publicada"
echo ""
echo "| Campo | Valor |"
echo "| --- | --- |"
echo "| Versao | \`${{ steps.meta.outputs.version }}\` |"
echo "| Imagem | \`${{ steps.meta.outputs.tag_version }}\` |"
echo "| Base | \`${BASE_IMAGE}\` |"
echo "| Commit | \`${GITHUB_SHA}\` |"
echo ""
echo "**Uma imagem serve todos os ambientes.** A config do frontend e"
echo "resolvida em runtime: monte o \`config.js\` do ambiente em"
echo "\`/var/www/frontend/config.js\` (modelo em \`docker/config.js.example\`)."
echo "Tags de release constroem a imagem e a publicam no registry."
echo "Deploy e migrations de producao continuam sendo passos manuais."
echo ""
echo "Migrations municipais so executam automaticamente em teste quando"
echo "\`AUTO_MIGRATE_TEST=true\` e \`MIGRATION_CONNECTIONS_TEST\` estao definidos."
} >> "$GITHUB_STEP_SUMMARY"
- name: Limpeza
if: always()
run: |
docker rmi "${{ steps.meta.outputs.tag_version }}" \
"${{ steps.meta.outputs.tag_latest }}" \
"${{ steps.meta.outputs.tag_sha }}" 2>/dev/null || true
docker logout "${REGISTRY}" 2>/dev/null || true