Uma pull request edita openapi.yaml. Os checks de CI ficam verdes. A especificação é válida, sem erros de linter, e dois revisores a aprovam. Três dias depois, um cliente mobile começa a apresentar crashes de null-pointer porque um campo de resposta que costumava estar lá desapareceu. Ninguém o removeu intencionalmente. Alguém renomeou uma propriedade durante um refator, e nada na revisão pegou isso.
Esta é a lacuna que um validador simples nunca vê. Uma especificação pode ser perfeitamente bem-formada e ainda assim quebrar todos os consumidores que dependem dela. A única maneira de saber é comparar a nova especificação com a versão que ela substitui, mudança por mudança, e fazer uma pergunta: isso quebraria um cliente que funcionou ontem? Essa comparação é um diff de OpenAPI, e executá-lo como um portão de merge é um dos checks de maior retorno que você pode adicionar a um repositório de API.
O que um diff de OpenAPI realmente compara
Um diff de OpenAPI pega duas especificações, uma base e uma head, e relata o que mudou entre elas. A base é geralmente a especificação em sua branch de destino (o que está ativo). A head é a especificação que sua pull request propõe. Uma boa ferramenta de diff não apenas despeja um delta textual da mesma forma que git diff faria. Ela entende a estrutura do OpenAPI, então pode diferenciar entre edições cosméticas e aquelas que quebram contratos.
Aqui está a distinção que importa. Algumas mudanças são aditivas e seguras:
- Adicionar um novo parâmetro de requisição opcional
- Adicionar um novo campo de resposta
- Adicionar um endpoint totalmente novo
- Adicionar um novo valor de enum a um corpo de requisição
Os clientes existentes continuam funcionando com todas essas mudanças. Eles enviam o que sempre enviaram e leem o que sempre leram. Outras mudanças são incompatíveis com versões anteriores, e estas são as que causam problemas:
- Remover um campo de resposta que um cliente lê
- Renomear uma propriedade (uma remoção mais uma adição, no que diz respeito a um cliente)
- Tornar um parâmetro anteriormente opcional como obrigatório
- Restringir um tipo, como de
stringparainteger - Remover um valor de enum que o cliente pode enviar
- Excluir um endpoint ou um método HTTP
O trabalho de uma ferramenta de diff de OpenAPI é escanear cada caminho, parâmetro, esquema e resposta em ambos os documentos e classificar cada mudança em um desses baldes. Essa classificação é o ponto principal. Um diff de linha bruto enterra um campo required removido sob cinquenta linhas de reformatação. Um diff estrutural o destaca como uma mudança disruptiva e informa em qual caminho ele se encontra.
Se você deseja o modelo mental subjacente para entender por que algumas mudanças quebram e outras não, o guia sobre como versionar e depreciar APIs em escala aborda as regras de compatibilidade em profundidade. A ferramenta de diff é como você aplica essas regras mecanicamente, em vez de esperar que um revisor as lembre.
oasdiff: o cavalo de batalha de código aberto
oasdiff é a ferramenta de código aberto que a maioria das equipes procura. É um único binário Go, é rápido e foi construído especificamente em torno da questão de mudanças disruptivas. Ele lê documentos OpenAPI 3.0 e 3.1 e oferece alguns subcomandos dependendo do que você deseja da comparação.
Os três que você mais usará:
diffrelata o conjunto completo de diferenças entre duas especificações.breakingrelata apenas as mudanças incompatíveis com versões anteriores.changelogproduz uma lista legível por humanos de cada mudança significativa, disruptiva ou não.
Para um portão de merge, breaking é o que importa. Aponte-o para sua especificação base e sua especificação head:
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
base-openapi.yaml é a especificação da branch de destino e head-openapi.yaml é a da pull request. O subcomando breaking imprime apenas as mudanças incompatíveis. A flag --fail-on ERR é o que transforma isso em um portão: ela faz com que o comando saia com um status diferente de zero quando encontra uma mudança classificada no nível ERR. Saída diferente de zero é o sinal universal que o CI lê como falha.
Esse modelo de severidade vale a pena ser compreendido. oasdiff classifica as mudanças disruptivas em níveis, e ERR é o grave, uma mudança que irá quebrar os clientes. WARN cobre mudanças que podem quebrar alguns clientes, dependendo de como eles são escritos, e INFO é informativo. Você decide onde traçar a linha. --fail-on ERR bloqueia apenas as quebras definitivas. --fail-on WARN é mais rigoroso e também pega as possíveis quebras.
Quando você quer o resumo legível para um changelog ou um comentário de PR, em vez de um passa/falha, o subcomando changelog é a saída mais amigável:
oasdiff changelog base-openapi.yaml head-openapi.yaml
oasdiff tem alguns toques realmente úteis. Ele faz a correspondência de endpoints que sobrevive a parâmetros de caminho renomeados, então não sinaliza {userId} se tornando {id} como uma exclusão-mais-adição quando o caminho é idêntico. Ele pode mesclar esquemas allOf antes de comparar para que a herança não produza ruído. E ele emite mais do que texto simples: HTML, JSON, YAML e Markdown estão todos disponíveis através das flags de saída, o que facilita alimentar o resultado em uma anotação de CI ou um changelog gerado. Para uma ferramenta que você pode colocar em um pipeline em cinco minutos e confiar para ser conservadora sobre o que chama de disruptivo, é difícil de superar.
openapi-diff: a alternativa JVM
Se sua stack já está na JVM, OpenAPITools/openapi-diff é uma sólida segunda opção e vale a pena conhecer. É uma ferramenta baseada em Java (Java 8 e superior) que compara duas especificações OpenAPI 3.x e renderiza a diferença como HTML, Markdown, AsciiDoc, JSON ou texto de console. Você pode executá-la a partir de um JAR compilado, via Maven, Homebrew ou como uma imagem Docker, então ela se adapta a uma variedade de configurações de build sem muito esforço.
Sua comparação vai fundo em parâmetros, respostas, endpoints e métodos HTTP, e traça a mesma linha que interessa a todos: mudanças que mantiveram a compatibilidade com versões anteriores versus mudanças que a quebraram. A CLI é direta:
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible
A flag --fail-on-incompatible retorna um código de saída diferente de zero apenas quando uma mudança quebra a compatibilidade retroativa, que é exatamente o comportamento de portão que você deseja. Há um --fail-on-changed mais rigoroso se você preferir falhar em qualquer mudança, e um modo --state que imprime apenas no_changes, compatible ou incompatible quando você quer uma resposta de uma palavra para scripts.
Onde ele brilha é na saída renderizada. Os relatórios em HTML e Markdown são limpos e detalhados, o que torna o openapi-diff uma ótima escolha quando você quer um artefato de diff que um humano realmente lerá, e não apenas um código de saída de CI. A desvantagem é a dependência da JVM e uma inicialização mais pesada do que um binário Go. Se sua equipe já é uma "Java shop", esse custo é zero e a ferramenta se encaixa perfeitamente. Se não, o oasdiff é a opção mais leve. Ambos respondem bem à questão das mudanças disruptivas; escolha o que melhor se adapta ao runtime que você já mantém.
Integrando o diff no CI como um portão de merge
Um diff que você executa manualmente não captura nada, porque o momento em que você se esquece de executá-lo é o momento em que a quebra é enviada. O portão precisa viver no pipeline e ser acionado em cada pull request que toca na especificação.
A única ressalva no CI é que você precisa das duas versões da especificação presentes ao mesmo tempo: a base da branch de destino e a head da PR. O checkout da PR fornece a head. Você puxa a base diretamente do histórico do git sem um segundo checkout:
name: openapi-diff
on:
pull_request:
paths:
- "openapi.yaml"
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get base spec
run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
- name: Install oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: Diff for breaking changes
run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR
Alguns detalhes carregam o peso aqui. fetch-depth: 0 puxa o histórico completo para que git show possa alcançar a branch base. A linha git show origin/<base>:openapi.yaml lê a especificação como ela existe na branch de destino e a grava em um arquivo, sem a necessidade de um clone extra. O filtro paths significa que o trabalho só é executado quando a especificação realmente muda, então PRs não relacionadas não pagam por isso. E o passo final é o portão: se oasdiff breaking encontra uma mudança de nível ERR, ele sai com status diferente de zero, o trabalho fica vermelho, e a PR mostra um check falhando antes que alguém clique em merge.
O autor vê precisamente qual mudança quebrou a compatibilidade, em qual caminho, enquanto o código ainda está em revisão. Esse é todo o valor. A quebra é detectada no momento mais barato possível, em vez de em um relatório de crash de cliente.
Nem toda mudança disruptiva é um erro, é claro. Às vezes, você está lançando uma versão principal deliberada e a quebra é intencional. O padrão limpo é bloquear por padrão e exigir uma sobreposição explícita para as exceções: um rótulo na PR, um incremento de versão em info.version ou um fluxo de trabalho aprovado separado. Dessa forma, uma quebra é sempre uma decisão que alguém fez de propósito, nunca um acidente que passou despercebido. O guia de estratégia de versionamento de API explica quando uma quebra justifica uma nova versão principal versus quando ela deve ser simplesmente evitada.
A lacuna que um diff não consegue fechar
Aqui está o limite de todas as ferramentas acima, e é um limite importante. Um diff compara dois arquivos. Ele diz que o novo documento é retrocompatível com o antigo. Não diz nada sobre se o seu serviço em execução realmente corresponde a um ou outro.
Essa é uma falha diferente, e é a que mais incomoda em produção. A especificação promete um campo created_at; a implementação parou silenciosamente de retorná-lo três sprints atrás. A especificação diz que um endpoint retorna 200; o serviço ativo retorna 500 sob uma condição que ninguém testou. O diff está limpo porque ambas as versões da especificação concordam. O contrato e o código não. Um diff estático não tem como saber, porque nunca se comunica com a API.
Fechar essa lacuna significa testar a API ao vivo contra o contrato, não apenas comparar o contrato consigo mesmo. Você gera testes a partir da especificação, os executa contra o serviço em execução e afirma que as respostas reais correspondem às formas documentadas. Isso é teste de contrato, e é a camada que captura a divergência entre o que você escreveu e o que você realmente enviou.
Fechando essa lacuna com Apidog e o Apidog CLI
Apidog é construído para este ciclo, o que o torna um companheiro natural para a etapa de diff, em vez de um substituto. Você importa ou sincroniza sua especificação OpenAPI para um projeto Apidog, e o Apidog pode gerar cenários de teste diretamente da especificação, com asserções derivadas do esquema. Os testes verificam se as respostas reais correspondem aos tipos documentados, campos obrigatórios e códigos de status. Você constrói e mantém esses cenários visualmente, em vez de escrever manualmente um conjunto paralelo de scripts de teste que se desalinham toda vez que o contrato muda.
Como o Apidog mantém design, mocking e testes em um único espaço de trabalho, a especificação permanece a fonte da verdade em todos eles. Você pode baixar o Apidog e importar uma especificação existente para experimentar o ciclo em sua própria API. Se você ainda está decidindo como manter essa especificação sob controle em várias versões em primeiro lugar, o passo a passo sobre controle de versão de uma especificação OpenAPI com Git combina bem com este fluxo de trabalho.
O Apidog CLI é o que executa esses cenários sem interface gráfica em seu pipeline. É um pacote npm:
npm install -g apidog-cli
Você executa um cenário por ID, aponta-o para o ambiente que deseja validar e solicita um relatório compatível com CI:
apidog run \
--access-token $APIDOG_ACCESS_TOKEN \
-t <scenarioId> \
-e <environmentId> \
-r junit,cli \
--out-dir ./apidog-reports
O token de acesso autentica a execução e reside em um segredo de CI, nunca em um arquivo commitado. A flag -t seleciona o cenário, -e seleciona o ambiente, e -r junit,cli emite XML JUnit legível por máquina para o seu dashboard de CI, juntamente com a saída de terminal legível para o log de build. Você não adivinha os IDs: você copia o comando exato, com os IDs reais do cenário e do ambiente já preenchidos, da aba CI/CD do cenário no Apidog. Se você quiser a superfície completa de opções, o guia completo do CLI documenta todas as flags, e apidog run --help as imprime sob demanda.
O comportamento do portão segue o mesmo princípio do diff. Quando uma asserção falha, porque uma resposta ao vivo não corresponde mais ao contrato, apidog run sai com um código de erro (diferente de zero). O CI lê o código de saída, marca a etapa como falha e bloqueia o merge. Nenhuma configuração extra. Enquanto a etapa de execução estiver no pipeline, uma regressão de contrato para a linha da mesma forma que um diff de mudança disruptiva.
A sequência completa de pré-merge
Junte as duas metades e você terá um pipeline que captura ambos os tipos de quebra. O diff captura mudanças que quebrariam um cliente ao ler a especificação. O teste de contrato captura um serviço que não honra mais a especificação ao exercitar a API em execução. Execute-os como trabalhos separados:
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
- run: curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR
contract-conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install -g apidog-cli
- name: Run contract tests
run: |
apidog run \
--access-token "$APIDOG_ACCESS_TOKEN" \
-t 605067 \
-e 1629989 \
-r junit,cli \
--out-dir ./apidog-reports
env:
APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: apidog-report
path: ./apidog-reports
Os dois trabalhos são executados em paralelo. O trabalho de diff lê arquivos e não precisa de nada além do git, então termina em segundos. O trabalho de conformidade precisa de um ambiente acessível, então geralmente é executado em um build de staging implantado. O if: always() no upload mantém o relatório fluindo mesmo quando os testes falham, que é exatamente quando você deseja lê-lo. Se qualquer um dos trabalhos ficar vermelho, a PR é bloqueada. Para mais informações sobre como executar o CLI em pipelines reais, o guia do Apidog CLI GitHub Actions e o passo a passo mais amplo de pipeline CI/CD aprofundam-se na configuração.
