docs(ci): capitalise le piège Node 18→20 des apps Bun/Nuxt (page 6b)

Nouvelle page runbook « CI des apps Bun/Nuxt » : le runner par défaut est en
Node 18, or nuxi/nuxt importent styleText de node:util (Node 20+) → la CI casse
au build. Parade tenue (validée sur arcodange/kadans) : container node:20-bookworm
+ Bun via npm. Inclut un ci.yml canonique à copier + les pistes infra runner.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
2026-07-12 13:31:41 +02:00
co-authored by Claude Opus 4.8
parent 3df2dd0700
commit f5c01da70d
@@ -0,0 +1,90 @@
[Factory](../../../README.md) > [Doc](../../README.md) > [Runbooks](../README.md) > [Nouvelle application web](README.md) > **6b. CI des apps Bun/Nuxt**
# 6b. CI des apps Bun/Nuxt (`.gitea/workflows/ci.yml`)
> **Status:** ✅ Active
> **Upstream:** [6. Workflows CI](06-ci-workflows.md)
> **Related:** [1. Dépôt Gitea](01-gitea-repo.md) (secrets d'org) · [Conventions de nommage](conventions.md) · [ADR CI/CD](../../adr/03_cicd_gitea_action_argocd.md)
---
## Summary
Une app **Bun + Nuxt** qui fait tourner ses gates (lint / typecheck / tests / `nuxt build`) **directement sur le runner** heurte un piège : le runner Gitea Actions par défaut expose **Node 18**, or `nuxi`/`nuxt` importent **`styleText` de `node:util`**, une API **ajoutée en Node 20** (v20.12) — la CI casse au premier `bun run build`. La parade tenue : lancer le job dans un **conteneur `node:20-bookworm`** et y installer Bun via npm. Ce workflow (`ci.yml`) est **distinct** des deux workflows de l'[étape 6](06-ci-workflows.md) (`vault.yaml` = `tofu apply`, `dockerimage.yaml` = build image) : il porte les **gates qualité** de l'app.
> [!WARNING]
> **Le piège Node 18 → 20.** Symptôme au premier run : un crash `nuxi` du type `The requested module 'node:util' does not provide an export named 'styleText'`, alors que `bun install` a réussi. Cause : `styleText` n'existe pas en Node 18. Ce n'est **pas** un souci de Bun — Bun orchestre, mais Nuxt/Nuxi s'exécutent sous le **Node** de l'environnement.
## Pourquoi le runner est en Node 18
Les runners `act_runner` de la plateforme enregistrent **deux labels qui pointent la même image** — [`ansible/…/playbooks/03_cicd.yml`](../../../ansible/arcodange/factory/playbooks/03_cicd.yml) :
```
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
```
Cette image est construite par [`ansible/…/playbooks/ssl/ssl.yml`](../../../ansible/arcodange/factory/playbooks/ssl/ssl.yml), `FROM gitea/runner-images:ubuntu-latest` (+ le CA du homelab). Le Node embarqué dans cette base upstream est **Node 18** : sans conteneur explicite, **tout job** (app ou factory) s'exécute avec ce Node.
## La parade : `container: node:20-bookworm`
Modèle de `.gitea/workflows/ci.yml` à copier dans une app Bun/Nuxt (repris de [`arcodange/kadans`](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/.gitea/workflows/ci.yml)) :
```yaml
name: CI
on:
push: { branches: [main] }
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
gates:
runs-on: ubuntu-latest
# Node 20 requis : nuxi/nuxt importent `styleText` de `node:util` (Node 20+),
# absent du Node 18 de l'image runner par défaut. `node:20-bookworm` = Node 20 + git.
container: node:20-bookworm
steps:
- uses: actions/checkout@v4
- name: Install Bun
run: npm install -g bun
- name: Install deps (lockfile figé)
run: bun install --frozen-lockfile
- name: Gates (lint / typecheck / tests — adapter aux scripts de l'app)
run: bun run check
- name: Build Nuxt
run: bun run build
```
Points de vigilance :
- **Bun via npm.** L'image `node:20-bookworm` n'a pas Bun ; `npm install -g bun` suffit. (Alternative : l'image `oven/bun:1` — mais elle n'embarque **pas** Node, ce qui rejoue le problème inverse pour les outils qui veulent un binaire `node`.)
- **`--frozen-lockfile`** en CI : échoue si le lockfile n'est pas à jour (garde-fou de reproductibilité).
- **`bookworm` (Debian) plutôt qu'`alpine`** : `alpine` (musl) casse certaines dépendances natives de l'écosystème Nuxt.
> [!CAUTION]
> **CA du homelab.** `node:20-bookworm` **ne fait pas confiance** au CA privé du homelab (contrairement à `runner-images:ubuntu-latest-ca`). La parade convient tant que les gates ne joignent que des endpoints **publics** (registre npm, `actions/checkout` via l'instance Gitea interne). Si un gate doit atteindre **`https://gitea.arcodange.lab`** (paquet privé, registre interne), le conteneur vanilla échouera la validation TLS — il faut alors une image Node 20 **avec** le CA (voir *Pistes* ci-dessous).
## Recommandation
Pour toute app **Bun/Nuxt** dont la CI fait `nuxt build`/`nuxi` **sur le runner**, ajouter `container: node:20-bookworm` au job (parade ci-dessus). C'est le contrat par défaut tant que l'image runner par défaut reste en Node 18.
Le **build d'image** (`dockerimage.yaml`, [étape 6](06-ci-workflows.md)) n'est **pas** concerné : la compilation Nuxt s'y fait **dans le `Dockerfile`** (base `node:20`/`oven/bun` au choix de l'app), pas sur le Node du runner.
## Pistes (infra runner — à valider par l'admin)
Ces options **retirent le contournement par-repo** mais touchent la **prod runner** (rebuild d'image + re-run Ansible `03_cicd` sur `pi1`/`pi3`, pas un simple merge). Laissées à l'arbitrage :
1. **Bumper l'image runner.** Dans [`ssl.yml`](../../../ansible/arcodange/factory/playbooks/ssl/ssl.yml), baser `runner-images:*-ca` sur une image `gitea/runner-images` qui embarque Node 20+ (vérifier le Node du tag), rebuild + push, puis re-run `03_cicd`. **Rayon d'impact large** : change l'environnement par défaut de **tous** les jobs, y compris les workflows factory `iac.yaml`/`postgres.yaml` (`runs-on: ubuntu-latest-ca`) — à revalider.
2. **Ajouter un label `node20` dédié.** Publier une image `runner-images:node20-ca` (`FROM node:20-bookworm` + CA du homelab) et l'exposer via un label `node20` dans [`03_cicd.yml`](../../../ansible/arcodange/factory/playbooks/03_cicd.yml). Les apps opt-in avec `runs-on: node20`, **sans** `container:` par job, et **avec** le CA (résout la limite ci-dessus). Rayon d'impact **opt-in** (plus sûr), mais ajoute une image à maintenir.
3. **Action réutilisable `bun-nuxt-ci`.** Sur le modèle des dépôts d'action de l'org (`arcodange-org/vault-action`), factoriser install-Bun + gates + build dans une action composite appelée en 3 lignes. Évite le copier-coller du YAML, indépendamment du Node du runner.
## Related
- [6. Workflows CI](06-ci-workflows.md) — `vault.yaml` (`tofu apply`) et `dockerimage.yaml` (build image) ; `ci.yml` (cette page) porte les gates de l'app.
- [1. Dépôt Gitea](01-gitea-repo.md) — secrets d'org hérités par la CI.
- Sources runner : [`03_cicd.yml`](../../../ansible/arcodange/factory/playbooks/03_cicd.yml) (labels) · [`ssl/ssl.yml`](../../../ansible/arcodange/factory/playbooks/ssl/ssl.yml) (image + CA).
- Exemple vivant : [`arcodange/kadans` — `ci.yml`](https://gitea.arcodange.lab/arcodange/kadans/src/branch/main/.gitea/workflows/ci.yml).