Deploy¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15
O que é¶
Como o bi-comercial-xls chega a produção. A release cria a tag vX.Y.Z; o
.github/workflows/pipeline.yml testa, builda a imagem native num runner do GitHub e pede o deploy ao
control-plane (central-backend), que dispara o recurso Coolify. O Coolify só puxa a imagem: não builda e
não tem auto-deploy. Depois do deploy, o smoke confere produção.
Push em master sem tag não deploya: roda o gate (se mudou código) e publica as docs dev.
flowchart TB
T["Tag vX.Y.Z<br/>(skill /xadm-release)"] --> G["gate<br/>./gradlew check + guardas"]
T --> RC["release-check<br/>SemVer × CHANGELOG × tag"]
T --> B["build_native<br/>Dockerfile.native no runner"]
B --> I[("fonte.xadm.biz/xadm/onpetro-xls<br/>native-amd64 e SHA-native")]
G --> D["deploy<br/>central-backend: /api/ci/deploy"]
RC --> D
B --> D
D --> C["central-backend<br/>control-plane"]
C --> K["Coolify<br/>recurso excel-native.onpetro"]
K -- "puxa native-amd64" --> I
D --> S["smoke<br/>commit no /health, rotas, GlitchTip"]
Topologia: o Traefik termina o TLS na borda e encaminha HTTP para a porta interna 8080. Postgres e
Garage são recursos compartilhados; o PowerSync (ps.onpetro, com o Mongo dele) é recurso próprio, do
repo onpetro-powersync.
| Item | Valor |
|---|---|
| Recurso Coolify | excel-native.onpetro, Build Pack Docker Image |
| Imagem | fonte.xadm.biz/xadm/onpetro-xls:native-amd64 (tag móvel canônica; nunca latest) |
| Tag imutável por entrega | fonte.xadm.biz/xadm/onpetro-xls:<sha>-native (alvo de reversão) |
| Domínios | https://excel.onpetro.xadm.biz (principal) e https://excel-native.onpetro.xadm.biz |
| Auto-deploy | desligado; quem dispara é o control-plane |
| Recurso jar | excel.onpetro-jar-pull (excel-jar.onpetro.xadm.biz), parado e sem imagem nova (decisão 0028) |
O control-plane casa o recurso pelo slug da imagem (onpetro-xls) e pelo alvo (native), não pelo uuid:
recriar o recurso não exige mudança no repo (decisão 0026).
Quando usar¶
- Subir uma versão nova: o fluxo normal é a skill
/xadm-release. - Provisionar um ambiente novo (banco, recurso Coolify e variáveis).
- Conferir ou ajustar as variáveis de ambiente do recurso.
- Reverter um deploy que o smoke reprovou.
Pré-requisitos¶
| Componente | Mínimo |
|---|---|
| Runtime | binário native (GraalVM CE 25) sobre ubuntu:24.04, porta 8080 |
| PostgreSQL | 18+ (uuidv7() nativo) |
| Garage | instância compartilhada arquivo-api.xadm.biz, bucket onpetro |
| Docker | 24+, só para build local e para a integração dos testes (Testcontainers) |
Secrets do repo no GitHub (lidos pelo pipeline.yml):
| Secret | Uso |
|---|---|
CENTRAL_DEPLOY_TOKEN |
POST /api/ci/deploy e aviso de publish das docs ao central-backend |
FORGEJO_USER, FORGEJO_TOKEN |
push da imagem em fonte.xadm.biz |
DOCS_S3_ENDPOINT, DOCS_S3_ACCESS_KEY, DOCS_S3_SECRET_KEY |
publicação das docs no Garage |
GLITCHTIP_API_TOKEN |
camada 3 do smoke (read-only). Com smoke.glitchtip declarado no docs/app.json, a falta dele reprova o smoke |
SMOKE_TOKEN, SMOKE_M2M_TOKEN |
rotas session (GET /processamentos) e m2m (GET /api/comercial/processamentos) do smoke. SMOKE_TOKEN é o mesmo valor da env do recurso; SMOKE_M2M_TOKEN é o valor do BI_COMERCIAL_XLS_API_TOKEN. Sem eles, as duas rotas viram só asserção negativa |
DEPLOY_SSH_KEY, COOLIFY_TOKEN e COOLIFY_NATIVE_UUID não são lidos por nenhum job. Se ainda existirem no
repo, apague-os e revogue o token no Coolify.
Variáveis de ambiente¶
Cadastradas no recurso Coolify, com senha e token marcados como Secret. Nunca commitar valor de segredo.
Core¶
| Variável | Padrão | Descrição |
|---|---|---|
BI_COMERCIAL_XLS_API_TOKEN |
— | Bearer estático do /api/** (openssl rand -hex 32; mesmo valor no cliente). Vazia, a lib recusa o boot em produção |
DATASOURCES_DEFAULT_URL |
jdbc:postgresql://localhost:5432/db_onpetro?reWriteBatchedInserts=false |
URL JDBC. O host é o nome do serviço Docker do Postgres na rede do Coolify, não localhost |
DATASOURCES_DEFAULT_USERNAME |
user_onpetro |
usuário do banco |
DATASOURCES_DEFAULT_PASSWORD |
(senha de dev) | senha do banco |
PORT |
8080 |
porta HTTP |
APP_BASE_URL |
https://excel.onpetro.xadm.biz |
base dos links nas notificações do Telegram e no linkStatus do reset REST |
BI_COMERCIAL_XLS_IMPORT_TOKEN |
(vazio) | segredo compartilhado da página aberta de import de XLS. Vazio, a página recusa tudo (403) |
CLIENTE |
OnPetro |
nome do cliente nas views e tag cliente nos eventos do GlitchTip |
reWriteBatchedInserts=false é obrigatório na URL JDBC
Com a flag ligada, executeBatch() retorna SUCCESS_NO_INFO e a métrica linhas_efetivas morre
(decisão 0009). Ao sobrescrever
DATASOURCES_DEFAULT_URL, mantenha o parâmetro.
Import manual de XLS. A rota canônica é /xls/importar; a herdada /compras/importar segue viva para o
botão "Importar XLS" do bi-comercial, que abre a página com ?u=<id>&n=<nome>&k=<segredo>. A página não
tem login Google: a barreira é o BI_COMERCIAL_XLS_IMPORT_TOKEN, com o mesmo valor embutido no
bi-comercial. Aceita a Relação de Compras e a Cota Petrobrás (no máximo uma Cota por lote). Contrato em
API REST.
Telegram¶
TELEGRAM_BOT_TOKEN e TELEGRAM_CHAT_ID. Sem qualquer um dos dois, as notificações ficam desligadas, sem
erro. Reupload que não muda nada (linhas_efetivas = 0 e linhas_removidas = 0) não notifica.
Object storage (Garage)¶
| Variável | Padrão | Descrição |
|---|---|---|
S3_FILE_STORAGE |
PSQL_GARAGE |
PSQL | PSQL_GARAGE | GARAGE, sempre em MAIÚSCULAS |
GARAGE_ENDPOINT |
http://localhost:3900 |
em produção, https://arquivo-api.xadm.biz |
GARAGE_REGION |
garage |
bate com o s3_region do garage.toml |
GARAGE_ACCESS_KEY_ID |
— | access key da key onpetro no Garage. Obrigatória em produção |
GARAGE_SECRET_ACCESS_KEY |
— | secret da mesma key. Obrigatória em produção |
GARAGE_BUCKET |
onpetro |
bucket dedicado deste app |
GARAGE_CRIAR_BUCKET |
false |
true cria o bucket no startup (dev e teste); em produção o bucket já é provisionado |
S3_FILE_STORAGE_BACKFILL |
false |
true liga a migração dos arquivo_bytes legados para o Garage no startup |
Credencial do Garage pelos nomes do broker
Os nomes são os que o broker da Central injeta no recurso: GARAGE_ACCESS_KEY_ID e
GARAGE_SECRET_ACCESS_KEY. Sem eles, a lib ainda lê os antigos GARAGE_ACCESS_KEY e
GARAGE_SECRET_KEY e loga WARN pedindo a migração; esse fallback sai numa versão futura da
xadm-comum-storage. Recurso com os nomes antigos: cadastre os novos, confira o app no ar sem o WARN
e só então apague os antigos. Sem nenhum dos dois pares, o cliente S3 não sobe.
PSQL_GARAGE: o upload grava nos dois backends e o processamento lê do Garage. Falha do Garage no upload responde503, sem linha persistida, e o cliente reenvia.GARAGE: o upload grava só no Garage (arquivo_bytesficaNULL); o backfill, se ligado, migra os legados e zera oarquivo_bytes.- Backfill: depois de mudar de modo, ligue
S3_FILE_STORAGE_BACKFILL=truepor um startup. É idempotente e isola falha por linha; sem dado legado, termina sem trabalho. - A coluna
arquivo_bytesnão é dropada: o modoGARAGEsó a zera em runtime, e voltar paraPSQLouPSQL_GARAGEé trocar a variável.
Desenho dos modos: etapa 04 e decisão 0005.
Error tracking (GlitchTip)¶
Os erros de nível ERROR vão ao GlitchTip (bug.xadm.biz, protocolo Sentry), projeto bi-comercial-xls.
O Sentry é inicializado no main, antes do Micronaut, e lê só variáveis de ambiente. Falha de
inicialização é logada e engolida: o app sobe sem error tracking.
| Variável | Padrão | Descrição |
|---|---|---|
SENTRY_DSN |
(vazio: desligado) | DSN do projeto, o mesmo do features.glitchtip.dsn do docs/app.json. É o único nome lido |
SENTRY_ENVIRONMENT |
production |
ambiente reportado nos eventos |
SENTRY_TRACES_SAMPLE_RATE |
0.0 |
amostragem de tracing (0.0 a 1.0); 0.0 envia só erros |
SENTRY_RELEASE |
(vazio) | não setar. Sem ela, o release é o commit da imagem, que é o que o smoke casa |
GLITCHTIP_DSNnão é lido. Presente semSENTRY_DSN, o boot logaWARNe o error tracking fica desligado; a camada do smoke que procura exceção nova passa sem ver nada.SENTRY_RELEASEsetada quebra o smoke: o release deixa de ser o sha da entrega, e o smoke não reconhece exceção nova deste deploy. Recurso que ainda a tenha: apague.- O commit vem da imagem, não do recurso: o pipeline carimba
XADM_COMMIT=<sha>como build-arg, e oDockerfile.nativeo promove aENV. Não cadastreXADM_COMMITnemSOURCE_COMMITno recurso. MICRONAUT_ENVIRONMENTScomdevoutestdesliga o Sentry; em produção, não a defina.
Reset da replicação PowerSync (POWERSYNC_* / COOLIFY_*)¶
O procedimento está no runbook Resetar a replicação; a tabela completa das variáveis mora aqui.
| Variável | Padrão | Descrição |
|---|---|---|
POWERSYNC_MONGODB_URI |
— | connection string do Mongo do PowerSync (o PS_MONGO_URI do container). Vazia, o reset só simula o drop. O nome do database vem do path da URI |
POWERSYNC_MONGODB_DATABASE |
(do path da URI) | override do nome do database Mongo (raro) |
POWERSYNC_NUKE_CONFIRMATION |
— | habilita o endpoint REST (a tela /admin não depende dela). O nome "nuke" é contrato |
POWERSYNC_SLOT |
(auto-descoberta) | vazio, descobre o slot lendo sync_rules com state='ACTIVE' no Mongo. Explícito só para forçar ou em recuperação |
POWERSYNC_PLUGIN |
pgoutput |
plugin de replicação lógica do slot |
POWERSYNC_BOOTSTRAP_WAIT |
60 |
segundos de espera depois do restart, antes da amostra final |
COOLIFY_API_TOKEN |
— | token do Coolify com permissão de escrita/deploy. Setá-lo liga o restart automático do PowerSync; sem ele, o restart é manual |
POWERSYNC_COOLIFY_NAME |
— | nome exato do recurso PowerSync no Coolify (ps.onpetro). NAME ou UUID |
POWERSYNC_COOLIFY_UUID |
— | alternativa ao _NAME. O nome sobrevive à recriação do recurso; o uuid não |
COOLIFY_API_URL |
https://coolify.xadm.biz |
no mesmo daemon Docker do Coolify, use http://coolify:8080 |
POWERSYNC_HEALTH_URL |
— | health check do PowerSync conferido depois do restart |
POWERSYNC_RESTART_TIMEOUT |
120 |
timeout, em segundos, do polling do deployment no Coolify |
COOLIFY_API_URL interna. O caminho externo passa por DNS público, hairpin NAT e o Traefik com o
certificado wildcard, que a truststore pode recusar (PKIX path building failed). A URL interna é HTTP
numa rede privada e dispensa a allowlist de IP da API do Coolify. Com o app em outra máquina, ou fora da
rede coolify, use a URL pública e ponha o IP de saída na allowlist.
Login Google das views (AUTH_*)¶
A tabela completa e o setup do Firebase estão no runbook Login Google das views. Sem as
cinco variáveis obrigatórias, as views ficam públicas em dev e test e fechadas (503) em
produção.
Passos¶
Deploy de versão nova¶
- Rode a skill
/xadm-releaseno repo: bump SemVer, CHANGELOG, tag anotadavX.Y.Ze push. Um trailerDeploy:na tag anotada escolhe os alvos; sem ele, vale obuild.targetsdodocs/app.json(native). - A tag dispara o
pipeline.yml, com estes jobs em paralelo:gate: guardas estáticas e./gradlew check, com a integração (Testcontainers com Postgres, Mongo e Garage);release-check: SemVer × CHANGELOG × tag (valida-release.py);docs: publica a versão das docs;build_native: builda oDockerfile.nativeno runner do GitHub, comXADM_COMMIT=<sha>, e publicafonte.xadm.biz/xadm/onpetro-xls:native-amd64e:<sha>-native.
- O job
deploysó roda comgate,release-checkebuild_nativeverdes. Ele lê ocommitdo/healthno ar (o alvo de reversão) e fazPOST https://central-backend.xadm.biz/api/ci/deploycom{"target":"native","image":"…:native-amd64"}e oCENTRAL_DEPLOY_TOKEN. Ocentral-backendresolve o recurso e chama o deploy do Coolify, que puxa a tag e faz o rolling: o container novo sobe, fica saudável, e o velho drena. A resposta é assíncrona (queued). -
O job
smoke(smoke.py, manifestosmokedodocs/app.json) espera o/healthresponder comcommitigual ao sha da tag, confere as rotas declaradas (hoje/health→200) e procura exceção nova no GlitchTip comfirstReleaseigual a esse sha. Reprovado, o job falha e o relatório diz o alvo de reversão. -
Deploy fora de release:
workflow_dispatchcomnativeetestsligados builda e deploya o commit do branch. Comtestsdesligado, a imagem é buildada e o deploy sai pulado. central-backendfora do ar: oPOSTnão responde e o deploy é à mão, de dentro da casa: Redeploy do recursoexcel-native.onpetrona UI do Coolify.
Provisionar ambiente novo¶
-
Banco, no Postgres compartilhado, como superuser, antes do app. A senha é gerada na hora (
openssl rand -base64 32) e vai só para oDATASOURCES_DEFAULT_PASSWORDdo recurso:CREATE DATABASE db_onpetro; CREATE USER user_onpetro WITH PASSWORD '<SENHA>'; GRANT ALL PRIVILEGES ON DATABASE db_onpetro TO user_onpetro; \c db_onpetro GRANT ALL ON SCHEMA public TO user_onpetro;A replicação para o PowerSync exige o
powersync_rolecomSELECTnasbi_*(decisão 0013); o reset da replicação exigeREPLICATIONna role do app (Resetar a replicação). 2. Garage: bucketonpetroe key com leitura e escrita nele. O broker da Central injetaGARAGE_ACCESS_KEY_IDeGARAGE_SECRET_ACCESS_KEYno recurso (skill/xadm-setup). 3. Recurso Coolify:+ New Resource→ Build Pack Docker Image →fonte.xadm.biz/xadm/onpetro-xls:native-amd64. Auto-deploy desligado. 4. Domínio:https://excel.onpetro.xadm.biz(DNS A no servidor; o certificado é automático). 5. Variáveis: as das tabelas acima, segredos como Secret; asAUTH_*conforme o runbook de login, incluindo o domínio nos Authorized domains do Firebase. 6. Volume em/app/data: guarda a trilha de cada processamento (app.logs.dir=data/execucoes, arquivosprocessamento-<id>.log), exibida na UI, entre redeploys — é dado, não log (Operação no Coolify). Recurso que montava o volume em/app/logsremonta o mesmo volume em/app/data: o conteúdo segue emdata/execucoes/, porque o volume nomeado já continhaexecucoes/. O log do servidor vai para stdout. Volume nomeado herda o donoappda imagem; bind mount não herda, e o dono no host tem de ser1001:1001. 7. Limite e reserva de memória, nunca0: o baseline da casa para server native é limite256Me reserva128M(Coolify, limite de memória); ajuste pelo consumo medido (docker stats). 8. Health check:/healthna porta8080. OHEALTHCHECKe a cadência já vêm na imagem. 9. Primeiro deploy: reconcile nocentral-backend(sob demanda) e depois uma release ou umworkflow_dispatchcomnativeetests.
As migrations Flyway rodam no startup, com o histórico em bi_comercial_flyway_schema_history (separado
de outros projetos Flyway no mesmo banco).
Os dados das planilhas ficam no PostgreSQL e no Garage, sempre atrás de HTTPS. A trilha de processamento em disco não substitui auditoria fiscal: mantenha as políticas de backup do banco e do volume.
Build local da imagem (diagnóstico)¶
docker build -f Dockerfile.native -t onpetro-xls:local-native . # o mesmo build do job build_native
docker build -t onpetro-xls:local . # par JVM, para diagnóstico
O build native leva por volta de 13 minutos e é limitado por RAM. Rodar o app localmente está em Como rodar.
Verificação¶
-
/healthcom o commit da tag e o flavor native:curl -fsS https://excel.onpetro.xadm.biz/health # {"status":"UP","versao":"X.Y.Z","commit":"<sha da tag>","flavor":"native"}versaosozinha não prova a entrega: dois deploys da mesma versão só se distinguem pelocommit. 2. O jobsmokedo run da tag está verde. 3.GET /processamentosabre depois do login Google. 4. Os logs do container no Coolify não têm erro de Flyway ou datasource, nem oWARNda credencial antiga do Garage ou doGLITCHTIP_DSN.
Observabilidade depois do deploy. linhas_efetivas e linhas_removidas do xls_processamento medem o
que chegou ao disco (e ao slot lógico do PowerSync), separado do que o app tentou
(registros_processados):
-- % de churn evitado nos últimos 30 dias, por tipo
SELECT
tipo_arquivo,
COUNT(*) AS uploads,
SUM(registros_processados) AS enviadas_total,
SUM(linhas_efetivas) AS efetivas_total,
SUM(linhas_removidas) AS removidas_total,
ROUND(100.0 * (1.0 - (SUM(linhas_efetivas) + SUM(linhas_removidas))::numeric
/ NULLIF(SUM(registros_processados), 0)), 1) AS churn_evitado_pct
FROM xls_processamento
WHERE concluido_em > NOW() - INTERVAL '30 days'
AND status = 'SUCESSO'
AND linhas_efetivas IS NOT NULL
GROUP BY tipo_arquivo
ORDER BY enviadas_total DESC;
-- os 50 reuploads mais recentes sem nenhum INSERT/UPDATE/DELETE
SELECT id, nome_arquivo, tipo_arquivo, concluido_em
FROM xls_processamento
WHERE linhas_efetivas = 0
AND COALESCE(linhas_removidas, 0) = 0
ORDER BY concluido_em DESC
LIMIT 50;
churn_evitado_pct perto de 100 indica reupload sem mudança real; perto de 0, todo upload muda dado
(esperado quando o ERP corrige NFs).
Reversão¶
Rollback automático pendente no control-plane
O smoke reprova e informa o alvo de reversão (a tag imutável do commit que estava no ar), mas não reverte sozinho: o control-plane ainda só aceita a tag móvel. A reversão é manual.
| Via | Efeito | O que não faz |
|---|---|---|
Recurso no Coolify → imagem fonte.xadm.biz/xadm/onpetro-xls:<sha anterior>-native → Redeploy |
volta o binário anterior; o <sha anterior> é o que o relatório do smoke dá |
não reverte migration. Enquanto o recurso apontar a tag imutável, um deploy do control-plane re-puxa essa tag: volte o recurso para native-amd64 antes da próxima release |
Nova release com a correção (/xadm-release) |
caminho definitivo, pelo fluxo normal | — |
| Stop do recurso no Coolify (via bruta) | tira o app do ar: uploads e views param | não termina o que está em andamento: no próximo startup, o processamento órfão em PROCESSANDO vira ERRO e o PENDENTE é reenfileirado |
| Religar o recurso jar parado | não é via: ele tem imagem velha, e religar a perna JVM exige release com jar no build.targets |
— |
| Rollback de Deployments do Coolify | não é via confiável: o recurso aponta a tag móvel native-amd64, que já é a imagem nova |
— |
As migrations Flyway não revertem. Elas seguem expand/contract: a release N não dropa o que a N-1 usa, e
por isso o binário anterior roda no schema novo. Migration destrutiva (marcada -- destrutivo-ok: no
.sql) quebra essa garantia: nesse caso, corrija para frente.