Módulos Dagger e bibliotecas Go das pipelines de CI/CD da Basis.
A partir da versão 3.0 o repositório é dirigido por configuração: um projeto consumidor não escreve mais um módulo Dagger próprio, apenas declara seus targets em ci/pipeline.toml e inclui o template de CI. O módulo orchestrator faz o resto.
| Componente | Papel |
|---|---|
|
Módulo Dagger genérico. Lê |
|
Utilitários de Git e registry (change detection, bump/commit, tag |
|
Módulos Dagger por tecnologia de build. Chamados pelo orchestrator, não pelo projeto. |
|
Biblioteca Go: schema da configuração ( |
|
Biblioteca Go: cliente de commit statuses do GitLab. |
O orchestrator é o módulo Dagger genérico da 3.0. Ele substitui o módulo Dagger por projeto que existia até a 2.x: todo o comportamento vem do arquivo declarativo do projeto consumidor, e os targets declarados lá viram simultaneamente as estratégias de build, os checks de qualidade e o mapa de imagens de check-images/promote.
Um projeto precisa de exatamente dois arquivos.
-
ci/pipeline.toml— os targets do repositório (ver O schema deci/pipeline.toml). -
.gitlab-ci.yml— cinco linhas incluindo o template compartilhado:
include:
- project: 'basis/iac/ci-templates'
ref: v1.0.0
file: 'templates/dagger-orchestrator.gitlab-ci.yml'O template define os stages check, build, pre-prod e promote, e chama as funções do orchestrator com as variáveis do GitLab. Duas variáveis podem ser sobrescritas no variables: local:
-
DAGGER_MODULE— defaultgithub.com/BasisTI/daggerverse/orchestrator@3.0.0. Projetos com build custom apontam para"."(módulo local, ver Projetos com build custom). -
PIPELINE_CONFIG— defaultci/pipeline.toml.
O construtor do módulo recebe --source (raiz do repositório) e --config-path:
dagger call -m github.com/BasisTI/daggerverse/orchestrator@3.0.0 \
--source . --config-path ci/pipeline.toml \
validateCarrega e valida o arquivo declarativo e devolve um relatório textual com os targets já resolvidos (com defaults aplicados), o mapa de imagens derivado e eventuais avisos. Não faz nenhuma chamada de rede — é o job de lint da configuração, disparado no MR quando ci/pipeline.toml muda.
Os avisos cobrem o que é válido no schema mas o orchestrator genérico não consegue executar: targets type = "custom".
Roda testes e análise estática nos targets alterados que declaram sonar = true. Recebe --base-branch, --commit-sha, --sonar-host, --sonar-token, o opcional --stop-on-first-fail e a tripla opcional --gitlab-host / --gitlab-token / --gitlab-project-id para reportar commit statuses.
Builda com a versão fixa 0.0.0-check (nada é publicado). Falha com mensagem explicativa se a configuração tiver targets custom.
Detecta os targets alterados entre --base-branch e o commit atual, builda e publica cada imagem em --registry, aplica a tag sha-<commit> e devolve a lista de imagens publicadas (uma por linha).
Se --git-remote-url for informado, faz o bump dos arquivos de versão dos targets publicados e commita de volta na --git-branch (default develop). Também falha se houver targets custom.
Verifica se todas as imagens derivadas da configuração já existem no registry, antes da promoção. Roda no MR para main, usando --build-branch (default origin/develop) para localizar o commit em que as imagens foram construídas.
Targets custom não são ignorados aqui: suas imagens existem no registry e precisam ser verificadas como qualquer outra.
Promove as imagens derivadas da configuração de --src-registry para --dst-registry (que, se vazio, é o próprio --src-registry). Assim como check-images, cobre também as imagens de targets custom.
Cada promoção percorre todas as imagens do projeto, não só as que mudaram — é assim que uma imagem antiga continua tendo sua tag production- presente. Mas a cópia só acontece quando ela muda alguma coisa: antes de copiar, o digest da tag de origem é comparado com o da tag de produção, e se forem iguais a imagem é pulada com ⏭️ Já promovida. Serviços parados há meses deixam de gerar tráfego de registry a cada merge em main, e o log passa a mostrar de relance o que de fato entrou na promoção.
Implementado em pipeline/config. O parse é estrito: qualquer campo desconhecido é erro.
| Campo | Tipo | Descrição |
|---|---|---|
|
int |
Obrigatório, hoje sempre |
|
tabela |
Metadados do repositório. |
|
tabela |
Defaults por tipo de build. |
|
tabela |
Um bloco por artefato buildável. Ao menos um é obrigatório. |
| Campo | Tipo | Descrição |
|---|---|---|
|
string |
Obrigatório. Grupo no registry: |
| Bloco | Campo | Tipo | Descrição |
|---|---|---|---|
|
|
string |
Imagem base dos builds Maven. |
|
|
bool |
Liga um daemon Docker no build (Testcontainers). |
|
|
string |
Imagem de build (ex: |
|
|
string |
Imagem de runtime (ex: |
|
|
string |
Imagem de build (ex: |
|
|
string |
Imagem de runtime (ex: |
| Campo | Tipo | Descrição |
|---|---|---|
|
string |
Obrigatório. Um de |
|
string |
Path de change detection. Default: o nome do target. |
|
string |
Diretório montado no build. Default: o |
|
string |
Nome da imagem no registry, sem grupo e sem tag. Default: o nome do target. Os nomes efetivos precisam ser únicos entre os targets. |
|
string |
Arquivo de versão bumpado pelo |
|
bool |
Indica que |
|
lista de string |
Paths adicionais que também disparam o build/check deste target (ex: uma lib compartilhada, o |
|
bool |
O target participa do |
|
string |
Build system que analisa o código do target, quando ele difere de como a imagem é construída. Um de |
type responde "como a imagem é construída"; quality-type responde "com que build system o código é testado e analisado". Na esmagadora maioria dos targets os dois são a mesma coisa, e quality-type é omitido.
Eles divergem quando um projeto precisa de um Dockerfile próprio para o runtime — display virtual, browser headless, drivers nativos — mas o código continua sendo um projeto uv/Maven/npm normal, com testes e análise estática. Sem quality-type esse target teria de escolher entre publicar a imagem certa e ter qualidade medida; com ele, publish-all builda pelo Dockerfile e check-quality roda pelo build system declarado.
É o caso dos scrapers do licitacao, que rodam navegador no runtime:
[targets.scrapers_mte]
type = "dockerfile" # a imagem sai do Dockerfile do scraper
path = "scrapers/mte"
sonar = true
quality-type = "uv" # o código é Python: testes e Sonar pelo módulo uvOs campos do build system de quality (run-subdir, uv-build-image, customizations, maven-image, …) valem normalmente no target e são o que o check-quality consome — e o validate os mostra num bloco quality (<tipo>) separado.
Duas regras de validação mantêm o campo honesto:
-
quality-typesemsonar = trueé erro: seria um campo inerte, que engana quem lê o arquivo; -
sonar = truenum targetdockerfilesemquality-typeé erro de validação, com a mensagem indicando exatamente o campo que falta — não uma falha tardia na hora de rodar o check.
| Campo | Tipo | Descrição |
|---|---|---|
|
string |
Sobrescreve |
|
bool |
Sobrescreve |
|
bool |
Build multi-módulo: monta a raiz e builda com |
|
string |
Path do módulo dentro do reactor. Obrigatório quando |
|
lista de string |
Opções extras passadas ao |
| Campo | Tipo | Descrição |
|---|---|---|
|
string |
Sobrescreve |
|
string |
Sobrescreve |
|
string |
Subdiretório de execução dentro do build (ex: |
|
lista de string |
Customizações de build do módulo uv: |
| Campo | Tipo | Descrição |
|---|---|---|
|
string |
Path do Dockerfile relativo ao |
type = "npm" não tem campos próprios: as imagens vêm exclusivamente de [defaults.npm].
Quatro targets — três Maven e um frontend Angular — todos com o nome do diretório igual ao nome do target, o que dispensa path e image:
schema-version = 1
[project]
group = "contavinculada"
[defaults.maven]
image = "maven:3.9.11-eclipse-temurin-21"
use-docker = false
[defaults.npm]
build-image = "node:22-alpine"
run-image = "nginx:alpine"
[targets.contavinculada]
type = "maven"
sonar = true
[targets.integracaosgo]
type = "maven"
sonar = true
[targets.snf]
type = "maven"
sonar = true
[targets.frontend]
type = "npm"
sonar = trueDois módulos de um mesmo reactor. Ambos montam a raiz (source-path = "."), compartilham o pom.xml da raiz como arquivo de versão (root-version-file = true) e são reconstruídos quando o contrato comum ou o pom da raiz mudam:
schema-version = 1
[project]
group = "triagem"
[defaults.maven]
image = "maven:3.9.11-eclipse-temurin-25"
use-docker = true
[targets.triagem-core]
type = "maven"
source-path = "."
reactor = true
module = "triagem-core"
version-file = "pom.xml"
root-version-file = true
extra-trigger-paths = ["triagem-contracts", "pom.xml"]
sonar = true
[targets.triagem-ingestao]
type = "maven"
source-path = "."
reactor = true
module = "triagem-ingestao"
version-file = "pom.xml"
root-version-file = true
extra-trigger-paths = ["triagem-contracts", "pom.xml"]
sonar = trueOutros exemplos reais estão em pipeline/config/testdata/.
Esta é a regra central do schema.
O mapa de imagens consumido por check-images e promote não é declarado: ele é derivado dos mesmos targets que descrevem o build, via Config.ProjectImages(), que devolve "<group>/<image efetiva>" → "<path efetivo>" para todos os targets.
Antes da 3.0, a lista de imagens a verificar e promover era mantida à mão, em paralelo aos build targets do orchestrator do projeto. Bastava adicionar um serviço novo e esquecer da segunda lista para a promoção silenciosamente pular a imagem — ou apontar para uma que não existia mais.
Com uma única fonte de verdade, é impossível divergir: o conjunto do que se builda e o conjunto do que se promove são literalmente a mesma coleção de targets. Targets type = "custom" entram no mapa normalmente — eles não são buildados pelo orchestrator genérico, mas suas imagens existem no registry e precisam ser verificadas e promovidas como as outras.
Alguns projetos têm builds que nenhum dos tipos genéricos cobre. Eles mantêm um módulo Dagger próprio, mas não duplicam a configuração: esse módulo carrega o mesmo ci/pipeline.toml via pipeline/config e faz o merge dos targets custom implementados à mão com os targets genéricos derivados do arquivo.
No schema, esses targets são marcados com type = "custom":
[targets.beneficios]
type = "maven"
path = "apps/beneficios"
sonar = true
[targets.rh-dp]
type = "custom"
[targets.lightdash-content]
type = "custom"
extra-trigger-paths = ["rh-dp/dbtrh"]O orchestrator genérico não sabe construir esses targets e falha com uma mensagem clara em publish-all e check-quality, indicando que o projeto precisa apontar DAGGER_MODULE para o módulo local. validate apenas emite um aviso, e check-images/promote continuam funcionando pelo módulo genérico, inclusive para as imagens desses targets.
Reactor multi-módulo é o caso em que um módulo depende de módulos irmãos do mesmo pom.xml de raiz. Buildar o diretório do módulo isoladamente não funciona: as dependências irmãs não estão no disco.
Com reactor = true o módulo maven (opção ReactorMode):
-
monta a raiz do reactor em vez do diretório do módulo;
-
builda com
-pl <módulo> -am, para que o Maven construa o módulo alvo e todos os que ele precisa; -
injeta
-Drevision=<versão>em cada invocação, em vez de rodarversions:set.
O versions:set é evitado de propósito: reescrever os POMs brigaria com o flatten-maven-plugin e quebraria a resolução das dependências irmãs que o -am acabou de construir.
-
O
pom.xmlda raiz declara<version>${revision}</version>e a property<revision>com um valor de desenvolvimento local:<version>${revision}</version> <properties> <revision>0.0.1-SNAPSHOT</revision> </properties>
-
O flatten-maven-plugin configurado, para que os artefatos instalados/publicados tenham a versão resolvida e não o literal
${revision}. -
O maven-install-plugin ativo no
verify. Isso é obrigatório: o stage do Jib roda sem-am, ou seja, sem reconstruir os módulos irmãos — eles precisam já estar no repositório local, instalados pelo stage de build e teste.
Ninguém escreve esse número à mão. O valor da property <revision> no arquivo não é usado pela pipeline: cada invocação do Maven recebe -Drevision=<CalVer>, e propriedade de linha de comando vence a do POM. O que estiver commitado ali só vale para quem roda mvn na própria máquina, sem passar -Drevision — por isso o valor inicial deve ser um SNAPSHOT de desenvolvimento, e não uma CalVer.
Depois de publicar as imagens, o publish-all grava a CalVer de volta no arquivo e commita: quando o <version> do pom é ${revision}, o orchestrator-utils edita a property <revision> do pom da raiz, e não o elemento <version>. É por isso que targets reactor declaram version-file = "pom.xml" com root-version-file = true.
Ou seja, o número que você encontra no repositório é o rastro da última pipeline bem-sucedida, não uma entrada que alguém precisa manter.
-
ParentPomeFullBuildModulesforam removidos. Multi-módulo agora éReactorMode(reactor = trueno schema), que builda com-pl <módulo> -ama partir da raiz. Ver Modo reactor do Maven. -
O default de
buildImagedeixou de ser a variante-alpinee passou a sermaven:3.9.11-eclipse-temurin-21. Alpine é baseada em musl e o binário do node que o frontend-maven-plugin baixa é linkado contra glibc, de modo que builds de frontend morriam nela.
-
EntryPointInfofoi removido doBuildTarget. O subdiretório de execução de targets Python passou a ser o camporun-subdirda configuração declarativa, repassado ao módulouv. -
BuildStrategyeQualityStrategynão carregam mais campos de configuração opacos: as estratégias fecham sobre o target resolvido via closure.
O repositório usa dois esquemas de tag em paralelo, porque tem dois tipos de artefato.
| Artefato | Formato da tag | Exemplo |
|---|---|---|
Módulos Dagger ( |
Tag global do repositório, sem prefixo |
|
Bibliotecas Go ( |
Tag com prefixo do diretório, exigido pelo Go modules |
|
Na prática: DAGGER_MODULE referencia …daggerverse/orchestrator@3.0.0, enquanto os go.mod referenciam github.com/BasisTI/daggerverse/pipeline v0.9.1. As duas numerações evoluem independentemente — uma release 3.0.0 dos módulos pode conviver com a pipeline/v0.9.1.
O publish-all faz bump dos arquivos de versão e commita de volta na branch. Esse commit não pode disparar uma nova pipeline, sob pena de loop infinito.
A supressão é feita exclusivamente pelo push option, em CommitAndPush (orchestrator-utils):
git push -o ci.skip <remote> HEAD:<branch>A mensagem do commit é apenas Bump versão para <versão>, sem o prefixo [skip ci]. O prefixo foi removido de propósito: o [skip ci] na mensagem é avaliado pelo GitLab também nas pipelines de merge request, então um MR contendo o commit de bump teria sua pipeline bloqueada — inclusive o check-quality. O push option, ao contrário, afeta só o push que o produziu.
O orchestrator-utils reconhece os dois formatos ao procurar o commit de build (--grep=^\[skip ci\] e --grep=^Bump versão para `, com `--invert-grep), de modo que check-images e promote ignoram commits de bump ao localizar a build branch.
O diretório gitlabci/ é uma biblioteca Go independente (github.com/BasisTI/daggerverse/gitlabci).
O gitlabci.Client reporta o progresso de cada stage como commit status, visível na UI de MR/commit. Chama POST /api/v4/projects/:id/statuses/:sha para transicionar cada stage de running para success ou failed.
client := &gitlabci.Client{
BaseURL: "https://gitlab.example.com",
Token: "glpat-...",
ProjectID: "12345",
}
client.SetCommitStatus(sha, gitlabci.StateRunning, "my-app: Build", "")
// ... roda o stage ...
client.SetCommitStatus(sha, gitlabci.StateSuccess, "my-app: Build", "")Os nomes seguem o padrão "{projeto}: {stage}", por exemplo portal-web: Install Dependencies ou api-gateway: Docker Build and Push.
O orchestrator monta o cliente a partir de --gitlab-host, --gitlab-token e --gitlab-project-id. Quando qualquer um deles está ausente, os módulos se comportam exatamente igual, apenas sem reportar statuses.
Orquestração genérica e type-safe. Usa generics para abstrair sobre os tipos do SDK do Dagger, permitindo que orchestrators com versões diferentes do SDK compartilhem a mesma lógica.
-
BuildStrategy[Dir, Secret]/QualityStrategy[Dir, Secret]— assinaturas que qualquer módulo implementa para se plugar na orquestração. -
DaggerOps[Dir, Secret]— callbacks que isolam as operações específicas do SDK (change detection, acesso a diretórios, tagging de imagem). -
PublishAll— detecta os projetos alterados, roda suas estratégias de build em sequência, agrega as imagens publicadas e as tageia comsha-<commit>. -
CheckQuality— roda testes e análise estática nos projetos alterados, com fail-fast opcional.
Leitura e validação de ci/pipeline.toml, e a derivação anti-drift do mapa de imagens (ProjectImages, ProjectImagesJSON). Consumido tanto pelo orchestrator genérico quanto pelos módulos custom dos projetos.
BuildOptions(host, token, projectKey, waitForQualityGate, extra) monta as opções -Dsonar.* na ordem canônica. Os módulos maven, npm e uv delegam para cá; cada um mantém apenas suas bordas (leitura do *dagger.Secret e o tratamento de config nula).
Estes módulos são chamados pelo orchestrator; um projeto normalmente não os invoca diretamente.
Build e teste (clean verify), análise Sonar (sonar:sonar) e publicação da imagem via plugin Jib. Suporta Maven Wrapper, cache do repositório local, daemon Docker opcional para Testcontainers, modo reactor e reporte de commit statuses.
Fora do modo reactor, a versão é aplicada com versions:set; no modo reactor, com -Drevision.
Projetos Node.js/Angular. Roda npm ci, npm run build, análise SonarQube opcional e publica o dist/ em um container nginx. Suporta override de versão via patch do package.json e reporte de commit statuses.
Projetos Python com uv. Sync de dependências em dois estágios (uv sync), análise SonarQube opcional e publicação de uma imagem Python slim. Suporta run-subdir e customizações de build para projetos dlt e dbt.
Utilitários de Git e registry usados pelo orchestrator: change detection entre branches, lookup de commit SHA, tagging de imagem com sha-<commit>, bump e commit dos arquivos de versão, checagem de existência de imagens e promoção entre registries.
Todas as operações Git rodam dentro de um container alpine/git; as operações de imagem usam crane.