feat(ci): construire l'image des jobs CI lourds sur chaque machine à runner

Les jobs CI du dépôt kadans réinstallent, à CHAQUE exécution, des choses qui
changent tous les trimestres. Mesuré le 2026-07-29 sur le run 628 (runner
ARM64, 2 vCPU / 3 Gio) :

  npm install -g bun                        →    8,6 s
  playwright install --with-deps chromium   →  104,0 s   (apt-get, à chaque run)
                                               ────────
                                                112,6 s jetées par run, sur le
                                                job qui EST le chemin critique

Le cache `actions/cache` ne peut rien contre ces 104 s : il couvre le NAVIGATEUR
(299 Mo déjà mis en cache), pas ses dépendances SYSTÈME — `--with-deps` relance
apt quoi qu'il arrive.

POURQUOI CONSTRUIRE ICI PLUTÔT QUE POUSSER UNE IMAGE. La tentative de publier
l'image au registre a échoué (kadans#224 puis #225) : 3,81 Go dont une couche
unique de 1,36 Go, `docker push` casse en « connection reset by peer » — 7
couches passent, 3 sont retentées 50 fois puis abandonnées. À titre de
comparaison, runner-images:ubuntu-latest-ca (534,7 Mo, plus grosse couche
261 Mo) passe sans problème : la limite est entre 261 Mo et ~500 Mo par couche.
Construire localement supprime le problème — aucune couche ne traverse le
réseau.

Et c'est bien sur CHAQUE machine : avec `capacity: 1`, le parallélisme vient de
plusieurs Raspberry, et `container:` est résolu par le runner. Un job qui
atterrit là où l'image manque échoue AVANT sa première étape.

Trois choix de conception :

1. L'image hérite de runner-images:ubuntu-latest-ca, donc du certificat de la CA
   interne (step-ca). Repartir de node:20-bookworm obligerait à réinjecter le CA
   à la main, et tout job parlant à gitea.arcodange.lab échouerait en TLS.

2. TROIS `RUN` séparés (bun / dépendances système / navigateur), délibérément.
   La version qui a échoué faisait une couche de 1,36 Go. Ne pas les fusionner
   pour « gagner une couche ».

3. L'image est ÉPINGLÉE par un conteneur factice — ce qui implémente enfin la
   section 1 de docs/adr/20260407-docker-storage-gitea-runner.md, restée à
   l'état de proposition : system_docker.yml n'applique que le data-root sur
   disque externe et les log-opts. Sans épinglage, le ramasse-miettes de Docker
   supprime l'image dès que le disque se remplit — panne déjà constatée sur les
   images de runner elles-mêmes.

Le rôle vérifie sa sortie (`docker run … bun --version`) au lieu de supposer que
le build a suffi, et l'image écrit ses versions dans /etc/ci-base.versions pour
que la CI de kadans puisse les confronter à son bun.lock et échouer FORT sur une
dérive, plutôt que de la découvrir en « Executable doesn't exist ».

Nouveau label runner `ci-node-playwright`. `container.force_pull: false` est
déjà en place et devient REQUIS pour ce label : sans lui, act_runner tenterait
un pull d'une image qui n'est dans aucun registre.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
CI Bot
2026-07-29 20:23:59 +02:00
co-authored by Claude Opus 5
parent 726456c5ed
commit fdecabf8ca
4 changed files with 178 additions and 1 deletions
@@ -5,6 +5,13 @@
roles:
- arcodange.factory.gitea_token # generate gitea_api_token used to replace generated token with set name if required
# Image de base des jobs CI lourds (Node + Bun + Chromium), construite ICI,
# sur chaque machine à runner, puis épinglée contre le ramasse-miettes Docker.
# Le même groupe d'hôtes que le runner, et ce n'est pas un détail : avec
# `capacity: 1` (ci-dessous), le parallélisme vient de PLUSIEURS machines, et
# `container:` est résolu par le runner — un job qui atterrit là où l'image
# manque échoue AVANT sa première étape.
- arcodange.factory.ci_base_image
tasks:
@@ -32,7 +39,7 @@
http://{{ hostvars[groups.gitea[0]].ansible_host }}:3000
GITEA_RUNNER_REGISTRATION_TOKEN: "{{ gitea_runner_token_cmd.stdout }}"
GITEA_RUNNER_NAME: arcodange_global_runner_{{ inventory_hostname }}
GITEA_RUNNER_LABELS: ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca
GITEA_RUNNER_LABELS: ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ci-node-playwright:docker://ci-node-playwright:latest
ports:
- "43707:43707"
networks:
@@ -91,6 +98,15 @@
labels:
- "ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca"
- "ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca"
# Jobs CI lourds (Node + Bun + Chromium préinstallés) —
# image construite LOCALEMENT par le rôle ci_base_image, sur
# cette machine. Elle n'est volontairement PAS dans le
# registre : 3,81 Go dont une couche de 1,36 Go, dont le
# push casse en « connection reset by peer » (mesuré
# 2026-07-29, kadans#225). `force_pull: false` ci-dessous
# est donc REQUIS pour ce label — sans lui, act_runner
# tenterait un pull et échouerait.
- "ci-node-playwright:docker://ci-node-playwright:latest"
cache:
# Enable cache server to use actions/cache.
@@ -0,0 +1,43 @@
---
# Image de base des jobs CI lourds (Node + Bun + Chromium), construite SUR CHAQUE
# machine qui héberge un runner Gitea.
#
# POURQUOI CONSTRUIRE PLUTÔT QUE POUSSER (mesuré le 2026-07-29, kadans#224/#225) :
# une image Node + Playwright + Chromium pèse 3,81 Go, avec une couche unique de
# 1,36 Go. Son `docker push` vers gitea.arcodange.lab casse en
# « connection reset by peer » : 7 couches passent, 3 sont réinitialisées et
# retentées 50 fois avant abandon. À titre de comparaison,
# `runner-images:ubuntu-latest-ca` (534,7 Mo, plus grosse couche 261 Mo) passe
# sans problème — la limite est donc entre 261 Mo et ~500 Mo par couche.
#
# Construire localement supprime le problème : aucune couche ne traverse le
# réseau. Et comme `capacity: 1` par runner (03_cicd.yml), le parallélisme vient
# de PLUSIEURS machines — l'image doit donc exister sur CHACUNE d'elles, ce que
# ce rôle garantit.
# On hérite de l'image de runner maison : elle porte DÉJÀ le certificat de la CA
# interne (step-ca). Repartir de `node:20-bookworm` obligerait à réinjecter le CA
# à la main, et un job qui parle à gitea.arcodange.lab échouerait en TLS.
ci_base_image_from: gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca
ci_base_image_name: ci-node-playwright
ci_base_image_tag: latest
# ⚠ Ces versions doivent suivre le `bun.lock` du dépôt kadans. Le dépôt s'en
# protège : l'image écrit ce qu'elle a cuit dans /etc/ci-base.versions, et la CI
# de kadans CONFRONTE ce fichier à son lockfile pour échouer FORT plutôt que de
# dériver en silence (des navigateurs qui ne correspondent plus au client
# Playwright donnent « Executable doesn't exist », loin de la cause).
ci_base_image_bun_version: '1.3.14'
ci_base_image_playwright_version: '1.61.1'
# Épinglage par conteneur factice — remède décrit par
# docs/adr/20260407-docker-storage-gitea-runner.md §1, jusqu'ici resté à l'état
# de proposition (system_docker.yml n'applique que le data-root et les log-opts).
# Sans lui, le ramasse-miettes de Docker supprime l'image dès que le disque se
# remplit, et la CI casse sur une image manquante — panne déjà constatée sur les
# images de runner elles-mêmes.
ci_base_image_pin: true
# Reconstruire même si l'image existe déjà (utile après un changement de version).
ci_base_image_force_rebuild: false
@@ -0,0 +1,65 @@
# Image de base des jobs CI lourds — construite SUR CHAQUE machine à runner.
#
# Elle cuit une fois pour toutes ce que chaque exécution de CI réinstallait.
# Mesures du dépôt kadans (run 628, 2026-07-29, runner ARM64 2 vCPU / 3 Gio) :
#
# npm install -g bun → 8,6 s par run
# playwright install --with-deps chromium → 104,0 s par run (apt-get)
# ────────
# 112,6 s jetées à CHAQUE run,
# sur le job du chemin critique
#
# Le cache `actions/cache` ne peut rien contre ces 104 s : il couvre le
# NAVIGATEUR (299 Mo déjà mis en cache), pas ses dépendances SYSTÈME —
# `--with-deps` relance `apt-get` quoi qu'il arrive.
#
# ⚠ ON HÉRITE DE L'IMAGE DE RUNNER MAISON, ET C'EST ESSENTIEL : elle porte le
# certificat de la CA interne (step-ca). Une image repartant de `node:20-bookworm`
# ne ferait pas confiance à gitea.arcodange.lab, et tout job qui lui parle
# échouerait en TLS.
ARG CI_BASE_FROM=gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca
FROM ${CI_BASE_FROM}
ARG BUN_VERSION=1.3.14
ARG PLAYWRIGHT_VERSION=1.61.1
ENV DEBIAN_FRONTEND=noninteractive
# Chemin des navigateurs, figé et hors du HOME : un job qui tourne sous un autre
# utilisateur doit les retrouver.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# ══════════════════════════════════════════════════════════════════════════
# ⚠ TROIS `RUN` SÉPARÉS, ET C'EST LE POINT DE CONCEPTION DE CE FICHIER.
#
# La première version faisait `npm i -g bun` puis `playwright install --with-deps`
# en deux couches, dont une de 1,36 Go — irrecevable par le registre. Ici, même
# si l'on décidait un jour de pousser cette image, chaque couche reste du même
# ordre de grandeur que celles qui passent déjà (261 Mo pour la plus grosse de
# `runner-images:ubuntu-latest-ca`).
#
# Ne pas fusionner ces `RUN` pour « gagner une couche » : le gain serait nul et
# la couche redeviendrait impossible à transporter.
# ══════════════════════════════════════════════════════════════════════════
# 1. Bun (~274 Mo)
RUN npm install -g "bun@${BUN_VERSION}" \
&& npm cache clean --force
# 2. Les dépendances SYSTÈME de Chromium — c'est CETTE couche qui rachète les
# 104 s d'apt-get de chaque run.
RUN npx --yes "playwright@${PLAYWRIGHT_VERSION}" install-deps chromium \
&& rm -rf /var/lib/apt/lists/*
# 3. Le navigateur lui-même, séparé de ses dépendances système : les deux ne
# bougent pas au même rythme, et Docker ne réinvalide alors que la bonne.
RUN npx --yes "playwright@${PLAYWRIGHT_VERSION}" install chromium
# La trace opposable de ce qui est réellement cuit ici. La CI de kadans la LIT et
# la confronte à son `bun.lock` : une dérive de version doit échouer FORT, avec sa
# cause, plutôt que de se manifester par un « Executable doesn't exist » à
# vingt minutes de là.
RUN printf 'bun=%s\nplaywright=%s\n' \
"$(bun --version)" "${PLAYWRIGHT_VERSION}" \
> /etc/ci-base.versions \
&& cat /etc/ci-base.versions
@@ -0,0 +1,53 @@
---
# Construit l'image de base des jobs CI sur la machine courante, puis l'épingle.
#
# À exécuter sur les MÊMES hôtes que le runner Gitea (03_cicd.yml) : comme
# `capacity: 1`, le parallélisme vient de plusieurs machines, et un job qui
# atterrit sur une machine sans l'image échouerait AVANT sa première étape —
# `runs-on`/`container:` est résolu par le runner, pas par le workflow.
- name: Construire {{ ci_base_image_name }}:{{ ci_base_image_tag }}
community.docker.docker_image_build:
name: '{{ ci_base_image_name }}'
tag: '{{ ci_base_image_tag }}'
path: '{{ role_path }}/files/'
rebuild: '{{ "always" if ci_base_image_force_rebuild else "never" }}'
args:
CI_BASE_FROM: '{{ ci_base_image_from }}'
BUN_VERSION: '{{ ci_base_image_bun_version }}'
PLAYWRIGHT_VERSION: '{{ ci_base_image_playwright_version }}'
register: ci_base_image_build
# ⚠ CE CONTENEUR NE TOURNE JAMAIS — il ne sert qu'à référencer l'image.
# Remède décrit par docs/adr/20260407-docker-storage-gitea-runner.md §1, resté
# jusqu'ici à l'état de proposition : `system_docker.yml` n'applique que le
# data-root sur disque externe et les log-opts. Sans épinglage, le ramasse-miettes
# de Docker supprime l'image dès que le disque se remplit — panne DÉJÀ constatée
# sur les images de runner elles-mêmes, et qui casse la CI de tous les dépôts.
#
# `state: present` (et non `started`) : Docker considère l'image comme utilisée
# tant qu'un conteneur la référence, même à l'arrêt. Aucun CPU, aucune mémoire.
- name: Épingler {{ ci_base_image_name }} contre le ramasse-miettes Docker
community.docker.docker_container:
name: 'pin-{{ ci_base_image_name }}'
image: '{{ ci_base_image_name }}:{{ ci_base_image_tag }}'
state: present
command: ['sh', '-c', 'sleep infinity']
auto_remove: false
restart_policy: 'no'
when: ci_base_image_pin
# Contrôle de sortie : on VÉRIFIE que l'image répond, plutôt que de supposer que
# le build a suffi. Une image construite mais dont `bun` n'est pas dans le PATH
# passerait le build et casserait tous les jobs.
- name: Vérifier que l'image livre bien bun et chromium
ansible.builtin.command:
cmd: >-
docker run --rm {{ ci_base_image_name }}:{{ ci_base_image_tag }}
sh -c "bun --version && cat /etc/ci-base.versions"
register: ci_base_image_check
changed_when: false
- name: Ce que l'image contient réellement
ansible.builtin.debug:
var: ci_base_image_check.stdout_lines