Zig no GitHub Actions: Release Multiplataforma com Binários Pequenos

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:

  1. Preparar versão fixa do Zig. O runner precisa usar a mesma versão que você usa localmente.
  2. Rodar verificações rápidas. zig fmt --check, zig build test e, quando existir, testes de integração.
  3. 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.
  4. Empacotar artefatos. Nomeie arquivos com projeto, versão, sistema e arquitetura.
  5. 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árioModo recomendado
CLI públicaReleaseSafe
API interna críticaReleaseSafe
Parser medido em benchmarkReleaseFast, se os testes cobrirem bem
Ferramenta embarcadaReleaseSmall
Debug de incidenteDebug 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 --check roda em todo pull request.
  • zig build test roda 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.

Continue aprendendo Zig

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