Um dos motivos para usar Zig em ferramentas de infraestrutura é simples: o mesmo projeto consegue gerar binários nativos para Linux, macOS, Windows, BSD e WebAssembly sem montar uma coleção frágil de toolchains externos. Essa vantagem fica ainda mais forte quando o release é automatizado no GitHub Actions.
Em vez de compilar manualmente na sua máquina e enviar um .zip por intuição, um pipeline bem desenhado roda testes, valida formatação, compila uma matriz de targets, empacota artefatos com nomes previsíveis e publica tudo em uma release. Para CLIs, servidores pequenos, agentes, parsers, ferramentas de build e utilitários internos, esse é o caminho entre “funciona aqui” e “qualquer pessoa baixa o binário certo”.
Este guia mostra um fluxo prático para projetos Zig. Ele complementa o tutorial de cross-compilation em Zig, o guia de containers Docker otimizados e o material de debugging e profiling. A diferença aqui é o foco operacional: como transformar o build em uma rotina repetível de CI/CD.
O que um bom pipeline Zig precisa fazer
Um pipeline mínimo que só executa zig build já ajuda, mas ainda deixa várias lacunas. Para release de verdade, pense em cinco etapas:
- Preparar versão fixa do Zig. O runner precisa usar a mesma versão que você usa localmente.
- Rodar verificações rápidas.
zig fmt --check,zig build teste, quando existir, testes de integração. - Compilar em matriz de targets. Linux x86_64, Linux ARM64, macOS ARM64, macOS x86_64 e Windows x86_64 costumam cobrir a maior parte dos usuários.
- Empacotar artefatos. Nomeie arquivos com projeto, versão, sistema e arquitetura.
- Publicar release só em tag. Pull requests validam; tags publicam.
Essa separação evita um erro comum: tratar CI e release como a mesma coisa. Todo commit deve testar. Nem todo commit deve gerar um pacote público.
Estrutura esperada do projeto
Vamos assumir um projeto padrão:
meu-cli/
build.zig
build.zig.zon
src/
main.zig
O build.zig deve expor as opções padrão de target e optimize. Se você usou zig init, provavelmente já tem algo parecido:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "meu-cli",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const test_step = b.step("test", "Roda testes");
test_step.dependOn(&b.addRunArtifact(run_tests).step);
}
O ponto importante é que o pipeline consiga chamar zig build -Dtarget=... -Doptimize=ReleaseSafe sem editar código. Isso é o que torna a matriz simples.
Workflow de CI para pull requests
Crie .github/workflows/ci.yml:
name: CI
on:
pull_request:
push:
branches: [main, master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.14.1
- name: Check formatting
run: zig fmt --check src build.zig
- name: Run tests
run: zig build test
- name: Build debug
run: zig build
O zig fmt --check merece ficar no início. Formatação quebrada é barata de corrigir e não vale gastar minutos de build antes de descobrir. Se seu projeto tem exemplos, adicione examples ao comando. Se tem mais diretórios Zig fora de src, inclua também.
Sobre a versão: fixe uma versão explícita. Usar master ou latest parece conveniente, mas transforma o pipeline em loteria. Zig ainda está antes da versão 1.0; mudanças na linguagem e na standard library são normais. Para projetos com vários contribuidores, registre a versão também no README e considere usar zigup localmente para alinhar ambiente.
Release com matriz de targets
Agora crie .github/workflows/release.yml:
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-linux-musl
os: ubuntu-latest
artifact: meu-cli-linux-x86_64
- target: aarch64-linux-musl
os: ubuntu-latest
artifact: meu-cli-linux-aarch64
- target: x86_64-macos
os: macos-latest
artifact: meu-cli-macos-x86_64
- target: aarch64-macos
os: macos-latest
artifact: meu-cli-macos-aarch64
- target: x86_64-windows-gnu
os: ubuntu-latest
artifact: meu-cli-windows-x86_64.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.14.1
- name: Build release
run: zig build -Dtarget=${{ matrix.target }} -Doptimize=ReleaseSafe
- name: Prepare artifact
shell: bash
run: |
mkdir -p dist
if [[ "${{ matrix.artifact }}" == *.exe ]]; then
cp zig-out/bin/meu-cli.exe "dist/${{ matrix.artifact }}"
else
cp zig-out/bin/meu-cli "dist/${{ matrix.artifact }}"
chmod +x "dist/${{ matrix.artifact }}"
fi
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: dist/${{ matrix.artifact }}
Esse job ainda não publica a release. Ele apenas compila e guarda artefatos. A etapa de publicação fica separada para não misturar permissões de escrita com cada build da matriz.
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Checksums
run: |
cd dist
sha256sum * > SHA256SUMS.txt
- uses: softprops/action-gh-release@v2
with:
files: |
dist/*
O arquivo SHA256SUMS.txt é pequeno, mas transmite seriedade. Quem usa sua ferramenta em automação pode verificar integridade antes de executar o binário baixado.
ReleaseSafe, ReleaseFast ou ReleaseSmall?
Para a maioria dos projetos publicados, comece com ReleaseSafe. Ele otimiza bastante, mas mantém verificações de segurança importantes. ReleaseFast remove checks para extrair performance máxima; use quando você mediu, entendeu o risco e tem testes bons. ReleaseSmall é excelente para utilitários embarcados, WebAssembly e imagens mínimas, mas pode sacrificar velocidade.
Uma política simples:
| Cenário | Modo recomendado |
|---|---|
| CLI pública | ReleaseSafe |
| API interna crítica | ReleaseSafe |
| Parser medido em benchmark | ReleaseFast, se os testes cobrirem bem |
| Ferramenta embarcada | ReleaseSmall |
| Debug de incidente | Debug ou ReleaseSafe com símbolos |
Se a sua ferramenta processa entrada não confiável, prefira segurança. Um binário 5% mais rápido não compensa comportamento indefinido em dados vindos de usuários.
Cache: útil, mas não mágico
Zig usa cache local para acelerar builds. No GitHub Actions, você pode guardar .zig-cache e o cache global, mas faça isso com cuidado. Cache errado gera bugs difíceis de entender.
- uses: actions/cache@v4
with:
path: |
.zig-cache
~/.cache/zig
key: zig-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('build.zig', 'build.zig.zon') }}
restore-keys: |
zig-${{ runner.os }}-${{ matrix.target }}-
Inclua matrix.target na chave. Um cache de Linux musl não deve ser reutilizado como se fosse Windows. Inclua build.zig.zon porque dependências mudam o resultado. Para projetos pequenos, cache pode nem valer a complexidade; para projetos com dependências C, LLVM pesado ou geração de código, costuma pagar rapidamente.
Versões, tags e changelog
Use tags semânticas como v0.3.0. O fluxo fica previsível:
git tag v0.3.0
git push origin v0.3.0
Antes da tag, rode localmente:
zig fmt --check src build.zig
zig build test
zig build -Doptimize=ReleaseSafe
Se o projeto usa dependências no build.zig.zon, não edite hashes manualmente sem entender o que está fazendo. Use zig fetch --save para registrar a dependência e deixar o manifesto refletir a origem real. Isso conversa diretamente com a filosofia de reprodutibilidade do Zig.
Containers e binários: quando usar cada um
Para ferramentas de linha de comando, publicar binários diretos costuma ser melhor que publicar apenas uma imagem Docker. O usuário baixa, coloca no PATH e acabou. Para serviços HTTP, agentes de infraestrutura e jobs de Kubernetes, imagem Docker também faz sentido.
A boa notícia é que os dois caminhos podem coexistir. O mesmo release gera binários e um job separado monta a imagem. Se o serviço usa HTTP, leia também o guia de Zig HTTP Server em produção, porque CI/CD não substitui limites de body, health check, logs e shutdown.
Para comparar com outras linguagens do portfólio, projetos Go têm um caminho parecido de release multiplataforma, mas normalmente dependem de GOOS e GOARCH. Veja o material de benchmarks e testes em Go para entender a diferença cultural: Go privilegia convenção; Zig privilegia controle explícito de target, modo de otimização e dependências.
Checklist final
Antes de publicar seu próximo release Zig, confirme:
- A versão do Zig está fixa no workflow.
zig fmt --checkroda em todo pull request.zig build testroda antes de qualquer release.- A matriz cobre os sistemas que seus usuários realmente usam.
- Os artefatos têm nomes claros, com sistema e arquitetura.
- A release inclui checksums SHA-256.
ReleaseSafeé o padrão, salvo motivo medido para trocar.- Cache inclui target e arquivos de build na chave.
- A tag é a única coisa que publica artefatos públicos.
Zig brilha quando o build é tratado como parte do produto. A linguagem já entrega cross-compilation, binários pequenos e integração forte com C; o pipeline só precisa não desperdiçar essas vantagens. Com GitHub Actions bem configurado, um projeto Zig deixa de ser “código que compila na máquina do mantenedor” e vira uma ferramenta distribuível, verificável e pronta para uso real.