From fdecabf8caff22ea491647f093bf1d9913684e27 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 29 Jul 2026 20:23:59 +0200 Subject: [PATCH 1/5] =?UTF-8?q?feat(ci):=20construire=20l'image=20des=20jo?= =?UTF-8?q?bs=20CI=20lourds=20sur=20chaque=20machine=20=C3=A0=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../arcodange/factory/playbooks/03_cicd.yml | 18 ++++- .../roles/ci_base_image/defaults/main.yml | 43 ++++++++++++ .../roles/ci_base_image/files/Dockerfile | 65 +++++++++++++++++++ .../roles/ci_base_image/tasks/main.yml | 53 +++++++++++++++ 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml create mode 100644 ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile create mode 100644 ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml diff --git a/ansible/arcodange/factory/playbooks/03_cicd.yml b/ansible/arcodange/factory/playbooks/03_cicd.yml index f274e65..353bc7e 100644 --- a/ansible/arcodange/factory/playbooks/03_cicd.yml +++ b/ansible/arcodange/factory/playbooks/03_cicd.yml @@ -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. diff --git a/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml b/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml new file mode 100644 index 0000000..39ce6f2 --- /dev/null +++ b/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml @@ -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 diff --git a/ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile b/ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile new file mode 100644 index 0000000..ea85c29 --- /dev/null +++ b/ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile @@ -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 diff --git a/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml b/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml new file mode 100644 index 0000000..427459c --- /dev/null +++ b/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml @@ -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 From 9f438c4968319b742968f59ab94561772fcf2ee5 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 29 Jul 2026 23:09:14 +0200 Subject: [PATCH 2/5] =?UTF-8?q?fix(ci=5Fbase=5Fimage)=20=E2=80=94=20l'imag?= =?UTF-8?q?e=20livrait=20Node=2018=20:=20deux=20d=C3=A9fauts=20que=20seule?= =?UTF-8?q?=20sa=20CONSTRUCTION=20a=20montr=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit J'avais écrit dans cette PR « je n'ai pas pu construire l'image, sa base vit derrière le certificat interne ». C'était une SUPPOSITION NON TESTÉE, et elle est fausse : `docker pull gitea.arcodange.lab/…/runner-images:ubuntu-latest-ca` passe sans rien configurer. En la construisant vraiment, deux défauts sont sortis — et aucun n'était visible à la lecture du Dockerfile. 1. `runner-images:ubuntu-latest-ca` LIVRE NODE 18 (v18.20.8). Or `nuxi` importe `node:util.styleText`, absent de Node 18 : c'est la raison d'être du `container: node:20-bookworm` de la CI de kadans, que son CLAUDE.md interdit de retirer. Sans correctif, basculer la CI sur cette image cassait `nuxt build` sur un message parlant d'un import introuvable — jamais d'une version de Node. → Node 20 installé depuis NodeSource. 2. ET INSTALLER NE SUFFISAIT PAS. Après l'installation, `node --version` rendait TOUJOURS v18.20.8 : l'image de base précuit un node pour le toolcache d'act et le met EN TÊTE du PATH. which node → /opt/acttoolcache/node/18.20.8/arm64/bin/node /usr/bin/node --version → v20.20.2 ← le bon, mais il PERD → l'entrée 18 du toolcache est retirée ; la résolution retombe sur /usr/bin/node. ⚠ Conséquence assumée : `actions/setup-node` ne trouvera plus de Node 18 préinstallé — aucun workflow de kadans ne l'utilise, et l'image n'est servie qu'aux jobs qui DEMANDENT le label. Le Dockerfile porte désormais une ASSERTION DE BUILD (`node --version | grep -q "^v${NODE_MAJOR}\."`) : l'image ne peut plus se construire si la résolution redevient mauvaise. Et le rôle vérifie la version au déploiement (`failed_when`), au lieu de la supposer. MESURES RÉELLES (construite en linux/arm64, l'architecture des runners) : TOTAL 4,58 Go ├─ playwright install chromium 1,01 Go ├─ playwright install-deps 405 Mo ├─ Node 20 (NodeSource) 183 Mo └─ bun 179 Mo Vérifié dans l'image : which node → /usr/bin/node v20.20.2 · bun 1.3.14 · chromium-1228 + headless-shell + ffmpeg · /etc/ci-base.versions cohérent. ⚠ Ce que ces chiffres tranchent : le découpage en RUN séparés N'A PAS suffi à rendre l'image poussable — 1,01 Go pour la plus grosse couche, soit ~4× les 261 Mo que le registre accepte (runner-images:ubuntu-latest-ca). Le build LOCAL n'est donc pas une préférence, c'est la seule voie. Mesuré, plus supposé. Co-Authored-By: Claude Opus 5 (1M context) --- .../roles/ci_base_image/defaults/main.yml | 9 ++++ .../roles/ci_base_image/files/Dockerfile | 42 +++++++++++++++++-- .../roles/ci_base_image/tasks/main.yml | 11 ++++- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml b/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml index 39ce6f2..e242e9a 100644 --- a/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml +++ b/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml @@ -31,6 +31,15 @@ ci_base_image_tag: latest ci_base_image_bun_version: '1.3.14' ci_base_image_playwright_version: '1.61.1' +# ⚠ NODE 20 EST OBLIGATOIRE, ET CE N'EST PAS UN CONFORT. +# `runner-images:ubuntu-latest-ca` livre Node **18** (v18.20.8, constaté en +# lançant l'image). Or `nuxi` importe `node:util.styleText`, absent de Node 18 : +# c'est la raison d'être du `container: node:20-bookworm` que portait la CI de +# kadans, et que son CLAUDE.md interdit de retirer. Sans cette surcharge, tout +# job Nuxt basculé sur cette image casse au premier build, sur un message qui +# parle d'un import introuvable et jamais d'une version de Node. +ci_base_image_node_major: 20 + # É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). diff --git a/ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile b/ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile index ea85c29..560aba0 100644 --- a/ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile +++ b/ansible/arcodange/factory/roles/ci_base_image/files/Dockerfile @@ -42,7 +42,43 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright # la couche redeviendrait impossible à transporter. # ══════════════════════════════════════════════════════════════════════════ -# 1. Bun (~274 Mo) +# 0. NODE 20, ET C'EST OBLIGATOIRE — pas une préférence. +# +# ⚠ `runner-images:ubuntu-latest-ca` livre **Node 18** (v18.20.8, vérifié en le +# lançant). Or `nuxi` importe `node:util.styleText`, ABSENT de Node 18 : c'est la +# raison d'être du `container: node:20-bookworm` que la CI de kadans portait, et +# que son `CLAUDE.md` interdit explicitement de retirer. +# +# Sans cette couche, une CI qui bascule sur cette image casse au premier `nuxt +# build` — et le message parle d'un import introuvable, pas d'une version de Node. +# Le défaut a été trouvé en CONSTRUISANT l'image puis en lançant `node --version` +# dedans ; aucune lecture du Dockerfile ne l'aurait montré. +# ⚠⚠ ET INSTALLER NE SUFFIT PAS — il faut aussi que `node` RÉSOLVE vers le bon. +# Constaté en lançant l'image : après l'installation de Node 20 par apt, +# `node --version` rendait toujours **v18.20.8**, parce que l'image de base +# précuit un node pour le toolcache d'act et le met EN TÊTE du PATH : +# +# which node → /opt/acttoolcache/node/18.20.8/arm64/bin/node +# PATH → /opt/acttoolcache/node/18.20.8/arm64/bin:/usr/local/sbin:/usr/bin:… +# /usr/bin/node --version → v20.20.2 ← le bon, mais il PERD +# +# On retire donc l'entrée 18 du toolcache : le segment de PATH devient inexistant +# (inoffensif) et la résolution retombe sur /usr/bin/node, en 20. +# ⚠ Conséquence assumée : `actions/setup-node` ne trouvera plus de Node 18 +# préinstallé dans cette image. Aucun workflow de kadans ne l'utilise, et l'image +# n'est servie qu'aux jobs qui DEMANDENT le label `ci-node-playwright`. +ARG NODE_MAJOR=20 +RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" -o /tmp/nodesource.sh \ + && bash /tmp/nodesource.sh \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -f /tmp/nodesource.sh \ + && rm -rf /var/lib/apt/lists/* \ + && rm -rf /opt/acttoolcache/node \ + && echo "node résolu : $(which node) $(node --version)" \ + && node --version | grep -q "^v${NODE_MAJOR}\." + +# 1. Bun (~180 Mo) — installé APRÈS Node 20, pour que son npm global soit celui +# de Node 20 et non celui de Node 18. RUN npm install -g "bun@${BUN_VERSION}" \ && npm cache clean --force @@ -59,7 +95,7 @@ RUN npx --yes "playwright@${PLAYWRIGHT_VERSION}" install chromium # 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}" \ +RUN printf 'node=%s\nbun=%s\nplaywright=%s\n' \ + "$(node --version)" "$(bun --version)" "${PLAYWRIGHT_VERSION}" \ > /etc/ci-base.versions \ && cat /etc/ci-base.versions diff --git a/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml b/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml index 427459c..5ac76f2 100644 --- a/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml +++ b/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml @@ -14,6 +14,7 @@ rebuild: '{{ "always" if ci_base_image_force_rebuild else "never" }}' args: CI_BASE_FROM: '{{ ci_base_image_from }}' + NODE_MAJOR: '{{ ci_base_image_node_major }}' BUN_VERSION: '{{ ci_base_image_bun_version }}' PLAYWRIGHT_VERSION: '{{ ci_base_image_playwright_version }}' register: ci_base_image_build @@ -40,13 +41,19 @@ # 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 +- name: Vérifier que l'image livre bien Node {{ ci_base_image_node_major }}, 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" + sh -c "node --version && bun --version && ls /ms-playwright && cat /etc/ci-base.versions" register: ci_base_image_check changed_when: false + # ⚠ La version de Node est VÉRIFIÉE, pas supposée : l'image de base en livre + # une trop ancienne (18), et une régression silencieuse ici casserait tout job + # Nuxt sur un message qui ne nomme pas la cause. + failed_when: >- + ci_base_image_check.rc != 0 + or ('v' ~ ci_base_image_node_major ~ '.') not in ci_base_image_check.stdout - name: Ce que l'image contient réellement ansible.builtin.debug: From ba791ed05581e9341c5fe35988d8d04d88e9b594 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 29 Jul 2026 23:41:32 +0200 Subject: [PATCH 3/5] =?UTF-8?q?fix(ci=5Fbase=5Fimage)=20=E2=80=94=20le=20c?= =?UTF-8?q?ontexte=20de=20build=20doit=20=C3=AAtre=20SUR=20la=20machine,?= =?UTF-8?q?=20pas=20sur=20le=20contr=C3=B4leur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le playbook 03_cicd est mort sur les deux hôtes : "/Users/…/roles/ci_base_image/files/" is not an existing directory `docker_image_build` s'exécute SUR LA CIBLE : son `path:` est un chemin de la cible. Je passais `{{ role_path }}/files/`, un chemin du CONTRÔLEUR. Le motif venait du rôle `playwright`, qui l'emploie LÉGITIMEMENT parce qu'il construit en local. Recopié pour un build distant, il ne pouvait pas marcher — et aucune relecture ne l'aurait montré, seule l'exécution le dit. ⚠ L'échec est arrivé AVANT les tâches qui déploient le runner : les deux runners sont restés `Up 6 days`, rien n'a été cassé. Le seul effet fut un jeton d'API Gitea créé par le rôle gitea_token, son comportement normal. Le contexte est désormais déposé sur la machine (`/tmp/ci-base-image`), et la RECONSTRUCTION DEVIENT CONDITIONNELLE : `never` en régime normal — le playbook ne rebâtit pas 3,3 Go à chaque passage — mais `always` dès que le Dockerfile a CHANGÉ sur la machine. Ajouter une bibliothèque devient donc effectif sans avoir à penser à un drapeau. Co-Authored-By: Claude Opus 5 (1M context) --- .../roles/ci_base_image/defaults/main.yml | 12 +++++- .../roles/ci_base_image/tasks/main.yml | 37 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml b/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml index e242e9a..9f88843 100644 --- a/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml +++ b/ansible/arcodange/factory/roles/ci_base_image/defaults/main.yml @@ -48,5 +48,15 @@ ci_base_image_node_major: 20 # 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). +# Où le contexte de build est déposé SUR LA MACHINE CIBLE. `docker_image_build` +# s'exécute sur la cible : son `path:` est un chemin de la cible, jamais du +# contrôleur. (Première version : `{{ role_path }}/files/` → le playbook mourait +# sur « is not an existing directory », sur les deux hôtes.) +ci_base_image_contexte: /tmp/ci-base-image + +# Reconstruire même si l'image existe déjà. ⚠ Inutile pour un simple changement +# du Dockerfile : le rôle le détecte et reconstruit tout seul (voir tasks/). +# Ce drapeau sert aux cas que le Dockerfile ne montre pas — une montée de +# `bun.lock` côté kadans, par exemple, qui change les VERSIONS attendues sans +# changer le fichier. ci_base_image_force_rebuild: false diff --git a/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml b/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml index 5ac76f2..f1ab121 100644 --- a/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml +++ b/ansible/arcodange/factory/roles/ci_base_image/tasks/main.yml @@ -6,12 +6,45 @@ # 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. +# ══════════════════════════════════════════════════════════════════════════ +# ⚠ LE CONTEXTE DE BUILD DOIT ÊTRE SUR LA MACHINE, PAS SUR LE CONTRÔLEUR. +# +# `docker_image_build` s'exécute SUR LA CIBLE : son `path:` est un chemin de la +# cible. La première version passait `{{ role_path }}/files/` — un chemin du +# CONTRÔLEUR — et le playbook mourait sur les deux hôtes : +# +# "/Users/…/roles/ci_base_image/files/" is not an existing directory +# +# Le motif venait du rôle `playwright`, qui l'utilise LÉGITIMEMENT parce qu'il +# construit en local ; recopié tel quel pour un build distant, il ne peut pas +# marcher. On copie donc le contexte d'abord. +# ══════════════════════════════════════════════════════════════════════════ +- name: Créer le répertoire de contexte de build sur la machine + ansible.builtin.file: + path: '{{ ci_base_image_contexte }}' + state: directory + mode: '0755' + +- name: Déposer le Dockerfile sur la machine + ansible.builtin.copy: + src: Dockerfile + dest: '{{ ci_base_image_contexte }}/Dockerfile' + mode: '0644' + register: ci_base_image_dockerfile + - 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" }}' + path: '{{ ci_base_image_contexte }}' + # RECONSTRUCTION CONDITIONNELLE : `never` en régime normal (le playbook ne + # rebâtit pas 3,3 Go à chaque passage), mais `always` dès que le Dockerfile + # a CHANGÉ sur la machine — c'est ce qui rend l'ajout d'une bibliothèque + # effectif sans avoir à penser à un drapeau. + rebuild: >- + {{ "always" + if (ci_base_image_force_rebuild or ci_base_image_dockerfile is changed) + else "never" }} args: CI_BASE_FROM: '{{ ci_base_image_from }}' NODE_MAJOR: '{{ ci_base_image_node_major }}' From 48e3d6827c56a10a20570965a47149d55829d85f Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 30 Jul 2026 08:54:43 +0200 Subject: [PATCH 4/5] =?UTF-8?q?fix(cicd)=20=E2=80=94=20=C3=A9pingler=20la?= =?UTF-8?q?=20version=20du=20runner=20:=20`latest`=20+=20`pull:=20missing`?= =?UTF-8?q?=20ne=20rafra=C3=AEchit=20JAMAIS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Réponse à « qu'est-ce qui nous empêche d'upgrade des deux côtés ? » : rien. Le playbook déployait `gitea/act_runner:latest` avec `pull: missing`, c'est-à-dire la pire combinaison possible — un tag FLOTTANT qui n'est JAMAIS rafraîchi. Chaque hôte garde donc ce que « latest » voulait dire le jour de son premier pull : pi1 : sha256:7bdc8d31… → v0.3.1 pi3 : sha256:0f65fa10… → v0.2.13 Deux machines censées être équivalentes, deux versions à trois mineures d'écart. Effets mesurés : le MÊME job, sur la MÊME image de CI, met 511 s sur pi1 et 397 s sur pi3 (114 s d'écart imputables à la machine) ; et pi3 a mal lu la définition d'un job dont il dépendait (« 'runs-on' key not defined », puis « No steps found »). ⚠ POURQUOI PAS `latest` + `pull: always`. `latest` vaut aujourd'hui **0.6.1** (Docker Hub, 30/04/2026), soit 3 à 4 versions mineures devant tout ce qui est éprouvé ici. Le runner exécute TOUTE la CI de la forge : une montée subie, non datée et non choisie s'y paie cher. On épingle donc, et on monte délibérément. ⚠ POURQUOI 0.3.1 ET PAS 0.6.1. 0.3.1 est la version que pi1 exécute DÉJÀ avec succès sur cette forge. Ce changement aligne donc pi3 VERS LE HAUT, sur du prouvé, sans saut de quatre versions. Passer ensuite à 0.6.1 devient une modification d'UNE ligne, datée et reculable — c'est tout l'intérêt de la variable. ⚠ Et `pull: missing` redevient CORRECT avec un tag épinglé : changer la version change le tag, donc l'image est absente, donc elle est tirée. Aucun besoin de `pull: always`, qui interrogerait le registre à chaque passage pour rien. ⚠ NE PAS jouer ce playbook pendant qu'une CI tourne : il recrée les conteneurs de runner et TUE les jobs en vol (journaux perdus). Vérifier `list_runs` avant — et se rappeler qu'un merge est un déclencheur. Refs arcodange-org/factory#50 Co-Authored-By: Claude Opus 5 (1M context) --- .../inventory/group_vars/all/gitea.yml | 23 +++++++++++++++++++ .../arcodange/factory/playbooks/03_cicd.yml | 9 +++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml b/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml index 837e292..825e5f1 100644 --- a/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml +++ b/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml @@ -9,3 +9,26 @@ # so the secret propagation playbook iterates over this list. gitea_secret_propagation_users: - arcodange + +# ══════════════════════════════════════════════════════════════════════════ +# VERSION DU RUNNER GITEA ACTIONS — ÉPINGLÉE, ET C'EST LE POINT. +# +# Le playbook 03_cicd déployait `gitea/act_runner:latest` avec `pull: missing`, +# c'est-à-dire la pire combinaison possible : un tag FLOTTANT qui n'est JAMAIS +# rafraîchi. Chaque hôte garde ce que « latest » voulait dire le jour de son +# premier pull — d'où deux machines censées être équivalentes qui divergent +# (constaté le 2026-07-30) : +# +# pi1 : sha256:7bdc8d31… → act_runner v0.3.1 +# pi3 : sha256:0f65fa10… → act_runner v0.2.13 +# +# Effet mesuré : le MÊME job, sur la MÊME image de CI, met 511 s sur pi1 et +# 397 s sur pi3 — 114 s d'écart imputables à la machine. Et pi3 (v0.2.13) a mal +# lu la définition d'un job dont il dépendait (« 'runs-on' key not defined », +# puis « No steps found »). +# +# ⚠ NE PAS remplacer par `latest` + `pull: always` : `latest` vaut aujourd'hui +# 0.6.1, soit 3 à 4 versions mineures devant tout ce qui est éprouvé ici. Le +# runner exécute TOUTE la CI de la forge — une montée subie, non datée et non +# choisie, s'y paie cher. Une version épinglée se relit, se date et se recule. +gitea_runner_version: "0.3.1" diff --git a/ansible/arcodange/factory/playbooks/03_cicd.yml b/ansible/arcodange/factory/playbooks/03_cicd.yml index 353bc7e..c0cf3d5 100644 --- a/ansible/arcodange/factory/playbooks/03_cicd.yml +++ b/ansible/arcodange/factory/playbooks/03_cicd.yml @@ -30,7 +30,14 @@ name: arcodange_factory_gitea_action services: gitea_action: - image: gitea/act_runner:latest + # ⚠ VERSION ÉPINGLÉE (inventory/group_vars/all/gitea.yml), PAS `latest`. + # `latest` + `pull: missing` = tag flottant JAMAIS rafraîchi : chaque + # hôte gardait ce que « latest » voulait dire à son premier pull, d'où + # pi1 en v0.3.1 et pi3 en v0.2.13 sur des machines censées être + # équivalentes (114 s d'écart mesurés sur le même job). + # Avec un tag épinglé, `pull: missing` redevient CORRECT : changer la + # version change le tag, donc l'image est absente, donc elle est tirée. + image: gitea/act_runner:{{ gitea_runner_version }} container_name: gitea_action restart: always environment: From 6bb27b0e5c8860c633a51fadb21c8f2b3307bffd Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 30 Jul 2026 09:03:40 +0200 Subject: [PATCH 5/5] =?UTF-8?q?feat(cicd)=20=E2=80=94=20Gitea=201.27.1=20e?= =?UTF-8?q?t=20Gitea=20Runner=202.3.0=20:=20l'image=20du=20runner=20a=20CH?= =?UTF-8?q?ANG=C3=89=20DE=20NOM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠ CORRIGE LE PREMIER JET DE CETTE BRANCHE, qui épinglait `gitea/act_runner:0.3.1`. `gitea/act_runner` est GELÉE à 0.6.1. Le successeur officiel est `gitea/runner` (binaire renommé `act_runner` → `gitea-runner`), aujourd'hui en **2.3.0**. Épingler l'ancien nom nous aurait enfermés dans une image morte — trouvé grâce aux notes de version de Gitea 1.27 signalées par le fondateur. VÉRIFIÉ AVANT DE BASCULER — c'est un remplacement DIRECT pour ce compose : • entrypoint identique : /sbin/tini -- run.sh • mêmes variables lues : CONFIG_FILE, GITEA_INSTANCE_URL, GITEA_RUNNER_{REGISTRATION_TOKEN,NAME,LABELS} • config.yaml compatible : capacity, labels, cache.*, container.force_pull, options, valid_volumes, host.workdir_parent — AUCUNE clé utilisée ici n'a disparu (comparé au `gitea-runner generate-config` de la 2.3.0) La 2.3.0 apporte en prime des réglages qui parlent à nos pannes connues : `health_check.min_free_disk_space_mb` (les images de runner supprimées quand le disque se remplit, ADR 20260407) et `state_report_interval` (les tâches tuées en zombie faute de rapport, factory#50). GITEA 1.25.5 → 1.27.1 : deux versions mineures, migrations de base IRRÉVERSIBLES. Sauvegardes du jour VÉRIFIÉES avant, pas supposées : /mnt/backups/postgres/backup_20260730.sql.gz 13 Mo, gzip -t OK, contient « CREATE DATABASE gitea » (pg_dumpall) /mnt/backups/gitea/backup_20260730.gitea.gz 1,7 Go, gzip -t OK ⚠ Le backup Gitea utilise `gitea dump --skip-db` : il ne contient PAS la base. C'est le dump postgres qui la porte — les deux sont nécessaires. Changements cassants de 1.27 et leur portée ici, vérifiée : • workflows réutilisables externes retirés → AUCUN dans front, kadans-api, factory (contrôlé programmatiquement, `uses:` au niveau job) • nonce CSP pour scripts inline → concerne les templates personnalisés, nous n'en avons pas • X-Content-Type-Options: nosniff par défaut Refs arcodange-org/factory#50 Co-Authored-By: Claude Opus 5 (1M context) --- .../factory/inventory/group_vars/all/gitea.yml | 17 ++++++++++++++++- .../inventory/group_vars/gitea/gitea.yml | 11 ++++++++++- ansible/arcodange/factory/playbooks/03_cicd.yml | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml b/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml index 825e5f1..2381e8d 100644 --- a/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml +++ b/ansible/arcodange/factory/inventory/group_vars/all/gitea.yml @@ -31,4 +31,19 @@ gitea_secret_propagation_users: # 0.6.1, soit 3 à 4 versions mineures devant tout ce qui est éprouvé ici. Le # runner exécute TOUTE la CI de la forge — une montée subie, non datée et non # choisie, s'y paie cher. Une version épinglée se relit, se date et se recule. -gitea_runner_version: "0.3.1" +# ⚠ L'IMAGE A CHANGÉ DE NOM. `gitea/act_runner` est gelée à 0.6.1 ; le +# successeur officiel est `gitea/runner`, et son binaire s'appelle désormais +# `gitea-runner` (plus `act_runner`). +# Vérifié avant de basculer — c'est un REMPLACEMENT DIRECT pour ce compose : +# • entrypoint identique : /sbin/tini -- run.sh +# • mêmes variables lues : CONFIG_FILE, GITEA_INSTANCE_URL, +# GITEA_RUNNER_{REGISTRATION_TOKEN,NAME,LABELS} +# • config.yaml compatible : capacity, labels, cache.*, container.force_pull, +# options, valid_volumes, host.workdir_parent — AUCUNE clé utilisée ici n'a +# disparu (comparé à `gitea-runner generate-config` de la 2.3.0). +# Le blog de Gitea 1.27 recommande « Gitea Runner 2.0.0 » ; 2.3.0 est la même +# lignée majeure, en plus récent. Gitea reste par ailleurs compatible fil-à-fil +# avec les runners plus anciens — il désactive simplement les fonctionnalités +# qu'ils n'annoncent pas. +gitea_runner_image: "gitea/runner" +gitea_runner_version: "2.3.0" diff --git a/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml b/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml index ed88c9a..6b0ac9f 100644 --- a/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml +++ b/ansible/arcodange/factory/inventory/group_vars/gitea/gitea.yml @@ -1,4 +1,13 @@ -gitea_version: 1.25.5 +# ⚠ Montée 1.25.5 → 1.27.1 : DEUX versions mineures, avec migrations de base +# IRRÉVERSIBLES (Gitea ne sait pas redescendre après migration). Sauvegardes du +# jour vérifiées avant la bascule (pg_dumpall 13 Mo intègre + archive fichiers +# 1,7 Go intègre, /mnt/backups). +# Changements cassants relevés dans les notes de version, et leur portée ICI : +# • workflows réutilisables externes retirés → AUCUN dans nos trois dépôts (vérifié) +# • nonce CSP exigé pour les scripts inline → concerne les templates +# personnalisés ; nous n'en avons pas +# • X-Content-Type-Options: nosniff par défaut +gitea_version: 1.27.1 gitea_database: db_name: gitea diff --git a/ansible/arcodange/factory/playbooks/03_cicd.yml b/ansible/arcodange/factory/playbooks/03_cicd.yml index c0cf3d5..532ddf8 100644 --- a/ansible/arcodange/factory/playbooks/03_cicd.yml +++ b/ansible/arcodange/factory/playbooks/03_cicd.yml @@ -37,7 +37,7 @@ # équivalentes (114 s d'écart mesurés sur le même job). # Avec un tag épinglé, `pull: missing` redevient CORRECT : changer la # version change le tag, donc l'image est absente, donc elle est tirée. - image: gitea/act_runner:{{ gitea_runner_version }} + image: "{{ gitea_runner_image }}:{{ gitea_runner_version }}" container_name: gitea_action restart: always environment: