Pular para conteúdo

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 responde 503, sem linha persistida, e o cliente reenvia.
  • GARAGE: o upload grava só no Garage (arquivo_bytes fica NULL); o backfill, se ligado, migra os legados e zera o arquivo_bytes.
  • Backfill: depois de mudar de modo, ligue S3_FILE_STORAGE_BACKFILL=true por um startup. É idempotente e isola falha por linha; sem dado legado, termina sem trabalho.
  • A coluna arquivo_bytes não é dropada: o modo GARAGE só a zera em runtime, e voltar para PSQL ou PSQL_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_DSN não é lido. Presente sem SENTRY_DSN, o boot loga WARN e o error tracking fica desligado; a camada do smoke que procura exceção nova passa sem ver nada.
  • SENTRY_RELEASE setada 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 o Dockerfile.native o promove a ENV. Não cadastre XADM_COMMIT nem SOURCE_COMMIT no recurso.
  • MICRONAUT_ENVIRONMENTS com dev ou test desliga 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

  1. Rode a skill /xadm-release no repo: bump SemVer, CHANGELOG, tag anotada vX.Y.Z e push. Um trailer Deploy: na tag anotada escolhe os alvos; sem ele, vale o build.targets do docs/app.json (native).
  2. 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 o Dockerfile.native no runner do GitHub, com XADM_COMMIT=<sha>, e publica fonte.xadm.biz/xadm/onpetro-xls:native-amd64 e :<sha>-native.
  3. O job deploy só roda com gate, release-check e build_native verdes. Ele lê o commit do /health no ar (o alvo de reversão) e faz POST https://central-backend.xadm.biz/api/ci/deploy com {"target":"native","image":"…:native-amd64"} e o CENTRAL_DEPLOY_TOKEN. O central-backend resolve 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).
  4. O job smoke (smoke.py, manifesto smoke do docs/app.json) espera o /health responder com commit igual ao sha da tag, confere as rotas declaradas (hoje /health → 200) e procura exceção nova no GlitchTip com firstRelease igual a esse sha. Reprovado, o job falha e o relatório diz o alvo de reversão.

  5. Deploy fora de release: workflow_dispatch com native e tests ligados builda e deploya o commit do branch. Com tests desligado, a imagem é buildada e o deploy sai pulado.

  6. central-backend fora do ar: o POST não responde e o deploy é à mão, de dentro da casa: Redeploy do recurso excel-native.onpetro na UI do Coolify.

Provisionar ambiente novo

  1. Banco, no Postgres compartilhado, como superuser, antes do app. A senha é gerada na hora (openssl rand -base64 32) e vai só para o DATASOURCES_DEFAULT_PASSWORD do 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_role com SELECT nas bi_* (decisão 0013); o reset da replicação exige REPLICATION na role do app (Resetar a replicação). 2. Garage: bucket onpetro e key com leitura e escrita nele. O broker da Central injeta GARAGE_ACCESS_KEY_ID e GARAGE_SECRET_ACCESS_KEY no 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; as AUTH_* 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, arquivos processamento-<id>.log), exibida na UI, entre redeploys — é dado, não log (Operação no Coolify). Recurso que montava o volume em /app/logs remonta o mesmo volume em /app/data: o conteúdo segue em data/execucoes/, porque o volume nomeado já continha execucoes/. O log do servidor vai para stdout. Volume nomeado herda o dono app da imagem; bind mount não herda, e o dono no host tem de ser 1001:1001. 7. Limite e reserva de memória, nunca 0: o baseline da casa para server native é limite 256M e reserva 128M (Coolify, limite de memória); ajuste pelo consumo medido (docker stats). 8. Health check: /health na porta 8080. O HEALTHCHECK e a cadência já vêm na imagem. 9. Primeiro deploy: reconcile no central-backend (sob demanda) e depois uma release ou um workflow_dispatch com native e tests.

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

  1. /health com 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"}
    

    versao sozinha não prova a entrega: dois deploys da mesma versão só se distinguem pelo commit. 2. O job smoke do run da tag está verde. 3. GET /processamentos abre depois do login Google. 4. Os logs do container no Coolify não têm erro de Flyway ou datasource, nem o WARN da credencial antiga do Garage ou do GLITCHTIP_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.