Cómo automatizar la compresión de imágenes en tu pipeline de CI con jpgboost-cli
Un repositorio que acumula JPEG y PNG sin comprimir degrada el LCP y, al mismo tiempo, hace más pesado cada clonado. jpgboost-cli permite automatizar su compresión directamente en tu pipeline de CI, sin depender de la vigilancia de cada persona que contribuye. Este artículo presenta su integración con GitLab CI, compara dos estrategias opuestas y repasa los errores que pueden salir caros en producción.
El costo real de las imágenes sin comprimir
Cada imagen que se hace commit sin comprimir deja un rastro en el historial de Git como un blob que contiene su contenido. Incluso cuando esa imagen se sustituye más adelante por una versión más ligera, el blob antiguo permanece en el historial. Un git clone también descarga ese historial. Así que el peso de las imágenes antiguas sigue lastrando el repositorio aunque ya no se usen.
En un repositorio que acumula capturas de pantalla, material de marketing o exportaciones de diseño durante varios meses, ese peso muerto alcanza rápidamente varios cientos de megabytes. Eso ralentiza los clonados en la CI y aumenta el consumo de ancho de banda de todas las personas que contribuyen.
En el lado de producción, el problema no se detiene en el repositorio. Una imagen sobredimensionada perjudica directamente el Largest Contentful Paint (LCP), sobre todo cuando la imagen más grande situada por encima del pliegue pasa a ser el elemento que determina la métrica. La ganancia de la compresión puede ser considerable: en el propio ejemplo de la documentación de jpgboost-cli, un JPEG de 4,2 MB baja a apenas 890 KB, una reducción del 79 %.
La compresión manual no escala. Depende del hábito individual. En cada merge request, cada persona que contribuye tiene que acordarse de optimizar las imágenes antes de hacer commit. En la práctica, ese paso se salta con facilidad en cuanto aprieta el tiempo o surge una urgencia. El problema suele descubrirse semanas más tarde, cuando una auditoría o un informe de rendimiento revela la deuda acumulada.
Por qué automatizar en la CI y no en local ni en el servidor
Comprimir las imágenes en local, por ejemplo con un hook pre-commit de Git, siempre depende de la máquina y de la disciplina de quien contribuye. Un hook se puede saltar, desinstalar o sencillamente faltar en el equipo de una diseñadora que hace commit de los archivos directamente.
La compresión en el servidor, por su parte, llega demasiado tarde. Las imágenes ya se han indexado y puede que ya se hayan servido alguna vez. El problema, por tanto, es visible para las primeras visitas, y luego hay que rehacer la optimización en cada despliegue.
La CI es el punto de control más fiable. Se ejecuta en cada merge request, no depende de la configuración de la máquina local y produce un resultado visible y trazable en los registros del job. Ese único punto de paso obligado es lo que hace que la automatización merezca la pena, aunque conlleve un costo de infraestructura que hay que tener en cuenta desde el principio.
Ese costo pesa más aquí porque jpgboost-cli no es un binario independiente que se pueda instalar con un gestor de paquetes. Se distribuye con JPGBoost.app, solo funciona en macOS 15 o posterior y requiere una licencia Pro.
El resto de este artículo se apoya, por tanto, en una premisa concreta: un runner de macOS autoalojado y persistente, para disponer de un entorno estable y mantener la instalación de JPGBoost.app de un job al siguiente.
Instalar jpgboost-cli y probarlo en local
Tanto en el runner como en la prueba local, el binario viene dentro del paquete de la aplicación. Un enlace simbólico creado una sola vez permite después invocarlo desde cualquier carpeta.
# Enlace simbólico, una sola vez, para invocar jpgboost-cli desde cualquier carpeta
sudo ln -s /Applications/JPGBoost.app/Contents/MacOS/jpgboost-cli /usr/local/bin/jpgboost-cli
# Comprobar que el comando responde
jpgboost-cli --help
La licencia Pro se activa después una sola vez, de forma manual, en el runner. Esto nunca ocurre dentro de la propia pipeline. Si esta es la primerísima instalación con la que se activa esta licencia, usa el token que recibiste por correo electrónico tras la compra.
# Identificador de esta máquina, por si hay que facilitárselo al soporte
jpgboost-cli --machine-id
# Primera activación, con el token recibido por correo tras la compra
jpgboost-cli --activate ACT-XXXX-XXXX-XXXX-XXXX-XXXX
Si la licencia ya se ha activado en otro sitio (normalmente en la aplicación, para uso personal), --activate deja de ser el enfoque correcto. El runner tiene que unirse a esa licencia existente en lugar de activar una nueva. Desde el dispositivo ya activado, genera un código y úsalo después en el runner.
# Desde el dispositivo ya activado (la app u otra CLI), genera un código
jpgboost-cli --add-device
# En el runner, se une a la licencia existente con este código
jpgboost-cli --pair XXXX-XXXX
Esta activación, por cualquiera de los dos métodos, cuenta como una de las dos instalaciones que cubre la licencia Pro. Como la aplicación y la CLI cuentan como dos instalaciones distintas, activar en cada job agotaría la cuota enseguida: con solo dos ejecuciones, la licencia ya estaría completamente consumida. Por eso el runner tiene que ser persistente, para conservar la activación de un job al siguiente.
Prueba rápida en local antes de seguir.
# Dos archivos a WebP, calidad 60
jpgboost-cli photo1.jpg photo2.png --quality 60 --format webp --output ./compressed
Configurar GitLab (runner y token de push)
Antes de pegar el archivo .gitlab-ci.yml de más abajo, hay que preparar dos cosas una sola vez en el lado de GitLab: registrar el runner y crear el token que el job de corrección necesita para hacer push.
Registrar el runner
En el Mac elegido como runner persistente (aquel donde jpgboost-cli y su licencia ya están instalados según la sección anterior), registra el runner con la etiqueta macos que usan los dos jobs.
gitlab-runner register \
--url https://gitlab.com \
--token <PROJECT_REGISTRATION_TOKEN>
El token de registro está en Settings > CI/CD > Runners > New project runner (la interfaz genera un token de un solo uso para este registro, en lugar del antiguo token de registro compartido). --executor shell es clave aquí: a diferencia de un executor de Docker, ejecuta los comandos directamente en el sistema del runner, algo imprescindible para encontrar jpgboost-cli ya instalado y su licencia ya activada de un job al siguiente, en vez de partir de un entorno limpio cada vez.
gitlab-runner register pide el nombre del runner y luego el executor de forma interactiva; responde shell a esta última pregunta
Crear el token de push
El job fix-image-weight hace push de un commit en la rama de la merge request. El CI_JOB_TOKEN que se proporciona automáticamente a cada job no tiene los permisos de escritura necesarios en una rama protegida, así que hace falta un token específico.
Un Project Access Token (Settings > Access Tokens del proyecto) es, en teoría, la opción más limpia: al estar vinculado al proyecto y no a una cuenta, sobrevive a la marcha de quien lo creó. Pero en un espacio de nombres personal con el plan gratuito, GitLab.com no ofrece esta funcionalidad, reservada a los grupos de pago: ese es el motivo más habitual para recurrir en su lugar a un Personal Access Token.
Un Personal Access Token se crea desde los ajustes de la cuenta de usuario, no desde los del proyecto (Avatar > Edit profile > Access Tokens), con:
- Ámbito
write_repository - Una fecha de caducidad coherente con tu política de rotación de tokens
Una contrapartida que conviene conocer antes de elegir esta vía: el token está vinculado a la cuenta que lo creó. Si esa cuenta se desactiva, pierde el acceso al proyecto o la persona deja el equipo, el job de corrección deja de poder hacer push, sin ningún aviso previo. En un proyecto de equipo, es mejor crearlo desde una cuenta de servicio específica que desde la cuenta personal de alguien que contribuye.
El token generado solo se muestra una vez: cópialo de inmediato y agrégalo después en Settings > CI/CD > Variables del proyecto con el nombre PUSH_TOKEN, con las opciones Masked y Protected marcadas si tus merge requests apuntan a una rama protegida.
PUSH_TOKEN, y se usa más adelante en el .gitlab-ci.ymlSi la rama de origen de tus merge requests está protegida a su vez, la cuenta asociada al token necesita además permiso para hacer push directamente en ella, en Settings > Repository > Protected branches > Allowed to push and merge; de lo contrario, el git push del job falla con una denegación de acceso pese a tener un token válido.
Dónde colocar el archivo .gitlab-ci.yml
GitLab solo busca la pipeline en un único sitio por defecto: un archivo llamado exactamente .gitlab-ci.yml (con el punto inicial), en la raíz del repositorio, al mismo nivel que el README. Un archivo colocado en una subcarpeta, o con otro nombre, simplemente se ignora: ningún error lo señala, el proyecto se comporta como si no tuviera ninguna CI configurada.
Si hace falta otra ubicación, por ejemplo un archivo de pipeline compartido entre varios repositorios o una estructura de monorepo, Settings > CI/CD > General pipelines > CI/CD configuration file permite apuntar a otra ruta, incluso en otro proyecto, con la sintaxis ruta/archivo.yml@grupo/proyecto:rama. Fuera de ese caso concreto, deja el campo vacío y mantén el archivo en la raíz.
Una vez que el archivo está en un commit y se ha hecho push, no hace falta ninguna activación manual: GitLab lo detecta automáticamente en el siguiente evento que coincida con una regla del archivo, aquí la apertura o actualización de una merge request, mediante $CI_PIPELINE_SOURCE == "merge_request_event". El resultado aparece en la pestaña Pipelines del proyecto y directamente en la pestaña del mismo nombre de la merge request. Para comprobar la sintaxis antes de hacer push, en lugar de descubrirlo con la primera pipeline fallida, el editor integrado (Build > Pipeline editor en el menú del proyecto) incluye un botón Validate que llama al linter de CI de GitLab sin iniciar una pipeline real.
verification y correctionIntegración con GitLab CI
El job de más abajo apunta a un runner etiquetado como macos, que corresponde al runner autoalojado persistente descrito antes y ya activado fuera de la pipeline. Se ejecuta únicamente en las pipelines de merge request y combina los dos modos que se presentan en la sección siguiente: la verificación del peso y luego la corrección automática.
# .gitlab-ci.yml
stages:
- verification
- correction
variables:
# Umbral a partir del cual una imagen comprimida hace fallar el job (en KB)
THRESHOLD_KB: "6000"
# Clonado completo: sin esto, un clonado superficial puede no contener
# CI_MERGE_REQUEST_DIFF_BASE_SHA y hacer fallar el "git diff" de más abajo.
GIT_DEPTH: "0"
check-image-weight:
stage: verification
tags: [macos]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- |
# Comprobar que jpgboost-cli responde antes de seguir
jpgboost-cli --help > /dev/null
# Mirar solo las imágenes agregadas o modificadas en esta merge request
# (ámbito actual: .jpg y .png)
FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
if [ -z "$FILES" ]; then
echo "Ninguna imagen modificada en esta merge request."
exit 0
fi
# Compresión de prueba a WebP, en una carpeta desechable, para comparar tamaños
mkdir -p /tmp/check-images
# (nada de "readarray": macOS todavía incluye bash 3.2, donde esta builtin
# no existe, se introdujo solo 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
# Hace fallar el job si una imagen sigue superando el umbral una vez comprimida
THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
OVER_LIMIT=$(jq --argjson threshold "$THRESHOLD_BYTES" '[.[] | select(.compressedSizeBytes > $threshold)] | length' /tmp/report.json)
if [ "$OVER_LIMIT" -gt 0 ]; then
echo "$OVER_LIMIT imagen(es) siguen superando $THRESHOLD_KB KB una vez comprimidas:"
jq --argjson threshold "$THRESHOLD_BYTES" -r '.[] | select(.compressedSizeBytes > $threshold) | .path' /tmp/report.json
exit 1
fi
echo "Todas las imágenes modificadas están por debajo del umbral de $THRESHOLD_KB KB."
fix-image-weight:
stage: correction
tags: [macos]
# Independiente de la etapa "verification": de lo contrario, este job nunca se
# alcanza en el único caso en que hace falta (que check-image-weight falle).
needs: []
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- |
# Solo .jpg/.png, ver el comentario equivalente en check-image-weight.
FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
if [ -z "$FILES" ]; then
echo "Ninguna imagen modificada en esta merge request."
exit 0
fi
rm -f /tmp/report.json
THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
MIN_QUALITY=20
REMAINING=""
# Convierte todos los archivos a JPEG (--format jpeg, el valor por defecto):
# un PNG comprime bastante peor que un JPEG para una fotografía (ver "Errores
# frecuentes"), y ese es el formato de destino esperado aquí. No sirve para un
# PNG con transparencia (JPEG no tiene canal alfa), lo cual no es problema para
# una foto, pero conviene revisarlo si algún día pasa un logotipo o un icono
# por esta pipeline.
#
# Va bajando la calidad por escalones mientras el resultado siga superando el
# umbral: una sola pasada con calidad fija (60) no siempre basta.
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
# El archivo de salida siempre es <base>.jpg: si el original no tenía ya esa
# extensión (por ejemplo .png), hay que eliminar el original del repositorio;
# si no, ambos conviven y el antiguo se queda sin comprimir indefinidamente.
case "$FILE" in
*.jpg) : ;;
*) git rm -q "$FILE" ;;
esac
if [ "$SIZE" -gt "$THRESHOLD_BYTES" ]; then
echo "$FILE sigue por encima de $THRESHOLD_KB KB tras la compresión (calidad $QUALITY, $((SIZE / 1000)) KB): hace falta una reducción manual."
REMAINING="$REMAINING $FILE"
fi
done <<< "$FILES"
# Ganancia de compresión, visible en el registro del job (ver "Medir la ganancia").
# -s: /tmp/report.json contiene un array JSON por archivo (un ">>" por
# iteración arriba), "add" los fusiona en uno solo antes de sumarlos.
echo "--- Ganancia de compresión ---"
jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) bytes (\(.ratio))"' /tmp/report.json
echo "Total antes: $(jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json) bytes"
echo "Total después: $(jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json) bytes"
# git diff --quiet no vería un .png eliminado (git rm) y sustituido por un .jpg
# sin seguimiento: status --porcelain también cubre los archivos sin seguimiento.
if [ -z "$(git status --porcelain)" ]; then
echo "Nada que confirmar, todas las imágenes ya estaban comprimidas."
exit 0
fi
git config user.name "jpgboost-ci"
git config user.email "ci@example.com"
git add -A
# [skip ci]: sin esto, este push inicia una nueva pipeline merge_request_event,
# que hace push de otro commit de corrección, y así sucesivamente (bucle infinito).
git commit -m "Comprimir las imágenes modificadas con jpgboost-cli [skip ci]"
# PUSH_TOKEN es un personal access token (ámbito write_repository), guardado como
# variable de CI/CD enmascarada: el CI_JOB_TOKEN por defecto no basta para hacer
# push a una rama protegida.
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}"
# El mejor resultado posible se sube igualmente (mejor que nada), pero el job
# falla si al menos un archivo sigue superando el umbral con la calidad mínima,
# para que quede visible en lugar de aceptarse en silencio.
if [ -n "$REMAINING" ]; then
echo "Archivo(s) todavía demasiado pesados pese a la compresión:$REMAINING"
exit 1
fi
Modo verificación o modo corrección
Los dos jobs de la sección anterior ilustran dos maneras opuestas de conectar jpgboost-cli a una merge request de GitLab. Uno bloquea, el otro corrige en lugar de quien contribuye.
En modo verificación, el job check-image-weight no cambia nada. Comprime cada imagen modificada en una carpeta desechable, compara el tamaño resultante con el umbral elegido y hace fallar la CI si se supera ese umbral. El repositorio queda intacto. Le corresponde a quien contribuye retrabajar la imagen y volver a hacer commit; la pipeline solo se niega a dejar que se fusione hasta que eso esté hecho.
En modo corrección, el job fix-image-weight va más allá. Comprime cada imagen directamente en su sitio dentro del repositorio, y después hace commit y push del resultado en la rama de la merge request. Quien contribuye ya no tiene nada que rehacer, pero el historial de Git gana un commit que no ha escrito.
| Criterio | Modo verificación | Modo corrección |
|---|---|---|
| Objetivo | Bloquear la CI si una imagen supera un umbral | Comprimir y hacer push de un commit en la merge request |
| Mecanismo | --json + jq, comparación con un umbral elegido, y luego un exit 1 explícito (no hay opción de umbral integrada) | Compresión directa, escritura en el sitio, y luego push |
| Efecto en el repositorio | Ninguno, el job solo observa | Un commit automático agregado a la merge request |
| Efecto en quien contribuye | Tiene que corregirlo y volver a hacer commit | Nada más que hacer |
| Autenticación necesaria | Ninguna | Project access token o deploy token en una variable enmascarada |
| Contrapartida | Fricción, la corrección manual queda en manos de quien contribuye | Reescribe el historial de Git automáticamente, riesgo de conflictos con una rama protegida |
En la práctica, el modo verificación encaja con un equipo que quiere conservar el control editorial sobre sus imágenes (recorte, retoque, elección de formato) antes de que entren comprimidas en el repositorio. El modo corrección encaja con un equipo que prefiere no tener que pensar nunca en ello, a costa de un commit automático más en el historial y de un token de push que gestionar.
Optimizar el job
Los dos jobs ya limitan el trabajo a los archivos realmente modificados, mediante git diff --name-only --diff-filter=ACM, en lugar de volver a escanear todo el repositorio en cada pipeline. En un repositorio que acumula cientos de imágenes, la diferencia en tiempo de ejecución es considerable.
Con un runner autoalojado persistente, la caché funciona de otra manera. La directiva cache: de GitLab CI existe sobre todo para restaurar dependencias en un runner efímero que parte de cero en cada job. Aquí, JPGBoost.app ya está instalado en el runner y no hay que volver a descargarlo. Agregar un bloque cache: no aportaría nada más allá de lo que ya ofrece el almacenamiento local del runner.
La opción --json proporciona la información necesaria para construir un mecanismo de idempotencia. Conservando compressedSizeBytes, o mejor aún un hash del archivo, de una ejecución a la siguiente, sería posible detectar los archivos sin cambios y evitar recomprimirlos en cada pipeline.
El paralelismo, por su parte, está cubierto de forma nativa mediante --jobs. Se pueden procesar varios archivos a la vez, y cada uno se decodifica y se libera de forma independiente. La huella de memoria depende, por tanto, sobre todo del valor de --jobs, y no del número total de archivos pendientes. En un lote de 12 archivos, la documentación indica una ganancia de aproximadamente 4× entre --jobs 8 y --jobs 1, un orden de magnitud útil para dimensionar este valor en tu propio runner, teniendo en cuenta que la codificación de AVIF y JPEG XL exige bastante más CPU que la de JPEG o HEIC.
Errores frecuentes
El primer error tiene que ver con el formato de salida. El script de más arriba convierte deliberadamente todas las imágenes, PNG incluidos, a JPEG. Para una fotografía, JPEG ofrece por lo general una mejor relación entre calidad y tamaño que PNG: forzar --format jpeg puede, por tanto, hacer que un archivo baje del umbral de tamaño allí donde una simple bajada de calidad no bastaría. La contrapartida es la pérdida de la transparencia, ya que JPEG no tiene canal alfa, sin que aparezca necesariamente ningún error en los registros. Esta elección funciona para las fotos, pero puede romper en silencio un logotipo o un icono PNG con fondo transparente. En un repositorio que mezcla ambos usos, es mejor restringir el ámbito del job (a una carpeta específica, por ejemplo) que imponer un único formato a todas las imágenes.
En GitLab CI, un job de una etapa determinada solo se ejecuta por defecto si todos los jobs de la etapa anterior han terminado bien. Pero fix-image-weight solo tiene sentido cuando check-image-weight detecta un exceso, precisamente el caso en el que, con ese comportamiento por defecto, nunca se ejecutaría. needs: [] libera al job de corrección de esa dependencia implícita y le permite ejecutarse de forma independiente, en paralelo con el job de verificación.
El job de corrección hace después push de un commit en la rama que ha iniciado la pipeline. Sin precauciones, ese push puede iniciar una nueva pipeline de merge request. Si el resultado se sigue considerando demasiado pesado, el job vuelve a corregir, hace push de otro commit y inicia otra pipeline más. Eso puede degenerar rápidamente en un bucle que genera decenas de commits y pipelines en cuestión de minutos. Agregar [skip ci] al mensaje del commit automático le indica a GitLab que no inicie una nueva pipeline para ese commit.
El git diff ejecutado contra CI_MERGE_REQUEST_DIFF_BASE_SHA también da por hecho que ese commit base está disponible en local. Sin embargo, GitLab clona con una profundidad limitada por defecto. En una merge request que ha acumulado muchos commits, o cuando la rama de destino ha divergido bastante, puede que el commit buscado sencillamente no esté ahí. El git diff falla entonces por un motivo que no tiene nada que ver con el peso de las imágenes. Fijar GIT_DEPTH: "0" fuerza un clonado completo y evita este problema, a costa de un clonado más largo en cada job.
Otro error tiene que ver con la detección de cambios. git diff --quiet solo detecta cambios en archivos a los que Git ya hace seguimiento. Pero en cuanto el script convierte una imagen a JPEG y elimina el original con git rm, el cambio real se compone de dos operaciones: la eliminación con seguimiento del .png y la creación de un .jpg nuevo y sin seguimiento. Un git diff normal puede entonces no informar de nada en absoluto. En la práctica, el job puede mostrar en sus registros una ganancia de compresión perfectamente real, concluir igualmente "Nada que confirmar" y no hacer push nunca del resultado. git status --porcelain, que también cubre los archivos sin seguimiento, no se deja engañar de la misma manera.
Las imágenes que ya están en el historial de Git son además un problema de tamaño por sí mismas. Aunque se sustituyan por una versión comprimida en un commit nuevo, las versiones antiguas permanecen en el historial como blobs de Git. El repositorio no recupera automáticamente el espacio que ocupan esos archivos. Solo reescribir el historial eliminaría esos datos antiguos, y esa es una operación destructiva que no tiene cabida en una pipeline automatizada.
El commit automático del modo corrección también puede chocar con las propias reglas del repositorio. Una rama protegida puede prohibir los push directos o exigir una revisión antes de la fusión. Del mismo modo, un hook u otro mecanismo de formateo podría tocar los mismos archivos y entrar en conflicto con el job de compresión. Estas interacciones hay que probarlas explícitamente, en lugar de dar por supuesto que van a convivir sin roces.
Por último, la pipeline solo cubre las imágenes que pasan por el repositorio y, más concretamente, por el flujo de merge request en cuestión. Una diseñadora que deje una imagen directamente en un CMS, en una carpeta compartida o en cualquier otro almacén de recursos fuera de Git se salta por completo esta comprobación. La pipeline mejora la calidad de las imágenes versionadas en el repositorio, pero no es, por sí sola, una política completa de gestión de recursos.
Medir la ganancia
fix-image-weight ya muestra este informe en su propio registro, justo después del bucle de compresión (ver la sección "Integración con GitLab CI" más arriba), visible desde la pestaña Pipelines de la merge request, sin nada que agregar. El detalle: el campo ratio de la salida --json da directamente el porcentaje de reducción por archivo, junto a originalSizeBytes y compressedSizeBytes.
jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) bytes (\(.ratio))"' /tmp/report.json
Para el total agregado, el comando exacto depende de cómo escriba el job /tmp/report.json. fix-image-weight escribe un array JSON por archivo a lo largo de su bucle (>>), así que el archivo final contiene varios arrays concatenados en lugar de uno solo. Entonces hace falta -s (slurp) para leerlo todo, pero eso envuelve esos arrays en un array adicional: map falla directamente sobre él (jq: error: Cannot index array with string) si no hay un add que los aplane antes. Esa es la forma que se usa en el script de más arriba.
# Total antes y después de la compresión, sobre todo el lote
# (archivo escrito a lo largo de varias llamadas: fix-image-weight)
jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json
jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json
Si construyes tu propio informe en otro sitio, en local (ver "Instalar jpgboost-cli") o en check-image-weight, que comprime todos los archivos en una sola llamada (>), ahí /tmp/report.json ya es un único array JSON, y entonces -s no es ni necesario ni correcto: produce el mismo error, por el motivo contrario, al envolver un array que ya está completo.
# El mismo cálculo, sobre un informe escrito en una sola llamada
# (prueba local, o check-image-weight)
jq 'map(.originalSizeBytes) | add' /tmp/report.json
jq 'map(.compressedSizeBytes) | add' /tmp/report.json
jpgboost-cli solo mide lo que produce él mismo: el tamaño antes y después. Las puntuaciones de rendimiento (LCP, puntuación de Lighthouse) hay que obtenerlas aparte, con una herramienta específica. Nada en la herramienta lo calcula de forma nativa, y asociarlo artificialmente al porcentaje de compresión sería una extrapolación no verificada.