Une pull request modifie openapi.yaml. Les vérifications CI sont au vert. La spécification est valide, elle est propre au linting, et deux relecteurs l'approuvent. Trois jours plus tard, un client mobile commence à provoquer des plantages par null-pointer car un champ de réponse qui était présent a disparu. Personne ne l'a supprimé intentionnellement. Quelqu'un a renommé une propriété lors d'un refactoring, et rien dans la revue n'a permis de le détecter.
C'est la lacune qu'un simple validateur ne voit jamais. Une spécification peut être parfaitement bien formée et pourtant casser tous les consommateurs qui en dépendent. La seule façon de le savoir est de comparer la nouvelle spécification à la version qu'elle remplace, modification par modification, et de poser une question : cela casserait-il un client qui fonctionnait hier ? Cette comparaison est un diff OpenAPI, et l'exécuter comme une étape de validation de fusion est l'une des vérifications les plus rentables que vous puissiez ajouter à un dépôt d'API.
Ce qu'un diff OpenAPI compare réellement
Un diff OpenAPI prend deux spécifications, une de base et une de tête, et signale ce qui a changé entre elles. La base est généralement la spécification de votre branche cible (ce qui est en production). La tête est la spécification que votre pull request propose. Un bon outil de diff ne se contente pas de produire un delta textuel comme le ferait git diff. Il comprend la structure OpenAPI, de sorte qu'il peut faire la distinction entre les modifications cosmétiques et celles qui rompent le contrat.
Voici la distinction qui compte. Certaines modifications sont additives et sûres :
- Ajouter un nouveau paramètre de requête optionnel
- Ajouter un nouveau champ de réponse
- Ajouter un tout nouvel endpoint
- Ajouter une nouvelle valeur d'énumération à un corps de requête
Les clients existants continuent de fonctionner malgré tout cela. Ils envoient ce qu'ils ont toujours envoyé et lisent ce qu'ils ont toujours lu. D'autres modifications sont incompatibles avec les versions antérieures, et ce sont celles qui posent problème :
- Supprimer un champ de réponse qu'un client lit
- Renommer une propriété (une suppression plus un ajout, du point de vue d'un client)
- Rendre obligatoire un paramètre auparavant optionnel
- Rétrécir un type, comme
stringàinteger - Supprimer une valeur d'énumération que le client pourrait envoyer
- Supprimer un endpoint ou une méthode HTTP
Le travail d'un outil de diff OpenAPI est de scanner chaque chemin, paramètre, schéma et réponse à travers les deux documents et de classer chaque modification dans l'une de ces catégories. Cette classification est tout l'intérêt. Un diff de lignes brut noie un champ required supprimé sous cinquante lignes de reformatage. Un diff structurel le révèle comme un changement cassant et vous indique le chemin où il se trouve.
Si vous voulez le modèle mental sous-jacent pour comprendre pourquoi certaines modifications cassent et d'autres non, le guide sur comment versionner et déprécier les API à grande échelle couvre en profondeur les règles de compatibilité. L'outil de diff est la manière de faire respecter ces règles mécaniquement au lieu d'espérer qu'un relecteur s'en souvienne.
oasdiff : le cheval de bataille open-source
oasdiff est l'outil open-source que la plupart des équipes utilisent. C'est un simple binaire Go, il est rapide et il est conçu spécifiquement autour de la question des changements cassants. Il lit les documents OpenAPI 3.0 et 3.1 et vous propose quelques sous-commandes en fonction de ce que vous attendez de la comparaison.
Les trois que vous utiliserez le plus :
diffsignale l'ensemble complet des différences entre deux spécifications.breakingsignale uniquement les modifications incompatibles avec les versions antérieures.changelogproduit une liste lisible par l'homme de chaque changement significatif, cassant ou non.
Pour une étape de validation de fusion, breaking est celui qui compte. Dirigez-le vers votre spécification de base et votre spécification de tête :
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
base-openapi.yaml est la spécification de la branche cible et head-openapi.yaml est celle de la pull request. La sous-commande breaking n'affiche que les modifications incompatibles. L'option `--fail-on ERR` est ce qui transforme cela en une étape de validation : elle fait sortir la commande avec un statut non nul lorsqu'elle trouve un changement classé au niveau `ERR`. Une sortie non nulle est le signal universel que la CI interprète comme un échec.
Ce modèle de gravité mérite d'être compris. oasdiff classe les changements cassants par niveaux, et `ERR` est le niveau sérieux, un changement qui cassera les clients. `WARN` couvre les changements qui pourraient casser certains clients selon la manière dont ils sont écrits, et `INFO` est informatif. C'est à vous de décider où tracer la ligne. `--fail-on ERR` bloque uniquement les cassures définitives. `--fail-on WARN` est plus strict et détecte également les éventualités.
Lorsque vous souhaitez un résumé lisible pour un changelog ou un commentaire de PR plutôt qu'un simple succès/échec, la sous-commande changelog offre une sortie plus conviviale :
oasdiff changelog base-openapi.yaml head-openapi.yaml
oasdiff possède quelques fonctionnalités réellement utiles. Il effectue une correspondance d'endpoints qui survit aux paramètres de chemin renommés, de sorte qu'il ne signale pas {userId} devenant {id} comme une suppression-plus-ajout lorsque le chemin est par ailleurs identique. Il peut fusionner les schémas allOf avant de les comparer afin que l'héritage ne produise pas de bruit. Et il génère plus que du texte brut : HTML, JSON, YAML et Markdown sont tous disponibles via les options de sortie, ce qui facilite l'intégration du résultat dans une annotation CI ou un changelog généré. Pour un outil que vous pouvez intégrer dans un pipeline en cinq minutes et auquel vous pouvez faire confiance pour être conservateur sur ce qu'il qualifie de cassant, il est difficile à battre.
openapi-diff : l'alternative JVM
Si votre stack repose déjà sur la JVM, OpenAPITools/openapi-diff est une seconde option solide et mérite d'être connue. C'est un outil basé sur Java (Java 8 et supérieur) qui compare deux spécifications OpenAPI 3.x et affiche la différence sous forme HTML, Markdown, AsciiDoc, JSON ou texte console. Vous pouvez l'exécuter à partir d'un fichier jar compilé, via Maven, Homebrew, ou comme une image Docker, ce qui lui permet de s'adapter à diverses configurations de build sans trop de tracas.
Sa comparaison va en profondeur dans les paramètres, les réponses, les endpoints et les méthodes HTTP, et elle trace la même ligne qui intéresse tout le monde : les changements qui ont maintenu la compatibilité ascendante par rapport aux changements qui l'ont rompue. L'interface CLI est simple :
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible
L'option --fail-on-incompatible sort avec un code non nul uniquement lorsqu'un changement a rompu la compatibilité ascendante, ce qui est exactement le comportement d'étape de validation que vous souhaitez. Il existe une option plus stricte --fail-on-changed si vous préférez échouer pour tout changement, et un mode --state qui affiche simplement no_changes, compatible ou incompatible lorsque vous souhaitez une réponse en un mot pour vos scripts.
Là où il excelle, c'est dans la sortie rendue. Les rapports HTML et Markdown sont clairs et détaillés, ce qui fait d'openapi-diff un excellent choix lorsque vous souhaitez un artefact de diff qu'un humain lira réellement, et pas seulement un code de sortie CI. Le compromis est la dépendance à la JVM et un démarrage plus lourd qu'un binaire Go. Si votre équipe est déjà une entreprise Java, ce coût est nul et l'outil s'intègre parfaitement. Si ce n'est pas le cas, oasdiff est l'approche plus légère. Les deux répondent bien à la question des changements cassants ; choisissez celui qui correspond à l'environnement d'exécution que vous maintenez déjà.
Intégrer le diff dans la CI comme étape de validation de fusion
Un diff que vous exécutez manuellement ne détecte rien, car le moment où vous oubliez de l'exécuter est celui où la rupture est livrée. L'étape de validation doit résider dans le pipeline et se déclencher sur chaque pull request qui touche la spécification.
La seule difficulté en CI est que vous avez besoin des deux versions de la spécification présentes simultanément : la base de la branche cible et la tête de la PR. Le checkout de la PR vous donne la tête. Vous extrayez la base directement de l'historique git sans un deuxième 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
Quelques détails sont importants ici. fetch-depth: 0 extrait l'historique complet afin que git show puisse atteindre la branche de base. La ligne git show origin/<base>:openapi.yaml lit la spécification telle qu'elle existe sur la branche cible et l'écrit dans un fichier, sans nécessiter de clonage supplémentaire. Le filtre paths signifie que le job ne s'exécute que lorsque la spécification change réellement, de sorte que les PR non liées n'en paient pas le coût. Et la dernière étape est la validation : si oasdiff breaking trouve un changement de niveau ERR, il quitte avec un code non nul, le job passe au rouge, et la PR affiche un contrôle échoué avant que quiconque ne clique sur fusionner.
L'auteur voit précisément quel changement a rompu la compatibilité, sur quel chemin, pendant que le code est encore en revue. C'est toute la valeur. La rupture est détectée au moment le moins coûteux possible au lieu de se retrouver dans un rapport d'erreur client.
Bien sûr, tous les changements cassants ne sont pas des erreurs. Parfois, vous livrez une version majeure délibérée et la rupture est intentionnelle. Le modèle propre consiste à valider par défaut et à exiger une dérogation explicite pour les exceptions : une étiquette sur la PR, une augmentation de version dans info.version, ou un workflow approuvé séparé. Ainsi, une rupture est toujours une décision prise intentionnellement, jamais un accident qui est passé inaperçu. Le guide de stratégie de versioning d'API explique quand une rupture justifie une nouvelle version majeure et quand elle devrait simplement être évitée.
La lacune qu'un diff ne peut pas combler
Voici la limite de chaque outil mentionné ci-dessus, et elle est importante. Un diff compare deux fichiers. Il vous indique que le nouveau document est compatible avec l'ancien. Il ne dit rien sur le fait que votre service en cours d'exécution corresponde réellement à l'un ou l'autre.
C'est un échec différent, et c'est celui qui frappe le plus durement en production. La spécification promet un champ created_at ; l'implémentation a discrètement cessé de le renvoyer il y a trois sprints. La spécification indique qu'un endpoint renvoie 200 ; le service en direct renvoie 500 dans une condition que personne n'a testée. Le diff est propre car les deux versions de la spécification concordent. Le contrat et le code ne le sont pas. Un diff statique n'a aucun moyen de le savoir, car il ne communique jamais avec l'API.
Combler cette lacune signifie tester l'API en direct par rapport au contrat, et pas seulement comparer le contrat à lui-même. Vous générez des tests à partir de la spécification, les exécutez contre le service en cours d'exécution et affirmez que les réponses réelles correspondent aux formes documentées. C'est le test de contrat, et c'est la couche qui détecte les dérives entre ce que vous avez écrit et ce que vous avez réellement livré.
Combler cette lacune avec Apidog et l'interface CLI Apidog
Apidog est conçu pour cette boucle, ce qui en fait un compagnon naturel de l'étape de diff plutôt qu'un remplacement. Vous importez ou synchronisez votre spécification OpenAPI dans un projet Apidog, et Apidog peut générer des scénarios de test directement à partir de la spécification, avec des assertions dérivées du schéma. Les tests vérifient que les réponses réelles correspondent aux types documentés, aux champs requis et aux codes de statut. Vous construisez et maintenez ces scénarios visuellement au lieu d'écrire à la main un ensemble parallèle de scripts de test qui se désynchronisent chaque fois que le contrat évolue.
Parce qu'Apidog maintient la conception, le mocking et les tests dans un seul espace de travail, la spécification reste la source de vérité pour tous. Vous pouvez télécharger Apidog et importer une spécification existante pour essayer la boucle sur votre propre API. Si vous êtes encore en train de décider comment contrôler cette spécification à travers les versions, le guide sur le contrôle de version d'une spécification OpenAPI avec Git s'accorde bien avec ce workflow.
L'interface CLI Apidog est ce qui exécute ces scénarios sans interface graphique dans votre pipeline. C'est un package npm :
npm install -g apidog-cli
Vous exécutez un scénario par ID, le dirigez vers l'environnement que vous souhaitez valider et demandez un rapport compatible CI :
apidog run \
--access-token $APIDOG_ACCESS_TOKEN \
-t <scenarioId> \
-e <environmentId> \
-r junit,cli \
--out-dir ./apidog-reports
Le jeton d'accès authentifie l'exécution et se trouve dans un secret CI, jamais dans un fichier commité. L'option -t sélectionne le scénario, -e sélectionne l'environnement, et -r junit,cli émet un XML JUnit lisible par machine pour votre tableau de bord CI, ainsi qu'une sortie terminal lisible pour le journal de build. Vous n'avez pas à deviner les IDs : vous copiez la commande exacte, avec les IDs de scénario et d'environnement réels déjà remplis, depuis l'onglet CI/CD du scénario dans Apidog. Si vous voulez toutes les options disponibles, le guide CLI complet documente chaque option, et apidog run --help les affiche à la demande.
Le comportement de la validation est le même principe que celui du diff. Lorsqu'une assertion échoue, parce qu'une réponse en direct ne correspond plus au contrat, apidog run quitte avec un code non nul. La CI lit le code de sortie, marque l'étape comme échouée et bloque la fusion. Aucune configuration supplémentaire. Tant que l'étape d'exécution est dans le pipeline, une régression de contrat interrompt la ligne de la même manière qu'un diff de changement cassant.
La séquence complète avant la fusion
Assemblez les deux moitiés et vous obtenez un pipeline qui détecte les deux types de ruptures. Le diff détecte les changements qui casseraient un client en lisant la spécification. Le test de contrat détecte un service qui ne respecte plus la spécification en sollicitant l'API en cours d'exécution. Exécutez-les comme des tâches séparées :
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
Les deux jobs s'exécutent en parallèle. Le job de diff lit les fichiers et n'a besoin que de Git, il se termine donc en quelques secondes. Le job de conformité a besoin d'un environnement accessible, il s'exécute donc généralement sur un build de staging déployé. Le if: always() sur l'upload permet au rapport de s'afficher même lorsque les tests échouent, ce qui est exactement le moment où vous voulez le lire. Si l'un des jobs passe au rouge, la PR est bloquée. Pour en savoir plus sur l'exécution de l'interface CLI dans des pipelines réels, le guide Apidog CLI GitHub Actions et le plus large tutoriel sur le pipeline CI/CD approfondissent le câblage.
