CI/CD para Rust: GitHub Actions e Deploy | Rust Brasil

Guia de CI/CD para projetos Rust: GitHub Actions, cargo-chef, Docker, testes e deploy automatizado.

Introdução

Um pipeline de CI/CD (Continuous Integration / Continuous Deployment) bem configurado é a espinha dorsal de qualquer projeto profissional em Rust. Ele garante que cada mudança no código seja compilada, testada e analisada automaticamente antes de chegar à produção.

Rust apresenta desafios únicos para CI/CD: tempos de compilação longos, binários grandes e a necessidade de cross-compilation para diferentes plataformas. Neste artigo, vamos construir pipelines otimizados que superam esses desafios, usando GitHub Actions como plataforma principal, com Docker multi-stage builds para deploy eficiente.

O Problema: CI Lento e Deploy Manual

Não Faça Isso: Workflow sem Cache

# ERRADO: .github/workflows/ci.yml — sem cache, sem otimização
name: CI
on: push

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: rustup update stable
      - run: cargo build          # Baixa e compila TODAS as dependências do zero
      - run: cargo test           # Compila tudo novamente
      - run: cargo clippy         # E mais uma vez...
      # Tempo total: 15-30 minutos para cada push

Não Faça Isso: Dockerfile Não Otimizado

# ERRADO: Imagem gigante, recompila tudo a cada mudança
FROM rust:latest
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/meu-app"]
# Resultado: Imagem de 2GB+, rebuild completo a cada alteração

A Solução: Pipeline Otimizado e Profissional

Faça Isso: GitHub Actions com Cache e Paralelismo

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: "-Dwarnings"

jobs:
  check:
    name: Verificar compilação
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Instalar Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt

      - name: Cache Cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Verificar formatação
        run: cargo fmt --all -- --check

      - name: Clippy
        run: cargo clippy --all-targets --all-features

  test:
    name: Testes
    runs-on: ubuntu-latest
    needs: check
    steps:
      - uses: actions/checkout@v4

      - name: Instalar Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            target/
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Instalar cargo-nextest
        uses: taiki-e/install-action@nextest

      - name: Executar testes
        run: cargo nextest run --all-features

      - name: Doc tests
        run: cargo test --doc

  security:
    name: Auditoria de segurança
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

  coverage:
    name: Cobertura de código
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4

      - name: Instalar Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Instalar cargo-tarpaulin
        run: cargo install cargo-tarpaulin

      - name: Gerar relatório de cobertura
        run: cargo tarpaulin --out xml --output-dir coverage

      - name: Upload cobertura
        uses: codecov/codecov-action@v4
        with:
          files: coverage/cobertura.xml

Faça Isso: Testes Rápidos com cargo-nextest

cargo-nextest executa testes até 3x mais rápido que cargo test:

# Instalar
cargo install cargo-nextest

# Executar testes (paralelo por padrão)
cargo nextest run

# Com retry para testes flaky
cargo nextest run --retries 2

# Apenas testes que falharam na última execução
cargo nextest run --run-ignored=only

Configure o nextest para seu projeto:

# .config/nextest.toml
[profile.default]
retries = 0
slow-timeout = { period = "60s", terminate-after = 2 }
fail-fast = true

[profile.ci]
retries = 2
fail-fast = false
# No GitHub Actions
- name: Testes no CI
  run: cargo nextest run --profile ci

Docker Multi-Stage Build Otimizado

Faça Isso: Dockerfile com Cache de Dependências

# Dockerfile

# ===== Stage 1: Builder =====
FROM rust:1.85-bookworm AS builder

WORKDIR /app

# Truque: copiar apenas os manifestos primeiro para cachear dependências
COPY Cargo.toml Cargo.lock ./

# Criar um projeto dummy para compilar dependências
RUN mkdir src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build --release && \
    rm -rf src

# Agora copiar o código real (dependências já estão compiladas e cacheadas)
COPY src/ src/

# Tocar no main.rs para forçar recompilação apenas do nosso código
RUN touch src/main.rs && \
    cargo build --release

# ===== Stage 2: Runtime =====
FROM debian:bookworm-slim

RUN apt-get update && \
    apt-get install -y --no-install-recommends ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# Criar usuário não-root
RUN useradd --create-home appuser
USER appuser

COPY --from=builder /app/target/release/meu-app /usr/local/bin/meu-app

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost:8080/health || exit 1

CMD ["meu-app"]

Resultado: Imagem de ~80MB (em vez de 2GB+), e rebuilds só recompilam seu código (não as dependências).

Faça Isso: Build Ainda Menor com Musl (Binário Estático)

# Dockerfile.musl
FROM rust:1.85-bookworm AS builder

RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get update && apt-get install -y musl-tools

WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
    cargo build --release --target x86_64-unknown-linux-musl && \
    rm -rf src

COPY src/ src/
RUN touch src/main.rs && \
    cargo build --release --target x86_64-unknown-linux-musl

# Imagem final: ~10MB com scratch
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/meu-app /meu-app
EXPOSE 8080
ENTRYPOINT ["/meu-app"]

Cross-Compilation para Múltiplas Plataformas

GitHub Actions para Releases Multi-Plataforma

# .github/workflows/release.yml
name: Release

on:
  push:
    tags: ['v*']

jobs:
  build:
    name: Build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            artifact: meu-app
          - target: x86_64-apple-darwin
            os: macos-latest
            artifact: meu-app
          - target: aarch64-apple-darwin
            os: macos-latest
            artifact: meu-app
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            artifact: meu-app.exe

    steps:
      - uses: actions/checkout@v4

      - name: Instalar Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Build
        run: cargo build --release --target ${{ matrix.target }}

      - name: Upload artefato
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.target }}
          path: target/${{ matrix.target }}/release/${{ matrix.artifact }}

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4

      - name: Criar release
        uses: softprops/action-gh-release@v2
        with:
          files: |
            x86_64-unknown-linux-gnu/meu-app
            x86_64-apple-darwin/meu-app
            aarch64-apple-darwin/meu-app
            x86_64-pc-windows-msvc/meu-app.exe

Cross-Compilation Local com cross

# Instalar cross (usa Docker para cross-compilation)
cargo install cross

# Compilar para Linux ARM64
cross build --release --target aarch64-unknown-linux-gnu

# Compilar para Windows a partir do Linux
cross build --release --target x86_64-pc-windows-gnu

Otimização do Cache no CI

Faça Isso: Cache Inteligente com sccache

# .github/workflows/ci.yml — com sccache
jobs:
  test:
    runs-on: ubuntu-latest
    env:
      SCCACHE_GHA_ENABLED: "true"
      RUSTC_WRAPPER: "sccache"
    steps:
      - uses: actions/checkout@v4

      - name: Instalar Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Configurar sccache
        uses: mozilla-actions/sccache-action@v0.0.6

      - name: Build e teste
        run: |
          cargo build --release
          cargo test --release

      - name: Estatísticas do cache
        run: sccache --show-stats

Armadilhas Comuns

1. Cache Invalidado com Frequência

# ERRADO: Cache por hash de todo o diretório
key: ${{ runner.os }}-cargo-${{ hashFiles('**/*.rs') }}
# Qualquer mudança em qualquer .rs invalida o cache

# CORRETO: Cache por hash do Cargo.lock (dependências)
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Invalidado apenas quando dependências mudam

2. Testes que Dependem de Serviços Externos

# CORRETO: Use service containers
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: teste
          POSTGRES_DB: app_teste
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Rodar testes
        env:
          DATABASE_URL: postgres://postgres:teste@localhost:5432/app_teste
        run: cargo nextest run

3. Builds de Release sem Otimização

# Cargo.toml — certifique-se de que o profile release está otimizado
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true

4. Ignorar Testes de Documentação

# Doc tests são testes legítimos — não os ignore
- name: Testes de documentação
  run: cargo test --doc

Exemplo do Mundo Real: Pipeline Completo

# .github/workflows/full-pipeline.yml
name: Pipeline Completo

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: "-Dwarnings"

jobs:
  # Job 1: Verificações rápidas (< 2 min)
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt
      - uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry/
            ~/.cargo/git/
            target/
          key: ${{ runner.os }}-lint-${{ hashFiles('**/Cargo.lock') }}
      - run: cargo fmt --check
      - run: cargo clippy --all-targets --all-features

  # Job 2: Testes (paralelo ao lint)
  test:
    name: Testes (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry/
            ~/.cargo/git/
            target/
          key: ${{ matrix.os }}-test-${{ hashFiles('**/Cargo.lock') }}
      - uses: taiki-e/install-action@nextest
      - run: cargo nextest run --all-features
      - run: cargo test --doc

  # Job 3: Segurança (paralelo)
  security:
    name: Segurança
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

  # Job 4: Build de release (após testes)
  build:
    name: Build Release
    needs: [lint, test, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry/
            ~/.cargo/git/
            target/
          key: ${{ runner.os }}-release-${{ hashFiles('**/Cargo.lock') }}
      - run: cargo build --release
      - uses: actions/upload-artifact@v4
        with:
          name: release-binary
          path: target/release/meu-app

  # Job 5: Deploy (após build)
  deploy:
    name: Deploy
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: release-binary
      - name: Deploy para produção
        run: echo "Implemente seu deploy aqui (Docker push, SSH, AWS, etc.)"

Checklist de CI/CD para Projetos Rust

  1. Cache do Cargo — Use actions/cache baseado em Cargo.lock
  2. cargo fmt –check — Formatação consistente
  3. cargo clippy — Linting com RUSTFLAGS="-Dwarnings"
  4. cargo nextest run — Testes rápidos e paralelos
  5. cargo test –doc — Não esqueça dos doc tests
  6. cargo audit — Vulnerabilidades de segurança
  7. Docker multi-stage — Imagens pequenas e seguras
  8. Cross-compilation — Binários para múltiplas plataformas
  9. sccache — Cache de compilação entre jobs
  10. Profile release otimizado — LTO, codegen-units=1, strip

Veja Também