Mettre en place l'intégration continue (Forgejo Actions) #6
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
priority
high
priority
low
priority
medium
question
step
backlog
step
delivered
step
done
step
in-progress
step
todo
wontfix
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
maxime/OrganisateurFamilial#6
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Description
Le dépôt vit sur Forgejo (
git.lozach.eu). Une intégration continue y est déclarée, elle se déclenche, et elle échoue : rien ne vérifie une branche avant son rebase surmain, à part la discipline de/branch-reviewlancé à la main.Ce qui existe déjà, et qui tourne mal
.github/workflows/ci.ymldécrit quatre jobs —lint,typecheck,test, puisbuildqui dépend des deux précédents — sur Node 22, chacun faisantmake installpuis sa cible. Forgejo lit ce répertoire : neuf exécutions existent sur l'instance, surpushversmain, la dernière sura888ec4. Le run 6 a réellement exécutélint(2 min 15) et a échoué sur le formatage d'alors ; les exécutions 7 et 8 échouent en 2 s, c'est-à-dire sans démarrer. Le fichier n'est donc pas mort, il est rouge — et un rouge que personne ne regarde s'installe.Ce qu'il faut trancher
0604bb33-109a-447c-b536-7f7857749495, labelsself-hosted,ubuntu-latest,linux_arm64) mais quelque chose l'a rendu indisponible : c'est ce que dit un job qui échoue en 2 s. C'est la seule dépendance d'infrastructure du ticket.lint,typecheckettestsont rapides et sans état. La suite end-to-end (cd e2e && npm test) demande une instance Nextcloud complète (make up), impose son propre état de départ et vide la corbeille des agendas — et surtout, elle pilote elle-mêmedocker compose, ce qui décide de la forme du job.mainn'accepte que des fast-forward, donc une CI surmainne valide que ce qui est déjà rentré. La valeur est sur la branche, avant le rebase.Dockerfileest multi-stage etmake imageconstruit l'image. Publier une image taguée par version rejoint la publication sur l'App Store : décider si la CI s'arrête à la vérification ou va jusqu'à la livraison.Deux affirmations de la description d'origine ont été corrigées ci-dessus après vérification : Forgejo n'ignore pas
.github/workflows/, etmake lintne casse plus surmain(0 erreur, 265 avertissements, Prettier vert) — le formatage d'e2e/tests/offline.spec.tsa été corrigé entre-temps. Le relevé est dans le commentaire de conception.Plan d'action
La conception est dans ce commentaire.
npm cimeurt surBad system call, c'est-à-dire SIGSYS : un filtre seccomp, pas une erreur de npmself-hosted:host,ubuntu-latest:host,linux_arm64:docker://node:22-bookworm: un seul label est conteneurisé, et ce n'est pas celui qu'on croyaitforgejo_runner.serviceet porteSystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap, dont l'action par défaut est de tuer. Drop-in avecSystemCallErrorNumber=EPERMplutôt que de deviner l'ensemble coupable. Sans effet surchecks, qui tourne en conteneur ; bloquant pour la e2e, qui doit rester sur l'hôteactions/checkoutéchoue à joindregit.lozach.eu:443depuis le bridge Docker (runs 9 et 10).container.options: --add-host=git.lozach.eu:host-gatewaydans/var/www/forgejo_runner/config.yml— mesuré : bridge par défaut vers l'IP publique passe, réseau dédié vers l'IP publique est refusé, réseau dédié vers la passerelle de l'hôte passeProtectSystem=fullpour la e2e, ou installer une fois à la main les dépendances système de Chromium et retirer--with-depsdu workflow —/usren lecture seule fait échouer l'apt-getque ce drapeau déclenchedocker composev5.5.0 répond en tant queforgejo_runner, port 8080 libre.github/workflows/ci.yml: les quatre jobs remplacés par un jobchecksunique — unmake install, puislint,typecheck,test,build— surpush, toute branche,runs-on: linux_arm64, Node 22.github/workflows/e2e.yml: job hôte (runs-on: self-hosted),pushsurmainetworkflow_dispatch;make install, Chromium,make build-frontend,make up,npm test, puismake downenif: always()e2e/playwright-report/ete2e/test-results/à l'échec, sans quoi un échec en CI n'est qu'une ligne rougechecks— run 34, 0 erreur de lint, 35 + 21 + 21 fichiers de tests passésactions/setup-nodedu jobcheckset le remplacer par un contrôle du majeur de Node — 6 min 30 sur 9 min 25 pour réinstaller la version déjà présente et attendre un cache injoignablemake downa tout retiré. Sur la branche et non surmain, via un déclencheur temporairee2e.yml— il avait atteintmain(c6c9fd6), retiré parc6748b2plutôt que par une réécriture :mainne fait que des avances rapidesRecette
make lint typecheck test— vertes au run 40 (0 erreur, 265 avertissements ; 426 + 344 + 344 tests)checks, et lui seul — runs 44 et 46, verts, sans e2e associéemaindéclenchecheckspuis la e2e — vérifié au rebase du 06/09 :checksvert en 1 min 56, la e2e enchaîneworkflow_dispatchlance bien la e2e sur une branchemake: *** [Makefile:67: test] Error 1)forgejo/upload-artifact@v4; leserror-context.md, captures ettrace.zipsont là et lisiblesmake downa bien tourné : aucun conteneur ne survit au job — vérifié aux runs 38 et 41Workflow: nouvelle section « What CI runs, and what it does not » (les deux workflows, pourquoichecksregarde les branches et pasmain, pourquoi la e2e ne tourne pas sur branche, et que la CI n'est pas une barrière d'intégration) ;Dev environment: nouvelle section sur le runner — labels, pourquoi la e2e doit être sur l'hôte,--add-hostpour joindre la forge, le durcissement systemd, ce que les workflows n'utilisent pas et pourquoi, et l'instabilité non résolue de la machinePlan de MEP
Implémenter les actionsto Mettre en place l'intégration continue (Forgejo Actions)Conception — la CI sur le runner Forgejo
1. L'état réel, relevé avant d'écrire quoi que ce soit
Deux affirmations de la description étaient fausses, et la conception change selon la
réponse. Les deux ont été vérifiées, pas supposées.
La CI est déjà branchée. Forgejo lit aussi
.github/workflows/: neuf exécutions deci.ymlexistent sur l'instance, déclenchées parpushsurmain, la dernière sura888ec4. Le fichier n'est donc pas mort — il est actif et rouge, ce qui est pire, parcequ'un rouge que personne ne regarde s'installe.
linttypecheck,testLe run 6 a vraiment exécuté
lint— 2 min 15, c'estsetup-nodeplusmake installpluseslint— et a échoué sur le formatage d'alors. Les 2 s qui suivent ne sont pas uneexécution : c'est un job qui n'a jamais démarré. Les logs ne sont lisibles que dans
l'interface web :
/api/v1/repos/…/actions/runs/{id}répond 404 sur cette version, et lejeton du robot n'est pas propriétaire du dépôt. L'hypothèse — le runner devenu
indisponible entre 14:56 et 20:35 le 30/08 — est vraisemblable et non établie, d'où la
première ligne du plan d'action.
make lintpasse surmain: 0 erreur, 265 avertissements, et Prettier ne signalerien. Le formatage d'
offline.spec.tsa été corrigé entre-temps. Il n'y a donc pas decorrectif à grouper avec l'activation.
Ce qui reste vrai de la description : rien ne se déclenche sur une branche, la suite
end-to-end ne tourne nulle part, et les trois dernières exécutions échouent.
2. Le fichier reste
.github/workflows/Le répertoire désigne une forge qu'on n'utilise pas, mais Forgejo le lit — c'est prouvé
ci-dessus, pas déduit d'une documentation — et un miroir GitHub, s'il arrive un jour, le
reprend tel quel. Déplacer vers
.forgejo/workflows/n'achèterait qu'un nom honnête, auprix d'un aller-retour le jour du miroir. Le coût est le nom ; il est noté ici pour qu'il
ne se redécouvre pas.
3. Un seul job
checks, à chaque push, sur toute brancheCe que coûtent les cibles, mesuré sur le poste :
make lintmake typecheckmake testmake buildVingt-cinq secondes de vérifications. Tout le reste du temps d'une exécution est
make install, c'est-à-dire troisnpm ci— racine,ex_app/lib,ex_app/src. Ledécoupage actuel en quatre jobs les paie quatre fois, et n'achète aucun parallélisme
en échange : le runner est à capacité 1, ce que le run 6 montre noir sur blanc —
lintfinit à 14:56:05,
typecheckdémarre à 14:56:08,testà 14:56:12.Donc un job unique,
checks, qui installe une fois puis enchaînelint,typecheck,test,builddans cet ordre. Ce que ça coûte : une seule coche, et unlintrougearrête avant les tests — il faut ouvrir le log pour savoir laquelle des quatre a cassé.
L'ordre est choisi pour ça : la cible la plus rapide et la plus probable d'abord.
Déclenchement : chaque push, sur toute branche.
mainn'accepte que desfast-forward et il n'y a pas de pull request : une CI qui ne regarde que
mainvalide cequi est déjà rentré. Le seul moment où un échec évite quelque chose est la branche
feat/…, avant le rebase.mainreste couvert, un push y étant un push.Trois détails qui ne sont pas des détails :
runs-on: ubuntu-latest— le label existe sur le runner (0604bb33-109a-447c-b536-7f7857749495,labels
self-hosted,ubuntu-latest,linux_arm64) et y a déjà exécuté un job.node:22-slim; le poste est en 24. La CIvérifie ce qui est livré, pas ce qui est confortable.
npm run lintinchangé. LaCI dit exactement ce que dit la commande locale — sans ça, elle devient un second
référentiel que personne ne peut reproduire chez soi. Les 265 avertissements ne sont pas
un chantier de ce ticket.
4. La e2e : pourquoi elle ne peut pas être un job comme les autres
Le point dur, et il n'est pas où on l'attend. Ce n'est pas que la suite ait besoin d'une
instance Nextcloud — c'est qui la pilote.
e2e/tests/instance.tsappelleexecFileSync('docker', ['compose', 'exec', …])depuis leprocessus Playwright lui-même, avec la racine du dépôt pour
cwd:psql()etocc()passent par là, et
bringToZeros'en sert avant chaque campagne et après. Deuxconséquences fermes :
services:du workflow. Les tests ne le trouveraientpas : ils ne connaissent que
docker compose, dans ce dépôt, avec ce nom de projet.docker, le socket de l'hôte, et le checkout au même chemin dedans et dehors — les
bind mounts de
compose.yaml(./ex_app/lib/src,./ex_app/src/dist/js,./ex_app/img)se résolvent côté hôte. Trois conditions dont la troisième se casse en silence : un
répertoire vide monté à la place des sources ne fait pas d'erreur, il fait un bundle
absent.
Le job e2e s'exécute donc sur l'hôte —
runs-on: self-hosted, le label sans image —là où
make up,docker compose execetlocalhost:8080valent ce qu'ils valent enlocal. À vérifier avant d'écrire le fichier : que
self-hostedest bien configuré enexécution hôte sur ce runner, et non associé à une image comme
ubuntu-latest.La séquence, dans cet ordre et pas un autre :
make installnpx playwright install --with-deps chromium, le cache du runner évitant letéléchargement aux tours suivants
make build-frontend— avantmake up. Compose monteex_app/src/dist/js; unrépertoire vide donne une suite qui teste l'absence de bundle et peut très bien passer.
C'est le premier des deux pièges que la page
Workflowdu wiki liste déjà.make up— postgres,nextcloud:34-apache, l'ExApp, puisdev/setup.shcd e2e && npm testmake down, enif: always()— sinon la pile survit au job et le suivant démarre surl'état du précédent, ce que la suite passe justement son temps à interdire
dev/setup.shest réutilisé tel quel, sans variante CI : c'est lui qui pose l'état quela suite attend (D29) — quotas DAV desserrés,
allow_local_remote_servers, comptealice,daemon
manual_install, enregistrement de l'ExApp. Une copie adaptée aurait divergé dansla semaine, et la divergence se serait vue comme un échec de test.
Cadence :
pushsurmain, plusworkflow_dispatch. 93 tests, un seul worker, ensérie (
workers: 1,fullyParallel: false— une instance partagée, D29), derrièrel'installation complète d'un Nextcloud, sur un runner à capacité 1 : sur chaque branche,
deux pushs rapprochés feraient la queue derrière plusieurs minutes chacun.
workflow_dispatchrend la suite lançable à la main sur une branche avant le rebase, quiest le moment où on en a besoin.
Elle est bloquante. Une suite dont l'échec n'a pas de conséquence cesse d'être lue en
quelques semaines, et on aura payé le runner pour rien.
À l'échec, les artefacts remontent :
e2e/playwright-report/ete2e/test-results/.La configuration pose déjà
trace: 'retain-on-failure'etscreenshot: 'only-on-failure';sans téléversement, tout ça reste sur le runner et un échec en CI n'est qu'une ligne rouge.
Deux risques nommés, à vérifier et non à supposer :
linux_arm64.nextcloud:34-apacheetpostgres:16-alpinesontmulti-arch, le Chromium de Playwright existe pour Ubuntu arm64 — c'est néanmoins la
première chose qui cassera, et ça se voit au premier tour.
les familles qu'elle trouve avant de démarrer. Elle ne doit jamais viser autre chose que
la pile que le job vient de monter.
5. Ce que ce ticket ne fait pas
make imageconstruit et pousse sur ghcr.io ; ça demandeun secret de registre dans Forgejo et une politique de tags par version. Ça appartient à
#11, qui attend précisément cette CI pour ne pas publier depuis un poste.
mainreste en fast-forward seul,il n'y a pas de pull request à bloquer : elle dit ce qui va, sur la branche, avant le
rebase. C'est une information, et la discipline reste la vôtre.
Le runner, et ce que ses logs corrigent dans la conception
1.
Bad system calln'est pas une erreur de npmBad system callest SIGSYS : un filtre seccomp qui tue le processus au lieu de luirefuser un appel système.
npm cimeurt donc instantanément, etmakene fait querapporter la mort de son fils. Aucun
npm cin'a jamais eu la moindre chance sur ces troisexécutions — ce qui explique les 2 s.
Sur un job qui s'exécute sur l'hôte, le filtre ne peut venir que de l'environnement du
processus runner. Le suspect par défaut est le durcissement systemd de son unité :
SystemCallFilter=sansSystemCallErrorNumber=a précisément SIGSYS pour action. Çarecolle aussi à la chronologie — 2 min 15 qui tournent à 14:53, puis plus rien à partir de
14:56 : un runner lancé à la main dans un terminal, puis installé en service.
Les deux commandes qui tranchent, sur la machine du runner :
La première nomme l'appel bloqué (
audit: type=1326 … syscall=NNN), la seconde dit qui lebloque. Correctif au choix : vider
SystemCallFilter=, ou poserSystemCallErrorNumber=EPERMpour que l'appel soit refusé plutôt que fatal.2. Les labels ne veulent pas dire ce que leur nom suggère
ubuntu-latestest de l'exécution hôte. La conception le supposait conteneurisé, et leci.ymlécrit hier faisait donc tournercheckssur la machine sans que ce soit un choix.Le seul label conteneurisé est
linux_arm64, et il désignenode:22-bookworm.Ce que ça change, dans les deux sens :
checkspasse enruns-on: linux_arm64. Le job s'isole de l'état de la machine, etle seccomp de l'hôte ne l'atteint pas : il peut redevenir vert sans attendre le correctif
système. Le coût est le nom du label, qui annonce une architecture et pas une intention —
d'où le commentaire dans le fichier, pour que ça ne se redécouvre pas.
self-hosted, et reste bloquée. Elle doit être sur l'hôte, c'esttout son intérêt (elle pilote
docker composeelle-même) ; elle ne peut donc pascontourner le filtre en changeant de label. Réparer le SIGSYS n'est pas optionnel, c'est
seulement devenu non bloquant pour la moitié rapide.
actions/setup-nodeest conservé dans le conteneur alors que l'image porte déjà Node 22 :il n'y reste que pour le cache npm des trois
package-lock.json, qui est la part dominantedu temps d'une exécution.
3. Répétition complète du job
checks, hors runnerLe contenu du job a été rejoué dans l'image du label, en arm64, sur un export propre de
HEAD— donc sansnode_moduleshérités — avec un cache npm froid :make installnpm cimake lintmake typecheckmake testmake buildnode:22-bookwormportemake,git,python3,g++etcurl: la chaîne native debetter-sqlite3est là, rien à ajouter à l'image.Un avertissement relevé au passage et laissé tel quel :
rollup-plugin-node-externals@9.0.1déclare
node >= 24alors que la CI et l'image de production sont en 22. C'est unEBADENGINE, npm ne l'applique pas, et la construction passe — mais c'est le genre dechose qui casse sans prévenir à la montée de version, et la répétition ci-dessus est la
seule raison pour laquelle on le sait.
4. Ce qui reste
checksest prêt et a été vérifié aussi loin qu'on peut le faire sans le runner ; il nemanque que le premier tour. La e2e est écrite mais ne peut pas être verte avant le
correctif seccomp — et Chromium en arm64 n'a toujours pas été essayé.
Premier tour : deux problèmes d'infrastructure, aucun dans le dépôt
Runs 9 et 10, sur
feat/ci-forgejo-actions(a1d6f17). Les deux rouges, et ni l'un nil'autre n'a atteint une seule cible du
Makefile.1. Ce que le conteneur a réglé
Le job a tourné 5 min 05, là où les trois exécutions précédentes mouraient en 2 s. Le
SIGSYS est donc bien propre à l'exécution « host » : le conteneur
linux_arm64n'estpas soumis au filtre du service. Le choix de label tient.
2. Le SIGSYS, cause réelle
L'unité s'appelle
forgejo_runner.service— avec un souligné, ce qui explique pourquoi lapremière recherche n'avait rien trouvé. Elle porte :
Une liste de refus, sans
SystemCallErrorNumber=: l'action par défaut est de tuer leprocessus, d'où
Bad system call. Quel ensemble est coupable reste inconnu. L'hypothèse«
npm cien root appellesetuid» ne tient pas :User=forgejo_runnerdit que le runnern'est pas privilégié, et un npm non privilégié n'appelle pas
setuid. Plutôt que de devinerà nouveau, le correctif retenu est
SystemCallErrorNumber=EPERM, qui fait refuser lesappels filtrés au lieu de tuer — suffisant pour presque tout outil, et le durcissement reste.
Ça ne concerne que la e2e : elle doit être sur l'hôte, c'est son intérêt même.
3. Le conteneur ne joint pas la forge
C'est ce qui a rougi les runs 9 et 10, et c'est nouveau — apparu avec le conteneur :
Trois tentatives, trois refus en ~100 ms. Le nom se résout, le TCP est refusé net — ce n'est
pas un problème de DNS ni de lenteur. Un job sur le bridge Docker n'atteint pas la forge :
pare-feu fermé au sous-réseau du bridge, ou proxy qui n'écoute que sur l'IP publique et
hairpin NAT qui ne repasse pas. Les jobs « host » se clonaient très bien, ce qui cadre.
Correctif visé :
container.options: --add-host=git.lozach.eu:host-gatewaydans/var/www/forgejo_runner/config.yml.Décidé : on répare le réseau et
checksreste en conteneur, plutôt que de le ramenersur l'hôte. L'isolation vis-à-vis de ce que la machine porte était la raison du choix, et le
correctif réseau sert tout job conteneurisé à venir.
4. Un troisième point, pas encore rencontré
ProtectSystem=fullmet/usren lecture seule. Le job e2e appellenpx playwright install --with-deps chromium, dont le--with-depsest unapt-get install: il échouera. Soit on lève la protection, soit on installe les dépendances systèmede Chromium une fois à la main et on retire le drapeau du workflow — la seconde garde le
durcissement et remplace une installation à chaque exécution par une opération unique.
5. État
Rien à corriger dans le dépôt : les deux workflows sont écrits, et le contenu de
checksaété répété vert de bout en bout dans l'image du label (79 s, arm64). Les trois lignes
restantes du plan d'action sont toutes sur la machine du runner.
Run 35 : un timeout sous charge, pas une régression
Deux tests frontend tombés sur le délai de 5 s :
Des assertions synchrones sur un formateur de dates. Rejouées dans l'image du runner,
sur un export propre de
HEAD: les deux fichiers font 25 tests en 66 ms. Et le mêmecommit était passé au run 34.
Ce qui a bougé, c'est la machine
Entre les deux exécutions, rien d'autre qu'une modification du workflow — qui ne touche pas
aux tests :
testscollecttestsUn facteur 6 à 18, dans les deux sens. Vitest dimensionne son pool sur les cœurs que Node
lui rapporte, et un conteneur sans limite de CPU se voit annoncer tous les cœurs de
l'hôte — y compris ceux qui servent déjà Forgejo et nginx. Les workers s'affament
mutuellement, et un test qui dure moins d'une milliseconde rate une échéance de cinq
secondes.
La contre-épreuve va dans le même sens : la suite frontend plafonnée à 1 CPU passe
entière — 344 tests, 349 ms de temps de test — simplement parce que Node n'y voit qu'un cœur
et vitest ne lance qu'un worker. Je n'ai pas réussi à faire échouer les tests ; j'ai réussi à
les rendre fiables, ce qui pointe la même cause sans la démontrer.
Le plafond retenu, et ce qu'il coûte
Mesuré sur dix cœurs, suite frontend complète :
maxWorkers: '50%'Une seconde d'horloge pour la moitié de la contention. Deux workers coûtaient 43 % de plus,
ce qui dépasse le prix du problème.
Le plafond va dans
vitest.config.ts, pas dans le workflow : ce qui passe sur le postedoit passer en CI, comme pour le lint. Le backend n'avait aucune configuration vitest, il en
reçoit une.
Vérifié dans l'image du runner : 426 tests backend, 344 frontend, 344 sous
Pacific/Auckland, lint et typecheck compris.