diff --git a/doc/runbooks/new-web-app/06b-bun-nuxt-ci.md b/doc/runbooks/new-web-app/06b-bun-nuxt-ci.md new file mode 100644 index 0000000..378bb52 --- /dev/null +++ b/doc/runbooks/new-web-app/06b-bun-nuxt-ci.md @@ -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).