cargo-semver-checks: SemVer na CI | Rust Brasil

Use cargo-semver-checks para detectar mudanças incompatíveis em APIs Rust antes do release. Guia com baseline, workspace, GitHub Actions e limites práticos.

O cargo-semver-checks é uma das formas mais práticas de impedir que uma mudança aparentemente pequena quebre usuários de uma crate Rust. Ele compara a API pública da versão em desenvolvimento com uma versão de referência — normalmente a última publicada — e sinaliza alterações incompatíveis antes do release. O fluxo básico é instalar a ferramenta, rodar cargo semver-checks check-release e colocar a mesma verificação na CI.

Isso não elimina a necessidade de testes nem decide sozinho qual versão publicar. O ganho é transformar parte da revisão de versionamento semântico (SemVer) em uma checagem reproduzível: se uma função pública sumiu, um método mudou de assinatura ou uma variante importante deixou de existir, a pull request recebe um alerta antes que consumidores descubram a quebra em produção.

Este guia mostra como usar o cargo-semver-checks, escolher uma baseline, integrar a análise ao workspace e à CI, interpretar os resultados e separar quebra estrutural de API de mudança comportamental. Ele é especialmente útil para quem mantém bibliotecas no crates.io, SDKs internos, crates compartilhadas entre serviços ou componentes públicos de um monorepo.

Por que SemVer é difícil mesmo com o Cargo

O arquivo Cargo.toml permite declarar uma versão como 1.4.2, mas o número só tem valor se o projeto mantiver um contrato previsível:

  • patch (1.4.21.4.3): correções compatíveis;
  • minor (1.4.21.5.0): funcionalidades novas sem quebrar consumidores;
  • major (1.4.22.0.0): mudanças incompatíveis na API ou no contrato.

O problema é que uma alteração pequena no diff pode ter um impacto grande para quem depende da crate. Remover um pub, restringir um tipo genérico, mudar um retorno, retirar uma implementação de trait ou adicionar requisitos a uma função pode fazer milhares de builds falharem.

O compilador protege o projeto atual, mas não compila automaticamente todos os consumidores que existem fora do repositório. O Cargo resolve dependências e respeita intervalos de versões; ele não prova que a nova API continua compatível com a anterior. É nessa lacuna que entra o cargo-semver-checks.

O que é cargo-semver-checks

cargo-semver-checks é um subcomando do Cargo voltado à compatibilidade de API. Em termos práticos, ele:

  1. constrói uma representação da API pública da versão atual;
  2. obtém ou recebe uma baseline para comparação;
  3. executa verificações sobre itens públicos, assinaturas e contratos estruturais;
  4. informa mudanças que podem violar a política SemVer;
  5. retorna falha quando encontra incompatibilidades relevantes, permitindo bloquear a CI.

A ferramenta trabalha sobre informações da documentação e da API Rust, não sobre uma comparação textual simples de arquivos. Isso reduz falsos sinais causados por refactors internos: reorganizar uma função privada pode produzir um diff enorme sem afetar consumidores, enquanto remover uma única variante pública pode ser uma quebra real.

Que tipos de problema ela ajuda a encontrar?

Exemplos típicos incluem:

  • remoção de função, struct, enum, trait ou módulo público;
  • mudança incompatível na assinatura de uma função;
  • remoção de método ou campo que fazia parte do contrato público;
  • alterações em variantes de enum;
  • remoção de implementações públicas relevantes;
  • mudança de visibilidade que torna um item inacessível;
  • restrições novas que impedem código consumidor de compilar.

A cobertura evolui com a ferramenta e com o próprio Rust. Portanto, trate o relatório como uma rede de segurança forte, não como uma prova matemática de compatibilidade total.

Instalação e primeiro check

Instale o binário usando o próprio Cargo:

cargo install cargo-semver-checks --locked
cargo semver-checks --version

Na raiz de uma biblioteca Rust, execute:

cargo semver-checks check-release

Em um fluxo comum, a ferramenta compara o código atual com uma versão anterior apropriada da crate. Antes de automatizar, confira as opções da versão instalada:

cargo semver-checks check-release --help

Esse passo é importante porque projetos diferentes precisam de baselines diferentes: última versão publicada, tag Git, branch principal ou artefato gerado localmente.

Teste controlado para entender o relatório

Crie uma biblioteca pequena:

cargo new --lib calculadora-api
cd calculadora-api

Versão inicial:

pub fn somar(a: i64, b: i64) -> i64 {
    a + b
}

Agora imagine que alguém altera a função para:

pub fn somar(valores: &[i64]) -> i64 {
    valores.iter().sum()
}

A implementação nova pode ser melhor para o projeto, mas é incompatível com chamadas existentes como somar(2, 3). Um teste unitário reescrito junto com a função passaria; uma comparação de API deve apontar que consumidores antigos deixam de compilar.

A correção SemVer não precisa ser desistir da nova interface. Você pode preservar a função antiga e adicionar outra:

pub fn somar(a: i64, b: i64) -> i64 {
    a + b
}

pub fn somar_lista(valores: &[i64]) -> i64 {
    valores.iter().sum()
}

Ou pode assumir a quebra, documentá-la e preparar uma versão major.

Como escolher a baseline correta

A qualidade do resultado depende da referência escolhida. A pergunta não é “o código mudou desde ontem?”, mas sim: “a versão que pretendo publicar continua compatível com a versão que os usuários instalaram?”

Última versão publicada

É a baseline mais natural para crates no crates.io. Ela representa o contrato que consumidores realmente podem estar usando. Esse fluxo é ideal para uma checagem executada antes do release.

Tag Git do último release

Funciona bem em SDKs privados e bibliotecas que não são publicadas no crates.io. Exige disciplina: cada release precisa de uma tag imutável e fácil de localizar.

Branch principal

Comparar uma PR com main ajuda a impedir que uma quebra entre no repositório. Porém, main pode conter alterações ainda não publicadas. Use essa comparação como feedback de revisão, não necessariamente como decisão final do número da próxima versão.

Baseline local

É útil em ambientes sem acesso à rede ou em pipelines que geram artefatos de API em uma etapa anterior. Também facilita reproduzir o mesmo resultado entre notebooks e CI.

A regra operacional é simples: documente no repositório qual baseline cada job usa. Sem isso, dois checks podem estar tecnicamente corretos e responder a perguntas diferentes.

Integração com GitHub Actions e Gitea Actions

Um job conceitual de CI pode ser enxuto:

name: semver

on:
  pull_request:
  push:
    tags: ["v*"]

jobs:
  semver-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

      - name: Instalar cargo-semver-checks
        run: cargo install cargo-semver-checks --locked

      - name: Verificar compatibilidade da API
        run: cargo semver-checks check-release

O mesmo desenho funciona em Gitea Actions quando o runner oferece actions compatíveis. O fetch-depth: 0 é útil se a estratégia de baseline depende de tags ou do histórico Git.

Em CI de produção, prefira pinar uma versão conhecida da ferramenta ou instalar um binário verificado. Compilar com cargo install em todo job é simples, mas aumenta tempo e introduz variação. Para reduzir esse custo, combine cache com sccache ou use uma imagem de ferramentas já preparada.

Deve bloquear toda pull request?

Para uma biblioteca pública madura, geralmente sim. Para um projeto em migração, você pode começar com um job informativo e promover para obrigatório depois de revisar os falsos positivos e organizar a baseline.

Uma política prática:

  1. PR comum: check obrigatório contra a referência definida pelo time;
  2. quebra intencional: label ou aprovação de mantenedor, acompanhada de plano de versão major;
  3. release: nova execução contra a última versão publicada;
  4. resultado: changelog e número da versão devem concordar com a mudança.

A CI não deve permitir um --ignore permanente sem justificativa. Exceções precisam ser pequenas, revisáveis e ligadas a uma decisão explícita.

cargo-semver-checks em workspaces

Workspaces complicam a análise porque nem todo pacote tem o mesmo público:

  • uma crate pode ser publicada no crates.io;
  • outra pode ser uma biblioteca interna compartilhada;
  • uma terceira pode ser apenas um binário;
  • algumas crates privadas usam publish = false.

Em workspaces Cargo e monorepos, defina primeiro quais pacotes possuem contrato de compatibilidade. Depois, execute a verificação para cada crate publicável ou para o conjunto suportado pela configuração adotada.

Exemplo de política:

Tipo de pacoteCheck SemVerBaseline
Biblioteca públicaObrigatórioÚltima versão publicada
SDK interno usado por vários timesObrigatórioÚltima tag de release
Crate privada de implementaçãoOpcionalBranch principal
Binário sem API Rust públicaNormalmente nãoTestes de CLI/configuração

Não transforme o workspace inteiro em “API pública” por acidente. Isso congela detalhes internos e torna refactors legítimos mais caros.

Mudanças que parecem inocentes, mas podem quebrar usuários

Remover um reexport

pub use modulo_interno::Cliente;

Mesmo que Cliente continue existindo em outro caminho, remover o reexport quebra imports dos consumidores. Caminhos públicos também são API.

Adicionar variante a um enum

Adicionar uma variante parece apenas aditivo, mas pode quebrar match exaustivo em código externo. A decisão depende do desenho do enum e de atributos como #[non_exhaustive]. Planeje extensibilidade antes do primeiro release estável.

Alterar bounds genéricos

pub fn processar<T: Read>(entrada: T) { /* ... */ }

Trocar para um conjunto mais restritivo de traits pode excluir tipos aceitos anteriormente. Mesmo quando o corpo da função fica melhor, a superfície pública encolhe.

Remover implementação de trait

Consumidores podem depender de Clone, Send, Sync, From, IntoIterator ou traits do domínio. Retirar uma implementação pode quebrar código que nunca chama um método da sua crate diretamente.

Mudar comportamento sem mudar assinatura

pub fn normalizar(valor: &str) -> String

A assinatura pode continuar idêntica enquanto o comportamento muda de “preserva espaços” para “remove espaços”, ou passa a retornar outro formato. Essa quebra não é necessariamente detectável por análise estrutural. É por isso que testes de contrato e documentação continuam obrigatórios.

O que a ferramenta não substitui

cargo-semver-checks não substitui:

  • testes unitários e de integração, cobertos no nosso guia de testes Rust;
  • testes de comportamento observável e snapshots bem revisados;
  • revisão de segurança e supply chain;
  • documentação de migração;
  • changelog e notas de release;
  • benchmark quando o contrato inclui performance;
  • revisão humana sobre intenção e experiência do usuário.

Uma biblioteca pode passar no check de API e ainda quebrar consumidores ao mudar formato JSON, mensagens, arquivos, protocolo de rede, feature defaults ou requisitos mínimos do compilador (MSRV). SemVer é maior do que a lista de símbolos públicos.

Fluxo recomendado antes de publicar uma crate

Use esta sequência:

  1. rode cargo fmt --check e Clippy;
  2. execute testes com cargo test ou cargo-nextest;
  3. gere a documentação com cargo doc --no-deps;
  4. rode cargo semver-checks check-release contra a baseline correta;
  5. revise alterações de features, arquivos, formatos e comportamento;
  6. atualize changelog e guia de migração;
  7. confirme se o incremento será patch, minor ou major;
  8. execute cargo publish --dry-run antes da publicação real.

Para pipelines mais amplos, conecte esse check ao guia de CI/CD para projetos Rust e à estratégia de release engineering para binários, CLIs e serviços.

Checklist de adoção no time

  • crates com contrato público estão identificadas;
  • a política SemVer está documentada no README ou em CONTRIBUTING.md;
  • a baseline de PR e a baseline de release estão definidas;
  • cargo-semver-checks roda localmente e na CI;
  • tags e versões publicadas são imutáveis;
  • quebra intencional exige aprovação e guia de migração;
  • mudanças comportamentais têm testes próprios;
  • MSRV, features e formatos de dados entram na revisão manual;
  • changelog e número da versão refletem o impacto real.

Perguntas frequentes

O que é cargo-semver-checks?

É uma ferramenta do ecossistema Cargo que compara a API pública de duas versões de uma crate e aponta alterações potencialmente incompatíveis com SemVer.

Como rodar cargo-semver-checks?

Instale com:

cargo install cargo-semver-checks --locked

Depois, na crate:

cargo semver-checks check-release

Consulte cargo semver-checks check-release --help para configurar baseline, pacote e cenário do seu repositório.

Ele decide automaticamente a próxima versão?

Não. O relatório informa possíveis quebras. O mantenedor ainda precisa avaliar o contrato, mudanças comportamentais e a política de versões da crate.

Funciona para aplicações binárias?

O foco é API Rust de bibliotecas. Para binários, compatibilidade costuma envolver flags de CLI, códigos de saída, arquivos de configuração, endpoints e formatos de dados; esses contratos precisam de testes específicos.

Posso usar em uma crate 0.x?

Sim, mas documente a política. Antes de 1.0.0, o componente minor costuma carregar mudanças incompatíveis no ecossistema Cargo. A ferramenta ajuda a identificar a quebra; o time decide como versioná-la e comunicá-la.

Conclusão

O cargo-semver-checks transforma uma pergunta difícil — “esta mudança quebra quem usa minha crate?” — em uma etapa objetiva do pipeline. Ele encontra várias incompatibilidades que passam por testes locais porque o próprio repositório já foi atualizado para a nova API.

A melhor adoção não é instalar a ferramenta e confiar cegamente. É combinar baseline correta, CI obrigatória, testes de comportamento, changelog e revisão humana. Assim, o time preserva liberdade para evoluir a biblioteca sem empurrar surpresas para consumidores.

Para continuar, revise o Cargo e suas ferramentas essenciais, organize o projeto com workspaces e aplique o check no pipeline de CI/CD Rust. Uma API confiável não é a que nunca muda; é a que muda com contrato, versão e caminho de migração claros.