Nouveau rôle ci_base_image : il construit sur chaque machine à runner une image Node 20 + Bun + Chromium, et l'épingle contre le ramasse-miettes Docker.
✅ L'image a été construite et lancée pour de vrai
En linux/arm64, l'architecture des runners. Deux défauts en sont sortis, qu'aucune lecture du Dockerfile n'aurait montrés — détaillés plus bas.
which node → /usr/bin/node v20.20.2 ✅
bun 1.3.14 ✅ chromium-1228 + headless-shell + ffmpeg ✅
/etc/ci-base.versions → node=v20.20.2 bun=1.3.14 playwright=1.61.1
⚠ Correction d'une affirmation antérieure de cette PR. J'avais écrit « je n'ai pas pu construire l'image, sa base vit derrière le certificat interne ». C'était une supposition non testée, et fausse : docker pull …/runner-images:ubuntu-latest-ca passe sans rien configurer. C'est en la construisant que les deux défauts ci-dessous sont apparus.
Le problème, mesuré
Les jobs CI de kadans réinstallent à chaque exécution des choses qui changent tous les trimestres. 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 par run, sur le job du chemin critique
actions/cache ne peut rien contre ces 104 s : il couvre le navigateur (299 Mo déjà en cache), pas ses dépendances système.
⚠ Défaut 1 — l'image de base livre Node 18
runner-images:ubuntu-latest-ca ships v18.20.8. Or nuxi importe node:util.styleText, absent de Node 18 : c'est la raison d'être du container: node:20-bookworm que porte la CI de kadans, et que son CLAUDE.mdinterdit de retirer. Sans correctif, la bascule cassait nuxt build sur un message parlant d'un import introuvable — jamais d'une version de Node.
→ Node 20 installé depuis NodeSource.
⚠ Défaut 2 — installer ne suffisait pas
Après installation, node --version rendait toujours v18.20.8 :
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
L'image de base précuit un node pour le toolcache d'act et le met en tête du PATH. → l'entrée 18 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é 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.
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.
Pourquoi construire ici plutôt que publier — désormais mesuré
Total
Plus grosse couche
Push
runner-images:ubuntu-latest-ca
534,7 Mo
261 Mo
✅
notre image, mesurée
4,58 Go
1,01 Go
❌
Décomposition réelle : playwright install chromium1,01 Go · install-deps405 Mo · Node 20 183 Mo · bun 179 Mo.
Le découpage en RUN séparés n'a PAS suffi à rendre l'image poussable : 1,01 Go, soit ~4× ce que le registre accepte. Le build local n'est donc pas une préférence, c'est la seule voie — et c'est mesuré, plus supposé.
Et c'est bien sur chaque machine : avec capacity: 1, le parallélisme vient de plusieurs Raspberry, et container:/runs-on est résolu par le runner — un job qui atterrit là où l'image manque échoue avant sa première étape.
L'épinglage — implémente enfin l'ADR 20260407
L'image est épinglée par un conteneur factice (state: present, jamais démarré). Cela implémente la section 1 de docs/adr/20260407-docker-storage-gitea-runner.md, restée à l'état de proposition : system_docker.yml n'applique aujourd'hui que le data-root sur disque externe et les log-opts. Sans épinglage, 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 que l'ADR décrit.
Vérifications
✅ Image construite en linux/arm64 et lancée : Node 20.20.2, bun, les trois navigateurs, trace de versions cohérente.
✅ Assertion de build sur la version de Node (le build échoue si la résolution casse).
✅playbooks/03_cicd.yml parse ; rôles, labels et force_pull: false relus depuis le YAML chargé.
✅ Dockerfile : docker build --check sans avertissement.
✅ La garde côté kadans (kadans#227) est éprouvée par sabotage dans cette image, codes de sortie relevés : témoin 0, Playwright désaccordé 1, trace absente 1, faux node 18 en tête du PATH 1.
Non vérifié : le rôle Ansible lui-même n'a pas été exécuté (je n'ai pas lancé de playbook sur les Pi). Le premier ansible-playbook 03_cicd.yml reste le vrai test du câblage.
Reste en draft : un déploiement CI/CD touche tous les dépôts de la forge, ce n'est pas à moi de décider quand il part.
Deux points annexes
Aucun dépôt source pour runner-images dans l'org : l'image ubuntu-latest-ca est publiée et référencée par 03_cicd.yml, mais son Dockerfile n'est versionné nulle part. Si elle se perd, elle n'est pas reconstructible.
Le commentaire de container.options documente l'incident du 2026-07-23 (nuxt generate à 3,5 Go RSS sur pi1 → load 150 → ingress et API k3s morts). Ce rôle ajoute une charge de build ponctuelle mais lourde (apt + Chromium, ~4,6 Go) sur ces mêmes machines : à lancer hors période d'usage.
Nouveau rôle `ci_base_image` : il construit **sur chaque machine à runner** une image Node 20 + Bun + Chromium, et l'**épingle** contre le ramasse-miettes Docker.
## ✅ L'image a été construite et lancée pour de vrai
En `linux/arm64`, l'architecture des runners. **Deux défauts en sont sortis, qu'aucune lecture du Dockerfile n'aurait montrés** — détaillés plus bas.
```
which node → /usr/bin/node v20.20.2 ✅
bun 1.3.14 ✅ chromium-1228 + headless-shell + ffmpeg ✅
/etc/ci-base.versions → node=v20.20.2 bun=1.3.14 playwright=1.61.1
```
> ⚠ **Correction d'une affirmation antérieure de cette PR.** J'avais écrit « je n'ai pas pu construire l'image, sa base vit derrière le certificat interne ». C'était une **supposition non testée, et fausse** : `docker pull …/runner-images:ubuntu-latest-ca` passe sans rien configurer. C'est en la construisant que les deux défauts ci-dessous sont apparus.
## Le problème, mesuré
Les jobs CI de `kadans` réinstallent à **chaque** exécution des choses qui changent tous les trimestres. 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 par run, sur le job du chemin critique
```
`actions/cache` ne peut rien contre ces 104 s : il couvre le **navigateur** (299 Mo déjà en cache), pas ses **dépendances système**.
## ⚠ Défaut 1 — l'image de base livre Node 18
`runner-images:ubuntu-latest-ca` ships **v18.20.8**. Or `nuxi` importe `node:util.styleText`, **absent de Node 18** : c'est la raison d'être du `container: node:20-bookworm` que porte la CI de kadans, et que son `CLAUDE.md` **interdit de retirer**. Sans correctif, la bascule cassait `nuxt build` sur un message parlant d'un import introuvable — jamais d'une version de Node.
→ Node 20 installé depuis NodeSource.
## ⚠ Défaut 2 — installer ne suffisait pas
Après installation, `node --version` rendait **toujours v18.20.8** :
```
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
```
L'image de base précuit un node pour le **toolcache d'act** et le met en tête du `PATH`. → l'entrée 18 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é 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`.
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.
## Pourquoi construire ici plutôt que publier — désormais mesuré
| | Total | Plus grosse couche | Push |
|---|---:|---:|---|
| `runner-images:ubuntu-latest-ca` | 534,7 Mo | **261 Mo** | ✅ |
| notre image, **mesurée** | **4,58 Go** | **1,01 Go** | ❌ |
Décomposition réelle : `playwright install chromium` **1,01 Go** · `install-deps` **405 Mo** · Node 20 **183 Mo** · bun **179 Mo**.
**Le découpage en `RUN` séparés n'a PAS suffi** à rendre l'image poussable : 1,01 Go, soit ~4× ce que le registre accepte. Le build **local** n'est donc pas une préférence, c'est la seule voie — et c'est mesuré, plus supposé.
**Et c'est bien sur *chaque* machine** : avec `capacity: 1`, le parallélisme vient de plusieurs Raspberry, et `container:`/`runs-on` est résolu par le runner — un job qui atterrit là où l'image manque échoue **avant sa première étape**.
## L'épinglage — implémente enfin l'ADR 20260407
L'image est épinglée par un conteneur factice (`state: present`, jamais démarré). Cela **implémente la section 1 de `docs/adr/20260407-docker-storage-gitea-runner.md`**, restée à l'état de proposition : `system_docker.yml` n'applique aujourd'hui que le `data-root` sur disque externe et les `log-opts`. Sans épinglage, 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 que l'ADR décrit.
## Vérifications
- ✅ Image **construite** en `linux/arm64` et **lancée** : Node 20.20.2, bun, les trois navigateurs, trace de versions cohérente.
- ✅ Assertion de build sur la version de Node (le build échoue si la résolution casse).
- ✅ `playbooks/03_cicd.yml` parse ; rôles, labels et `force_pull: false` relus depuis le YAML chargé.
- ✅ Dockerfile : `docker build --check` sans avertissement.
- ✅ La garde côté kadans ([kadans#227](https://gitea.arcodange.lab/arcodange/kadans/pulls/227)) est **éprouvée par sabotage dans cette image**, codes de sortie relevés : témoin 0, Playwright désaccordé 1, trace absente 1, faux node 18 en tête du PATH 1.
**Non vérifié :** le rôle Ansible lui-même n'a pas été exécuté (je n'ai pas lancé de playbook sur les Pi). Le premier `ansible-playbook 03_cicd.yml` reste le vrai test du câblage.
Reste en **draft** : un déploiement CI/CD touche tous les dépôts de la forge, ce n'est pas à moi de décider quand il part.
## Deux points annexes
- **Aucun dépôt source pour `runner-images`** dans l'org : l'image `ubuntu-latest-ca` est publiée et référencée par `03_cicd.yml`, mais son Dockerfile n'est versionné nulle part. Si elle se perd, elle n'est pas reconstructible.
- Le commentaire de `container.options` documente l'incident du 2026-07-23 (`nuxt generate` à 3,5 Go RSS sur pi1 → load 150 → ingress et API k3s morts). Ce rôle ajoute une charge de build **ponctuelle mais lourde** (apt + Chromium, ~4,6 Go) sur ces mêmes machines : à lancer hors période d'usage.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
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) <[email protected]>
✅ Éprouvé sur le vrai matériel — et ça prouve que ce rôle est nécessaire, pas confortable
J'ai monté une sonde jetable côté kadans (workflow workflow_dispatch, branche non mergée) qui construit l'image sur un runner puis tente de l'utiliser depuis un autre job. Run 637.
Ce qui marche
Job construire, sur pi1 — réussi en 337 s :
node résolu : /usr/bin/node v20.20.2 ← le correctif Node 20 tient sur le matériel réel
TOTAL 3.34GB
couches : 1.01GB · 403MB · 179MB · 169MB ← conforme aux mesures faites sur Mac
épinglé: pin-ci-node-playwright (Created) ← l'épinglage de l'ADR 20260407 §1 fonctionne
Donc : l'image se construit sur un Raspberry ARM64 en ~5 min 40, Node 20 résout correctement, et l'épinglage opère.
Ce qui NE marche pas — et c'est le point
Job consommer, sur pi3 — échec en 2 s :
arcodange_global_runner_pi3(version:v0.2.13) received task
🐳 docker pull image=ci-node-playwright:latest platform= username= forcePull=false
Image exists? false
pulling image 'docker.io/library/ci-node-playwright:latest'
Error response from daemon: pull access denied for ci-node-playwright
Le job a atterri sur pi3 alors que la construction avait eu lieu sur pi1.force_pull: false a bien été respecté — il cherche en local d'abord (Image exists? false) — mais l'image n'existait pas sur cette machine, et il est retombé sur Docker Hub.
Ce que ça établit
Construire l'image dans un workflow ne couvre qu'une machine sur trois. C'est exactement pourquoi ce rôle tourne sur raspberries:&local:!gitea et non dans une CI. La justification n'est plus déduite de capacity: 1 — elle est constatée.
⚠ Découverte annexe : les runners sont hétérogènes
Machine
act_runner
pi1
v0.3.1
pi3
v0.2.13
Et pi3 a mal lu la définition du job dont il dépendait : 'runs-on' key not defined in Sonde image CI/construire puis No steps found. Ça n'a pas causé l'échec (le pull l'a fait), mais un runner en retard de trois versions mineures peut interpréter différemment un workflow — de quoi expliquer des comportements et des durées qui varient selon la machine qui prend le job. Hors périmètre de cette PR ; je le signale.
⚠ État laissé sur pi1
La sonde y a laissé l'image ci-node-playwright:latest (3,34 Go, sur le disque externe) et son conteneur d'épinglage pin-ci-node-playwright. C'est exactement ce que ce rôle produirait, donc c'est sans danger et même utile — mais c'est un état posé hors Ansible, et je préfère le dire. docker rm -f pin-ci-node-playwright && docker rmi ci-node-playwright:latest sur pi1 si tu veux repartir de zéro avant le playbook.
## ✅ Éprouvé sur le vrai matériel — et ça prouve que ce rôle est *nécessaire*, pas confortable
J'ai monté une sonde jetable côté `kadans` (workflow `workflow_dispatch`, branche non mergée) qui construit l'image **sur un runner** puis tente de l'utiliser depuis un **autre job**. Run 637.
### Ce qui marche
**Job `construire`, sur `pi1` — réussi en 337 s :**
```
node résolu : /usr/bin/node v20.20.2 ← le correctif Node 20 tient sur le matériel réel
TOTAL 3.34GB
couches : 1.01GB · 403MB · 179MB · 169MB ← conforme aux mesures faites sur Mac
épinglé: pin-ci-node-playwright (Created) ← l'épinglage de l'ADR 20260407 §1 fonctionne
```
Donc : l'image se construit sur un Raspberry ARM64 en **~5 min 40**, Node 20 résout correctement, et l'épinglage opère.
### Ce qui NE marche pas — et c'est le point
**Job `consommer`, sur `pi3` — échec en 2 s :**
```
arcodange_global_runner_pi3(version:v0.2.13) received task
🐳 docker pull image=ci-node-playwright:latest platform= username= forcePull=false
Image exists? false
pulling image 'docker.io/library/ci-node-playwright:latest'
Error response from daemon: pull access denied for ci-node-playwright
```
**Le job a atterri sur `pi3` alors que la construction avait eu lieu sur `pi1`.** `force_pull: false` a bien été respecté — il cherche en local d'abord (`Image exists? false`) — mais l'image n'existait pas *sur cette machine*, et il est retombé sur Docker Hub.
### Ce que ça établit
> **Construire l'image dans un workflow ne couvre qu'une machine sur trois.** C'est exactement pourquoi ce rôle tourne sur `raspberries:&local:!gitea` et non dans une CI. La justification n'est plus déduite de `capacity: 1` — elle est constatée.
### ⚠ Découverte annexe : les runners sont hétérogènes
| Machine | act_runner |
|---|---|
| `pi1` | **v0.3.1** |
| `pi3` | **v0.2.13** |
Et `pi3` a mal lu la définition du job dont il dépendait : `'runs-on' key not defined in Sonde image CI/construire` puis `No steps found`. Ça n'a pas causé l'échec (le `pull` l'a fait), mais **un runner en retard de trois versions mineures** peut interpréter différemment un workflow — de quoi expliquer des comportements et des durées qui varient selon la machine qui prend le job. Hors périmètre de cette PR ; je le signale.
### ⚠ État laissé sur `pi1`
La sonde y a laissé l'image `ci-node-playwright:latest` (3,34 Go, sur le disque externe) et son conteneur d'épinglage `pin-ci-node-playwright`. C'est **exactement** ce que ce rôle produirait, donc c'est sans danger et même utile — mais c'est un état posé hors Ansible, et je préfère le dire. `docker rm -f pin-ci-node-playwright && docker rmi ci-node-playwright:latest` sur pi1 si tu veux repartir de zéro avant le playbook.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
arcodange
marked the pull request as ready for review 2026-07-29 23:33:13 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Nouveau rôle
ci_base_image: il construit sur chaque machine à runner une image Node 20 + Bun + Chromium, et l'épingle contre le ramasse-miettes Docker.✅ L'image a été construite et lancée pour de vrai
En
linux/arm64, l'architecture des runners. Deux défauts en sont sortis, qu'aucune lecture du Dockerfile n'aurait montrés — détaillés plus bas.Le problème, mesuré
Les jobs CI de
kadansréinstallent à chaque exécution des choses qui changent tous les trimestres. Run 628 (runner ARM64 2 vCPU / 3 Gio) :actions/cachene peut rien contre ces 104 s : il couvre le navigateur (299 Mo déjà en cache), pas ses dépendances système.⚠ Défaut 1 — l'image de base livre Node 18
runner-images:ubuntu-latest-caships v18.20.8. Ornuxiimportenode:util.styleText, absent de Node 18 : c'est la raison d'être ducontainer: node:20-bookwormque porte la CI de kadans, et que sonCLAUDE.mdinterdit de retirer. Sans correctif, la bascule cassaitnuxt buildsur un message parlant d'un import introuvable — jamais d'une version de Node.→ Node 20 installé depuis NodeSource.
⚠ Défaut 2 — installer ne suffisait pas
Après installation,
node --versionrendait 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. → l'entrée 18 est retirée ; la résolution retombe sur/usr/bin/node.⚠ Conséquence assumée :
actions/setup-nodene 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 labelci-node-playwright.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.Pourquoi construire ici plutôt que publier — désormais mesuré
runner-images:ubuntu-latest-caDécomposition réelle :
playwright install chromium1,01 Go ·install-deps405 Mo · Node 20 183 Mo · bun 179 Mo.Le découpage en
RUNséparés n'a PAS suffi à rendre l'image poussable : 1,01 Go, soit ~4× ce que le registre accepte. Le build local n'est donc pas une préférence, c'est la seule voie — et c'est mesuré, plus supposé.Et c'est bien sur chaque machine : avec
capacity: 1, le parallélisme vient de plusieurs Raspberry, etcontainer:/runs-onest résolu par le runner — un job qui atterrit là où l'image manque échoue avant sa première étape.L'épinglage — implémente enfin l'ADR 20260407
L'image est épinglée par un conteneur factice (
state: present, jamais démarré). Cela implémente la section 1 dedocs/adr/20260407-docker-storage-gitea-runner.md, restée à l'état de proposition :system_docker.ymln'applique aujourd'hui que ledata-rootsur disque externe et leslog-opts. Sans épinglage, 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 que l'ADR décrit.Vérifications
linux/arm64et lancée : Node 20.20.2, bun, les trois navigateurs, trace de versions cohérente.playbooks/03_cicd.ymlparse ; rôles, labels etforce_pull: falserelus depuis le YAML chargé.docker build --checksans avertissement.Non vérifié : le rôle Ansible lui-même n'a pas été exécuté (je n'ai pas lancé de playbook sur les Pi). Le premier
ansible-playbook 03_cicd.ymlreste le vrai test du câblage.Reste en draft : un déploiement CI/CD touche tous les dépôts de la forge, ce n'est pas à moi de décider quand il part.
Deux points annexes
runner-imagesdans l'org : l'imageubuntu-latest-caest publiée et référencée par03_cicd.yml, mais son Dockerfile n'est versionné nulle part. Si elle se perd, elle n'est pas reconstructible.container.optionsdocumente l'incident du 2026-07-23 (nuxt generateà 3,5 Go RSS sur pi1 → load 150 → ingress et API k3s morts). Ce rôle ajoute une charge de build ponctuelle mais lourde (apt + Chromium, ~4,6 Go) sur ces mêmes machines : à lancer hors période d'usage.🤖 Generated with Claude Code
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]>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) <[email protected]>✅ Éprouvé sur le vrai matériel — et ça prouve que ce rôle est nécessaire, pas confortable
J'ai monté une sonde jetable côté
kadans(workflowworkflow_dispatch, branche non mergée) qui construit l'image sur un runner puis tente de l'utiliser depuis un autre job. Run 637.Ce qui marche
Job
construire, surpi1— réussi en 337 s :Donc : l'image se construit sur un Raspberry ARM64 en ~5 min 40, Node 20 résout correctement, et l'épinglage opère.
Ce qui NE marche pas — et c'est le point
Job
consommer, surpi3— échec en 2 s :Le job a atterri sur
pi3alors que la construction avait eu lieu surpi1.force_pull: falsea bien été respecté — il cherche en local d'abord (Image exists? false) — mais l'image n'existait pas sur cette machine, et il est retombé sur Docker Hub.Ce que ça établit
⚠ Découverte annexe : les runners sont hétérogènes
pi1pi3Et
pi3a mal lu la définition du job dont il dépendait :'runs-on' key not defined in Sonde image CI/construirepuisNo steps found. Ça n'a pas causé l'échec (lepulll'a fait), mais un runner en retard de trois versions mineures peut interpréter différemment un workflow — de quoi expliquer des comportements et des durées qui varient selon la machine qui prend le job. Hors périmètre de cette PR ; je le signale.⚠ État laissé sur
pi1La sonde y a laissé l'image
ci-node-playwright:latest(3,34 Go, sur le disque externe) et son conteneur d'épinglagepin-ci-node-playwright. C'est exactement ce que ce rôle produirait, donc c'est sans danger et même utile — mais c'est un état posé hors Ansible, et je préfère le dire.docker rm -f pin-ci-node-playwright && docker rmi ci-node-playwright:latestsur pi1 si tu veux repartir de zéro avant le playbook.🤖 Generated with Claude Code