init commit
All checks were successful
build-api-image / build (push) Successful in 51s

This commit is contained in:
Antonio Lopes dos Santos
2026-08-04 16:52:02 -03:00
commit afec302c3e
11594 changed files with 1636728 additions and 0 deletions

103
docker/deploy/deploy-over-ssh.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
#
# Executa deploy.sh em um host remoto via SSH.
#
# ./deploy-over-ssh.sh <ambiente> <tag>
#
# O script e o arquivo <ambiente>.env do checkout sao copiados somente para um
# diretorio temporario no host remoto. A configuracao sensivel da aplicacao
# continua no CONFIG_DIR daquele host; ela nunca passa pelo job nem pelo SSH.
set -euo pipefail
AMBIENTE="${1:-}"
TAG="${2:-}"
SSH_HOST="${SSH_HOST:-}"
SSH_PORT="${SSH_PORT:-22}"
SSH_USER="${SSH_USER:-}"
SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:-}"
SSH_KNOWN_HOSTS="${SSH_KNOWN_HOSTS:-}"
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
REGISTRY_TOKEN="${REGISTRY_TOKEN:-}"
REGISTRY_USER="${REGISTRY_USER:-${ACTOR:-}}"
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${AQUI}/${AMBIENTE}.env"
log() { echo "[deploy-ssh] $*"; }
fail() { echo "[deploy-ssh][ERRO] $*" >&2; exit 1; }
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag>"
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag>"
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
[ -n "${SSH_HOST}" ] || fail "SSH_HOST nao definido"
[ -n "${SSH_USER}" ] || fail "SSH_USER nao definido"
[ -n "${SSH_PRIVATE_KEY}" ] || fail "SSH_PRIVATE_KEY nao definido"
[ -n "${SSH_KNOWN_HOSTS}" ] || fail "SSH_KNOWN_HOSTS nao definido"
[ -n "${REGISTRY_TOKEN}" ] || fail "REGISTRY_TOKEN nao definido"
[ -n "${REGISTRY_USER}" ] || fail "REGISTRY_USER/ACTOR nao definido"
command -v ssh >/dev/null || fail "cliente ssh nao encontrado no job"
command -v scp >/dev/null || fail "cliente scp nao encontrado no job"
TMP_LOCAL="$(mktemp -d)"
KEY_FILE="${TMP_LOCAL}/id_ed25519"
KNOWN_HOSTS_FILE="${TMP_LOCAL}/known_hosts"
REMOTE_DIR=""
cleanup() {
local status=$?
trap - EXIT
if [ -n "${REMOTE_DIR}" ]; then
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
"docker logout '${REGISTRY}' >/dev/null 2>&1 || true; rm -rf -- '${REMOTE_DIR}'" \
>/dev/null 2>&1 || true
fi
rm -rf -- "${TMP_LOCAL}"
exit "${status}"
}
trap cleanup EXIT
printf '%s\n' "${SSH_PRIVATE_KEY}" > "${KEY_FILE}"
printf '%s\n' "${SSH_KNOWN_HOSTS}" > "${KNOWN_HOSTS_FILE}"
chmod 600 "${KEY_FILE}" "${KNOWN_HOSTS_FILE}"
SSH_OPTS=(
-i "${KEY_FILE}"
-p "${SSH_PORT}"
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
)
SCP_OPTS=(
-i "${KEY_FILE}"
-P "${SSH_PORT}"
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=yes
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
)
log "conectando em ${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
REMOTE_DIR="$(ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" 'umask 077; mktemp -d /tmp/conectasus-deploy.XXXXXX')"
case "${REMOTE_DIR}" in
/tmp/conectasus-deploy.*) ;;
*) fail "diretorio temporario remoto inesperado: ${REMOTE_DIR}" ;;
esac
log "enviando script de deploy temporariamente"
scp "${SCP_OPTS[@]}" \
"${AQUI}/deploy.sh" \
"${ENV_FILE}" \
"${SSH_USER}@${SSH_HOST}:${REMOTE_DIR}/"
# O login e feito no host remoto porque o docker pull sera executado por ele.
# A senha trafega pelo stdin e nao aparece na linha de comando nem nos logs.
printf '%s' "${REGISTRY_TOKEN}" | \
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
"docker login '${REGISTRY}' -u '${REGISTRY_USER}' --password-stdin" >/dev/null
log "executando deploy remoto de ${AMBIENTE}:${TAG}"
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" \
"chmod 700 '${REMOTE_DIR}/deploy.sh' && '${REMOTE_DIR}/deploy.sh' '${AMBIENTE}' '${TAG}'"
log "deploy remoto concluido"

159
docker/deploy/deploy.sh Executable file
View File

@@ -0,0 +1,159 @@
#!/bin/bash
#
# Deploy de um ambiente do ConectaSUS V2.
#
# ./deploy.sh <ambiente> <tag>
#
# Le os parametros do ambiente de docker/deploy/<ambiente>.env, troca a imagem
# do container e so considera o deploy bem-sucedido depois de verificar que a
# aplicacao responde. Se nao responder, volta sozinho para a imagem anterior.
#
# Roda a partir do runner, que usa o docker.sock do host — por isso todos os
# caminhos de mount sao caminhos do HOST, nao do job.
set -euo pipefail
AMBIENTE="${1:-}"
TAG="${2:-}"
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
IMAGE_NAME="${IMAGE_NAME:-rkm/conectasus-api}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-90}"
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log() { echo "[deploy] $*"; }
fail() { echo "[deploy][ERRO] $*" >&2; exit 1; }
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag>"
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag>"
ENV_FILE="${AQUI}/${AMBIENTE}.env"
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
# shellcheck disable=SC1090
set -a; . "${ENV_FILE}"; set +a
# Gabarito nao preenchido: melhor recusar do que implantar em lugar errado.
if grep -q 'TROCAR' "${ENV_FILE}"; then
fail "${ENV_FILE} ainda tem valores de gabarito (TROCAR)."
fi
for v in CONTAINER_NAME CONFIG_DIR PORT_API PORT_SPA PORT_WS; do
[ -n "${!v:-}" ] || fail "${v} nao definido em ${ENV_FILE}"
done
IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
log "ambiente : ${AMBIENTE}"
log "container: ${CONTAINER_NAME}"
log "imagem : ${IMAGE}"
# ---------------------------------------------------------------------------
# 1. Guarda a imagem atual para poder voltar. Vale o digest, nao a tag: a tag e
# mutavel e pode ja apontar para outra coisa na hora do rollback.
# ---------------------------------------------------------------------------
ANTERIOR=""
if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
ANTERIOR=$(docker inspect "${CONTAINER_NAME}" --format '{{.Image}}')
log "imagem atual: ${ANTERIOR}"
else
log "container ainda nao existe; primeiro deploy"
fi
log "puxando ${IMAGE}"
docker pull "${IMAGE}" >/dev/null || fail "pull falhou (registry acessivel? credencial presente?)"
# ---------------------------------------------------------------------------
# 2. Config do host. O shell do job pode rodar dentro de um container do
# act_runner e, portanto, nao enxergar os caminhos do host. A verificacao
# precisa passar pelo daemon Docker: e ele que avaliara o bind mount no
# host e depois subira a aplicacao com esses mesmos arquivos.
#
# --mount, diferentemente de -v, recusa uma origem inexistente em vez de
# criar um diretorio vazio silenciosamente.
# ---------------------------------------------------------------------------
log "validando configuracao no host Docker"
if ! docker run --rm \
--mount "type=bind,src=${CONFIG_DIR},dst=/config,readonly" \
--entrypoint sh \
"${IMAGE}" \
-c '
set -eu
for f in .env connections.json config.js; do
[ -f "/config/${f}" ] || {
echo "arquivo obrigatorio ausente: /config/${f}" >&2
exit 1
}
done
[ -d /config/storage ] || {
echo "diretorio obrigatorio ausente: /config/storage" >&2
exit 1
}
'
then
fail "configuracao invalida ou inacessivel no host Docker: ${CONFIG_DIR}"
fi
subir() {
local img="$1"
docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
docker run -d \
--name "${CONTAINER_NAME}" \
--restart unless-stopped \
${NETWORK:+--network "${NETWORK}"} \
-p "${PORT_API}:80" \
-p "${PORT_SPA}:8080" \
-p "${PORT_WS}:6001" \
-v "${CONFIG_DIR}/.env:/var/www/api/.env:ro" \
-v "${CONFIG_DIR}/connections.json:/var/www/api/connections.json:ro" \
-v "${CONFIG_DIR}/config.js:/var/www/frontend/config.js:ro" \
-v "${CONFIG_DIR}/storage:/var/www/api/storage" \
"${img}" >/dev/null
}
saudavel() {
local fim=$((SECONDS + HEALTH_TIMEOUT))
while [ ${SECONDS} -lt ${fim} ]; do
if [ "$(docker inspect -f '{{.State.Running}}' "${CONTAINER_NAME}" 2>/dev/null)" != "true" ]; then
return 1
fi
local spa api
spa=$(docker exec "${CONTAINER_NAME}" curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/ 2>/dev/null || echo 000)
api=$(docker exec "${CONTAINER_NAME}" curl -s -o /dev/null -w '%{http_code}' http://localhost:80/ 2>/dev/null || echo 000)
# A API nao tem rota "/", entao 404 e resposta valida; 000 e que nao e.
if [ "${spa}" = "200" ] && [ "${api}" != "000" ]; then
log "saudavel (spa=${spa} api=${api})"
return 0
fi
sleep 3
done
return 1
}
log "subindo ${CONTAINER_NAME}"
subir "${IMAGE}"
if saudavel; then
log "deploy de ${AMBIENTE} concluido em ${IMAGE}"
exit 0
fi
# ---------------------------------------------------------------------------
# 3. Nao respondeu. Um container no ar mas quebrado e pior do que a versao
# anterior no ar, entao volta.
# ---------------------------------------------------------------------------
echo "::error::deploy de ${AMBIENTE} falhou na verificacao de saude"
docker logs "${CONTAINER_NAME}" 2>&1 | tail -30 || true
if [ -z "${ANTERIOR}" ]; then
fail "sem versao anterior para voltar; container deixado no ar para diagnostico"
fi
log "revertendo para ${ANTERIOR}"
subir "${ANTERIOR}"
if saudavel; then
fail "deploy revertido: ${AMBIENTE} voltou para a versao anterior"
fi
fail "deploy falhou E a reversao tambem nao respondeu — ${AMBIENTE} precisa de atencao manual"

16
docker/deploy/homolog.env Normal file
View File

@@ -0,0 +1,16 @@
# Ambiente de HOMOLOG — parametros de deploy.
#
# TROCAR os valores abaixo. A esteira recusa implantar enquanto encontrar essa
# palavra aqui — melhor falhar do que implantar no lugar errado.
#
# CONFIG_DIR precisa conter, no HOST: .env, connections.json, config.js e
# storage/. O config.js e o que define para qual API o frontend aponta.
CONTAINER_NAME=conectasus-homolog
CONFIG_DIR=/TROCAR/caminho/no/host/homolog/config
NETWORK=TROCAR
PORT_API=TROCAR
PORT_SPA=TROCAR
PORT_WS=TROCAR

103
docker/deploy/migrate.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
#
# Aplica migrations do ConectaSUS para uma lista explicita de municipios.
#
# ./migrate.sh <ambiente> <tag> <municipio[,municipio...]>
#
# A migration roda em um container descartavel da imagem informada. Assim ela
# nao depende de qual versao esta no ar e usa os mesmos .env,
# connections.json e storage montados no host do ambiente.
set -euo pipefail
AMBIENTE="${1:-}"
TAG="${2:-}"
MUNICIPIOS_CSV="${3:-}"
REGISTRY="${REGISTRY:-192.168.0.41:3001}"
IMAGE_NAME="${IMAGE_NAME:-rkm/conectasus-api}"
AQUI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log() { echo "[migrate] $*"; }
fail() { echo "[migrate][ERRO] $*" >&2; exit 1; }
[ -n "${AMBIENTE}" ] || fail "uso: $0 <ambiente> <tag> <municipio[,municipio...]>"
[ -n "${TAG}" ] || fail "uso: $0 <ambiente> <tag> <municipio[,municipio...]>"
[ -n "${MUNICIPIOS_CSV}" ] || fail "informe ao menos um municipio"
ENV_FILE="${AQUI}/${AMBIENTE}.env"
[ -f "${ENV_FILE}" ] || fail "ambiente desconhecido: ${AMBIENTE} (esperado ${ENV_FILE})"
# shellcheck disable=SC1090
set -a; . "${ENV_FILE}"; set +a
if grep -q 'TROCAR' "${ENV_FILE}"; then
fail "${ENV_FILE} ainda tem valores de gabarito (TROCAR)."
fi
for v in CONFIG_DIR; do
[ -n "${!v:-}" ] || fail "${v} nao definido em ${ENV_FILE}"
done
IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
# Nomes de conexao do Laravel nao devem conter espacos nem caracteres que
# possam alterar a linha de comando. A lista e propositalmente explicita:
# conexoes tecnicas (por exemplo, sigtap) nao devem receber migrations por
# acidente.
IFS=',' read -r -a MUNICIPIOS_BRUTOS <<< "${MUNICIPIOS_CSV}"
declare -a MUNICIPIOS=()
declare -A VISTOS=()
for bruto in "${MUNICIPIOS_BRUTOS[@]}"; do
municipio="$(echo "${bruto}" | tr -d '[:space:]')"
[ -n "${municipio}" ] || fail "ha um municipio vazio na lista: ${MUNICIPIOS_CSV}"
[[ "${municipio}" =~ ^[A-Za-z0-9_-]+$ ]] || fail "nome de municipio invalido: ${municipio}"
[ -z "${VISTOS[${municipio}]:-}" ] || fail "municipio repetido na lista: ${municipio}"
VISTOS["${municipio}"]=1
MUNICIPIOS+=("${municipio}")
done
log "ambiente : ${AMBIENTE}"
log "imagem : ${IMAGE}"
log "municipios: ${MUNICIPIOS[*]}"
log "puxando ${IMAGE}"
docker pull "${IMAGE}" >/dev/null || fail "pull falhou (registry acessivel? credencial presente?)"
# --mount valida as fontes no host do Docker. Isso e essencial quando o job
# roda em container do act_runner, cujo filesystem nao contem CONFIG_DIR.
MOUNTS=(
--mount "type=bind,src=${CONFIG_DIR}/.env,dst=/var/www/api/.env,readonly"
--mount "type=bind,src=${CONFIG_DIR}/connections.json,dst=/var/www/api/connections.json,readonly"
--mount "type=bind,src=${CONFIG_DIR}/storage,dst=/var/www/api/storage"
)
PHP_RUN=(docker run --rm "${MOUNTS[@]}" --workdir /var/www/api --entrypoint php "${IMAGE}")
# Confere o JSON e todas as conexoes antes de alterar qualquer banco. Nao lista
# nem imprime o conteudo do arquivo, que pode conter credenciais.
for municipio in "${MUNICIPIOS[@]}"; do
if ! "${PHP_RUN[@]}" -r '
$connections = json_decode(file_get_contents("connections.json"), true);
if (!is_array($connections) || !array_key_exists($argv[1], $connections)) {
fwrite(STDERR, "conexao nao encontrada no connections.json: " . $argv[1] . PHP_EOL);
exit(1);
}
' "${municipio}"; then
fail "validacao da conexao falhou; nenhum banco recebeu migration"
fi
done
MIGRATE_RUN=("${PHP_RUN[@]}")
if [ -n "${NETWORK:-}" ]; then
MIGRATE_RUN=(docker run --rm --network "${NETWORK}" "${MOUNTS[@]}" --workdir /var/www/api --entrypoint php "${IMAGE}")
fi
total="${#MUNICIPIOS[@]}"
for i in "${!MUNICIPIOS[@]}"; do
municipio="${MUNICIPIOS[${i}]}"
log "[$((i + 1))/${total}] aplicando migrations em ${municipio}"
"${MIGRATE_RUN[@]}" artisan migrate --database="${municipio}" --force
done
log "migrations concluidas para ${total} municipio(s)"

16
docker/deploy/prod.env Normal file
View File

@@ -0,0 +1,16 @@
# Ambiente de PROD — parametros de deploy.
#
# TROCAR os valores abaixo. A esteira recusa implantar enquanto encontrar essa
# palavra aqui — melhor falhar do que implantar no lugar errado.
#
# CONFIG_DIR precisa conter, no HOST: .env, connections.json, config.js e
# storage/. O config.js e o que define para qual API o frontend aponta.
CONTAINER_NAME=conectasus-prod
CONFIG_DIR=/TROCAR/caminho/no/host/prod/config
NETWORK=TROCAR
PORT_API=TROCAR
PORT_SPA=TROCAR
PORT_WS=TROCAR

12
docker/deploy/teste.env Normal file
View File

@@ -0,0 +1,12 @@
# Ambiente de TESTE — parametros de deploy.
# Valores extraidos do container que ja roda hoje (stack antonio-santos-v2).
CONTAINER_NAME=conectasus-test
CONFIG_DIR=/dados/home/antonio.santos/development/V2/conectasus_test/config
# Rede do stack, para o container enxergar o banco pelo nome do servico.
NETWORK=antonio-santos-v2_default
PORT_API=3200
PORT_SPA=3202
PORT_WS=3201