Comment automatiser la compression d'images dans votre pipeline CI avec jpgboost-cli
Un dépôt qui accumule des JPEG et PNG non compressés dégrade le LCP tout en alourdissant chaque clone. jpgboost-cli permet d'automatiser leur compression directement dans votre pipeline CI, sans dépendre de la vigilance de chaque contributeur. Cet article présente son intégration avec GitLab CI, compare deux stratégies opposées et revient sur les pièges susceptibles de coûter cher en production.
Le poids réel des images non compressées
Chaque image committée sans compression laisse une trace dans l'historique Git sous la forme d'un blob contenant son contenu. Même si l'image est ensuite remplacée par une version plus légère, l'ancien blob reste présent dans l'historique. Lors d'un git clone, l'historique du dépôt est récupéré lui aussi. Le poids des anciennes images continue donc de peser sur le dépôt, même si elles ne sont plus utilisées.
Sur un dépôt qui accumule pendant plusieurs mois des captures d'écran, des visuels marketing ou des exports de design, ce poids mort peut rapidement atteindre plusieurs centaines de mégaoctets. Cela allonge les clones en CI et augmente la consommation de bande passante pour chaque contributeur.
Côté production, le problème ne s'arrête pas au dépôt. Une image trop lourde pénalise directement le Largest Contentful Paint (LCP), notamment lorsque l'image la plus volumineuse située au-dessus de la ligne de flottaison devient l'élément déterminant de la métrique. Le gain apporté par la compression peut être considérable. Dans l'exemple de la documentation de jpgboost-cli, un JPEG de 4,2 Mo passe à seulement 890 Ko, soit une réduction de 79 %.
La compression manuelle ne tient donc pas à l'échelle. Elle repose sur un réflexe individuel. À chaque pull request, chaque contributeur doit penser à optimiser les images avant de les committer. En pratique, cette étape est facilement oubliée dès que la pression ou l'urgence augmente. Le problème n'est généralement découvert que des semaines plus tard, lorsqu'un audit ou un rapport de performance vient révéler la dette accumulée.
Pourquoi automatiser en CI plutôt qu'en local ou côté serveur
Compresser les images en local, par exemple avec un hook Git pre-commit, dépend toujours de la machine et de la discipline du contributeur. Un hook peut être contourné, désinstallé ou tout simplement ne pas être présent sur le poste d'un designer qui commit directement ses fichiers.
La compression côté serveur intervient, elle, trop tard. Les images ont déjà été indexées et peuvent déjà avoir été servies une première fois. Le problème est donc visible par les premiers visiteurs et l'optimisation doit ensuite être rejouée à chaque déploiement.
La CI constitue le point de contrôle le plus fiable. Elle s'exécute sur chaque pull request, ne dépend pas de la configuration du poste de travail et produit un résultat visible et traçable dans les logs du job. C'est ce point de passage unique qui rend l'automatisation intéressante, même si elle implique un coût d'infrastructure qu'il faut prendre en compte dès le départ.
Ce coût est d'autant plus important que jpgboost-cli n'est pas un binaire indépendant installable via un gestionnaire de paquets. Il est fourni avec JPGBoost.app, fonctionne uniquement sous macOS 15 ou ultérieur et nécessite une licence Pro.
La suite de cet article part donc d'une hypothèse précise. Elle utilise un runner macOS self-hosted et persistant, afin de disposer d'un environnement stable et de conserver l'installation de JPGBoost.app d'un job à l'autre.
Installer jpgboost-cli et le tester en local
Sur le runner, comme en local pour les tests, le binaire est inclus dans le bundle de l'application. Un lien symbolique créé une seule fois permet ensuite de l'appeler depuis n'importe quel dossier.
# Lien symbolique une fois, pour appeler jpgboost-cli depuis n'importe quel dossier
sudo ln -s /Applications/JPGBoost.app/Contents/MacOS/jpgboost-cli /usr/local/bin/jpgboost-cli
# Vérifie que la commande répond
jpgboost-cli --help
La licence Pro s'active ensuite, une seule fois et manuellement sur le runner. Elle ne se fait jamais dans le pipeline lui-même. Si c'est la toute première installation à activer cette licence, utilisez le jeton reçu par e-mail après achat.
# Identifiant de cette machine, à communiquer si besoin au support
jpgboost-cli --machine-id
# Première activation, avec le jeton reçu par e-mail après achat
jpgboost-cli --activate ACT-XXXX-XXXX-XXXX-XXXX-XXXX
Si la licence a déjà été activée ailleurs, typiquement sur l'application pour un usage personnel, --activate n'est plus la bonne approche. Le runner doit rejoindre cette licence existante plutôt qu'en réactiver une nouvelle. Depuis l'appareil déjà activé, générez un code, puis utilisez-le sur le runner.
# Depuis l'appareil déjà activé (l'application, ou une autre CLI), génère un code
jpgboost-cli --add-device
# Sur le runner, rejoint la licence existante avec ce code
jpgboost-cli --pair XXXX-XXXX
Cette activation, par l'une ou l'autre méthode, compte comme l'une des deux installations couvertes par la licence Pro. L'application et la CLI étant considérées comme deux installations distinctes, une activation répétée à chaque job épuiserait rapidement le quota. Avec deux exécutions seulement, la licence serait déjà entièrement consommée. C'est pourquoi le runner doit rester persistant afin de conserver l'activation d'un job à l'autre.
Test rapide en local avant d'aller plus loin.
# Deux fichiers vers WebP, qualité 60
jpgboost-cli photo1.jpg photo2.png --quality 60 --format webp --output ./compresse
Configurer GitLab (runner et jeton de push)
Avant de coller le fichier .gitlab-ci.yml ci-dessous, deux réglages sont à faire une seule fois côté GitLab : enregistrer le runner, et créer le jeton dont le job de correction a besoin pour pousser.
Enregistrer le runner
Sur le Mac choisi comme runner persistant, celui où jpgboost-cli et sa licence sont déjà installés à la section précédente, enregistrez le runner avec le tag macos utilisé par les deux jobs.
gitlab-runner register \
--url https://gitlab.com \
--token <JETON_D_ENREGISTREMENT_DU_PROJET>
Le jeton d'enregistrement se trouve dans Settings > CI/CD > Runners > New project runner (l'interface génère un jeton à usage unique pour cet enregistrement, plutôt que l'ancien jeton d'enregistrement partagé). L'--executor shell a son importance : contrairement à un executor Docker, il exécute les commandes directement sur le système du runner, ce qui est indispensable pour retrouver jpgboost-cli déjà installé et sa licence déjà activée d'un job à l'autre, sans repartir d'un environnement vierge à chaque fois.
gitlab-runner register demande le nom du runner puis l'executor de façon interactive ; répondez shell à cette dernière question
Créer le jeton de push
Le job fix-image-weight pousse un commit sur la branche de la merge request. Le CI_JOB_TOKEN fourni automatiquement à chaque job n'a pas les droits d'écriture nécessaires sur une branche protégée ; il faut donc un jeton dédié.
Un Project Access Token (Settings > Access Tokens du projet) est en théorie le choix le plus propre : rattaché au projet plutôt qu'à un compte, il survit au départ de la personne qui l'a créé. Mais sur un namespace personnel en offre gratuite, GitLab.com ne propose pas cette fonctionnalité, réservée aux groupes payants : c'est le cas le plus courant qui pousse à utiliser à la place un Personal Access Token.
Un Personal Access Token se crée depuis les réglages du compte utilisateur, pas ceux du projet (Avatar > Edit profile > Access Tokens), avec :
- Scope
write_repository - Une date d'expiration cohérente avec votre politique de rotation des jetons
Contrepartie à connaître avant de choisir cette voie : le jeton est lié au compte qui l'a créé. Si ce compte est désactivé, perd l'accès au projet, ou que la personne quitte l'équipe, le job de correction cesse de pouvoir pousser, sans avertissement avant que ça arrive. Sur un projet d'équipe, mieux vaut le créer depuis un compte de service dédié plutôt que le compte personnel d'un contributeur.
Le jeton généré n'est affiché qu'une seule fois : copiez-le immédiatement, puis ajoutez-le dans Settings > CI/CD > Variables du projet sous le nom PUSH_TOKEN, avec les options Masqué et Protégé cochées si les merge requests ciblent une branche protégée.
PUSH_TOKEN, utilisé plus loin dans le .gitlab-ci.ymlSi la branche source de vos merge requests est elle-même protégée, il faut en plus que le compte associé au jeton ait le droit d'y pousser directement, dans Settings > Repository > Protected branches > Allowed to push and merge, sans quoi le git push du job échoue avec un refus d'accès malgré un jeton valide.
Où placer le fichier .gitlab-ci.yml
GitLab ne détecte le pipeline qu'à un seul endroit par défaut : un fichier nommé exactement .gitlab-ci.yml (avec le point initial), à la racine du dépôt, au même niveau que le README. Un fichier placé dans un sous-dossier, ou nommé différemment, est simplement ignoré : aucune erreur ne le signale, le projet se comporte juste comme s'il n'avait aucune CI configurée.
Si un emplacement différent est nécessaire, par exemple un fichier de pipeline partagé entre plusieurs dépôts ou une structure de monorepo, Settings > CI/CD > General pipelines > CI/CD configuration file permet de pointer vers un autre chemin, y compris dans un projet différent avec la syntaxe chemin/fichier.yml@groupe/projet:branche. En dehors de ce cas particulier, laissez ce champ vide et gardez le fichier à la racine.
Une fois le fichier committé et poussé, aucune activation manuelle n'est nécessaire : GitLab le détecte automatiquement au prochain événement qui correspond à une règle du fichier : ici, l'ouverture ou la mise à jour d'une merge request, via $CI_PIPELINE_SOURCE == "merge_request_event". Le résultat apparaît dans l'onglet Pipelines du projet et directement dans l'onglet du même nom sur la merge request. Pour vérifier la syntaxe avant de pousser, plutôt que de le découvrir au premier pipeline en échec, l'éditeur intégré (Build > Pipeline editor dans le menu du projet) inclut un bouton Valider qui appelle le linter CI de GitLab sans déclencher de pipeline réel.
verification et correctionIntégration GitLab CI
Le job ci-dessous cible un runner tagué macos, correspondant au runner self-hosted persistant décrit plus haut et déjà activé en dehors du pipeline. Il s'exécute uniquement sur les pipelines de merge request et combine les deux modes présentés dans la section suivante, la vérification du poids puis la correction automatique.
# .gitlab-ci.yml
stages:
- verification
- correction
variables:
# Seuil au-delà duquel une image compressée fait échouer le job (en Ko)
THRESHOLD_KB: "6000"
# Clone complet : sans ça, un clone superficiel peut ne pas contenir
# CI_MERGE_REQUEST_DIFF_BASE_SHA et faire échouer le "git diff" ci-dessous.
GIT_DEPTH: "0"
check-image-weight:
stage: verification
tags: [macos]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- |
# Vérifie que jpgboost-cli répond avant d'aller plus loin
jpgboost-cli --help > /dev/null
# Ne regarde que les images ajoutées ou modifiées dans cette merge request
# (périmètre actuel : .jpg et .png)
FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
if [ -z "$FILES" ]; then
echo "Aucune image modifiée dans cette merge request."
exit 0
fi
# Compression de test vers WebP, dans un dossier jetable, pour comparer les tailles
mkdir -p /tmp/check-images
# (pas de "readarray" : macOS embarque encore bash 3.2, où cette builtin
# n'existe pas, introduite seulement en bash 4)
FILES_ARR=()
while IFS= read -r LINE; do
FILES_ARR+=("$LINE")
done <<< "$FILES"
jpgboost-cli "${FILES_ARR[@]}" --format webp --quality 75 --jobs 4 --output /tmp/check-images --json > /tmp/report.json
# Fait échouer le job si une image dépasse encore le seuil une fois compressée
THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
OVER_LIMIT=$(jq --argjson seuil "$THRESHOLD_BYTES" '[.[] | select(.compressedSizeBytes > $seuil)] | length' /tmp/report.json)
if [ "$OVER_LIMIT" -gt 0 ]; then
echo "$OVER_LIMIT image(s) dépassent $THRESHOLD_KB Ko une fois compressées :"
jq --argjson seuil "$THRESHOLD_BYTES" -r '.[] | select(.compressedSizeBytes > $seuil) | .path' /tmp/report.json
exit 1
fi
echo "Toutes les images modifiées respectent le seuil de $THRESHOLD_KB Ko."
fix-image-weight:
stage: correction
tags: [macos]
# Indépendant du stage "verification" : sinon ce job n'est jamais atteint
# dans le cas précis où on en a besoin (check-image-weight en échec).
needs: []
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- |
# Uniquement .jpg/.png, voir le commentaire équivalent dans check-image-weight.
FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
if [ -z "$FILES" ]; then
echo "Aucune image modifiée dans cette merge request."
exit 0
fi
rm -f /tmp/report.json
THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
MIN_QUALITY=20
REMAINING=""
# Convertit chaque fichier en JPEG (--format jpeg, la valeur par défaut) :
# un PNG compresse nettement moins bien qu'un JPEG pour une photo (voir
# « Pièges courants »), et c'est le format cible attendu ici. Ne convient
# pas à un PNG avec transparence (JPEG n'a pas de canal alpha), pas un
# souci pour une photo, à revoir si un jour un logo ou une icône passe
# par ce pipeline.
#
# Redescend la qualité par paliers tant que le résultat dépasse encore le
# seuil : une seule passe à qualité fixe (60) ne suffit pas toujours.
while IFS= read -r FILE; do
FOLDER=$(dirname "$FILE")
QUALITY=60
while :; do
RESULT=$(jpgboost-cli "$FILE" --format jpeg --quality "$QUALITY" --output "$FOLDER" --json)
SIZE=$(echo "$RESULT" | jq '.[0].compressedSizeBytes // 0')
if [ "$SIZE" -le "$THRESHOLD_BYTES" ] || [ "$QUALITY" -le "$MIN_QUALITY" ]; then
break
fi
QUALITY=$((QUALITY - 15))
if [ "$QUALITY" -lt "$MIN_QUALITY" ]; then
QUALITY=$MIN_QUALITY
fi
done
echo "$RESULT" >> /tmp/report.json
# Le fichier de sortie est toujours <base>.jpg : si l'original n'avait
# pas déjà cette extension (ex. .png), il faut retirer l'original du
# repo, sinon les deux coexistent et l'ancien reste non compressé
# indéfiniment.
case "$FILE" in
*.jpg) : ;;
*) git rm -q "$FILE" ;;
esac
if [ "$SIZE" -gt "$THRESHOLD_BYTES" ]; then
echo "$FILE toujours au-dessus de $THRESHOLD_KB Ko après compression (qualité $QUALITY, $((SIZE / 1000)) Ko) : réduction manuelle nécessaire."
REMAINING="$REMAINING $FILE"
fi
done <<< "$FILES"
# Gain de compression, visible dans le log du job (voir « Mesurer le gain »).
# -s : /tmp/report.json contient un tableau JSON par fichier (un ">>" par
# itération plus haut), "add" les fusionne en un seul avant de les sommer.
echo "--- Gain de compression ---"
jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) octets (\(.ratio))"' /tmp/report.json
echo "Total avant : $(jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json) octets"
echo "Total après : $(jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json) octets"
# git diff --quiet ne verrait pas un .png supprimé (git rm) remplacé par un
# .jpg non tracké : status --porcelain couvre aussi les fichiers untracked.
if [ -z "$(git status --porcelain)" ]; then
echo "Rien à committer, toutes les images étaient déjà compressées."
exit 0
fi
git config user.name "jpgboost-ci"
git config user.email "ci@example.com"
git add -A
# [skip ci] : sans ça, ce push relance un nouveau pipeline merge_request_event,
# qui repousse un nouveau commit de correction, etc. (boucle infinie).
git commit -m "Compresse les images modifiées avec jpgboost-cli [skip ci]"
# PUSH_TOKEN est un personal access token (scope write_repository), stocké en
# variable CI/CD masquée : le CI_JOB_TOKEN par défaut ne suffit pas pour
# pousser sur une branche protégée.
git remote set-url origin "https://gitlab-ci-token:${PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
git push origin "HEAD:${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}"
# Le meilleur résultat possible est poussé dans tous les cas (mieux que rien),
# mais le job échoue si au moins un fichier dépasse encore le seuil à qualité
# minimale, pour que ça reste visible plutôt que silencieusement accepté.
if [ -n "$REMAINING" ]; then
echo "Fichier(s) toujours trop lourd(s) malgré la compression :$REMAINING"
exit 1
fi
Mode vérification ou mode correction
Les deux jobs de la section précédente illustrent deux façons opposées de brancher jpgboost-cli sur une merge request GitLab. L'une bloque, l'autre corrige à la place du contributeur.
En mode vérification, le job check-image-weight ne modifie rien. Il compresse chaque image modifiée dans un dossier jetable, compare la taille obtenue au seuil choisi, et fait échouer la CI si ce seuil est dépassé. Le dépôt reste inchangé. C'est au contributeur de retravailler et de recommitter l'image ; le pipeline se contente de refuser la fusion tant que ce n'est pas fait.
En mode correction, le job fix-image-weight va plus loin. Il compresse chaque image directement à sa place dans le dépôt, puis committe et pousse le résultat sur la branche de la merge request. Le contributeur n'a rien à refaire, mais l'historique Git gagne un commit qu'il n'a pas écrit lui-même.
| Critère | Mode vérification | Mode correction |
|---|---|---|
| Objectif | Bloquer la CI si une image dépasse un seuil | Compresser et pousser un commit sur la merge request |
| Mécanisme | --json + jq, comparaison à un seuil choisi, puis exit 1 explicite (aucun flag natif de seuil) | Compression directe, écriture en place, puis push |
| Effet sur le dépôt | Aucun, le job ne fait qu'observer | Un commit automatique ajouté à la merge request |
| Effet sur le contributeur | Doit corriger et recommitter lui-même | N'a rien à faire de plus |
| Authentification requise | Aucune | Project access token ou deploy token en variable masquée |
| Compromis | Friction, correction manuelle laissée au contributeur | Modifie l'historique Git automatiquement, risque de conflits avec une branche protégée |
En pratique, le mode vérification convient à une équipe qui veut garder le contrôle éditorial sur ses images (recadrage, retouche, choix du format) avant qu'elles n'entrent dans le dépôt compressées. Le mode correction convient à une équipe qui préfère ne jamais avoir à y penser, au prix d'un commit automatique de plus dans l'historique et d'un token de push à administrer.
Optimiser le job
Les deux jobs limitent déjà le traitement aux fichiers réellement modifiés via git diff --name-only --diff-filter=ACM, plutôt que de reparcourir l'ensemble du dépôt à chaque pipeline. Sur un dépôt qui accumule des centaines d'images, la différence de temps d'exécution est considérable.
Avec un runner self-hosted persistant, la situation est différente en matière de cache. La directive cache: de GitLab CI sert principalement à restaurer des dépendances sur un runner éphémère qui repart de zéro à chaque job. Ici, JPGBoost.app est déjà installé sur le runner et n'a donc pas besoin d'être retéléchargé. Ajouter un bloc cache: n'apporterait rien de plus que ce que le stockage local du runner fournit déjà.
L'option --json fournit les informations nécessaires pour construire un mécanisme d'idempotence. En conservant, d'une exécution à l'autre, compressedSizeBytes ou, mieux encore, un hash du fichier, il serait possible de détecter les fichiers inchangés et d'éviter de les recompresser à chaque pipeline.
Le parallélisme, quant à lui, est géré nativement par --jobs. Plusieurs fichiers peuvent être traités simultanément, chacun étant décodé puis libéré indépendamment. L'empreinte mémoire dépend donc principalement de la valeur de --jobs, et non du nombre total de fichiers en attente. Sur un lot de 12 fichiers, la documentation indique un gain d'environ 4× entre --jobs 8 et --jobs 1. C'est un ordre de grandeur utile pour dimensionner cette valeur sur votre propre runner, en gardant à l'esprit que les encodages AVIF et JPEG XL sont nettement plus coûteux en temps CPU que JPEG ou HEIC.
Pièges courants
Le premier piège concerne le format de sortie. Le script ci-dessus convertit délibérément toutes les images, y compris les PNG, en JPEG. Pour une photographie, le JPEG offre généralement un meilleur ratio qualité/poids qu'un PNG : forcer --format jpeg peut donc permettre de passer sous le seuil de taille là où une simple baisse de qualité ne suffirait pas. Le revers est la perte de la transparence, puisque le JPEG ne gère pas de canal alpha, sans qu'aucune erreur ne soit nécessairement remontée dans les logs. Ce choix convient aux photos, mais peut casser silencieusement un logo ou une icône PNG à fond transparent. Dans un dépôt qui mélange ces usages, mieux vaut donc restreindre le périmètre du job, par exemple à un dossier dédié, plutôt que d'imposer un format unique à toutes les images.
Sur GitLab CI, un job d'un stage ne s'exécute par défaut que si tous les jobs du stage précédent ont réussi. Or fix-image-weight n'a de sens que lorsque check-image-weight détecte un dépassement, précisément le cas dans lequel, avec ce comportement par défaut, il ne serait jamais exécuté. needs: [] l'affranchit de cette dépendance implicite et lui permet de s'exécuter indépendamment, en parallèle du job de vérification.
Le job de correction pousse ensuite un commit sur la branche à l'origine du pipeline. Sans précaution, ce push peut déclencher un nouveau pipeline de merge request. Si le résultat est toujours jugé trop lourd, le job corrige à nouveau, pousse un nouveau commit, puis relance encore un pipeline. On peut alors rapidement se retrouver avec une boucle générant des dizaines de commits et de pipelines en quelques minutes. Ajouter [skip ci] au message du commit automatique indique à GitLab de ne pas déclencher de nouveau pipeline pour ce commit.
Le git diff effectué par rapport à CI_MERGE_REQUEST_DIFF_BASE_SHA suppose également que le commit de base est disponible localement. Or GitLab effectue par défaut un clone avec une profondeur limitée. Sur une merge request qui accumule de nombreux commits, ou lorsque la branche cible a fortement divergé, le commit recherché peut donc ne pas être présent. Le git diff échoue alors pour une raison sans rapport avec le poids des images. Définir GIT_DEPTH: "0" force un clone complet et évite ce problème, au prix d'un temps de clonage plus important à chaque job.
Un autre piège concerne la détection des changements. git diff --quiet ne détecte que les modifications apportées à des fichiers déjà suivis par Git. Or, dès lors que le script convertit une image en JPEG et supprime l'original avec git rm, le résultat est composé de deux opérations : la suppression trackée du .png et la création d'un nouveau .jpg non tracké. Un git diff classique peut alors ne rien signaler. En pratique, le job peut afficher un gain de compression bien réel dans ses logs, conclure malgré tout « Rien à committer » et ne jamais pousser le résultat. git status --porcelain, qui inclut également les fichiers non trackés, est mieux adapté à ce cas.
Les images déjà présentes dans l'historique Git posent également un problème de taille. Même lorsqu'elles sont remplacées par une version compressée dans un nouveau commit, les anciennes versions restent dans l'historique sous forme de blobs Git. Le dépôt ne récupère donc pas automatiquement l'espace occupé par ces fichiers. Seule une réécriture de l'historique permettrait de supprimer ces anciennes données, mais c'est une opération destructive qui n'a pas sa place dans un pipeline automatique.
Le commit automatique du mode correction peut par ailleurs se heurter aux règles du dépôt. Une branche protégée peut interdire les pushes directs ou exiger une revue avant fusion. De la même manière, un hook ou un autre mécanisme de formatage peut modifier les mêmes fichiers et entrer en conflit avec le job de compression. Ces interactions doivent être testées explicitement plutôt que de supposer que les différents mécanismes fonctionneront sans friction.
Enfin, le pipeline ne couvre que les images qui transitent par le dépôt et, plus précisément, par le flux de merge request concerné. Un designer qui dépose directement une image dans un CMS, un dossier partagé ou tout autre espace servant des assets en dehors de Git contourne complètement ce contrôle. Le pipeline améliore donc la qualité des images versionnées dans le dépôt, mais ne constitue pas à lui seul une politique globale de gestion des assets.
Mesurer le gain
fix-image-weight affiche déjà ce rapport dans son propre log, juste après la boucle de compression (section « Intégration GitLab CI » plus haut), visible depuis l'onglet Pipelines de la merge request, sans rien à ajouter. Le détail : le champ ratio de la sortie --json donne directement le pourcentage de réduction par fichier, à côté de originalSizeBytes et compressedSizeBytes.
jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) octets (\(.ratio))"' /tmp/report.json
Pour le total agrégé, la commande exacte dépend de la façon dont le job écrit /tmp/report.json. fix-image-weight écrit un tableau JSON par fichier au fil de sa boucle (>>), donc le fichier final contient plusieurs tableaux concaténés plutôt qu'un seul. -s (slurp) est alors nécessaire pour tout lire, mais il enveloppe ces tableaux dans un tableau supplémentaire : map échoue directement dessus (jq: error: Cannot index array with string) sans un add pour les aplatir d'abord. C'est la forme utilisée dans le script plus haut.
# Total avant et après compression, sur l'ensemble du lot
# (fichier écrit en plusieurs appels : fix-image-weight)
jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json
jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json
Si vous construisez votre propre rapport ailleurs, en local (section « Installer jpgboost-cli ») ou dans check-image-weight, qui compresse tous les fichiers en un seul appel (>), /tmp/report.json y est déjà un unique tableau JSON, et -s n'est alors ni nécessaire ni correct : il produit la même erreur, pour la raison inverse, en enveloppant un tableau déjà complet.
# Même calcul, sur un rapport écrit en un seul appel (test local, ou check-image-weight)
jq 'map(.originalSizeBytes) | add' /tmp/report.json
jq 'map(.compressedSizeBytes) | add' /tmp/report.json
jpgboost-cli ne mesure que ce qu'il produit lui-même, le poids avant et après. Le score de performance (LCP, score Lighthouse) reste à obtenir séparément, avec un outil dédié. Rien dans l'outil ne le calcule nativement, et l'associer artificiellement au pourcentage de compression serait une extrapolation non vérifiée.