Guide CI Perf Sentinel
Côté CI : comment exécuter Perf Sentinel en mode batch contre un fixture de traces produit par votre stage de tests d'intégration, et faire remonter les findings sur chaque pull request. Pour les topologies, voir Intégration. Pour l'instrumentation côté application, voir Instrumentation.
Sommaire
- Mode CI (analyse batch) : l'invocation CLI sous-jacente et la sémantique des codes de sortie derrière chaque recette ci-dessous.
- Recettes d'intégration CI : templates copier-coller pour GitHub Actions, GitLab CI et Jenkins, plus la philosophie du quality gate et le chemin du rapport HTML interactif pour chaque fournisseur.
- Détection de régressions sur PR (sous-commande
diff) : compare un ensemble de traces de PR à un ensemble de traces baseline pour signaler les régressions.
Mode CI (analyse batch)
Pour les pipelines CI, utilisez le mode batch au lieu du mode daemon :
perf-sentinel analyze --ci --input traces.jsonProduire traces.json
Le mode batch a besoin d'un fichier de traces, et la façon dont une suite de tests en fournit un dépend entièrement du langage. Seuls C++, PHP et, à partir de l'agent 2.32.0 via la configuration déclarative, Java implémentent un exporteur OTLP qui écrit dans un fichier au chemin de votre choix. Une JVM de test forkée par Maven ne peut même pas vous donner sa sortie standard, que Surefire utilise comme canal de commande. La réponse portable consiste à laisser l'application exporter par le réseau, comme elle le fait en production, et à écouter :
perf-sentinel capture --output traces.json -- ./scripts/run-integration-tests.sh
perf-sentinel analyze --ci --input traces.jsonLa suite de tests n'a besoin que du standard OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317. Préfixez l'étape de test que vous avez déjà plutôt que d'en ajouter une à côté, sans quoi la suite d'intégration tourne deux fois. Quand l'étape ne peut pas être préfixée, lancez capture en arrière-plan et arrêtez-le par un signal une fois les tests terminés. Voir Référence CLI pour les deux formes et Instrumentation pour la configuration par langage, y compris l'alternative du Collector.
Le code de sortie est non-zéro si le quality gate (un ensemble configurable de seuils pass/fail, la même idée qu'un quality gate SonarQube ou un gate de couverture) échoue. Le fichier doit exister quand --config le désigne : un fichier absent fait sortir analyze en 75, une erreur d'outillage et non un résultat de gate. Sans fichier de configuration, retirez --config et les valeurs par défaut s'appliquent. Configurez les seuils dans .perf-sentinel.toml :
[thresholds]
n_plus_one_sql_critical_max = 0
n_plus_one_http_warning_max = 3
n_plus_one_messaging_warning_max = 3
io_waste_ratio_max = 0.30Codes de sortie
Les sous-commandes batch (analyze, report, diff, tempo, jaeger-query, pg-stat, mysql-stat, calibrate, explain, bench, demo) partagent un contrat de codes de sortie stable depuis la 0.9.17 :
0: succès. Sous--ci, ça veut aussi dire que le quality gate est passé.1: quality gate FAILED. Émis uniquement paranalyze --ci(outempo --ci/jaeger-query --ci, qui partagent le même chemin de gate viaemit_report_and_gate) quand un seuil de[thresholds]est dépassé. L'analyse elle-même a réussi et a trouvé une régression. Un dépassement du gate a la priorité sur un échec d'écriture du rapport simultané, donc une vraie régression sur un pipe cassé ou un disque plein sort quand même en1, jamais le75tolérable. Toute autre commande batch n'a ni flag--cini quality gate, donc aucune n'émet jamais1.2: une erreur d'usage CLI. Émise à la fois parclappour les erreurs au niveau du parsing (un flag requis manquant, ex.mysql-statsans--input) et par la validation post-parse de Perf Sentinel pour les combinaisons de flags queclapne peut pas exprimer (ex.report --pg-stat-topsans--pg-stat, oubench --iterations 0). Une erreur d'usage est une faute d'invocation permanente, donc elle bloque toujours et reste hors du panier tolérable75.75: erreur d'outillage/interne (alignée surEX_TEMPFAIL, sysexits.h, la valeur sentinelle que le template GitLab CI utilise déjà au niveau shell). Couvre tout échec runtime qui atteint le code propre de Perf Sentinel et n'est ni une erreur d'usage ni un dépassement du quality gate : un fichier--input/--config/acquittements/baseline manquant ou illisible, des données de traces/config/acquittements malformées, un échec de récupération pourtempo/jaeger-query, un trace-not-found d'explain, ou un échec d'écriture du rapport SARIF/JSON/HTML. Jamais émis pour un dépassement de seuil, et ne signifie jamais que l'analyse a tourné et a été en désaccord avec votre config.
Les deux codes d'échec au-dessus du plancher clap sont distincts pour qu'un pipeline CI puisse brancher sur le code exact plutôt que d'inférer la cause depuis l'existence d'un fichier ou le résultat d'un step. Voir Échecs d'outillage vs dépassements du quality gate plus bas pour la façon dont chacun des trois templates officiels utilise ça. Avant la 0.9.17, les échecs d'outillage sortaient aussi en 1. Les pipelines qui vérifient seulement un code de sortie non-zéro ne sont pas affectés.
Recettes d'intégration CI
Des templates prêts à copier pour les trois principaux fournisseurs CI sont disponibles dans docs/ci-templates/. Choisissez celui qui correspond à votre fournisseur, déposez-le dans votre dépôt, adaptez les trois variables identifiées dans le bloc de commentaire en tête du template (version épinglée, chemin du fichier de traces, chemin de la config) et c'est terminé.
La colonne "Ce qui apparaît" ci-dessous référence trois formats côté CI : SARIF (Static Analysis Results Interchange Format, le schéma JSON standard OASIS que GitHub et GitLab utilisent pour les annotations inline sur les PR, spec), GitHub Code Scanning (la surface où GitHub affiche les findings SARIF sur les PR, anciennement l'onglet "Security"), et Warnings Next Generation (un plugin Jenkins qui agrège les findings d'analyse statique de plusieurs plugins dans un arbre unifié et une courbe de tendance, projet).
| Fournisseur | Template | Ce qui apparaît |
|---|---|---|
| GitHub Actions | github-actions.yml | SARIF dans GitHub Code Scanning + commentaire sticky sur la PR |
| GitLab CI | gitlab-ci.yml | Artefact SARIF + widget Code Quality sur la MR |
| Jenkins | jenkinsfile.groovy | Arbre de findings Warnings Next Generation + courbe de tendance |
Philosophie du quality gate
Les trois templates exécutent perf-sentinel analyze --ci comme étape de gate. Le flag --ci fait sortir le processus avec le code 1 dès qu'un seuil de [thresholds] est dépassé. Les templates traduisent ce code de sortie différemment selon le déclencheur :
| Déclencheur | Comportement | Justification |
|---|---|---|
| Pull request | Le gate bloque (build rouge) | L'auteur est encore dans le contexte, le coût de correction est le plus faible |
| Push vers trunk | Le gate est informatif seulement, le SARIF est tout de même remonté | Un commit fusionné ne doit pas être retenu par Perf Sentinel entre le merge et la release |
Cette séparation évite le mode d'échec classique des gates PR qui bloquent aussi sur trunk : main reste rouge, l'équipe contourne, et l'outil finit par être désactivé.
La configuration recommandée produit le rapport une seule fois par job, sans --ci (SARIF + JSON, toujours disponibles pour la relecture), puis décide du pass/fail séparément. Jenkins et GitLab CI le font en relançant perf-sentinel analyze --ci une seconde fois et en lisant son code de sortie. GitHub Actions lit à la place quality_gate.passed directement dans le rapport JSON déjà sur disque, puisque le résultat du gate est calculé à chaque run quel que soit --ci. Seul le code de sortie diffère. Dans les deux cas, la décision du gate ne s'exécute qu'après la réussite du run report-only.
Mécaniques par fournisseur pour la séparation PR vs trunk :
- GitHub Actions : le step PR tourne quand
github.event_name == 'pull_request'et appelleexit 1sur dépassement. Le step trunk émet une annotation::warning::sans faire échouer le job. - GitLab CI utilise
allow_failure: truesur la règle$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHet sur une dernière règle attrape-tout, pour que seule la règle merge request puisse bloquer. Le job retourne quand même le code de sortie 1 sur dépassement, mais le badge de pipeline reste vert et le job apparaît avec une icône d'avertissement jaune. La règle attrape-tout lance aussi le rapport, et l'archive, sur un push de branche sans merge request ouverte. Une branche avec une merge request ouverte lance alors deux pipelines, un pipeline de branche et un pipeline de merge request. En choisir un relève deworkflow:rules, une décision à l'échelle du projet que le template vous laisse. Le job tourne aussi sur la branche par défaut pour une seconde raison : GitLab marque un finding Code Quality comme nouveau ou résolu par rapport à un pipeline de référence sur la branche cible. Le template prend lasignaturedu finding commefingerprintCode Quality, stable tant que le finding garde son type, son service, son endpoint et son template (voir Acquittements), et son type commecheck_name. Les findings qui partagent une signature apparaissent comme une seule entrée. - Jenkins utilise une garde
when { expression { env.CHANGE_ID != null } }sur le stageQuality gate (PR only).CHANGE_IDest renseigné par MultiBranch Pipeline uniquement sur les builds de pull request. Sur les builds de branche, le stage est sauté entièrement. LequalityGatesde Warnings NG est aussi rendu conditionnel surCHANGE_IDpour que le bloc post ne réintroduise pas le blocage sur trunk.
Échecs d'outillage vs dépassements du quality gate
Un code de sortie 1 de --ci est ambigu à lui seul : il peut signifier un dépassement de seuil, ou il peut signifier que perf-sentinel n'a pas tourné (téléchargement bloqué, release corrompue, crash sur des traces malformées). Traiter les deux cas de la même façon permet à un accroc réseau passager survenu un vendredi après-midi de bloquer toutes les PR du dépôt jusqu'à ce que quelqu'un s'en aperçoive et relance la CI. Les trois templates isolent les deux modes d'échec pour que seul un dépassement de seuil puisse faire rougir une PR :
- GitHub Actions : le step de téléchargement tolère l'échec (
continue-on-error: true), mais le step de vérification du checksum juste après ne le tolère pas. Une release corrompue ou altérée doit toujours faire échouer le job, jamais rejoindre le panier tolérant à l'outillage. Le step d'analyse report-only porte lui aussicontinue-on-error: true. Chaque step en aval (envoi SARIF, commentaire de PR, les deux steps de gate) vérifiesteps.analyze.outcome == 'success'plutôt que l'existence d'un fichier. La redirection shell>crée son fichier cible avant même que la commande tourne, donc un analyze qui plante laisserait quand même unfindings.sarifvide derrière lui et mettrait en défaut un testhashFiles(). Le step d'analyse écrit aussi dans un chemin.tmppuis renomme au succès, un second garde-fou indépendant contre ce même piège. Un dernier stepReport tooling failureémet un::warning::quand analyze n'a pas réussi, pour qu'un souci d'outillage reste visible. - GitLab CI : chaque commande de téléchargement sort explicitement avec le code
75(EX_TEMPFAIL, sysexits.h) au lieu de propager le code de sortie brut de l'outil qui a échoué.allow_failure: exit_codes: [75], porté par le job, exclut uniquement ce code précis du blocage d'une merge request. Il est sur le job et non dans la règle merge request parce querules:allow_failuren'accepte qu'un booléen : le lint CI de GitLab rejette le fichier entier si une règle porte la forme mapping. La vérification du checksum et la conversionjqvers Code Quality sont exclues de cette convention exit-75 : un mauvais checksum signifie une release altérée, et un échecjqsignifie un bug dans le filtre de conversion. Ni l'un ni l'autre n'est un accroc d'outillage à tolérer. La relance finale avec--cigarde son propre code de sortie (normalement1sur un vrai dépassement), qui continue de bloquer. Le téléchargement tourne dans son propre jobperf-sentinel-install, au stage.pre, avec sa propre image, et passe le binaire aux jobs de test et d'analyse en artefact : les images de test n'embarquent souvent pascurl(eclipse-temurinn'a nicurlniwget), donc une installation dans le job de test l'arrêterait avant les tests. Ce job choisit l'asset de release qui correspond au runner (uname -m), et une architecture non prise en charge sort avec75. Sesrules:doivent sélectionner tous les pipelines où tourne le job de test, car un job absent d'un pipeline ne peut pas figurer dansdependencies:. Sa dernière règle attrape-tout les couvre tous. Le job de test commenté lance quand même les tests sanscapturequand le binaire manque, n'impose aucune option shell à votre commande, et garde sa capture avecartifacts:when: always. Le job d'analyse portewhen: alwayssur ses règles, pour qu'une série de tests en échec ait quand même son rapport. - Jenkins : la moitié téléchargement du stage
Install perf-sentinelest enrobée dans uncatchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE'). La vérification du checksum et l'installation tournent juste après, sans enrobage, donc un mauvais checksum fait toujours échouer le build. Le stageperf-sentinel analyzeécrit son SARIF/JSON dans un chemin.tmppuis renomme au succès, pour la même raison que GitHub Actions ci-dessus : sans ça,fileExists()ne pourrait pas distinguer un crash d'un vrai rapport. Le stageQuality gate (PR only)ajoute une conditionfileExists('perf-sentinel-results.sarif')en plus de la gardeCHANGE_IDexistante, pour ne lancer le contrôle de seuil qu'une fois que le stage report-only a produit un SARIF. Jenkins réutilise le workspace d'un build à l'autre, donc le stage d'installation supprime d'abord le binaire, la capture et les rapports du build précédent. Sans ça, une étape en échec archiverait les anciens rapports comme ceux de ce build, et sur un build de PRfileExists()laisserait le gate tourner dessus. Le téléchargement choisit l'asset de release qui correspond à l'agent (uname -m), et une architecture non prise en charge compte comme un souci d'outillage. Le stage commentéIntegration testslance quand même les tests sanscapturequand le binaire manque, et garde rouge une série de tests en échec aveccatchError(buildResult: 'FAILURE')pendant que le pipeline continue jusqu'au rapport.
Dans les trois cas, un souci d'outillage se manifeste par un avertissement visible ou un build unstable/jaune, distinct du build rouge produit par un dépassement de seuil, et il ne bloque jamais un merge ni un push sur trunk à lui seul. Un échec de checksum ou de logique de conversion, à l'inverse, bloque toujours, quel que soit le déclencheur, parce que ce n'est pas le genre d'échec que cette isolation doit tolérer.
Rapport interactif via GitHub Pages
Le sticky comment de PR (bloc markdown avec comptage des findings et statut du quality gate) donne aux reviewers une vue d'ensemble immédiate. Pour une inspection plus approfondie (arbre des spans avec les N+1 surlignés, correctifs suggérés propres au framework, drill-down pg_stat, Diff complet contre trunk), le template GitHub Actions publie en option un dashboard HTML complet sur GitHub Pages à chaque PR, lié depuis le sticky comment sous la forme :
📊 Interactive report (Diff view) → https://<owner>.github.io/<repo>/perf-sentinel-reports/pr-<N>/index.html#diff
Cliquer sur le lien ouvre le rapport sur l'onglet Diff, qui est la vue naturelle pour un reviewer : nouveaux findings introduits par la PR, findings résolus (régressions corrigées), changements de sévérité et deltas des métriques I/O par endpoint. Les autres onglets (Findings, Explain, pg_stat, Correlations, GreenOps) sont à un clic via la barre d'onglets.
Chaque rapport tient dans un seul fichier HTML autonome avec routage par hash pour les liens profonds, donc partager un finding précis revient à copier l'URL depuis la barre d'adresse.
Offre GitHub Pages requise. Sur un compte GitHub Free personnel, Pages n'est disponible que pour les dépôts publics. Les dépôts privés nécessitent GitHub Pro, Team ou Enterprise Cloud. Voir les plans GitHub pour la liste à jour. Activer Pages sur un dépôt privé avec un compte Free laisse le push de branche réussir, mais Pages sert du 404 en permanence sans erreur dans le log Actions. Il faut soit mettre à niveau le compte, soit rendre le dépôt public, soit sauter le bloc Pages et rester sur le mode SARIF + sticky comment markdown.
Mise en place (opt-in, nécessite GitHub Pages sur le dépôt) :
- Créer une branche
gh-pagesvide dans le dépôt (amorçage standard GitHub Pages, à faire une seule fois). - Activer GitHub Pages dans
Settings -> Pages, source = branchegh-pages, dossier =/ (root). - Copier le workflow baseline compagnon depuis
docs/ci-templates/github-actions-baseline.ymlvers.github/workflows/perf-sentinel-baseline.yml. Il tourne sur chaque push versmainet stocke le rapport baseline sousgh-pages/perf-sentinel-reports/baseline.json. - Copier le workflow de cleanup depuis
docs/ci-templates/github-actions-report-cleanup.ymlvers.github/workflows/perf-sentinel-report-cleanup.yml. Il tourne à la fermeture de PR et supprime le répertoire de la PR. - Décommenter les blocs
Download baseline from gh-pages,Generate interactive HTML report,Checkout gh-pages worktreeetPublish report to gh-pagesdans votre workflow principal (le commentaire d'en-tête dansdocs/ci-templates/github-actions.ymlles localise). - Dans ce même workflow principal, passer
contents: readàcontents: writedans le blocpermissions:. Le step de publication pousse le rapport HTML vers la branchegh-pages, ce qu'unGITHUB_TOKENen lecture seule ne peut pas faire (le push échoue avec un 403). Les workflows baseline et cleanup déclarent déjàcontents: write, donc seul le workflow principal nécessite ce changement.
Une fois les trois workflows en place, chaque PR obtient son propre rapport interactif à une URL stable :
https://<owner>.github.io/<repo>/perf-sentinel-reports/pr-<N>/Le baseline est rafraîchi à chaque push vers main, donc l'onglet Diff compare toujours les traces de la PR contre le dernier état fusionné.
Deux propriétés découlent de ce flux. Le premier passage sur le tronc est l'événement d'amorçage : avant lui, la récupération renvoie 404 et le rapport s'affiche sans onglet Diff, et un scénario tout neuf reste de même en Nouveau jusqu'à son premier run de tronc. Et rien n'est transporté entre deux pushes d'une même PR : chaque exécution se compare au baseline du tronc, donc le Diff montre tout le delta qu'introduit la PR, pas celui de son dernier commit.
Le workflow baseline se termine par analyze --ci, après la publication. Les deux étapes répondent à des questions différentes :
- le Diff montre ce qu'une PR a changé. Un finding présent des deux côtés ne tombe dans aucune colonne, donc personne n'est accusé d'une régression héritée, et personne n'en est alerté non plus.
- le gate vérifie si un état est acceptable, sur des seuils absolus, sans aucun baseline.
Sans gate sur le tronc, une régression fusionnée est invisible dans tous les Diff et ressort comme le gate rouge de la prochaine PR sans rapport qui exerce le même endpoint. Poser le gate sur l'état fusionné ramène l'alarme sur le merge fautif. La publication tourne toujours d'abord : un tronc rouge doit continuer à rafraîchir le baseline, sinon toutes les PRs suivantes perdent leur onglet Diff en plus d'être bloquées.
Le temps que le correctif arrive, acquittez le finding avec un expires_at, ce qui débloque la file sans toucher à un seuil. Posez aussi son service et son source_endpoint, pour que le run de tronc rapporte l'entrée comme supprimable une fois la correction fusionnée. Voir Acquittements.
Si GitHub Pages n'est pas activé, le template se rabat sur le sticky comment markdown seulement. Aucun changement de comportement pour les adoptants existants.
Limitations des PRs fork. Le step Post PR comment est marqué continue-on-error: true parce que les PRs fork reçoivent un GITHUB_TOKEN en lecture seule quelles que soient les permissions: déclarées au niveau workflow. Sans la tolérance, chaque PR fork ferait passer la CI en rouge au step sticky-comment même quand le reste de la pipeline a réussi. Avec la tolérance en place, les PRs fork envoient quand même leur SARIF dans l'onglet Security et l'UI Checks montre le résultat du quality gate, mais aucun sticky comment n'apparaît dans la conversation de la PR. Les PRs internes au même dépôt (contributeurs internes, même org) gardent l'expérience complète, sticky comment inclus. Les projets pour qui le sticky comment sur PRs fork est une exigence ferme doivent migrer vers le pattern pull_request_target + workflow_run documenté par GitHub Security Lab. Ce pattern sépare la pipeline en un workflow en lecture seule qui construit et envoie des artefacts, et un workflow avec droits d'écriture déclenché par workflow_run qui télécharge ces artefacts et poste le commentaire. Il n'est pas le défaut du template parce qu'il double la surface YAML et demande un passage d'artefacts soigné, ce qui est disproportionné pour un template de démarrage. Le step Publish report to gh-pages est gardé de la même façon (il ne tourne que si github.event.pull_request.head.repo.full_name == github.repository), pour qu'une PR fork n'échoue jamais sur un push que le token en lecture seule ne pourrait pas faire.
Compromis sur la concurrence. La garde concurrency.group: gh-pages-deploy sérialise les runs de ce workflow avec les workflows baseline et cleanup, pour que trois PRs fermées dans la même minute ne se marchent pas dessus sur gh-pages. Comme la garde est déclarée au niveau workflow, elle sérialise aussi les runs qui ne toucheraient pas Pages (par exemple quand les blocs Pages sont commentés). Les dépôts à fort débit de PRs peuvent isoler les étapes Pages dans un job dédié et restreindre la concurrence à ce job. Sauté ici pour garder le template compact.
Dépendances. Le déploiement utilise git directement contre la branche gh-pages, authentifié par le GITHUB_TOKEN intégré et une permission contents: write. Les workflows baseline et cleanup la déclarent par défaut. Le workflow principal est livré en contents: read et vous la passez à write en activant les blocs de publication (étape 6 ci-dessus). Aucune action tierce de déploiement n'est requise, ce qui garde le template exempt de surface supply-chain pour le chemin d'envoi. Seule actions/checkout (épinglée) est réutilisée dans les trois workflows.
Empreinte de stockage. Un rapport part d'environ 450 Ko quel que soit son contenu, les polices et logos embarqués qui rendent le fichier autonome étant portés même par un rapport sans rien à montrer. Il grossit ensuite avec le nombre de findings, jusqu'au plafond de 5 Mio où le générateur commence à élaguer. Dimensionnez un quota sur ce plafond, pas sur le plancher. Avec la rétention gérée par le workflow de cleanup, la branche gh-pages ne porte que les rapports des PRs ouvertes plus l'unique baseline.json.
Autres fournisseurs. Voir "Rapport interactif via GitLab Pages" et "Rapport interactif via Jenkins HTML Publisher" ci-dessous.
Rapport interactif via GitLab Pages
Équivalent du chemin GitHub Pages ci-dessus, adapté à la surface de déploiement native de GitLab. Deux blocs de template sont fournis dans docs/ci-templates/gitlab-ci.yml, choisir celui qui correspond à l'offre GitLab.
Note sur l'offre. Le mode de déploiement par MR (pages.path_prefix) est documenté comme Experiment, Tier: Premium ou Ultimate, et n'est pas disponible sur gitlab.com Free. En Free, le déploiement MR apparaît comme réussi dans la liste des environnements mais n'est pas servi. Un repli compatible Free est fourni à côté.
| Bloc | Offre | Comportement |
|---|---|---|
perf-sentinel-pages-simple | Free | Un seul déploiement sur la branche par défaut. Publie le snapshot trunk du rapport ET le baseline JSON à la racine des Pages du projet. Les reviewers de MR voient la vue trunk, pas l'analyse de leur MR. |
perf-sentinel-pages | Premium ou Ultimate | Un déploiement par MR sous le path prefix mr-<IID>, expiration auto 30 jours via expire_in. Baseline sur la branche par défaut à la racine Pages. Bouton natif "View deployment" sur l'UI MR. |
Choisir un bloc, pas les deux (ils se disputeraient le déploiement racine).
Mise en place (opt-in, nécessite GitLab Pages activé sur le projet) :
- Activer GitLab Pages dans
Settings -> Pagessi ce n'est pas déjà fait. - Décommenter exactement un bloc dans
docs/ci-templates/gitlab-ci.yml. Les deux tournent dans le stageperf-sentinel, prennent le binaire du jobperf-sentinel-installet réutilisentPERF_SENTINEL_TRACES / PERF_SENTINEL_CONFIGdéjà déclarées pour le job principal. - Pour
perf-sentinel-pages, confirmer GitLab 17.9 ou plus récent. Non requis pourperf-sentinel-pages-simple.
Comportement de perf-sentinel-pages (Premium ou Ultimate). Le job différencie deux chemins de déclenchement via son bloc rules: :
- Sur merge request (
$CI_PIPELINE_SOURCE == "merge_request_event"), récupère le baseline de trunk depuis la racine Pages du projet (retire le préfixe MR deCI_PAGES_URLvia${CI_PAGES_URL%/mr-[0-9]*}, repli 404 silencieux quand absent), produitpublic/index.htmlviaperf-sentinel report --output public/index.html, déploie avecpath_prefix: "mr-${CI_MERGE_REQUEST_IID}"etpages.expire_in: 30 days.environment.urlpointe vers le${CI_PAGES_URL}actif, que GitLab résout vers l'URL du déploiement propre à la MR au runtime. - Sur push vers la branche par défaut, produit
public/perf-sentinel-reports/baseline.jsonviaperf-sentinel analyze --format json, déploie avec unpath_prefixvide pour que le fichier atterrisse à la racine du site et que les futurs déploiements de MR puissent le récupérer.
Comportement de perf-sentinel-pages-simple (Free). Tourne uniquement sur la branche par défaut. Écrit à la fois public/index.html (snapshot trunk interactif) et public/perf-sentinel-reports/baseline.json en une passe, puis déploie un seul site Pages à la racine du projet.
Rétention. perf-sentinel-pages délègue la rétention à GitLab. Les déploiements parallèles sont supprimés immédiatement quand la MR est fermée ou fusionnée. Le pages.expire_in: 30 days du template sert de filet pour les MRs ouvertes qui stagnent (le défaut GitLab est de 24 heures quand non renseigné, nous l'élargissons pour qu'une MR longue garde son rapport en ligne). Mettre expire_in: never désactive l'expiration temporelle et ne s'appuie que sur les événements de fermeture/fusion. N'utiliser never que si l'équipe ferme ou fusionne ses MRs de façon fiable, sinon les MRs abandonnées s'accumulent jusqu'à saturer le quota. perf-sentinel-pages-simple n'a pas de question de rétention, il garde un seul déploiement écrasé à chaque push sur la branche par défaut.
Quota. gitlab.com autorise jusqu'à 100 déploiements parallèles supplémentaires sur Premium et 500 sur Ultimate, par namespace en plus du déploiement principal. Les instances self-managed exposent la limite via la configuration admin. perf-sentinel-pages-simple étant un déploiement unique, il n'est pas concerné. Pour les projets proches du plafond sur perf-sentinel-pages, expire_in peut être réduit, ou les MRs doivent être fermées/fusionnées rapidement pour libérer des emplacements.
Empreinte de stockage. Un rapport part d'environ 450 Ko de polices et de logos embarqués et grossit avec les findings jusqu'au plafond d'élagage de 5 Mio, et un baseline JSON fait 10 à 50 Ko. Avec la rétention active sur le chemin Premium, seules les MRs ouvertes plus le baseline courant consomment de l'espace. Le chemin Free stocke un seul déploiement.
Dépendances. Aucun composant GitLab CI tiers. Le job prend le binaire Perf Sentinel épinglé du job perf-sentinel-install et utilise le mot-clé natif pages: pour le déploiement. perf-sentinel-pages utilise aussi curl pour récupérer le baseline de trunk sur les merge requests. Aucun deploy token ou runner token au-delà du CI_JOB_TOKEN par défaut n'est requis.
Rapport interactif via Jenkins HTML Publisher
Équivalent des chemins GitHub et GitLab ci-dessus, adapté au plugin HTML Publisher pré-installé sur la plupart des Jenkins entreprise. Le plugin expose le rapport à une URL stable ${BUILD_URL}perf-sentinel/ et ajoute un lien "perf-sentinel" dans la barre latérale du build, à côté du rapport Warnings NG déjà configuré par le template.
Ouvrir ce lien amène le reviewer sur l'onglet Findings (vue d'arrivée par défaut quand aucun baseline n'est branché, voir la note Diff ci-dessous). Les cinq autres onglets (Explain, pg_stat, Correlations, GreenOps et un onglet Diff grisé) sont à un clic via la barre d'onglets.
Prérequis du pipeline Jenkins :
- Utiliser un MultiBranch Pipeline avec un plugin de branch-source installé (GitHub Branch Source, Bitbucket Branch Source, GitLab Branch Source ou Gitea Branch Source). Le test
env.CHANGE_IDqui garde le stage de quality gate sur les builds PR n'est positionné que par ces plugins. Dans un Pipeline classique single-branch,CHANGE_IDest toujours null et le quality gate ne bloque jamais. - Utiliser un agent Linux (ou un controller sans agent sur un hôte Linux). Le template s'appuie sur
sh,curl,sha256sum,chmod, dont aucun n'est disponible par défaut sur les agents Windows.
Mise en place (opt-in, nécessite le plugin HTML Publisher sur le controller) :
- Vérifier que le plugin HTML Publisher (>= 1.10 pour la compatibilité CSP) est installé.
Manage Jenkins -> Plugins -> Installed plugins, rechercher "HTML Publisher". Si absent, installer puis redémarrer le controller. Le plugin Warnings Next Generation utilisé par le reste du template doit être en >= 9.11.0 pour l'outil SARIF. - Décommenter le stage
Generate interactive HTML reportdansdocs/ci-templates/jenkinsfile.groovy, placé juste avant le stageQuality gate (PR only). - Décommenter le bloc
publishHTML([...])dans la sectionpost { always }du même fichier. Il est apparié au stage ci-dessus, donc les deux doivent être activés ensemble pour que le lien apparaisse.
Une fois activé, chaque build (branche ou pull request) produit un rapport disponible à ${JENKINS_URL}/job/<job-name>/<build-number>/perf-sentinel/. La barre latérale du build porte un lien "perf-sentinel" qui pointe toujours vers le rapport du dernier build via alwaysLinkToLastBuild: true. L'option keepAll: true retient les rapports par build, les anciens builds restent donc navigables.
Si le rapport apparaît sans style avec une navigation par onglets cassée, voir Configurer Jenkins pour rendre le rapport interactif ci-dessous. Jenkins applique par défaut une Content Security Policy stricte qui bloque le CSS et le JavaScript inline, ce qui est la cause la plus fréquente d'une page Perf Sentinel sans style ouverte depuis la barre latérale.
Configurer Jenkins pour rendre le rapport interactif.
Jenkins applique par défaut une Content Security Policy stricte au contenu servi depuis les workspaces de build. Le rapport HTML Perf Sentinel embarque CSS et JavaScript inline dans un seul fichier autonome, ce que le CSP par défaut bloque. Sans relâcher la politique ou utiliser une Resource Root URL, cliquer sur le lien ${BUILD_URL}perf-sentinel/ de la barre latérale affiche une page HTML sans style avec une navigation par onglets cassée, et aucun message dans le log du build.
Deux options pour corriger, par ordre de préférence :
Option A : configurer une Resource Root URL (Jenkins 2.200+, recommandée). Sert le contenu utilisateur depuis un domaine séparé, ce qui fait que le CSP de l'instance principale ne s'applique plus. Définir l'URL dans Manage Jenkins > System > Resource Root URL. Voir l'aide intégrée pour les détails. Aucun changement de template requis, tous les rapports de tous les jobs en bénéficient immédiatement.
Option B : relâcher le CSP (legacy, portée plus large). Définir la propriété système Java suivante au démarrage du controller Jenkins (ou la lancer une fois via la Script Console pour un test à portée de session) :
System.setProperty(
"hudson.model.DirectoryBrowserSupport.CSP",
"sandbox allow-scripts; default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';"
)Compromis :
- Affecte tout le contenu HTML servi par tous les jobs de l'instance, pas seulement les rapports Perf Sentinel.
- Ajoute
'unsafe-inline'pour les styles et les scripts. Acceptable sur une instance Jenkins où vous faites confiance aux jobs exécutés, risqué sur une instance multi-tenant avec des contributeurs non fiables. - Revient au défaut au redémarrage de Jenkins, sauf si persisté via les options de démarrage (
JAVA_OPTS,jenkins.xmlou unit systemd).
Découper le rapport en fichiers CSS et JavaScript voisins ne servirait à rien, et aucune release ne le fera pour cette raison. La politique Jenkins par défaut bloque les scripts deux fois : elle ne déclare aucun script-src, un script externe retombe donc sur default-src 'none', et sa directive sandbox omet allow-scripts, ce qui coupe le scripting du document quelle que soit la provenance. Mesuré sur une page témoin servie sous cette politique exacte : ni un script inline ni un script voisin ne tournent, alors qu'une feuille de style voisine et une image de même origine se chargent, elles.
Déplacer le contenu dans le DOM statique ne suffirait pas non plus. Le <style> inline est bloqué de la même façon (style-src 'self' ne porte pas 'unsafe-inline'), et les polices et logos en URI data: tombent sous default-src 'none'. Un rapport qui s'affiche sous cette politique, c'est un artefact multi-fichiers sans script, sans style inline et sans ressource embarquée, soit un second format de sortie plutôt qu'un correctif de l'actuel. Les options A et B restent les deux réponses.
Ce que le rapport porte depuis la 0.9.25, c'est un avis en haut de page, en texte brut non stylé, qui dit que son contenu est construit par script, qu'une politique restrictive est la raison habituelle pour laquelle il n'a pas tourné, et qui renvoie ici. Un script placé juste après le retire pendant le parsing, un chargement normal ne l'affiche donc jamais. Le dashboard ne s'affiche toujours pas, mais le lecteur obtient cette explication au lieu d'une page blanche et d'une fouille dans ses logs de build.
La contrainte est propre à Jenkins. GitHub Pages et GitLab Pages servent le rapport sans politique à eux, et les deux chemins ci-dessus l'affichent comme le ferait un navigateur ouvrant le fichier en local, navigation par onglets comprise. Rien à configurer ni sur l'un ni sur l'autre.
Onglet Diff via le plugin Copy Artifact. Contrairement à GitHub Actions et GitLab CI où un workflow baseline compagnon rafraîchit baseline.json à chaque push sur la branche par défaut, Jenkins n'a pas de dépôt d'artefacts intégré vers lequel publier une baseline de trunk. Les fonctions utilitaires baseBranchJob() et fetchBaseline() du template (en tête de docs/ci-templates/jenkinsfile.groovy) utilisent à la place le plugin Copy Artifact pour tirer perf-sentinel-report.json directement depuis un build précédent. Même modèle que la "new code period" de SonarQube ("comparer avec ce dans quoi on fusionne"). Sur un build de PR (env.CHANGE_TARGET posé par MultiBranch Pipeline), la baseline est le dernier build réussi du job de la branche cible. Hors PR (pas de CHANGE_TARGET à résoudre, et ce stage tourne sans checkout git donc la base ne peut pas être déduite autrement), on se rabat sur le dernier build réussi du job lui-même. Les deux recherches se font au mieux (optional: true) : un job sans aucun build réussi, ou un tout premier build sans historique, produit le rapport sans onglet Diff, comme si l'évolution était désactivée. À activer en décommentant le stage Generate interactive HTML report. Les fonctions utilitaires y sont déjà branchées.
Pas de publication automatique de commentaire de PR. Jenkins n'a pas de mécanisme natif de commentaire de pull request équivalent au sticky comment GitHub ou au widget Code Quality GitLab. Les reviewers qui suivent un build Jenkins consultent la page du build directement, comme pour les findings Warnings NG. Les équipes qui veulent un commentaire de PR peuvent brancher la CLI gh ou une API REST propre à la forge depuis le pipeline, mais cela nécessite de gérer un token de forge dans les credentials Jenkins et reste hors du périmètre de ce template.
Empreinte de stockage par build et retenue indéfiniment (keepAll: true). Un rapport part d'environ 450 Ko de polices et de logos embarqués et grossit avec les findings jusqu'au plafond d'élagage de 5 Mio. Pour des controllers Jenkins de longue durée avec un gros volume de builds, appairer publishHTML keepAll: true avec le build discarder dans la config du job (par exemple garder les N derniers builds) pour plafonner l'empreinte.
Où SARIF apparaît selon le fournisseur
- GitHub Code Scanning liste chaque finding dans l'onglet Security du dépôt, avec des annotations en ligne sur le diff de la PR quand le champ
code_locationest présent. Nécessitepermissions.security-events: writesur le workflow. - Le widget Code Quality de GitLab apparaît sur la page de merge request, avec des couleurs de sévérité dérivées du champ
severityde Perf Sentinel (critical -> critical,warning -> major,info -> info). - Jenkins Warnings Next Generation publie un arbre de findings structuré avec une courbe de tendance par build. Le plugin comprend nativement SARIF v2.1.0 et prend en charge sa propre déclaration
qualityGatescomme défense en profondeur en plus du code de sortie--cide Perf Sentinel.
Détection de régressions sur PR (sous-commande diff)
La sous-commande diff compare deux jeux de traces et émet un rapport delta qui liste les findings nouveaux, les findings résolus, les changements de sévérité et les deltas de comptage I/O par endpoint. L'usage naturel est une vérification de PR qui compare les traces de la branche PR à celles de la branche de base.
La comparaison est une différence d'ensembles sur les identités de findings, pas une comparaison d'exécutions de tests. Rien ne lui indique quels scénarios chaque côté a exécutés, donc le sens d'une colonne dépend de ce que les deux campagnes ont couvert :
Trois conséquences :
- Un finding présent des deux côtés ne tombe dans aucune colonne, donc une PR n'est jamais accusée d'une régression héritée.
- L'asymétrie de couverture ne fait que flatter. Un scénario renommé, retiré ou non exécuté place ses findings en Résolu sans aucun changement de code derrière.
- Un finding acquitté est filtré des deux côtés, donc sa vraie correction ne résout rien ici. Le signal, c'est le warning
unmatched_acknowledgment, voir Acquittements.
Note de montée de version (0.9.22) : l'identité d'un finding est indexée sur (type, service, source_endpoint, template), et source_endpoint résout désormais des points d'entrée qui rapportaient auparavant unknown (voir Acquittements). L'effet sur la première comparaison après montée de version dépend de la nature de la baseline. Une baseline qui persiste des findings, comme le flux gh-pages report --before baseline.json ci-dessous, montre chaque finding déplacé une fois comme résolu et une fois comme nouveau, sans aucun changement applicatif derrière : re-capturez-la contre 0.9.22 d'abord. Une baseline qui est un corpus de traces passé à diff --before n'en subit aucun effet, car les deux côtés sont ré-analysés par le binaire courant.
Note de montée de version (0.11.2) : l'identité des findings se déplace pour trois raisons distinctes, aussi bien sur OTLP que sur Jaeger et Zipkin. Des findings peuvent passer d'une route interne du framework ou de unknown à la route la plus externe de leur chaîne contiguë du même service, y compris quand les exports du daemon arrivent séparément. Une route de framework ne contenant aucun / cède devant un url.path exploitable, mais du côté entrant seulement, donc une instrumentation sans kind conserve le nom de route. Enfin, une route dépourvue de slash initial en gagne un, ce qui déplace des findings déjà rattachés correctement. Une baseline de findings persistée montrera chaque identité déplacée une fois comme résolue et une fois comme nouvelle, régénérez-la donc avec 0.11.2 avant d'évaluer les changements applicatifs. Sur une flotte instrumentée en conventions sémantiques legacy, les findings HTTP levés sur des spans SERVER disparaissent au lieu de se déplacer, et ressortent comme résolus sans aucun correctif derrière. Les baselines constituées de corpus de traces restent stables, car les deux côtés sont ré-analysés par le binaire courant.
Cette même porte SERVER peut faire virer au rouge un gate qui passait. Les appels entrants fantômes comptaient comme des opérations d'I/O, et les retirer réduit le dénominateur du ratio de gaspillage sans qu'aucune opération évitable ne parte avec, donc io_waste_ratio monte à trafic identique. Une trace de huit sauts instrumentés au-dessus d'un N+1 de six requêtes passe de 5/14 à 5/6, de high à critical. Ré-étalonnez [thresholds] io_waste_ratio_max sur une exécution 0.11.2 de votre propre corpus avant le premier pipeline qui l'applique.
# .github/workflows/perf-sentinel-diff.yml
name: perf-sentinel diff
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
diff:
runs-on: ubuntu-latest
env:
PERF_SENTINEL_VERSION: "0.43.2"
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- name: Install perf-sentinel
run: |
set -euo pipefail
BASE_URL="https://github.com/Institut-du-Numerique-Responsable/perf-sentinel/releases/download/v${PERF_SENTINEL_VERSION}"
curl -sSLf -o perf-sentinel-linux-amd64 "${BASE_URL}/perf-sentinel-linux-amd64"
curl -sSLf -o SHA256SUMS.txt "${BASE_URL}/SHA256SUMS.txt"
grep 'perf-sentinel-linux-amd64' SHA256SUMS.txt | sha256sum -c -
mkdir -p "${GITHUB_WORKSPACE}/bin"
install -m 0755 perf-sentinel-linux-amd64 "${GITHUB_WORKSPACE}/bin/perf-sentinel"
echo "${GITHUB_WORKSPACE}/bin" >> "${GITHUB_PATH}"
# Lancer les tests d'intégration sur la branche PR et capturer les traces.
# Votre script produit le fichier de traces, il reçoit ici le chemin de
# sortie en argument. Voir Instrumentation pour la façon dont
# chaque langage en produit un, Java notamment demande `capture`, ou
# l'agent 2.32.0+ pour un export fichier.
- name: Collect PR-branch traces
run: ./scripts/run-integration-tests.sh pr-traces.json
# Rejouer sur la branche de base.
- name: Collect base-branch traces
run: |
git checkout ${{ github.event.pull_request.base.sha }} -- .
./scripts/run-integration-tests.sh base-traces.json
- name: Diff
run: |
perf-sentinel diff \
--before base-traces.json \
--after pr-traces.json \
--config .perf-sentinel.toml \
--format json \
--output diff.json
# SARIF pour GitHub Code Scanning (uniquement les nouveaux findings).
perf-sentinel diff \
--before base-traces.json \
--after pr-traces.json \
--config .perf-sentinel.toml \
--format sarif \
--output diff.sarif
- name: Upload SARIF
if: hashFiles('diff.sarif') != ''
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
sarif_file: diff.sarif
category: perf-sentinel-diff
- name: Comment regression summary on PR
run: |
NEW=$(jq '.new_findings | length' diff.json)
RESOLVED=$(jq '.resolved_findings | length' diff.json)
REGRESSIONS=$(jq '[.severity_changes[] | select(.after_severity == "critical" or (.after_severity == "warning" and .before_severity == "info"))] | length' diff.json)
{
echo "## perf-sentinel diff vs base"
echo
echo "- $NEW new finding(s)"
echo "- $RESOLVED resolved finding(s)"
echo "- $REGRESSIONS severity regression(s)"
} > pr-comment.md
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: perf-sentinel-diff
path: pr-comment.md
- name: Fail on regression
run: |
NEW=$(jq '.new_findings | length' diff.json)
REGRESSIONS=$(jq '[.severity_changes[] | select(.after_severity == "critical")] | length' diff.json)
if [ "$NEW" -gt 0 ] || [ "$REGRESSIONS" -gt 0 ]; then
echo "::error::diff introduces $NEW new finding(s) and $REGRESSIONS critical regression(s)"
exit 1
fiAjustez la logique de seuil de la dernière étape selon la politique de votre équipe. Certaines équipes bloquent sur tout nouveau finding, d'autres tolèrent les nouveaux findings Info et n'échouent que sur des régressions Warning ou Critical.