CI no GitLab: do card à imagem em produção
A pipeline não está no .gitlab-ci.yml. Esse arquivo tem de 3 a 8 linhas e só inclui um
template versionado, compartilhado por todos os projetos. A pipeline é o ci/pipeline.toml
— o que este projeto tem de próprio — mais um programa Dagger em Go que também roda na sua
máquina. Quem abre o .gitlab-ci.yml procurando o build acha um include e conclui que o
projeto "não tem CI".
O TOML é desse jeito por um motivo que vale entender antes de mexer nele: o mapa de
imagens que o promote consome é derivado dos mesmos targets do build, não declarado à
parte. Build e promoção não podem divergir porque a divergência não é representável.
Dois pares não-negociáveis, e errar qualquer um dos dois é caro:
developé staging,mainé produção.- Squash na MR feature→develop. Nunca na develop→main.
Quando usar
Alguém diz: "como nomeio a branch", "posso marcar squash?", "minha pipeline falhou", "o Sonar não comentou na MR", "o promote subiu versão velha", "a pipeline aparece skipped e deixou mergear", "quero rodar a pipeline na minha máquina", "subir isso pra produção". Ou vai integrar um projeto novo na CI e pergunta o que precisa existir.
Esta skill não cobre manifesto, kustomize, ArgoCD, Image Updater ou operadores — isso é
basis-k8s-deploy. A fronteira é a imagem no registry com a tag de produção: até lá, aqui;
dali em diante, lá.
Os três hábitos que resolvem
1. Abra o ci/pipeline.toml antes do .gitlab-ci.yml. O segundo é um include; o
primeiro é o projeto. A pergunta "por que esse job não rodou" quase sempre se responde nos
dois juntos, nessa ordem.
2. Rode a função localmente antes de empurrar commit para ver pipeline. É a vantagem de
a pipeline ser um programa. Empurrar commit para descobrir se o TOML está válido é o ciclo
mais caro que existe aqui — e validate responde em segundos.
3. Verde não é prova. Pergunte o que a checagem avaliou. Um quality gate PASSED pode
não ter avaliado nada, e um job failed pode ter feito o trabalho todo. As duas coisas
acontecem neste ambiente e estão documentadas abaixo.
0. O mapa de identidade do projeto
Seis nomes que quase nunca são iguais entre si, e que o agente precisa para agir sem adivinhar:
| Chave | Exemplo | Onde é usada |
|---|---|---|
| Projeto no GitLab | basis/ponto |
glab, include:, MR |
| Id numérico no GitLab | 5371 |
chamadas de API, binding do Sonar |
| Projeto no Taiga | sistema-de-ponto (id 35) |
branch TG-xxx, rastreabilidade |
| Grupo no registry | ponto |
[project] group do TOML |
| Projeto no Sonar | ponto |
chave permanente da análise |
| Pasta no IaC | manifests/ponto/ |
handoff para basis-k8s-deploy |
Ordem de resolução: o que veio no prompt → o AGENTS.md do repositório → perguntar.
Nunca deduzir de um find, de um git remote ou da semelhança entre nomes. O grupo do
registry e a chave do Sonar coincidirem no exemplo acima é acidente, não regra — no
colaboradados a chave do Sonar é colaboradados-beneficios e o target se chama
beneficios.
Bloco para colar no AGENTS.md do repositório:
## Identidade do projeto
| Onde | Valor |
|------------|--------------------------|
| GitLab | basis/<repo> (id <NNNN>) |
| Taiga | <slug> (id <NN>) |
| Registry | <group> |
| Sonar | <chave> |
| ArgoCD app | <app> |
| IaC | manifests/<app>/ |
Onde já existe CLAUDE.md, ele vira uma linha de ponteiro (Ver @AGENTS.md) — o conteúdo
mora num arquivo só, e o formato aberto é lido também por Cursor, Codex e OpenCode.
$REPO_IAC é opcional e só interessa a quem for cruzar para o repositório de IaC — o
momento de precisar dele já é o handoff para basis-k8s-deploy.
1. O caminho de uma mudança
card no Taiga → branch TG-xxx (de develop) → commits → MR para develop
→ check-quality → merge (squash) → publish-develop → staging
→ MR develop→main (sem squash) → promote → produção
Branch: criada a partir de develop, nomeada TG-xxx onde xxx é o número da user
story no Taiga. Não é estética: existe integração GitLab↔Taiga, e é o nome que costura o
código ao card.
Commit: verbo no infinitivo, e - TG-xxx no fim.
Corrigir configuração de volume no docker-compose - TG-58
O teste é completar a frase "Aplicar esse commit vai…". Se não completar, a mensagem está descrevendo o que você fez, não o que o commit faz — e é a segunda que serve a quem lê o log depois.
MR de feature para develop: marque Delete Branch e Squash commits.
MR de develop para main: não marque Squash. Esmagar aqui destruiria o histórico
de várias features numa entrada só, e é justamente esse histórico que o promote e a
auditoria de produção consultam.
2. O Taiga pelo MCP
O servidor MCP taiga expõe as ferramentas mcp__taiga__*. Serve para o agente ler a
story antes de nomear a branch e escrever o commit, em vez de pedir o número a quem já
está com o card aberto.
| Para | Ferramenta |
|---|---|
| Descobrir o id do projeto | taiga_projects_list (aceita search) |
| Ler a story que vai virar branch | taiga_stories_get |
| Listar o que está em aberto | taiga_stories_list, taiga_tasks_list |
| Criar ou atualizar | taiga_stories_create, taiga_stories_update |
| Fechar | taiga_stories_archive_or_close |
| Conferir a conexão | taiga_diagnostics |
As três ferramentas de delete estão em permissions.deny. Os boards são compartilhados
e estão em produção; apagar story ou task alheia é irreversível e invisível para quem
depende dela. A saída correta para "tirar do board" é archive_or_close, que é reversível —
foi deixada disponível de propósito.
Resolva o id pelo taiga_projects_list, não por lista mantida à mão. Índices escritos à
mão envelhecem em silêncio: o que existe hoje na vault omite dois projetos ativos. Se a
skill não estiver configurada com o servidor, pergunte — não tente adivinhar o id a partir
do nome do repositório, porque eles divergem (triagem.ai no GitLab é triagemai no
Taiga; contavinculada é conta-vinculada).
Detalhes de cada ferramenta em references/taiga-mcp.md.
3. ci/pipeline.toml é a fonte de verdade
Um arquivo por repositório, declarativo. Dele saem, ao mesmo tempo, o que buildar, o que
analisar no Sonar e o mapa de imagens de check-images/promote.
schema-version = 1
[project]
group = "ponto" # vira <registry>/ponto/<image>
[defaults.maven]
image = "maven:3.9.11-eclipse-temurin-25"
[targets.ponto]
type = "maven" # obrigatório
path = "."
sonar = true
O parse é estrito. Chave desconhecida é erro, não aviso — um sonar-key no lugar de
sonar-project-key derruba a pipeline com mensagem de parse, e isso é deliberado: falhar
alto é melhor que ignorar em silêncio uma configuração que alguém acha que está valendo.
A regra que explica o desenho: quando "o que dispara o build" e "o que resolve a versão
publicada" usam listas de caminhos diferentes, há drift — e o drift é silencioso. No
kaizenstat, antes da versão declarativa, a lista de imagens era mantida à mão em paralelo
ao mapa de builds, e as duas divergiram: dois serviços estavam na lista de promoção sem
ter target de build. Hoje a lista é derivada dos targets, e essa divergência deixou de ser
representável.
O mesmo princípio tem uma segunda face, que já custou uma promoção errada: um target com
extra-trigger-paths é reconstruído quando qualquer um dos caminhos muda, então a versão
publicada precisa ser resolvida pelo commit mais recente entre todos eles — resolver só
pelo caminho primário promove uma imagem velha, com a pipeline toda verde.
Validar antes de empurrar:
dagger call -m github.com/BasisTI/daggerverse/orchestrator@<versão> \
--source . --config-path ci/pipeline.toml validate
Schema completo — cada chave, default, e as validações cruzadas — em
references/pipeline-toml-schema.md.
4. O .gitlab-ci.yml é um include
include:
- project: 'basis/iac/ci-templates'
ref: v1.11.1
file: 'templates/dagger-orchestrator.gitlab-ci.yml'
É isso. Projetos com módulo Dagger próprio acrescentam variables: {DAGGER_MODULE: "."}, e
é a única exceção em uso.
Qual job roda quando — a tabela que responde "por que esse job não rodou":
| Job | Stage | Dispara em |
|---|---|---|
validate-pipeline-config |
check | MR que toca ci/pipeline.toml |
check-quality |
check | MR com destino develop |
publish-develop |
build | push em develop |
check-images-ready |
pre-prod | MR develop→main e push em main |
promote-production |
promote | push em main |
Repare que check-quality só roda com destino develop. Uma MR de feature aberta
direto para main não passa por análise nenhuma.
Pin o ref, e confira qual está em uso. Os projetos hoje divergem — há repositórios em
v1.11.0 e em v1.11.1. Herdar de main faria uma mudança no template quebrar todos os
projetos ao mesmo tempo.
Todo mundo que abre MR precisa de leitura em basis/iac/ci-templates. O GitLab resolve
include: project: com a permissão do usuário que disparou a pipeline — não do runner,
não de um token de serviço. Sem acesso, a pipeline nem começa:
Project `basis/iac/ci-templates` not found or access denied!
A mensagem é deliberadamente ambígua (não revela se o projeto existe), então parece erro de digitação no caminho. É permissão.
Variáveis que o projeto precisa ter em Settings → CI/CD: EXTERNAL_REGISTRY_URL,
EXTERNAL_REGISTRY_USER, EXTERNAL_REGISTRY_PASSWORD, SONAR_HOST, SONAR_TOKEN,
GITLAB_STATUS_TOKEN. O runner precisa da tag dagger.
Template anotado em references/template-gitlab-ci.md.
5. Rodar a pipeline na sua máquina
A pipeline é um programa, e é por isso que se escolheu Dagger. As seis funções do orchestrator:
dagger call -m github.com/BasisTI/daggerverse/orchestrator@<versão> \
--source . --config-path ci/pipeline.toml <função>
validate · sonar-project-keys · check-quality · publish-all · check-images ·
promote
Na prática, validate é a que se usa toda hora e a que mais economiza ciclo.
E o limite, que precisa vir na mesma seção para a promessa não ficar maior que a entrega: o engine do Dagger não usa o resolvedor DNS da VPN do host. Um hostname interno resolve, dentro do contêiner, para outra máquina na borda da rede. O sintoma é erro de certificado:
Failed to query server version: ... (certificate_unknown) The certificate chain is not trusted
e a causa é DNS. Não troque certificado nem CA — verifique para qual IP o nome resolve
dentro do contêiner versus no host. Consequência prática: o que depende de serviço
interno (a análise do Sonar) só roda no runner. validate e o build em si rodam local.
Mais em references/dagger-local.md.
6. Qualidade: por que a análise não rodou, não decorou, ou não avaliou nada
O projeto precisa existir no Sonar antes da primeira MR. Como check-quality só roda em
evento de merge request, a primeira análise de qualquer projeto é sempre uma análise de PR —
e o plugin de branch valida a branch base antes de o servidor auto-provisionar o
projeto. A análise morre e o projeto sequer é criado:
[ERROR] No branch exists in Sonarqube with the name main
Conceder permissão de provisionamento ao token não resolve, porque a validação acontece
antes. O script canônico que semeia projeto e binding é
basis/iac/ci-templates/scripts/seed-sonar-projects.sh — rode antes de abrir a MR.
Sem os três parâmetros de pull request, o Sonar não comenta na MR. A decoração é função
exclusiva de análise de PR. Sem sonar.pullrequest.key/branch/base, a análise entra
como análise de branch, mesmo estando dentro de uma pipeline de merge request — o
resultado aparece no servidor e nunca na MR. O template já passa os três; um módulo Dagger
local precisa passar também.
Quality gate PASSED na primeira análise não prova nada. Sem análise anterior não
existe período de new code, a API devolve period: None, nenhuma condição é avaliada e o
gate passa por vacuidade. Um projeto recém-integrado com 0% de cobertura fica verde. Antes
de confiar no primeiro verde, olhe se havia análise anterior.
Bootstrap, tipos de token, cobertura e reactor em
references/sonar-analise-e-quality-gate.md.
7. Promover para produção
MR de develop para main, sem squash. O merge dispara promote-production.
As tags, em ordem: sha-<commit> durante o build, a CalVer YYYY.MM.DD.<pipeline_iid> na
publicação, e production-<calver> na promoção. A CalVer é montada em bash no
before_script, não dentro do Dagger:
export APP_VERSION="$(date +%Y.%m.%d).$CI_PIPELINE_IID"
É CI_PIPELINE_IID — o contador por projeto —, não CI_PIPELINE_ID, que é global. A
distinção importa para quem raciocina sobre ordenação ou unicidade de tag.
Por que a pipeline da MR develop→main costuma aparecer skipped. Depois de publicar, o
orchestrator commita o bump de versão e empurra com git push -o ci.skip, para não disparar
uma pipeline duplicada. Como develop é a origem da MR develop→main já aberta, esse
ci.skip suprime também a pipeline daquela MR: o head fica sem job nenhum, aparece como
skipped, e não bloqueia o merge. Não é falha.
É exatamente por isso que check-images-ready roda duas vezes — na MR (onde
frequentemente é suprimida) e obrigatoriamente no push para main. A segunda é a que de
fato garante que as imagens existem antes do promote.
Daqui em diante é basis-k8s-deploy. A imagem está no registry com a tag de produção; o
Image Updater a detecta e escreve no kustomization.yaml do overlay.
8. As CLIs e o que cada uma alcança
| CLI | Login | Para |
|---|---|---|
glab |
já configurado | MR, pipeline, job, trace, variáveis |
argocd |
argocd login argocd.basis.com.br --sso (abre browser, Keycloak) |
estado e sync de Application |
sonar |
sonar auth login |
analisar arquivo, consultar API |
kubectl |
contexto de produção | log do Image Updater |
glab ci run não serve para rodar a pipeline de uma MR. Ele cria pipeline de branch, e
ela nasce vazia — os jobs de check exigem merge_request_event. É a armadilha em que se cai
com quase certeza, porque é o comando óbvio. A forma que funciona:
glab api -X POST "projects/:id/merge_requests/<iid>/pipelines"
Log do Image Updater, quando a imagem foi promovida e a versão não subiu:
kubectl -n argocd logs deploy/argocd-image-updater-controller \
-l app.kubernetes.io/name=argocd-image-updater --tail=100
Receitas verificadas em references/glab-argocd-cli.md.
Assinatura — sintoma e causa
| Observação | Causa |
|---|---|
Project ... not found or access denied no include |
Quem abriu a MR não tem leitura em basis/iac/ci-templates |
| Erro de parse citando uma chave do TOML | Parse estrito — chave desconhecida, provavelmente nome errado |
| Job esperado não aparece na pipeline | rules do job — quase sempre destino da MR ou branch errada |
Pipeline da MR develop→main skipped, merge liberado |
git push -o ci.skip do bump de versão suprimiu |
Quality gate PASSED num projeto recém-integrado |
Sem análise anterior não há new code — nada foi avaliado |
| Análise roda e o Sonar não comenta na MR | Faltam os três parâmetros de PR — virou análise de branch |
| Cobertura 0% com os testes passando | ${jacocoArgLine} no argLine do surefire; precisa ser @{jacocoArgLine} |
No plugin found for prefix 'sonar' |
Invocação por prefixo em projeto que não declara o plugin |
Maven session does not declare a top level project |
Reactor com -pl; o Sonar precisa de -f <módulo>/pom.xml |
promote subiu versão velha, pipeline verde |
Versão resolvida só pelo caminho primário, ignorando extra-trigger-paths |
Job failed no promote com a imagem publicada |
Timeout de shutdown do engine depois do trabalho pronto |
Erro de certificado ao chamar serviço interno do dagger call local |
DNS: o engine não usa o resolvedor da VPN |
Target dockerfile não encontra o arquivo |
O nome é Dockerfile, case-sensitive |
O que engana
Error: cleanup failed / context deadline exceeded no fim do job. Vem do shutdown da
sessão do Dagger, e pode aparecer depois de o trabalho ter terminado com sucesso. Leia o
log até o fim antes de concluir que a lógica falhou — reexecutar um promote que já
funcionou é seguro (ele é idempotente), mas reexecutar por diagnóstico errado gasta tempo.
crane digest ... 404 MANIFEST_UNKNOWN em vermelho. É o caminho feliz: a checagem "essa
tag já existe?" que o promote faz antes de copiar. 404 significa "ainda não promovida".
Um .gitlab-ci.yml de quatro linhas. Parece projeto sem CI. É o padrão.
O primeiro quality gate verde. Ver §6.
Uma pipeline skipped na MR de produção. Ver §7.
O abaco como modelo de anotação do Image Updater. Ele usa producao-[0-9.]+, em
português e frouxo; é anterior à convenção atual e o único assim. A convenção viva é
production-\d{4}\.\d{2}\.\d{2}[-.]\d+. Copie de um projeto recente.
Checklist
Antes de abrir a MR para develop:
- Branch nomeada
TG-xxxcom o número da story - Mensagens de commit passam no teste "Aplicar esse commit vai…" e terminam em
- TG-xxx -
validatedo orchestrator rodou local e passou - Se o projeto é novo no Sonar, foi semeado antes
- Delete Branch e Squash commits marcados
Antes de promover para main:
- Squash desmarcado
- As imagens dos targets alterados existem no registry com a CalVer esperada
- Se a pipeline da MR está
skipped, é oci.skipdo bump — confira o push emmain
Scripts
| Script | Muta? | Uso |
|---|---|---|
scripts/estado-pipeline.sh |
Não | Reconcilia projeto, ref do template, presença das variáveis, pipeline da branch e da MR, e o trace do primeiro job que falhou |
References
references/pipeline-toml-schema.md— o schema completo e estrito, chave a chave, com os defaults por tipo e as validações cruzadas.references/template-gitlab-ci.md— o template compartilhado anotado: stages,rules, tags, variáveis e a mecânica doinclude:.references/sonar-analise-e-quality-gate.md— bootstrap, tipos de token, análise de PR, cobertura e reactor.references/dagger-local.md— rodar o orchestrator na máquina, o que dá e o que não dá para reproduzir.references/glab-argocd-cli.md— receitas verificadas deglab,argocd,sonarekubectl.references/taiga-mcp.md— as ferramentasmcp__taiga__*, o que devolvem, e as que estão bloqueadas.