Le même audit, en contrôle de build.
apiglow audit lance l’audit de schéma en ligne de commande. C’est le même moteur et les mêmes 137 règles que la page #/audit, et le même rapport en sortie. La commande ajoute seulement ce qu’un pipeline réclame autour : un code de sortie, des rapports qu’une machine ou une plateforme de CI sait lire, et une baseline pour qu’une API existante adopte le contrôle sans d’abord solder tout son passé.
Elle est livrée dans le paquet npm, sous la même licence MIT, et tourne sur Node 24 ou plus récent, comme le bake. Pas de compte, pas de service, pas de jeu de règles à écrire.
La lancer
npx apiglow@0.3.0 audit openapi.yamlLe rapport part sur stdout. Tout ce qui concerne le passage — chaque contrôle, le verdict, les avertissements — part sur stderr :
PASS --fail-on error: 0 finding(s) at this severity or above
Audit passedÉpinglez la version. Une nouvelle version peut ajouter une règle, et une règle ajoutée sous un pipeline, c’est un build qui casse sans que personne n’ait rien changé. Montez l’épingle exprès, comme vous montez le tag du CDN.
Ce que la commande accepte :
- Un schéma — un chemin ou une URL, en JSON ou en YAML, valide ou non. Un fichier abîmé a quand même son rapport (voir le code de sortie).
- Plusieurs schémas — plusieurs chemins, ou un motif entre guillemets pour que ce soit la commande qui l’étende et non le shell :
'apis/**/openapi.{yaml,json}'. Chaque fichier est audité comme sa propre spec, sous son chemin relatif au répertoire de travail : les rapports et la baseline nomment le fichier, et ajouter un schéma ne décale jamais les autres.node_moduleset.gitne sont jamais parcourus. Un motif qui ne trouve rien donne un avertissement ;--fail-on-unmatched-globsen fait un arrêt. --config apidoc.config.json— l’objet JSON que votre page place dans#api-doc-config. L’audit porte alors sur ce que la documentation montre vraiment : ses overlays appliqués, ses opérations cachées marquées comme telles, chaque spec d’une installation multi-spec. Les fichiers que la configuration nomme se lisent depuis son propre répertoire, comme le bake les lit.
Ce qui fait échouer le build
Quatre contrôles, chacun sur sa propre ligne PASS / FAIL. Un seul contrôle en échec fait échouer le passage.
| Option | Échoue quand | Par défaut |
|---|---|---|
--fail-on error|warning|info|none | un constat de cette sévérité, ou d’une pire, est présent | error |
--min-grade A|B|C|D|F | la note globale est en dessous | désactivé |
--min-score 0-100 | le score global est en dessous | désactivé |
--baseline <fichier> | la baseline liste des constats qui n’existent plus | actif avec une baseline |
Quand --fail-on échoue, stderr liste les constats responsables — les vingt premiers, chacun en fichier:ligne:colonne, puis un décompte :
FAIL --fail-on warning: 14 finding(s) at this severity or above
warning responses-success — openapi.yaml:2149:9 · GET /failures/forbidden · /paths/~1failures~1forbidden/get/responses
warning oauth-legacy-flows — openapi.yaml:2924:11 · components.securitySchemes.petstore_auth · …Ces lignes de la commande restent en anglais quelle que soit la langue du rapport.
Le code de sortie
| Code | Signification |
|---|---|
0 | tous les contrôles sont passés |
1 | un contrôle a échoué |
2 | l’audit n’a pas pu tourner : une option inconnue, une valeur hors bornes, un schéma injoignable, une configuration de règles illisible |
Chaque option est validée avant le moindre chargement : une faute de frappe dans un seuil ne coûte ni un téléchargement, ni un « pas de seuil » silencieux.
Un fichier que la commande peut ouvrir a toujours son rapport, aussi abîmé soit-il. Une clé en double, une tabulation en guise d’indentation, une virgule de trop, un fichier tronqué : chacun devient un constat document-syntax à sa ligne et sa colonne, et le reste du fichier est noté quand même. Un fichier sans aucune description OpenAPI — vide, une liste, du Swagger 1.x, de l’OpenAPI 4 — devient un constat document-openapi. Les deux sont des erreurs : le --fail-on error par défaut fait donc échouer le passage avec le code 1. Le code 2 est réservé à un schéma qu’on n’a pas pu atteindre.
L’adopter sur une API existante
Une API qui a des années d’histoire ne passera pas le premier jour, et n’a pas à le faire. Une baseline enregistre les constats que l’équipe a vus et acceptés. Versionnée à côté du schéma, elle fait que --fail-on ne compte que les constats qu’elle ne liste pas :
# une fois : enregistrer l'existant
npx apiglow@0.3.0 audit openapi.yaml --write-baseline audit-baseline.json
# en CI : n'échouer que sur la nouveauté
npx apiglow@0.3.0 audit openapi.yaml --baseline audit-baseline.jsonSon comportement :
- Un constat s’identifie par sa règle et son pointeur JSON — les deux choses qui survivent à une modification sans rapport. Les messages et les numéros de ligne, non : un paramètre renommé réécrit les deux.
- Elle compte, ce n’est pas un ensemble. Un pointeur listé deux fois couvre deux occurrences ; une troisième est nouvelle.
- La note et le score l’ignorent. Ils décrivent le document. Une baseline accepte des constats, pas une note plus basse.
- Ce n’est pas une liste d’exclusion. Chaque constat reste dans le rapport ; en JSON, les connus portent
known: true. - Le fichier est trié de bout en bout. Le réécrire sur un schéma inchangé le reproduit octet pour octet : un diff de revue ne montre que ce qui a bougé.
{ "format": "apiglow-audit-baseline", "version": 1,
"specs": { "default": { "parameter-described": ["/paths/~1pets/get/parameters/0"] } } }Les entrées périmées font échouer le passage. Quand un constat est corrigé, son entrée ne correspond plus à rien. Laissée là, elle masquerait un jour un nouveau constat tombé sur le même pointeur — le passage échoue donc et la liste. --prune-baseline <fichier> réécrit la baseline sans ses entrées périmées (ce peut être le fichier --baseline lui-même) et passe. --allow-stale-baseline se contente de les lister.
Un fichier qui n’est pas une baseline écrite par cette version arrête le passage avec le code 2. Il n’est jamais lu comme vide, ce qui ferait échouer chaque constat, ni comme couvrant tout, ce qui laisserait passer une régression.
Les rapports
--format | Pour | Ce que c’est |
|---|---|---|
text | un terminal | le rapport console, replié par règle comme la page ; le format par défaut |
markdown | une personne | l’export Markdown de la page — un commentaire de pull request, un résumé de job GitHub |
json | un script ou un agent | le rapport complet, dans un contrat versionné |
sarif | le code scanning de GitHub | SARIF 2.1.0 : les constats s’ouvrent et se ferment comme des alertes |
github | une pull request GitHub | des commandes de workflow qui annotent le diff à la ligne |
codequality | une merge request GitLab | GitLab Code Quality, pour le widget de merge request |
Un passage, plusieurs rapports. --format et --output règlent le rapport sur stdout. --report <format>=<fichier>, répétable, écrit un rapport de plus par occurrence, depuis le même passage — le JSON pour un script, le Markdown pour le résumé du job, sans auditer deux fois :
npx apiglow@0.3.0 audit openapi.yaml --format github \
--report markdown="$GITHUB_STEP_SUMMARY" --report sarif=audit.sarifChaque constat est placé. Un schéma lu depuis un fichier donne à chaque constat son fichier:ligne:colonne — l’endroit où son nœud se trouve dans le fichier que vous éditez, en suivant les $ref jusque dans d’autres fichiers. Terminaux et éditeurs en font un lien, et les trois formats de CI s’en servent pour poser le constat sur la bonne ligne.
Ce qu’un rapport liste, dans tous les formats :
--min-severity error|warning|infogarde les constats de cette sévérité ou pire. Le schéma REST de GitHub donne quelque 5 000 constats, dont 4 000 notes.--only-new, avec--baseline, ne garde que les constats que la baseline ne liste pas.
Aucun des deux ne change le verdict : les comptes, les scores, la note et chaque contrôle lisent toujours le rapport entier. Le texte et le Markdown disent combien de constats chaque filtre a laissés de côté. Un --fail-on moins sévère que --min-severity est refusé, puisqu’un job échouerait sur des constats que son rapport ne montre pas.
--language fr écrit le rapport en français : messages, justifications, correctifs. Les lignes de la commande elle-même, sur stderr, restent en anglais.
Le rapport JSON
Ce que lit un script ou un agent. format et version nomment la forme, et tout changement de forme incrémente version : un consommateur peut refuser ce qu’il ne connaît pas.
{ "format": "apiglow-audit-report", "version": 1,
"tool": { "name": "apiglow", "version": "0.3.0" },
"passed": false,
"specs": [{ "id": "default", "source": "openapi.yaml", "passed": false,
"gates": [{ "gate": "fail-on", "threshold": "error", "actual": 2, "passed": false }],
"report": { "…": "…", "categories": [{ "findings": [{ "fingerprint": "9f3c…" }] }] } }],
"rules": { "parameter-described": { "category": "completeness",
"label": "…", "message": "…", "why": "…", "fix": "…" } } }- Chaque constat porte une empreinte qui reste la même d’un passage à l’autre tant que le document change autour de lui. C’est sur elle que s’appuie une plateforme de CI ou un agent qui suit un constat.
rulesporte, une seule fois, les textes de chaque règle déclenchée — son libellé, pourquoi elle compte, comment corriger — si bien qu’un consommateur qui explique un constat n’a besoin de rien d’autre que le fichier.
La configuration des règles
Éteignez une règle, ou changez sa sévérité, pour tout le document ou sous certains chemins seulement. Le bloc est celui que lit la page : la note publiée et la note de CI concordent.
{
"audit": {
"rules": { "property-described": "off", "parameter-described": "error" },
"overrides": [
{ "paths": ["/paths/~1legacy~1*"], "rules": { "response-example": "off" }, "reason": "frozen legacy API" }
]
}
}La CLI le lit dans la configuration passée par --config, ou dans --audit-config <fichier> — un fichier JSON qui ne contient que le bloc, pour un passage sans configuration de page, ou qui doit noter autrement que la page publiée. Le guide de l’audit détaille l’ordre de priorité.
Deux écarts avec la page. Une entrée que la CLI ne sait pas lire arrête le passage avec le code 2, chaque entrée nommée : un pipeline ne passe jamais sur une configuration que personne n’a écrite. Et features.audit: false ne s’applique pas — ce réglage retire la page de la documentation publiée, alors qu’un auteur qui lance la commande a demandé le rapport.
Une note calculée avec l’un de ces réglages porte la mention Règles personnalisées partout où elle apparaît : elle ne passe jamais pour la note par défaut.
Le réseau
Un schéma, un $ref externe ou un overlay derrière une URL sont téléchargés. Deux options rendent ça prévisible dans un job de CI :
--fetch-timeout <secondes>(30 par défaut) : un hôte qui accepte la connexion et ne répond jamais fait échouer le job dans sa première minute, pas au délai maximal de la plateforme.--offlinerefuse tout téléchargement et arrête le passage avec le code2— même celui qui, sinon, serait ignoré avec un avertissement, comme un overlay. Un job censé rester hors réseau ne passe jamais sur un autre document.
Les règles, depuis le terminal
npx apiglow@0.3.0 audit --explain oauth-legacy-flows --language fr # une règle : catégorie, sévérité, pourquoi, comment corriger
npx apiglow@0.3.0 audit --list-rules # toutes les règles, en JSONLes deux respectent --language et refusent tout le reste sur la ligne : un passage qui aurait l’air d’un audit et imprimerait une règle tromperait quiconque lit son code de sortie.
Les recettes
GitHub Actions
L’action apiglow/audit-action lance la commande en une étape : des annotations sur le diff de la pull request, le rapport Markdown dans le résumé du job, et du SARIF vers le code scanning quand vous le demandez. Son tag est la version de la CLI qu’elle lance.
on: pull_request
jobs:
schema-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: apiglow/audit-action@v0.3.0
with:
schema: openapi.yaml
baseline: audit-baseline.json| Entrée | Par défaut | |
|---|---|---|
schema | — | Chemin, URL ou motif entre guillemets ; un par ligne pour plusieurs |
config | — | La configuration JSON de la page, à la place de schema |
baseline | — | Un fichier de baseline : seuls les constats qu’il ne liste pas font échouer le job |
fail-on | error | error, warning, info ou none |
min-severity | info | Ne lister que les constats de cette sévérité ou pire |
annotations | true | Annoter le diff |
summary | true | Écrire le rapport Markdown dans le résumé du job |
sarif | false | Envoyer le SARIF au code scanning — le job a besoin de security-events: write |
args | — | Toute autre option : --min-grade B --language fr |
version | celle de l’action | La version d’apiglow à lancer |
node-version | 24 | La version de Node.js sur laquelle la lancer |
Ses sorties sont passed, le chemin du report JSON, et celui du fichier sarif quand il y en a un.
Le même job à la main, chaque pièce en vue :
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- name: Schema audit
run: |
npx --yes apiglow@0.3.0 audit openapi.yaml --baseline audit-baseline.json \
--format github --report markdown="$GITHUB_STEP_SUMMARY" --report sarif=audit.sarif
- if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: audit.sarifGitHub montre dix annotations de chaque type par étape et jette le reste sans prévenir : les nouveaux constats passent donc en premier, et une dernière notice dit combien ont été laissés de côté. Le code scanning lit au plus 25 000 résultats par passage ; au-delà, les nouveaux constats passent d’abord, puis les plus sévères.
GitLab CI
Les constats dans le widget de merge request, le rapport JSON gardé comme artefact :
schema-audit:
image: node:24
script:
- npx --yes apiglow@0.3.0 audit 'apis/**/openapi.yaml'
--baseline audit-baseline.json
--report codequality=gl-code-quality-report.json --report json=audit.json
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
paths: [audit.json]C’est l’empreinte qui permet au widget de distinguer les nouveaux constats des constats résolus.
pre-commit
Un hook local. pass_filenames: false, parce que la commande nomme son schéma elle-même — si on lui passait les fichiers indexés, elle n’auditerait qu’eux, sous des identifiants que la baseline ne connaît pas. Toute modification de YAML le déclenche, puisqu’un $ref peut vivre dans un autre fichier.
repos:
- repo: local
hooks:
- id: apiglow-audit
name: Schema audit
entry: npx --yes apiglow@0.3.0 audit openapi.yaml --baseline audit-baseline.json
language: system
files: \.ya?ml$
pass_filenames: falseLes ruptures de contrat
Hors périmètre, à dessein. L’audit note une version du document, et sa baseline répond à « qu’est-ce qui est nouveau », pas à « le contrat a-t-il cassé ». oasdiff compare deux versions ; lancez-le à côté de l’audit :
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Breaking changes
run: |
git show "origin/$GITHUB_BASE_REF:openapi.yaml" > base.yaml
docker run --rm -v "$PWD:/specs:ro" tufin/oasdiff:v1.32.1 \
breaking /specs/base.yaml /specs/openapi.yaml --fail-on ERRUn agent qui corrige le schéma
Donnez-lui le rapport JSON. Chaque constat est placé en fichier:ligne:colonne, le message, la justification et le correctif de sa règle sont dans rules, et l’empreinte survit aux modifications autour. La boucle : auditer, corriger, ré-auditer jusqu’à ce que chaque contrôle passe ; avec une baseline, --only-new garde l’agent sur ce qu’une modification a introduit.
npx apiglow@0.3.0 audit openapi.yaml --format json --min-severity warningLe paquet npm livre cette boucle sous forme de skill d’agent : skills/apiglow-audit/SKILL.md. Copiez le dossier depuis node_modules/apiglow/skills/ là où votre agent lit ses skills — .claude/skills/ pour Claude Code, par exemple. Au-delà des commandes, la skill dit ce qu’un agent ne doit pas faire de lui-même : inventer ce que fait l’API pour remplir une description ou un exemple, ou faire passer le contrôle en éteignant une règle, en écrivant une baseline ou en baissant --fail-on. Chacune de ces décisions revient au propriétaire.
Toutes les options
| Option | Ce qu’elle fait |
|---|---|
<spec>… | Chemin, URL ou motif entre guillemets du schéma ; plusieurs sont audités chacun sous son chemin |
--config <fichier> | Auditer ce que la documentation montre : overlays, opérations cachées, configuration des règles, chaque spec |
--audit-config <fichier> | La configuration des règles seule ; remplace le bloc audit de la configuration |
--fail-on-unmatched-globs | S’arrêter quand un motif ne trouve aucun fichier |
--fail-on <sévérité> | Échouer sur un constat de cette sévérité ou pire : error (par défaut), warning, info, none |
--min-grade <lettre> | Échouer sous cette note |
--min-score <0-100> | Échouer sous ce score |
--baseline <fichier> | Ne compter que les constats que la baseline ne liste pas ; échouer sur les entrées périmées |
--write-baseline <fichier> | Enregistrer tous les constats actuels, et passer |
--prune-baseline <fichier> | Avec --baseline : la réécrire sans ses entrées périmées |
--allow-stale-baseline | Avec --baseline : lister les entrées périmées sans échouer |
--format <format> | text (par défaut), markdown, json, sarif, github, codequality |
--output <fichier> | Écrire le rapport dans un fichier plutôt que sur stdout |
--report <format>=<fichier> | Écrire un rapport de plus depuis le même passage ; répétable |
--min-severity <sévérité> | Ne lister que les constats de cette sévérité ou pire — le verdict ne change pas |
--only-new | Avec --baseline : ne lister que les constats qu’elle ne connaît pas |
--language <code> | en (par défaut) ou fr, pour les textes du rapport |
--offline | Ne jamais télécharger ; une URL où que ce soit arrête le passage |
--fetch-timeout <secondes> | Attente maximale pour chaque document téléchargé (30 par défaut) |
--explain <règle> | Imprimer une règle, ne rien auditer |
--list-rules | Imprimer toutes les règles en JSON, ne rien auditer |