GitLab CI para Zig: Pipeline de Build, Teste e Release

GitLab CI combina bem com Zig porque os dois favorecem rotinas explícitas: um arquivo versionado descreve o pipeline, o build.zig descreve a compilação e o resultado pode ser um binário único sem runtime pesado. Para equipes brasileiras que hospedam código no GitLab, em instâncias próprias ou em mirrors corporativos, isso reduz uma dor comum: transformar um projeto Zig local em uma entrega repetível, testada e rastreável.

Este guia mostra um pipeline prático de GitLab CI para Zig com etapas de formatação, testes, build, cross-compilation, artefatos e release. A ideia não é copiar uma receita cega, mas montar uma base segura para CLIs, serviços HTTP, ferramentas internas e componentes de sistemas. Se você usa GitHub Actions, veja também Zig no GitHub Actions: release multiplataforma. Para a parte de targets, o complemento natural é o guia de cross-compilation em Zig. Para serviços, leia Zig HTTP Server em produção e systemd para serviços Zig.

O que um pipeline Zig precisa provar

Um pipeline não deve existir só para mostrar um selo verde no README. Ele precisa responder perguntas operacionais antes do código chegar em produção ou em um release público.

Para um projeto Zig, as perguntas mínimas são:

  • o código está formatado com zig fmt?
  • os testes passam com a versão Zig esperada?
  • o build funciona em modo otimizado, não apenas em debug?
  • os binários finais são gerados para os targets necessários?
  • os artefatos têm nome, versão e checksum previsíveis?
  • segredos de deploy não aparecem em logs?
  • releases marcadas por tag conseguem ser reproduzidas?

Essa lista é mais importante do que a ferramenta específica. GitLab CI é apenas o executor. A disciplina está em manter o pipeline pequeno, rápido e fiel ao build.zig.

Estrutura inicial do .gitlab-ci.yml

Comece com três stages: check, build e release. Em projetos maiores, você pode adicionar deploy, security ou package, mas a base deve continuar legível.

stages:
  - check
  - build
  - release

variables:
  ZIG_VERSION: "0.14.1"
  ZIG_LOCAL_CACHE_DIR: "$CI_PROJECT_DIR/.zig-cache"
  ZIG_GLOBAL_CACHE_DIR: "$CI_PROJECT_DIR/.cache/zig"

cache:
  key: "zig-${CI_COMMIT_REF_SLUG}"
  paths:
    - .zig-cache/
    - .cache/zig/

before_script:
  - apt-get update -y
  - apt-get install -y curl xz-utils ca-certificates
  - curl -fsSLo zig.tar.xz "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz"
  - tar -xf zig.tar.xz
  - export PATH="$CI_PROJECT_DIR/zig-linux-x86_64-${ZIG_VERSION}:$PATH"
  - zig version

Para projetos simples, baixar Zig no before_script é suficiente. Em pipelines muito usados, vale criar uma imagem Docker interna com Zig pré-instalado para economizar tempo. Não faça isso no primeiro dia se ainda não existe gargalo; otimize depois de medir.

A variável ZIG_GLOBAL_CACHE_DIR evita depender do diretório global do usuário dentro do runner. Isso torna o cache mais previsível e facilita debugar runners efêmeros.

Formatação e testes

A primeira etapa deve falhar rápido. Rode zig fmt --check antes dos testes para evitar gastar minutos compilando código que já está fora do padrão do projeto.

format:
  stage: check
  script:
    - zig fmt --check build.zig src tests

unit_tests:
  stage: check
  script:
    - zig build test --summary all

Se o projeto ainda não tem diretório tests, ajuste a lista do zig fmt. O importante é versionar a decisão. Um erro comum é rodar apenas zig build, deixando testes fora do caminho principal. Em Zig, testes costumam viver perto do código e são baratos o suficiente para rodar em todo commit.

Para bibliotecas, adicione testes por target quando fizer sentido. Para ferramentas internas, pelo menos rode os testes no target nativo do runner e faça build cross-platform separado.

Build otimizado para merge requests

O modo debug é ótimo para desenvolvimento, mas releases e serviços precisam provar que compilam em modo otimizado. Um job de build para merge requests pode gerar só o target principal.

build_linux:
  stage: build
  script:
    - zig build -Doptimize=ReleaseSafe
  artifacts:
    when: always
    expire_in: 7 days
    paths:
      - zig-out/bin/

Use ReleaseSafe como padrão inicial. Ele preserva checks de segurança importantes e costuma ser adequado para serviços, CLIs internas e primeiras versões públicas. ReleaseFast pode entrar depois, com benchmark e cobertura de testes suficientes. Para medir performance de forma séria, use o guia de benchmarking em Zig e não apenas sensação de velocidade.

Matrix de cross-compilation

GitLab CI tem parallel:matrix, que deixa a matriz de targets explícita. Isso é útil quando você distribui uma CLI ou agente para Linux, macOS e Windows.

build_matrix:
  stage: build
  parallel:
    matrix:
      - ZIG_TARGET: "x86_64-linux-gnu"
        ARTIFACT_SUFFIX: "linux-x86_64"
      - ZIG_TARGET: "aarch64-linux-gnu"
        ARTIFACT_SUFFIX: "linux-aarch64"
      - ZIG_TARGET: "x86_64-macos"
        ARTIFACT_SUFFIX: "macos-x86_64"
      - ZIG_TARGET: "aarch64-macos"
        ARTIFACT_SUFFIX: "macos-aarch64"
      - ZIG_TARGET: "x86_64-windows-gnu"
        ARTIFACT_SUFFIX: "windows-x86_64"
  script:
    - zig build -Doptimize=ReleaseSafe -Dtarget=$ZIG_TARGET
    - mkdir -p dist
    - cp zig-out/bin/* "dist/app-${ARTIFACT_SUFFIX}" || cp zig-out/bin/*.exe "dist/app-${ARTIFACT_SUFFIX}.exe"
  artifacts:
    expire_in: 14 days
    paths:
      - dist/

A linha de cp é intencionalmente simples, mas projetos reais devem nomear o binário de forma determinística no build.zig. Se o nome do executável muda conforme feature flag ou pacote, normalize isso antes de publicar release. Artefato com nome imprevisível vira retrabalho em deploy, documentação e suporte.

Checksums e release por tag

Para releases, restrinja o job a tags. Isso impede que cada branch gere um release acidental.

release_artifacts:
  stage: release
  needs:
    - job: build_matrix
      artifacts: true
  rules:
    - if: '$CI_COMMIT_TAG'
  script:
    - cd dist
    - sha256sum * > SHA256SUMS.txt
    - cd ..
  artifacts:
    expire_in: never
    paths:
      - dist/

Se você usa GitLab Releases, pode adicionar release-cli para anexar links. Mas mesmo sem essa camada, publicar dist/ com checksums já resolve a parte mais importante: qualquer pessoa consegue baixar o binário e conferir integridade.

Não pule checksums. Em Zig, binários únicos são fáceis de distribuir; justamente por isso, também são fáceis de copiar para fora do pipeline. Um SHA256SUMS.txt reduz ambiguidade quando alguém pergunta se o binário em produção corresponde à tag.

Dependências e build.zig.zon

Projetos Zig modernos podem usar build.zig.zon para dependências. O pipeline deve tratar esse arquivo como parte do contrato de build. Se ele muda, o cache pode reaproveitar downloads, mas o job ainda precisa compilar tudo a partir do repositório.

Uma prática segura é manter o cache por branch e deixar o próprio Zig validar hashes. Não salve dependências vendorizadas em locais obscuros do runner. Se uma dependência é crítica para builds offline, documente e versione a estratégia em vez de depender de cache acidental.

Também vale adicionar um job curto que verifica se build.zig.zon está coerente quando o projeto já usa pacotes:

build_dependency_check:
  stage: check
  script:
    - zig build --fetch
    - zig build test --summary all

Se o projeto não usa dependências externas, não invente complexidade. Um dos pontos fortes de Zig é justamente manter muitas ferramentas internas sem árvore grande de pacotes.

Segredos de deploy no GitLab

Deploy exige mais cuidado do que build. Tokens, chaves SSH e URLs privadas devem ficar em CI/CD Variables protegidas e mascaradas. Nunca escreva segredo em echo, nunca grave .env como artifact e nunca use set -x em job que manipula credenciais.

Um deploy via SSH pode seguir este formato:

deploy_production:
  stage: release
  rules:
    - if: '$CI_COMMIT_TAG'
  before_script:
    - apt-get update -y && apt-get install -y openssh-client
    - eval $(ssh-agent -s)
    - echo "$DEPLOY_SSH_KEY" | tr -d '
' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan "$DEPLOY_HOST" >> ~/.ssh/known_hosts
  script:
    - scp dist/app-linux-x86_64 "$DEPLOY_USER@$DEPLOY_HOST:/opt/minha-app/releases/$CI_COMMIT_TAG/app"
    - ssh "$DEPLOY_USER@$DEPLOY_HOST" "sudo systemctl restart minha-app"

Esse exemplo é deliberadamente conservador: deploy só por tag, chave protegida, host key registrada e comando remoto curto. Para serviços Linux, combine isso com systemd para serviços Zig para ter restart, logs e rollback mais previsíveis.

Caches: úteis, mas não obrigatórios

Cache em CI é uma otimização, não uma dependência. O pipeline deve continuar funcionando quando o cache está vazio. Em Zig, os caches principais são .zig-cache e o cache global configurado por ZIG_GLOBAL_CACHE_DIR. Salvar ambos acelera rebuilds, mas não substitui testes.

Evite cache global compartilhado entre projetos sem necessidade. Ele pode economizar minutos, mas também dificulta explicar por que um runner específico compila e outro não. Prefira cache por projeto e branch; use imagem base customizada quando a instalação do Zig virar o gargalo dominante.

Regras para branches, merge requests e tags

Um pipeline útil diferencia três momentos:

EventoO que rodarPor quê
Push em branchformatação, testes, build principalfeedback rápido
Merge requestchecks completos e build otimizadoproteger branch principal
Tagmatriz de targets, checksums e releaseproduzir artefatos finais

Com rules, isso fica explícito:

rules:
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
  - if: '$CI_COMMIT_TAG'

Não rode deploy em todo push para main se o processo de release da equipe é por tag. Misturar esses modelos cria surpresa: às vezes uma mudança está em produção sem versão clara, às vezes uma tag existe mas não corresponde ao binário rodando.

Validações extras que valem o custo

Depois que o básico estiver verde, adicione validações que capturam bugs reais:

  • zig fmt --check para build.zig, src/ e tests/;
  • zig build test -Doptimize=ReleaseSafe se testes dependem de comportamento otimizado;
  • build para x86_64-linux-musl quando você distribui binário estático;
  • smoke test executando zig-out/bin/app --version;
  • verificação de sha256sum no job de release;
  • teste de container se o serviço roda em Docker;
  • parsing de arquivo de configuração de exemplo;
  • chamada de health check local para servidor HTTP.

Um smoke test simples pega muitos erros bobos:

smoke:
  stage: build
  script:
    - zig build -Doptimize=ReleaseSafe
    - ./zig-out/bin/app --version
    - ./zig-out/bin/app --help

Se --help precisa de banco, rede ou segredo, o problema está no design da CLI. Ajuda e versão devem funcionar sem ambiente externo.

Pipeline completo de referência

Juntando as peças, um .gitlab-ci.yml enxuto fica assim:

stages: [check, build, release]

variables:
  ZIG_VERSION: "0.14.1"
  ZIG_LOCAL_CACHE_DIR: "$CI_PROJECT_DIR/.zig-cache"
  ZIG_GLOBAL_CACHE_DIR: "$CI_PROJECT_DIR/.cache/zig"

cache:
  key: "zig-${CI_COMMIT_REF_SLUG}"
  paths: [.zig-cache/, .cache/zig/]

before_script:
  - apt-get update -y
  - apt-get install -y curl xz-utils ca-certificates
  - curl -fsSLo zig.tar.xz "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz"
  - tar -xf zig.tar.xz
  - export PATH="$CI_PROJECT_DIR/zig-linux-x86_64-${ZIG_VERSION}:$PATH"
  - zig version

format:
  stage: check
  script: ["zig fmt --check build.zig src tests"]

unit_tests:
  stage: check
  script: ["zig build test --summary all"]

build_linux:
  stage: build
  script:
    - zig build -Doptimize=ReleaseSafe
    - ./zig-out/bin/app --version || true
  artifacts:
    expire_in: 7 days
    paths: [zig-out/bin/]

build_matrix:
  stage: build
  rules:
    - if: '$CI_COMMIT_TAG'
  parallel:
    matrix:
      - ZIG_TARGET: "x86_64-linux-gnu"
        ARTIFACT_SUFFIX: "linux-x86_64"
      - ZIG_TARGET: "aarch64-linux-gnu"
        ARTIFACT_SUFFIX: "linux-aarch64"
      - ZIG_TARGET: "x86_64-macos"
        ARTIFACT_SUFFIX: "macos-x86_64"
      - ZIG_TARGET: "x86_64-windows-gnu"
        ARTIFACT_SUFFIX: "windows-x86_64.exe"
  script:
    - zig build -Doptimize=ReleaseSafe -Dtarget=$ZIG_TARGET
    - mkdir -p dist
    - cp zig-out/bin/* "dist/app-${ARTIFACT_SUFFIX}"
  artifacts:
    expire_in: 30 days
    paths: [dist/]

checksums:
  stage: release
  rules:
    - if: '$CI_COMMIT_TAG'
  needs:
    - job: build_matrix
      artifacts: true
  script:
    - cd dist
    - sha256sum * > SHA256SUMS.txt
  artifacts:
    expire_in: never
    paths: [dist/]

Antes de colar em produção, ajuste três coisas: nome real do binário, versão Zig usada pelo projeto e lista de targets que você realmente suporta. Uma matriz gigante impressiona no começo, mas cada target vira compromisso de suporte.

Armadilhas comuns

A primeira armadilha é deixar o pipeline baixar master de Zig sem versão fixa. Isso torna builds irreproduzíveis. Use uma versão explícita e atualize de propósito.

A segunda é tratar ReleaseFast como padrão universal. Ele pode ser correto para uma CLI madura, mas ReleaseSafe é uma base melhor enquanto o projeto ainda muda muito.

A terceira é publicar artefatos sem testar que executam. Compilar não prova que o binário inicia. Pelo menos rode --version ou um comando de help.

A quarta é confundir cache com vendorização. Cache pode sumir; o build precisa sobreviver.

A quinta é transformar GitLab CI em linguagem de programação paralela ao build.zig. O pipeline deve orquestrar; o conhecimento de build deve morar no projeto. Se uma flag é essencial, exponha no build.zig com nome claro.

Próximos passos

Depois do pipeline básico, escolha a melhoria que reduz risco real. Para uma CLI pública, invista em releases assinados e checksums. Para um serviço HTTP, adicione teste de health check e deploy com rollback. Para biblioteca, rode testes em mais targets. Para ferramenta interna, mantenha o pipeline curto e documente como reproduzir localmente.

Zig facilita CI/CD porque o artefato final é simples: código fonte, build.zig, versão do compilador e binário. GitLab CI fecha o ciclo quando torna essa simplicidade repetível. O resultado ideal é um fluxo em que qualquer merge request prova formatação e testes, qualquer tag gera artefatos verificáveis e nenhum deploy depende de memória humana.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.