Aller au contenu

Pipeline GitLab CI

Référence factuelle de la pipeline d'intégration continue Kirexo. La configuration est éclatée entre un orchestrateur racine (.gitlab-ci.yml) et plusieurs fichiers spécialisés sous .gitlab/ci/.

Pour les procédures opérationnelles — créer un tag, configurer le domaine GitLab Pages — voir les guides correspondants :

Valider la config en local avant de pousser

Après tout changement dans .gitlab-ci.yml ou .gitlab/ci/, lancer castor ci:local : sans argument, la cible parse la config, fusionne les include / !reference / needs, valide le schéma JSON et liste tous les jobs via --list-all sans rien exécuter (ni Docker requis — marche aussi bien depuis le devcontainer que depuis l'hôte). Elle peut aussi exécuter un job précis (castor ci:local quality:phpstan), ce qui nécessite cette fois le socket Docker et donc un terminal hôte. Penser à git add tout nouveau fichier d'include avant de valider : gitlab-ci-local ne lit que les fichiers suivis par git. C'est un garde-fou de configuration — pas un substitut à la pipeline distante.

Fichiers

Fichier Rôle
.gitlab-ci.yml Orchestrateur racine. Inclut les fichiers .gitlab/ci/*.yml, déclare les stages, la workflow: (sources de pipeline acceptées, auto_cancel) et les templates .php_* réutilisés par les jobs PHP. Fixe PHP_VERSION et les noms d'images KIREXO_*_IMAGE (dont KIREXO_DOCS_IMAGE).
.gitlab/ci/quality.yml Jobs qualité et tests — miroir de castor all. Stages quality (sans services, dont build:vendor qui amorce le cache Composer) et test (avec Postgres, Redis, Typesense, dont test:unit qui porte le gate de couverture).
.gitlab/ci/docker.yml Build et push des cinq images kirexo-base, kirexo-dev, kirexo-prod (Dockerfile racine), kirexo-docs (mkdocs/Dockerfile) et kirexo-supply-chain (supply-chain/Dockerfile) dans le registry GitLab, plus la signature keyless cosign de kirexo-prod sur tag (docker:sign-prod).
.gitlab/ci/release.yml Déploiement de la documentation sur GitLab Pages, génération du SBOM (release:sbom, SPDX + CycloneDX via syft) et création de la release GitLab avec attachement des SBOM en assets — déclenchés uniquement sur tag.
.gitlab/ci/tag.yml Pipeline isolée qui crée un tag YYYYMMDDHHMM via l'API GitLab — déclenchée par la variable CREATE_TAG=true.
.gitlab/ci/security.yml Scanners de sécurité natifs GitLab + gating/alerting custom : SAST Semgrep PHP, Secret Detection (secret_detection + gate bloquant security:secret-gate), Container Scanning de l'image de prod (container_scanning + security:container-issue) et des images builders (container_scanning:builders + security:container-issue:builders pour kirexo-base / kirexo-dev), et audit quotidien des dépendances Composer (security:composer-audit, ouvre une MR de patch si l'update suffit, fallback issue sinon). Inclut les templates Security/SAST.gitlab-ci.yml, Jobs/Secret-Detection.gitlab-ci.yml et Jobs/Container-Scanning.gitlab-ci.yml.
.gitlab/ci/dependencies.yml Maintenance des dépendances et des outils en cron (schedule) : dependencies:renovate (montées de version automatiques) et tools:version-check (versions épinglées par ARG dans le Dockerfile).
renovate.json (racine) Configuration de Renovate : managers (composer, npm, images Docker, jobs CI), regroupement des mises à jour mineures/patch, isolation des majeures, vulnerabilityAlerts, lockFileMaintenance. Voir Configurer les montées de version automatiques (Renovate).

Stages

L'ordre de déclaration des stages dans .gitlab-ci.yml est l'ordre d'exécution. Au sein d'un même stage, les jobs tournent en parallèle.

Stage Rôle Quand
quality Amorçage du cache Composer (build:vendor, qui exécute en plus composer validate --strict --no-check-publish pour bloquer une MR qui modifie composer.json sans régénérer le lock), puis vérifications rapides sans services (style PHP, typage TypeScript, shellcheck, build mkdocs strict quality:docs-build, audit Composer bloquant). MR, push sur main. Sur tag de release : seul quality:composer-audit tourne (filet contre une CVE Composer publiée entre le merge et le tag). Les autres jobs de ce stage sont skippés via .rules:no-tag — leur résultat ne pourrait pas changer entre la pipeline main validée par tag:create et la pipeline du tag sur le même SHA (cf. Optimisation pipeline de tag).
test Analyse statique, lint, validation schéma Doctrine, tests unitaires/intégration (avec gate de couverture porté par test:unit, échoue sous 100 %), tests Bash, E2E. MR, push sur main. Aucun job de ce stage ne tourne sur tag (cf. Optimisation pipeline de tag).
docker Build et push des images Docker dans le registry, dont l'image de build de la doc kirexo-docs et l'image utilitaire kirexo-supply-chain (cosign + syft). Images du Dockerfile racine : manuel sur main/MR, automatique sur tag de release. docker:build-docs et docker:build-supply-chain : automatique sur main si le Dockerfile correspondant change (manuel sinon), manuel sur MR, automatique sur tag de release.
release Déploiement GitLab Pages (via l'image kirexo-docs) + release GitLab avec changelog. Tag de release au format YYYYMMDDHHMM ou schedule hebdo avec REBUILD_DOCS=true pour pages (rebuild de la doc figée sur le dernier tag — cf. Job pages).
maintenance Jobs périodiques et non bloquants : SAST informatif (semgrep-sast, sur chaque MR et chaque push main), audit Composer quotidien (security:composer-audittente d'ouvrir une MR de patch, fallback issue), Renovate (dependencies:renovate), contrôle des outils épinglés (tools:version-check), scan CVE de l'image kirexo-prod (container_scanning, en cron quotidien ET sur tag de release), scan CVE des images builders (container_scanning:builders sur kirexo-base et kirexo-dev, cron quotidien uniquement) et ouverture d'issue sur CVE conteneur (security:container-issue + security:container-issue:builders). Le garde-fou de sécurité bloquant reste quality:composer-audit, dans le stage quality. MR + push main pour le SAST ; schedule quotidien pour les jobs de maintenance ; tag de release pour container_scanning + security:container-issue (scan de l'image fraîchement poussée).
tag Création d'un tag YYYYMMDDHHMM via l'API GitLab. CREATE_TAG=true et depuis la branche main uniquement.

Sources de pipeline acceptées

La clause workflow:rules filtre les déclenchements à la racine — toute pipeline qui n'entre pas dans cette liste n'est même pas créée :

Source Condition Effet
Merge Request $CI_PIPELINE_SOURCE == "merge_request_event" Pipeline complète quality + test sur chaque push de MR.
Push sur main $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH Pipeline complète quality + test à chaque commit poussé sur main.
Tag de release $CI_COMMIT_TAG =~ /^[0-9]{12}$/ quality:composer-audit (seul rescapé des stages quality/test — cf. Optimisation pipeline de tag) + docker (auto, dont docker:build-docs) + release (Pages + release GitLab) + scan/issue CVE de l'image fraîchement poussée. Seul le format YYYYMMDDHHMM (12 chiffres) déclenche la pipeline : pousser un tag annexe (v1.0, wip-foo, prototypes) ne crée AUCUNE pipeline — pas de release, pas de build d'images, pas de redéploiement Pages. La même regex est répétée sur tous les jobs de release (.rules:standard, .rules:docker, pages, release:create, docker:build-docs, container_scanning, security:container-issue) pour rester cohérent avec ce filtre racine.
Création de tag $CREATE_TAG == "true" (uniquement depuis main) Pipeline isolée stage tag uniquement — ne joue rien d'autre. La rule du job tag:create exige en plus $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH : déclencher CREATE_TAG=true depuis une feature branch ne crée pas de tag. Le job refuse aussi de taguer si aucune pipeline success n'existe pour le commit ciblé, sauf FORCE_TAG=true (cf. Pipeline de création de tag).
Schedule $CI_PIPELINE_SOURCE == "schedule" Les jobs de maintenance du stage maintenance : security:composer-audit (audit Composer + tentative de MR de patch), dependencies:renovate (montées de version), tools:version-check (versions d'outils épinglées), container_scanning (scan CVE de kirexo-prod:latest), container_scanning:builders (matrix sur kirexo-base et kirexo-dev:php$PHP_VERSION) et les jobs d'alerting security:container-issue / security:container-issue:builders (ouvrent une issue par image vulnérable). Un seul schedule quotidien suffit à les déclencher tous (cf. stage maintenance). Une seconde schedule hebdo, distincte (variable REBUILD_DOCS=true), réveille le job pages du stage release pour rafraîchir le rendu de la doc sans attendre la prochaine release (cf. Job pages).

Le bloc workflow:auto_cancel (.gitlab-ci.yml) porte deux directives complémentaires :

Directive Valeur Effet
on_new_commit interruptible Annule automatiquement toute pipeline interruptible: true en cours dès qu'un nouveau commit est poussé sur la même branche — économise du runner sur les MRs où plusieurs corrections s'enchaînent.
on_job_failure all Dès qu'un job échoue, annule tous les jobs encore en cours de la pipeline. Sans elle, seuls les jobs en aval via needs: étaient bloqués ; les jobs parallèles du même stage continuaient inutilement jusqu'à leur terme. Avec elle, le premier échec arrête toute la pipeline — feedback plus rapide, runner libéré au plus tôt.

Les jobs docker:* (dont docker:build-docs), release:*, pages, tag:create, dependencies:renovate et tools:version-check sont explicitement interruptible: false (un build d'image, une création de tag ou une ouverture de MRs de montée de version ne doit jamais être interrompu à mi-parcours) — interruptible: false les protège aussi de l'annulation déclenchée par on_job_failure: all.

Règles réutilisables (.rules:*)

Plutôt que de répéter le bloc rules: dans chaque job, l'orchestrateur .gitlab-ci.yml définit six hidden jobs réutilisables. Les jobs concernés y font référence via la syntaxe !reference [.rules:NAME, rules], que GitLab aplatit dans leur propre rules: à la compilation. Une seule définition à maintenir, comportement identique partout.

Hidden job Périmètre Filtre MR Comportement sur main et tag
.rules:standard Jobs quality / test qui doivent tourner aussi sur tag — aujourd'hui réservé à quality:composer-audit (filet CVE entre merge et tag). Aucun filtre changes: — tourne sur toute MR. Tourne toujours, y compris sur tag (hors pipeline CREATE_TAG=true).
.rules:no-tag Jobs quality / test dont le résultat ne peut pas changer entre la pipeline main validée par tag:create et la pipeline du tag sur le même SHA : quality:cs-fix, quality:shellcheck, quality:docs-build, quality:phpstan, quality:lint. Même filtre MR que .rules:standard mais sans la branche tag (cf. Optimisation pipeline de tag). Aucun filtre changes: — tourne sur toute MR. Tourne sur main (hors CREATE_TAG=true). Ne tourne pas sur tag.
.rules:tests-php Réservé (plus aucun job ne l'utilise depuis la migration vers .rules:tests-php-no-tag). Allowlist changes: partagée par YAML anchor avec la variante -no-tag. Allowlist changes: — voir détail ci-dessous. Tourne sur main (hors CREATE_TAG=true) et sur tag.
.rules:tests-php-no-tag test:unit, test:bash, test:e2e. Même allowlist changes: que .rules:tests-php (partagée via YAML anchor — zéro duplication). Allowlist changes: identique à .rules:tests-php. Tourne sur main (hors CREATE_TAG=true). Ne tourne pas sur tag.
.rules:ts-check Réservé (idem .rules:tests-php). Allowlist changes: sur les sources TypeScript et leurs déclencheurs. Tourne sur main et sur tag.
.rules:ts-check-no-tag quality:ts-check. Même allowlist que .rules:ts-check (partagée via YAML anchor). Allowlist changes: sur les sources TypeScript et leurs déclencheurs (package.json, tsconfig.json, **/*.ts, **/*.tsx, Dockerfile, CI). Tourne sur main (hors CREATE_TAG=true). Ne tourne pas sur tag.

Conséquence pratique : sur une MR qui ne touche que la doc ou des fichiers de cadrage, la pipeline reste verte rapidement (les jobs de test PHP / TS ne tournent même pas). Sur main, le filet est complet — pas de skip silencieux d'un test au merge. Sur tag, seul quality:composer-audit rejoue le filet de sécurité ; les jobs déjà validés sur main (style, analyse statique, tests) sont skippés pour ne pas dépenser de minutes runner inutilement.

Allowlist changes: de .rules:tests-php

GitLab ne sait pas faire « tout sauf X » dans rules:changes: — il faut lister ce qui doit déclencher les tests. La liste vit dans .gitlab-ci.yml, à proximité immédiate du hidden job pour rester lisible :

Chemin / fichier Pourquoi
src/**/*, tests/**/* Le code de prod et de test.
config/**/*, templates/**/*, public/**/*, migrations/**/*, bin/**/* Tout ce qui peut influencer le comportement à l'exécution.
composer.json, composer.lock Changement de dépendances → re-tester.
phpunit.dist.xml, phpstan.dist.neon, .php-cs-fixer.dist.php Changement de configuration des outils qualité.
castor.php, castor/**/* Les cibles Castor pilotent l'exécution de la CI.
.gitlab-ci.yml, .gitlab/ci/**/* Toute modification de la CI elle-même.
Dockerfile, .dockerignore, compose*.yaml Changement d'environnement d'exécution.
.env, .env.test Variables d'environnement effectives en CI.

À l'inverse, le périmètre considéré comme doc-only — donc skippé sur MR par .rules:tests-php — est : docs/**, mkdocs.yml, mkdocs/**, Étapes/**, README.md, CLAUDE.md, apprentissage.md, .claude/**. Une MR qui ne touche que ces fichiers ne lance ni test:unit, ni test:bash, ni test:e2e.

Tout nouveau dossier de prod doit être ajouté à l'allowlist

L'allowlist est exhaustive : si on crée un nouveau dossier de prod (ex. app/, lib/, plugins/) et qu'on oublie de l'ajouter à .rules:tests-php dans .gitlab-ci.yml, ses changements ne déclencheront pas les tests sur MR. Le filet ne tombera qu'au moment du merge sur main (où les tests tournent toujours) — trop tard pour un feedback de review. Réflexe à avoir au moment de créer le dossier : éditer aussi .gitlab-ci.yml dans la même MR.

Optimisation pipeline de tag

La pipeline déclenchée par un tag YYYYMMDDHHMM ne rejoue ni les vérifications de qualité, ni les tests, ni le build du cache vendor : ces jobs ont déjà tourné sur le même SHA dans la pipeline main qui a précédé le tag, et tag:create (cf. Pipeline de création de tag) refuse explicitement de poser le tag si aucune pipeline success n'existe pour ce commit. Rejouer ces jobs serait donc de la duplication pure — même code, même résultat.

Concrètement, sur tag, les jobs suivants sont skippés via les variantes -no-tag de leurs .rules: :

Job Rule appliquée Pourquoi skippé sur tag
quality:cs-fix .rules:no-tag Style PHP — résultat invariant sur le même SHA.
quality:ts-check .rules:ts-check-no-tag Typage TypeScript — idem.
quality:shellcheck .rules:no-tag Lint shell — idem.
quality:docs-build .rules:no-tag Build mkdocs strict — déjà validé en MR via preview ; le déploiement réel est porté par pages sur tag.
quality:phpstan .rules:no-tag Analyse statique — idem.
quality:lint .rules:no-tag Lint Twig/YAML/conteneur + doctrine:schema:validate — idem.
test:unit .rules:tests-php-no-tag Tests unitaires + gate de couverture — idem.
test:bash .rules:tests-php-no-tag Tests bats — idem.
test:e2e .rules:tests-php-no-tag Tests E2E Panther — idem.
build:vendor rules custom sans branche tag Cache Composer reproductible depuis composer.lock — les jobs docker:* rebuilent leur propre vendor/ à partir du même lockfile pour produire l'image, ils n'ont pas besoin du cache.

Seuls les jobs qui produisent un livrable ou dont le résultat dépend de l'instant T du tag continuent de tourner :

  • quality:composer-audit (.rules:standard, branche tag conservée) — filet de sécurité : une CVE Composer peut être publiée entre le merge sur main et la création du tag ; on ne veut pas qu'un audit silencieusement obsolète passe en prod ;
  • docker:build-base, docker:build-dev, docker:build-prod, docker:build-docs, docker:build-supply-chain — production des images du tag ;
  • docker:sign-prod — signature keyless de l'image fraîchement poussée ;
  • pages, release:create, release:sbom — déploiement de la doc + release GitLab + SBOM ;
  • container_scanning + security:container-issue — scan CVE de l'image kirexo-prod:$CI_COMMIT_TAG au moment de la release (cf. Jobs container_scanning et security:container-issue).

Gain typique : une douzaine de jobs supprimés sur chaque pipeline de tag, qui représentaient l'essentiel des minutes runner consommées par la pipeline de release. La validation de qualité reste pleine et entière sur main — elle ne diminue pas, elle se déplace au seul endroit où elle apporte un signal.

Graphe de dépendances (needs:)

Les jobs des stages quality et test ne s'exécutent pas en cascade stage-par-stage : un DAG needs: les fait démarrer dès que leur dépendance directe est verte, sans attendre la fin du stage précédent. Gain de vitesse sur chaque pipeline.

Job Stage needs: Démarre quand
build:vendor quality [] (détaché) Début de pipeline — amorce le cache vendor/ (cf. Cache Composer).
quality:cs-fix quality build:vendor Dès que vendor/ est en cache.
quality:ts-check quality Début de pipeline.
quality:shellcheck quality Début de pipeline.
quality:docs-build quality [] (détaché) Début de pipeline.
quality:composer-audit quality [] (détaché) Début de pipeline.
quality:phpstan test quality:cs-fix Dès que le style passe.
quality:lint test quality:cs-fix Dès que le style passe.
test:unit test quality:cs-fix Dès que le style passe.
test:e2e test quality:cs-fix Dès que le style passe.
test:bash test quality:shellcheck Dès que le lint shell passe.

Conséquence : build:vendor est en tête de la chaîne PHP — tous les jobs PHP descendent de lui par needs et lisent le cache vendor/ qu'il a poussé, au lieu de le télécharger chacun en parallèle (cf. Cache Composer). Si quality:cs-fix échoue, les jobs PHP du stage test ne tournent pas (économie de runner) ; inutile d'analyser du code dont le style est déjà cassé. test:bash ne dépend que de quality:shellcheck — pas la peine de tester des fonctions bash qui ne passent même pas shellcheck. La métrique de couverture est portée par test:unit seul (cf. Tests et gate de couverture).

Job quality:composer-audit

Audit bloquant des vulnérabilités connues de composer.lock, sur chaque MR, main et tag. C'est le vrai garde-fou de sécurité de la pipeline — il vit dans le stage quality, en tête (needs: []), pas dans le stage maintenance. Complémentaire de security:composer-audit (cron quotidien du stage maintenance, ouvre une issue) : ici, une dépendance vulnérable bloque le merge — feedback immédiat au moment du changement.

Caractéristique Valeur
Stage quality
Image KIREXO_BASE_IMAGE (binaire composer disponible).
needs: [] — détaché du DAG, démarre dès le début de pipeline.
Commande composer audit --no-interaction --locked — audite composer.lock sans installer vendor/, donc très rapide (pas de composer install).
Déclencheurs MR, main (hors CREATE_TAG=true), tag (.rules:standard).
Effet en cas de vulnérabilité Le job échoue → la MR ne peut pas être mergée tant que la dépendance n'est pas mise à jour.

Job quality:docs-build

Build mkdocs en mode strict avec exposition d'une preview dans la MR. Sert à détecter dès la MR les liens cassés, snippets manquants et avertissements de plugin qui, sans ce job, ne sortiraient qu'au job pages (déclenché uniquement sur tag de release) — c'est-à-dire trop tard. La preview permet en plus de review visuellement le rendu avant merge.

Caractéristique Valeur
Stage quality
Image $KIREXO_DOCS_IMAGE (mkdocs-material + plugins déjà installés ; entrypoint: [""] pour neutraliser l'entrypoint mkdocs hérité de l'image et laisser GitLab CI exécuter le script via le shell).
needs: [] — détaché du DAG, démarre dès le début de pipeline.
GIT_DEPTH 0 (histoire git complète pour git-revision-date-localized, sinon dégradation silencieuse sur la date des pages).
Commande mkdocs build --strict -d public
Artifact public/ — conservé 1 semaine (expire_in: 1 week). Exposé via expose_as: "Documentation preview" : GitLab affiche un bouton « View exposed artifact » dans la sidebar du widget de la MR qui ouvre directement public/index.html, sans télécharger l'archive.
Déclencheurs .rules:standard — MR, main (hors CREATE_TAG=true), tag de release.

Le rendu produit par pages (sur tag de release) repose sur la même image kirexo-docs : un build qui passe ici passe également au déploiement. La preview MR est jetable (1 semaine) — la version « officielle » reste celle servie par GitLab Pages sur le dernier tag (cf. Job pages).

Pourquoi expire_in: 1 week

expose_as exige un artifact accessible directement (ici public/index.html). Sans expiration, ces previews s'accumuleraient (une par MR × N pushs). Une semaine est largement suffisant pour la durée de vie d'une review.

Cache Composer (build:vendor)

build:vendor (.gitlab/ci/quality.yml, stage quality) amorce le cache Composer en tête de pipeline. C'est le seul job en policy: pull-push : il réalise le composer install lourd au cache miss (téléchargement des dépendances) et télécharge le binaire CLI Tailwind via bin/console tailwind:install, puis pousse vendor/ et var/tailwind/ en cache. Tous les autres jobs PHP héritent du template .php_quality_job qui est passé en policy: pull (lecture seule) : ils récupèrent ce cache via la chaîne de needs (build:vendorquality:cs-fix → reste) au lieu de télécharger vendor/ ou le binaire Tailwind chacun de leur côté.

Caractéristique Valeur
Stage quality
Image KIREXO_BASE_IMAGE (via .php_quality_job).
needs: [] — détaché du DAG, démarre dès le début de pipeline.
cache.policy pull-push (override du pull hérité de .php_quality_job).
Clé de cache composer.lock (le cache est invalidé dès que le lock change).
cache.paths vendor/ et var/tailwind/ — partagent la même clé composer.lock (le binaire Tailwind est versionné par symfonycasts/tailwind-bundle, présent dans le lockfile).
script composer validate --strict --no-check-publish puis bin/console tailwind:install — le travail utile (composer install) est dans le before_script du template ; le push du cache se fait à la fin du job.

Le composer validate --strict --no-check-publish détecte une MR qui modifie composer.json sans régénérer le composer.lock (un classique). --strict transforme les warnings (dépendances abandonnées, lock désynchro) en erreurs ; --no-check-publish ignore les champs « publication » (name/description/license) sans intérêt vu que le projet n'est pas un package Composer publié.

Le cache var/tailwind/

bin/console tailwind:install télécharge le binaire CLI tailwindcss (~30 Mo, Go statique) dans var/tailwind/. La version concrète est pilotée par symfonycasts/tailwind-bundle, lui-même épinglé dans composer.lock — d'où la clé de cache partagée avec vendor/ : tant que le lockfile ne bouge pas, le binaire en cache reste valide.

Sans cet amorçage, chaque job aval qui dépend de Tailwind re-téléchargerait ces ~30 Mo. Les consommateurs concrets dans la pipeline sont :

Job before_script consommateur Pourquoi
test:unit bin/console tailwind:build (via .php_test_job) Les tests d'intégration WebTestCase qui font GET / chargent base.html.twig via l'importmap → un asset tailwindcss manquant déclencherait un 500.
test:e2e bin/console tailwind:build (via .php_e2e_job) Panther charge la page comme un vrai navigateur → public/tailwind.css doit exister, sinon 500 « Built Tailwind CSS file does not exist ».

tailwind:build consomme le binaire installé dans var/tailwind/ ; il ne le télécharge pas lui-même. C'est ce découplage install (cher) / build (rapide) qui rend le cache utile.

Objectif principal du job : éviter qu'une demi-douzaine de jobs PHP téléchargent vendor/ en parallèle au cache miss et écrasent concurremment le même cache. En dégradé (cache absent), le composer install du before_script recrée vendor/ dans chaque job et tailwind:build retéléchargera son binaire — plus lent mais jamais cassé.

Tests et gate de couverture

Trois jobs du stage test portent la suite de tests : test:unit (suites unit + integration), test:e2e (suite Panther) et test:bash (fonctions bash via bats). La mesure de couverture est portée uniquement par test:unit : c'est lui qui calcule le pourcentage, porte la métrique coverage: (badge GitLab) et fait échouer le job si l'un des deux gates ci-dessous n'est pas tenu :

  • Lignes : 100,00 % (gate historique, règle CLAUDE.md).
  • Branches : ≥ 90,00 % (gate ajouté pour durcir le contrôle — une ligne exécutée ne vaut pas une condition testée dans les deux sens).

Pourquoi la couverture sur test:unit seul

Choix assumé : la couverture est jugée sur les seules suites unit + integration. C'est suffisant tant que tout le code de prod y est couvert — ce qui est la règle par défaut du projet.

CLAUDE.md autorise un cas particulier : un controller invokable trivial peut être couvert uniquement par son test E2E. Comme test:e2e ne produit plus aucune couverture, un tel controller apparaîtrait alors comme non couvert côté test:unit et ferait tomber le job sous 100 %. La parade est de le couvrir aussi en integration (un test WebTestCase), de sorte qu'il compte dans la mesure de test:unit.

Job test:unit

Caractéristique Valeur
Stage test
Template .php_test_job (override de l'image).
Image KIREXO_DEV_IMAGE (Xdebug requis pour la couverture — kirexo-base ne l'a pas).
needs: quality:cs-fix
Suites PHPUnit unit,integration
XDEBUG_MODE coverage
Rapport JUnit junit-unit.xml (tests dans la MR).
Cobertura cobertura-unit.xml (--coverage-cobertura) — affichage des lignes couvertes dans le diff de la MR et support du gate branches via l'attribut branch-rate.
coverage: regex /Lines:\s+(\d+(?:\.\d+)?%)/ — capte le Lines: NN.NN% réémis en fin de script. Seul job de la pipeline qui porte cette clé.
Effet sous l'un des seuils Le gate inline termine en exit 1 → le job échoue → la MR ne peut pas être mergée.

Les deux gates sont inline en shell, sans script externe. Le job lance phpunit --coverage-cobertura cobertura-unit.xml --coverage-text=coverage.txt, puis applique deux contrôles successifs sur les rapports produits.

Gate de lignes (100,00 %)

Extrait la première ligne Lines: du résumé texte (le total global ; les détails par fichier viennent après), échoue si elle n'est pas à 100.00, puis réémet ce total en dernière ligne Lines: du log pour que le regex coverage: du job capture bien le pourcentage global (et non un Lines: par fichier).

Gate de branches (≥ 90,00 %)

Le rapport Cobertura porte l'attribut branch-rate="X.YY" (valeur entre 0 et 1) sur la balise racine <coverage>. Le gate :

  1. Lit cette première occurrence dans cobertura-unit.xml via sed -n 's/.*branch-rate="\([0-9.]*\)".*/\1/p' | head -1.
  2. Convertit en pourcentage à deux décimales avec awk (pas de bc dans kirexo-dev).
  3. Échoue strictement sous 90,00 % ; émet le pourcentage observé en log sinon.

Si le branch-rate est absent du rapport, le gate échoue explicitement avec un message pointant sur pathCoverage="true" — sans cet attribut sur la balise racine <phpunit> de phpunit.dist.xml, PHPUnit ne collecte pas les branches et le rapport Cobertura porte un branch-rate="0" qui ferait échouer le job à tort. L'attribut est donc une dépendance directe du gate, pas un détail de configuration optionnel.

Dépendance vis-à-vis de PHP-CS-Fixer

L'atteignabilité du gate ≥ 90 % dépend de la règle native_function_invocation configurée dans .php-cs-fixer.dist.php. Sans elle, chaque appel à une fonction interne dans un namespace émet un opcode JMP_FRAMELESS dont le slow path compte comme branche non couverte côté Cobertura. La règle est donc une dépendance directe du gate au même titre que pathCoverage="true". Mécanique détaillée et conséquences pour les contributeurs : Exécuter les tests — Ne pas désactiver native_function_invocation.

Job test:e2e

test:e2e exécute la suite Panther (parcours navigateur, Chromium headless) et publie son JUnit. Il ne produit aucune couverture : pas de XDEBUG_MODE, pas d'option --coverage-* — ce qui allège le run E2E.

Caractéristique Valeur
Stage test
Template .php_e2e_job
Image KIREXO_DEV_IMAGE (Chromium + chromedriver).
needs: quality:cs-fix
Suite PHPUnit e2e
Rapport JUnit junit-e2e.xml (tests dans la MR).
Couverture Aucune — la mesure est portée par test:unit.
artifacts.paths var/error-screenshots/, var/log/test.log — exposés when: always, expirent au bout d'1 semaine.

test:e2e conserve un seul garde-fou spécifique à Panther/Chromium : un timeout réduit pour ne pas immobiliser un runner sur un navigateur bloqué.

Caractéristique Valeur
timeout 20m — borne le job sous le timeout projet pour ne pas immobiliser un runner sur un navigateur bloqué.

Pas de retry: configuré. Raison : workflow.auto_cancel.on_job_failure: all annule tous les jobs en cours de la pipeline dès qu'un job échoue, avant qu'un retry n'ait eu le temps de repartir. Un E2E qui flake une fois coupait donc l'intégralité de la pipeline pour rien — le retry ne servait à rien. La flakiness est désormais traitée à la source (timing, fixtures, sélecteurs) plutôt que masquée par un rejeu.

Artifacts de debug

var/error-screenshots/ n'est créé par Panther que sur échec d'au moins un test — la directive PANTHER_ERROR_SCREENSHOT_DIR du phpunit.dist.xml pointe sur ce dossier, et Panther dump un PNG du DOM à l'instant du KO pour chaque assertion en erreur. Une capture vaut mieux qu'une stack trace XML : elle montre littéralement l'état réel de la page au moment de l'échec (champ vide, erreur 500 stylée Symfony, redirection inattendue, modal qui n'a pas eu le temps de s'ouvrir). Sur une MR rouge, les captures sont consultables depuis l'onglet Artifacts → Browse du job sans git pull ni rerun verbeux.

var/log/test.log capture la sortie applicative en parallèle. Sur succès, ces deux paths restent essentiellement vides — GitLab n'accepte qu'un seul bloc artifacts: par job avec une seule directive when:, le compromis when: always garde le contrat « JUnit toujours publié » sans gymnastique sur les paths.

Images publiées dans le registry

Cinq images sont publiées dans le registry du projet ($CI_REGISTRY_IMAGE). Trois sont construites à partir des stages du Dockerfile racine ; les deux autres (kirexo-docs, kirexo-supply-chain) à partir de Dockerfiles dédiés. Le nom complet d'une image est de la forme registry.gitlab.com/<group>/<project>/<image>:<tag>.

Image Dockerfile / stage Tag mobile Contenu Usage CI
kirexo-base racine, frankenphp_base :php8.5 Extensions PHP, sans Chromium ni Node. Tourne en root. Jobs PHP « légers » sans driver de couverture (build:vendor, quality:cs-fix, quality:composer-audit, quality:phpstan, quality:lint).
kirexo-dev racine, frankenphp_dev :php8.5 Hérite de base + Chromium, chromedriver, Node, Xdebug, outils dev. test:bash (bats inclus), test:e2e (Panther + Chromium headless), test:unit (Xdebug requis pour la couverture).
kirexo-prod racine, frankenphp_prod :php8.5 Image de production. Déploiement — n'est jamais utilisée comme image de runner. C'est elle que compose.prod.yaml pull (cf. Image de production).
kirexo-docs mkdocs/Dockerfile :latest mkdocs-material + plugins (imaging via Pillow/CairoSVG, git-revision-date). Job pages (build mkdocs strict, cf. Job pages).
kirexo-supply-chain supply-chain/Dockerfile :latest Alpine 3.22 + binaires officiels cosign et syft, copiés depuis les images upstream (gcr.io/projectsigstore/cosign et anchore/syft) via un multi-stage Docker COPY --from=. Jobs docker:sign-prod (signature keyless) et release:sbom (génération SPDX + CycloneDX) — cf. Image supply chain.

Sur création de tag, les tags poussés diffèrent selon l'image :

Image Tags poussés sur création de tag
kirexo-base :php8.5 + :latest
kirexo-dev :php8.5 + :latest
kirexo-prod :php8.5 + :latest + :$CI_COMMIT_TAG (le tag Git YYYYMMDDHHMM)
kirexo-docs :latest (tag unique)
kirexo-supply-chain :latest (tag unique)

Le double tag :php8.5 ET :latest permet aux pipelines suivantes de toujours pointer vers :php8.5 (version PHP pinnée) tout en gardant un :latest lisible côté UI.

kirexo-prod reçoit en plus un troisième tag immuable :$CI_COMMIT_TAG (le tag Git YYYYMMDDHHMM). C'est ce tag de version que la production épingle via KIREXO_VERSION dans compose.prod.yaml pour un pin de version et un rollback déterministe (cf. Déployer Kirexo en production). kirexo-base et kirexo-dev n'en ont pas besoin : côté CI elles sont toujours référencées par :php8.5. Les images kirexo-docs et kirexo-supply-chain ne portent qu'un tag :latest unique : elles ne dépendent pas de PHP et leur contenu (un environnement d'outillage CI) n'a pas de version à épingler — leur versionnement se fait via le tag des FROM de leur Dockerfile respectif, suivi automatiquement par le manager dockerfile de Renovate.

Le tag de version PHP n'est pas codé en dur — il est dérivé de la variable racine PHP_VERSION (.gitlab-ci.yml). Une montée de version PHP se résume à modifier cette ligne puis à rebuilder les trois images du Dockerfile racine.

Jobs de build

Les trois jobs partagent un template .docker_job qui charge docker:27 + docker:27-dind, s'authentifie sur le registry via CI_REGISTRY_USER/CI_REGISTRY_PASSWORD et active BUILDKIT_INLINE_CACHE=1 pour que --cache-from reste efficace d'un job au suivant (DinD repart à vide à chaque job).

Job Cible / Dockerfile Tag produit Déclencheur
docker:build-base racine, frankenphp_base $CI_REGISTRY_IMAGE/kirexo-base:php8.5 Manuel sur main / MR. Automatique sur tag.
docker:build-dev racine, frankenphp_dev $CI_REGISTRY_IMAGE/kirexo-dev:php8.5 Manuel sur main / MR. Automatique sur tag. Attend docker:build-base si présent (needs: optional: true).
docker:build-prod racine, frankenphp_prod $CI_REGISTRY_IMAGE/kirexo-prod:php8.5 (+ :latest et :$CI_COMMIT_TAG sur tag) Manuel sur main / MR. Automatique sur tag. Attend docker:build-base si présent.
docker:build-docs mkdocs/Dockerfile $CI_REGISTRY_IMAGE/kirexo-docs:latest Automatique sur main quand mkdocs/Dockerfile change, manuel sur main sinon. Manuel sur MR. Automatique sur tag. needs: [] — indépendant de docker:build-base.
docker:build-supply-chain supply-chain/Dockerfile $CI_REGISTRY_IMAGE/kirexo-supply-chain:latest Automatique sur main quand supply-chain/Dockerfile change, manuel sur main sinon. Manuel sur MR. Automatique sur tag. needs: [] — indépendant de docker:build-base.

Chaînage docker:build-basedev / prod

frankenphp_dev et frankenphp_prod dérivent tous deux de frankenphp_base (FROM frankenphp_base dans le Dockerfile). Pour réutiliser le stage base fraîchement poussé via --cache-from (et non celui du tag précédent), docker:build-dev et docker:build-prod portent needs: [{ job: docker:build-base, optional: true }] :

  • sur tag : docker:build-base tourne automatiquement (on_success) ; dev et prod attendent sa fin et pullent le base à jour → les couches base sont réutilisées au lieu d'être reconstruites ;
  • sur main / MR : docker:build-base est manuel et n'est généralement pas déclenché ; optional: true laisse alors dev et prod démarrer seuls pour un rebuild ponctuel, en retombant sur le dernier base publié dans le registry.

docker:build-docs et docker:build-supply-chain sont needs: [] : leurs images ne dérivent d'aucun stage du Dockerfile racine, ils ne dépendent donc pas de docker:build-base.

Smoke-test post-push de docker:build-prod

Entre le docker push $KIREXO_PROD_IMAGE (qui pousse kirexo-prod:php8.5) et le double tag final (:latest + :$CI_COMMIT_TAG, uniquement sur tag de release), le job exécute deux smoke-tests sur l'image fraîchement poussée. Coût ~3 s. Objectif : détecter avant de poser le tag de version (et donc avant qu'un déploiement de prod ne puisse les pull) un autoload cassé, un fichier source manquant dans l'image, ou une extension PHP requise par une dépendance et absente du Dockerfile.

# Commande Capture
1 docker run --rm --entrypoint composer $KIREXO_PROD_IMAGE check-platform-reqs --no-dev Version PHP de l'image et ensemble des extensions ext-* requises — directes ET transitives (la vérification lit composer.lock embarqué dans l'image). Capture le cas typique où une dépendance Symfony/Doctrine exige ext-intl ou ext-zip qui n'est pas listée dans composer.json directement et qui manque du Dockerfile.
2 docker run --rm --entrypoint php $KIREXO_PROD_IMAGE -r "require '/app/vendor/autoload.php'; class_exists('App\\Kernel') \|\| exit(1);" Autoload chargeable et classe App\Kernel résoluble depuis l'image. Capture un COPY trop restrictif dans le Dockerfile (src/ oublié), un .dockerignore qui exclut un dossier source, ou un autoload corrompu.

Les deux commandes overrident --entrypoint parce que le docker-entrypoint du projet déclenche un wait-for-database dès qu'il détecte php/bin/console/frankenphp et que DATABASE_URL est défini (et il l'est : .env est embarqué dans l'image). Sans cet override, un docker run kirexo-prod bin/console about se bloquerait 60 s à attendre une Postgres absente du runner CI. Pour la même raison, on n'utilise pas bin/console about : la commande exigerait APP_SECRET (obligatoire au boot du framework en prod), absent du runner CI sans injection d'un secret factice. Un class_exists suffit pour vérifier la chargeabilité.

Effet en cas d'échec : si l'une des deux commandes sort non-zéro, le job échoue, les tags :latest et :$CI_COMMIT_TAG ne sont pas posés, et la chaîne tag → release est interrompue avant que la production ne puisse pull l'image cassée. Le tag mobile :php8.5 est en revanche déjà poussé à ce stade — la prochaine pipeline sur tag re-tentera et l'écrasera.

Règles de déclenchement dédiées de docker:build-docs

Contrairement aux trois images du Dockerfile racine (qui partagent .rules:docker), docker:build-docs porte ses propres rules:. Ordre des règles (la première qui matche gagne) :

Contexte Condition Déclenchement
main, mkdocs/Dockerfile modifié $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CREATE_TAG != "true" + changes: [mkdocs/Dockerfile] Automatique (when: on_success).
main, sans modif du Dockerfile $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CREATE_TAG != "true" Manuel (bouton ▶, allow_failure: true).
Merge Request $CI_PIPELINE_SOURCE == "merge_request_event" Manuel (bouton ▶, allow_failure: true).
Tag $CI_COMMIT_TAG Automatique (when: on_success) — l'image alimente pages.

La règle « main + modif du Dockerfile » est placée avant la règle « main » générique : comme GitLab retient la première règle qui matche, l'ordre garantit que le rebuild automatique l'emporte sur le manuel dès que mkdocs/Dockerfile change. Les deux règles main portent la garde $CREATE_TAG != "true" pour ne pas se déclencher dans la pipeline isolée de création de tag.

kirexo-docs:latest est l'environnement de build de la doc, consommé par le job pages sur tag. Le rebuild automatique sur main à chaque évolution de mkdocs/Dockerfile garantit que le prochain déploiement pages (sur tag) part d'une image à jour, sans intervention manuelle.

Bootstrap initial obligatoire — sauf pour la doc

Avant le premier passage de la CI, les images kirexo-base et kirexo-dev doivent exister dans le registry, sinon tous les jobs PHP/E2E échouent immédiatement sur ErrImagePull. Déclencher une fois manuellement les jobs docker:build-base puis docker:build-dev depuis l'UI GitLab (Pipelines → cliquer sur ▶ sur le job manuel).

Les images kirexo-docs et kirexo-supply-chain n'ont pas besoin de ce bootstrap manuel : sur tag, docker:build-docs et docker:build-supply-chain (stage docker) tournent automatiquement avant leurs consommateurs respectifs du stage release (pages pour la doc, docker:sign-prod et release:sbom pour la supply chain) — les images sont donc toujours fraîches au moment où ces jobs s'exécutent.

Image supply chain (docker:build-supply-chain)

L'image kirexo-supply-chain embarque les binaires officiels cosign et syft sur une base Alpine 3.22 (cf. supply-chain/Dockerfile). Elle est consommée par docker:sign-prod (signature keyless de kirexo-prod) et release:sbom (génération SPDX + CycloneDX). Les motivations de cette image custom sont détaillées dans Pourquoi une image custom kirexo-supply-chain — en résumé : les images officielles upstream sont distroless (pas de sh), incompatibles avec le wrapper sh -c "step_script" du GitLab Runner SaaS.

Caractéristique Valeur
Stage docker
Base alpine:3.22.
Outils embarqués cosign et syft, copiés depuis les images upstream via un multi-stage Docker : FROM gcr.io/projectsigstore/cosign:<tag> AS cosign-stage et FROM anchore/syft:<tag> AS syft-stage, puis COPY --from=cosign-stage /ko-app/cosign /usr/local/bin/cosign et COPY --from=syft-stage /syft /usr/local/bin/syft. Les versions vivent dans le tag des deux FROM source — pas d'ARG, pas de SHA256 manuel.
Surface minimale Pas de curl, pas de tar : les binaires arrivent par COPY --from=, aucun outil de download n'a besoin d'être installé puis supprimé. Seul ca-certificates est ajouté (requis par cosign à l'exécution pour Fulcio / Rekor en HTTPS).
Chaîne de confiance Héritée des registries source : gcr.io/projectsigstore (Sigstore publie ses propres images cosign) et Docker Hub anchore/syft (Anchore publie ses propres images syft). Pas de vérification SHA256 manuelle — c'est le même modèle de confiance que n'importe quel FROM du Dockerfile racine.
needs: [] — détaché du DAG, comme docker:build-docs.
Smoke-test post-push docker run --rm $KIREXO_SUPPLY_CHAIN_IMAGE cosign version et ... syft version — valide l'image fraîchement poussée avant qu'un job de release ne s'y appuie. Le Dockerfile fait déjà ce check au build (RUN cosign version && syft version) ; le run sur l'image pushed valide aussi l'environnement d'exécution (PATH, permissions, libs dynamiques).
Tag poussé $CI_REGISTRY_IMAGE/kirexo-supply-chain:latest (tag unique — pas de versionnement de l'image elle-même).

Règles de déclenchement

Identiques à celles de docker:build-docs (ordre des règles : la première qui matche gagne) :

Contexte Condition Déclenchement
main, supply-chain/Dockerfile modifié $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CREATE_TAG != "true" + changes: [supply-chain/Dockerfile] Automatique (when: on_success).
main, sans modif du Dockerfile $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CREATE_TAG != "true" Manuel (bouton ▶, allow_failure: true).
Merge Request $CI_PIPELINE_SOURCE == "merge_request_event" Manuel (bouton ▶, allow_failure: true).
Tag $CI_COMMIT_TAG =~ /^[0-9]{12}$/ Automatique (when: on_success) — l'image alimente docker:sign-prod et release:sbom.

Bumper cosign ou syft

Le bump est automatique via Renovate : les versions vivent dans le tag des deux FROM du supply-chain/Dockerfile, suivis nativement par le manager dockerfile de Renovate (cf. Configurer les montées de version automatiques (Renovate)). À chaque nouvelle release upstream, Renovate ouvre une MR qui édite le tag du FROM concerné ; au merge, docker:build-supply-chain rebuild automatiquement l'image (règle changes: [supply-chain/Dockerfile]).

Bump manuel équivalent si nécessaire (urgence sécurité avant que Renovate ne tourne, par exemple) : éditer le tag du FROM ciblé dans supply-chain/Dockerfile (FROM gcr.io/projectsigstore/cosign:<tag> ou FROM anchore/syft:<tag>), commit, push sur main — l'effet est identique.

Cache des couches Docker

Chaque job de build pull l'image précédente du registry (docker pull ... || true) avant de builder, puis passe les images comme --cache-from. Le flag BUILDKIT_INLINE_CACHE=1 incorpore les métadonnées de cache dans l'image poussée, ce qui rend le cache cross-runner exploitable (les runners GitLab partagés sont des VM neuves à chaque job — sans ce flag, BuildKit recommence du FROM à chaque build).

Signature keyless de kirexo-prod (docker:sign-prod)

Sur tag de release, le job docker:sign-prod signe les trois références poussées par docker:build-prod avec cosign en mode keyless (sigstore). Pas de clé privée à gérer côté CI ni côté projet : l'authenticité du signataire est attestée par le pipeline GitLab lui-même via OIDC.

Caractéristique Valeur
Stage docker
Image $KIREXO_SUPPLY_CHAIN_IMAGE (image custom Alpine + cosign + syft, cf. Image supply chain). Pas d'override d'entrypoint nécessaire — l'image embarque déjà sh.
needs: docker:build-prod (strict, pas optional — la signature porte sur le digest de l'image fraîchement poussée ; signer un digest absent du registry échouerait en manifest unknown) et docker:build-supply-chain (strict — sans cette image, le job n'a pas de binaire cosign).
id_tokens.SIGSTORE_ID_TOKEN.aud sigstore — audience attendue par Fulcio pour accepter le JWT OIDC émis par GitLab.
COSIGN_YES true — désactive l'invite interactive (sécurité ceinture-bretelles avec --yes).
Déclencheur $CI_COMMIT_TAG =~ /^[0-9]{12}$/ uniquement. Les builds main / MR (manuels) ne sont pas livrés en prod — les signer polluerait le log de transparence.
interruptible false — l'attestation publiée dans Rekor est immuable ; une coupure mi-job laisserait une trace orpheline.
Variables CI/CD à provisionner Aucune nouvelle. CI_REGISTRY_USER/CI_REGISTRY_PASSWORD (déjà injectés par GitLab) servent au cosign login ; le JWT OIDC est généré automatiquement par GitLab via la directive id_tokens.

Références signées (toutes sur tag) :

Référence Origine
$CI_REGISTRY_IMAGE/kirexo-prod:php8.5 Tag mobile, poussé par docker:build-prod.
$CI_REGISTRY_IMAGE/kirexo-prod:latest Tag mobile, poussé par docker:build-prod sur tag.
$CI_REGISTRY_IMAGE/kirexo-prod:$CI_COMMIT_TAG Tag immuable de version (YYYYMMDDHHMM), poussé par docker:build-prod sur tag. C'est celui qu'épingle la prod via KIREXO_VERSION (cf. Déployer Kirexo en production).

Chaîne de confiance keyless

Le mode keyless dématérialise la PKI : aucune clé privée n'est conservée par Kirexo. À chaque signature, cosign enchaîne trois échanges réseau avec l'infra publique sigstore :

Acteur Rôle
GitLab CI Émet un JWT OIDC court-vivant pour le job (id_tokens.SIGSTORE_ID_TOKEN, audience sigstore). Le JWT contient l'identité du pipeline : URL du projet, refs, claims sub du type https://gitlab.com/<group>/<project>//.gitlab-ci.yml@refs/tags/<tag>.
Fulcio (CA publique sigstore) Reçoit le JWT, le vérifie auprès du provider OIDC GitLab, et émet en retour un certificat X.509 éphémère (validité ~10 min) lié à cette identité. Aucune clé privée à long terme n'est jamais matérialisée — la clé est générée à la volée par cosign dans le runner, le cert est révoqué à l'expiration.
Rekor (log de transparence) Reçoit la signature + le certificat Fulcio et publie une entrée immuable dans le log de transparence public. C'est cette entrée qui permet à un vérificateur (cf. ci-dessous) d'attester a posteriori qu'une signature a bien été produite par ce pipeline, à ce moment.

Côté Kirexo, la seule maintenance possible est de monter cosign en version majeure suivante (v3 quand il sortira). Le binaire cosign est embarqué dans l'image custom kirexo-supply-chain via un COPY --from=gcr.io/projectsigstore/cosign:<tag> du supply-chain/Dockerfile (cf. Image supply chain) — le manager dockerfile natif de Renovate suit ce tag automatiquement et ouvre la MR de bump à chaque release upstream.

Vérifier une signature côté consommateur

cosign verify $CI_REGISTRY_IMAGE/kirexo-prod:$TAG \
    --certificate-identity-regexp='^https://gitlab\.com/shaurifr/kirexo' \
    --certificate-oidc-issuer='https://gitlab.com'

Sur succès, cosign affiche les claims OIDC capturés par Fulcio (identité du pipeline, ref, sha du commit) et le hash de l'entrée Rekor associée — la chaîne de confiance remonte jusqu'au pipeline GitLab du tag.

La procédure côté serveur de production est documentée dans Vérifier la signature de l'image avant de la pull.

Stage maintenance

Le stage maintenance regroupe les jobs périodiques et non bloquants : le SAST informatif et les jobs de cron. Son nom reflète son rôle — « périodique / maintenance » — et non « tout ce qui touche la sécurité ». Le garde-fou de sécurité bloquant, lui, est quality:composer-audit (stage quality, voir Job quality:composer-audit).

Les jobs sont répartis sur deux fichiers d'inclusion. Les noms de fichiers (.gitlab/ci/security.yml) et de jobs (security:composer-audit) conservent volontairement le préfixe « security » — ils restent regroupés par thème ; seul le stage s'appelle désormais maintenance.

Fichier Jobs
.gitlab/ci/security.yml sast (umbrella neutralisé), semgrep-sast, secret_detection, security:secret-gate, container_scanning, security:container-issue, container_scanning:builders, security:container-issue:builders, security:composer-audit.
.gitlab/ci/dependencies.yml dependencies:renovate, tools:version-check.

Les sources de pipeline qui déclenchent ces jobs diffèrent : MR/main pour le SAST et la Secret Detection (+ son gate), schedule pour l'audit Composer, Renovate, le version-check et les scans CVE d'images builders. container_scanning et security:container-issue (image kirexo-prod) ont deux déclencheurs : schedule (scan quotidien de kirexo-prod:latest) et tag de release YYYYMMDDHHMM (scan de l'image kirexo-prod:$CI_COMMIT_TAG fraîchement poussée — signal CVE au moment de la release, non H+24). Les jobs builders (container_scanning:builders / security:container-issue:builders), eux, ne tournent qu'en cron : ils scannent les images qui alimentent la chaîne de build CI, pas un livrable de release.

L'audit Composer bloquant vit dans le stage quality, pas ici

Le job quality:composer-audit (.gitlab/ci/quality.yml, stage quality) audite composer.lock de façon bloquante sur chaque MR, main et tag — il fait partie des vérifications qualité, pas du stage maintenance. Voir Job quality:composer-audit. Le security:composer-audit documenté ci-dessous est, lui, le cron quotidien du stage maintenance qui ouvre une issue.

Job semgrep-sast

Hérité du template natif Security/SAST.gitlab-ci.yml. GitLab détecte automatiquement le langage du projet (PHP) et lance l'analyseur Semgrep correspondant (semgrep-sast). Le rapport JSON produit est exposé dans l'onglet Security de la MR. Le job parent « umbrella » sast du template est neutralisé (rules: - when: never) — il ne sert qu'à la configuration.

Caractéristique Valeur
Stage maintenance (override — le template par défaut place l'analyseur dans le stage test).
needs: [] — détaché du DAG. Le job démarre dès le début de la pipeline sur une MR, sans attendre quality/test. Comme il est non bloquant (allow_failure: true), autant l'avoir tôt sans freiner les vérifications bloquantes.
allow_failure true — informatif. Un finding SAST ne bloque jamais le merge.
Déclencheurs $CI_PIPELINE_SOURCE == "merge_request_event" ou $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH, jamais si $CREATE_TAG == "true".
interruptible true — annulé automatiquement si un nouveau commit arrive sur la même MR / branche.
Exclusions de scan SAST_EXCLUDED_PATHS : vendor, node_modules, var, public/assets, site, .phpunit.cache. Évite le bruit sur les dossiers générés ou vendor.

L'override des règles exclut la pipeline de création de tag ($CREATE_TAG == "true") : sans elle, le template GitLab par défaut ferait tourner SAST dans cette pipeline isolée, ce qui n'a pas de sens (le commit n'a pas changé, le SAST a déjà tourné sur la MR qui a produit ce commit).

Job security:composer-audit

Audit quotidien des dépendances Composer contre la base packagist/security-advisories. Hérite de .php_quality_job (image kirexo-base, pas de services Postgres/Redis nécessaires).

Stratégie MR-first / fallback issue : le job tente d'abord d'ouvrir une merge request de patch (composer update --with-all-dependencies ciblé sur les packages vulnérables), et retombe sur une simple issue d'alerte si l'update ne suffit pas.

Caractéristique Valeur
Stage maintenance
Image KIREXO_BASE_IMAGE (via .php_quality_job), avec jq installé en before_script.
Déclencheur $CI_PIPELINE_SOURCE == "schedule" uniquement — nécessite une schedule manuellement créée (cf. Configurer l'audit de sécurité quotidien).
GIT_DEPTH 0git push refuse de pousser une branche depuis un repo en « shallow update not allowed ».
interruptible false — un audit lancé n'est pas annulé même si un nouveau commit arrive.
Artifact audit-composer.json, conservé 1 mois (when: always).
Token requis GITLAB_TOKEN, scopes api + write_repository. Le scope write_repository est requis pour pousser la branche security/composer-* ; le scope api couvre l'ouverture de MR et d'issue.
Identité du commit ci-bot@kirexo.app (Kirexo CI Bot) — bot dédié pour ne pas polluer git log avec un nom humain.
Push CI-skip git push -o ci.skip — la pipeline tournera au merge de la MR, pas à l'ouverture (qui la déclenche déjà via l'événement MR).
Labels (MR ou issue) security, dependencies

Cinématique

Le script enchaîne :

  1. Audit initialcomposer audit --no-interaction --format=json > audit-composer.json. Le code retour est lu à la main (set +e) pour distinguer « rien à faire » de « vulnérable » et de « audit cassé ».
  2. Court-circuit dédup MR — interroge GET /projects/:id/merge_requests?state=opened&labels=security,dependencies filtré sur source_branch préfixée par security/composer-. Si une MR de patch est déjà ouverte, le job sort en 0 sans créer de doublon (le cron quotidien ne refait pas la même MR tant qu'elle n'est pas mergée).
  3. Tentative d'updatecomposer update --no-interaction --with-all-dependencies $VULN_PACKAGES, où $VULN_PACKAGES est la liste dédupliquée extraite du rapport. --with-all-dependencies remonte les transitives nécessaires pour satisfaire les nouvelles contraintes.
  4. Ré-auditcomposer audit --no-interaction --locked après l'update. S'il repasse, la voie MR est ouverte ; sinon, fallback issue.

Tableau des cas

Cas Détection Effet
Aucune vulnérabilité composer audit retourne 0. Le job passe, aucun appel API.
MR de patch déjà ouverte Réponse API non vide à la requête de dédup. Le job log « on ne duplique pas » et sort en 0.
Update suffit à corriger composer update réussit et ré-audit clean. Push d'une branche security/composer-YYYYMMDD-HHMMSS puis POST /projects/:id/merge_requests (labels: security,dependencies, remove_source_branch: true, squash: true). Titre : Patch sécurité Composer du YYYY-MM-DD. Le job termine en 0.
Update insuffisant ou en conflit composer update échoue ou ré-audit toujours rouge (versions patchées hors des contraintes composer.json, conflit de resolver). Fallback : POST /projects/:id/issues listant les packages, versions affectées, titres et CVE. Labels security,dependencies. Le job termine en exit 1 pour rester visible en rouge.
Audit cassé Code retour ≠ 0 et JSON non exploitable (réseau, packagist HS, composer.lock corrompu). Aucune issue ni MR créée (jq -e '.advisories \| type == "object"' échoue avant tout appel API). Le job échoue avec le code de sortie de composer audit.

Diagramme

flowchart TD
    A[composer audit] -->|exit 0| Z[Job vert - rien à faire]
    A -->|exit ≠ 0, JSON cassé| ERR[Job échoue - aucun appel API]
    A -->|exit ≠ 0, JSON OK| B{MR security/composer-*<br/>déjà ouverte ?}
    B -->|oui| Z2[Job vert - dédup]
    B -->|non| C[composer update --with-all-dependencies]
    C -->|update KO| F[Fallback issue]
    C -->|update OK| D[composer audit --locked]
    D -->|toujours vulnérable| F
    D -->|clean| E[Push branche security/composer-*<br/>+ POST merge_requests]
    F -->|POST issues| FX[Job rouge - alerte humaine]
    E --> EX[Job vert - MR ouverte]

Voir Configurer l'audit de sécurité quotidien pour la procédure de création de la schedule.

Job dependencies:renovate

Lance Renovate en mode self-hosted contre le seul dépôt Kirexo. Renovate ouvre automatiquement des merge requests de montée de version (Composer, npm, images Docker, jobs CI) et des MRs de sécurité prioritaires. La configuration vit dans renovate.json à la racine — voir Configurer les montées de version automatiques (Renovate).

Caractéristique Valeur
Stage maintenance (défini dans .gitlab/ci/dependencies.yml).
Image renovate/renovate:43 (tag majeur épinglé, pas :latest — Renovate suit son propre tag via le manager gitlabci).
Déclencheur $CI_PIPELINE_SOURCE == "schedule" uniquement.
interruptible false.
Variables clés RENOVATE_PLATFORM: gitlab, RENOVATE_ENDPOINT: $CI_API_V4_URL, RENOVATE_TOKEN: $GITLAB_TOKEN, RENOVATE_AUTODISCOVER: "false", RENOVATE_REPOSITORIES: $CI_PROJECT_PATH.
Token requis GITLAB_TOKEN — un Personal Access Token (les project access tokens sont réservés à Premium/Ultimate), scopes api + write_repository. Renovate le lit via le mapping RENOVATE_TOKEN: $GITLAB_TOKEN ; aucune variable RENOVATE_TOKEN distincte n'existe (cf. Variables CI/CD GitLab).

RENOVATE_AUTODISCOVER: "false" + RENOVATE_REPOSITORIES: $CI_PROJECT_PATH cible explicitement ce dépôt : sans ça, Renovate scannerait tous les projets accessibles par le token.

Job tools:version-check

Surveille les versions épinglées par directive ARG dans le Dockerfile racine que Renovate ne détecte pas nativement (Node, Claude Code, Castor, glab) et ouvre une issue GitLab récapitulative si au moins un outil est en retard.

Caractéristique Valeur
Stage maintenance (défini dans .gitlab/ci/dependencies.yml).
Image KIREXO_BASE_IMAGE (via .php_quality_job) — même image que security:composer-audit, jq y est déjà embarqué (stage frankenphp_base du Dockerfile).
Déclencheur $CI_PIPELINE_SOURCE == "schedule" uniquement.
interruptible false.
Token requis GITLAB_TOKEN (scope api suffit ici — pas de push de branche) — pour créer l'issue. Le même PAT que tag:create / security:composer-audit.
Labels d'issue créée dependencies, tooling.
Anti-doublon Avant d'ouvrir une issue, le job appelle castor ci:check-open-issue --labels="dependencies,tooling" : tant qu'une issue précédente avec ces deux labels reste ouverte, aucune nouvelle issue n'est créée. Sans ce garde-fou, le cron quotidien dupliquerait l'issue chaque jour tant que les outils restent en retard. Aligné avec le pattern de dédup MR de security:composer-audit.

Le job extrait quatre directives ARG du Dockerfile, les compare aux dernières versions publiées et ouvre une issue listant ce qui est en retard.

ARG du Dockerfile Source de la dernière version Particularité
NODE_VERSION nodejs.org/dist/index.json filtré sur la même ligne majeure (les bumps de major restent des décisions manuelles). Sans préfixe v côté épinglage.
CLAUDE_CODE_VERSION curl https://registry.npmjs.org/@anthropic-ai/claude-code puis jq '.["dist-tags"].latest'. Curl direct sur le registry npm pour ne pas avoir à embarquer Node (npm view) uniquement pour cette lecture. Sans préfixe.
CASTOR_VERSION https://api.github.com/repos/jolicode/castor/releases/latest.tag_name. Rate limit GitHub non authentifié (60/h) — suffisant pour un cron quotidien. Tag avec préfixe v (GitHub publie ses tags avec v) — le préfixe est conservé dans ARG pour respecter la sémantique d'origine.
GLAB_VERSION https://gitlab.com/api/v4/projects/gitlab-org%2Fcli/releases?per_page=1.[0].tag_name. API GitLab publique, pas de token requis sur les projets publics. Sans préfixe (le v éventuel est strippé via sed).

Tous les curl portent --retry 3 --retry-delay 2 --retry-connrefused pour absorber les blips transitoires des APIs publiques sans transformer le cron en faux positif (cf. plus bas).

Si une API renvoie une chaîne vide (panne réseau, registry HS), l'outil concerné est ignoré plutôt que faussement signalé comme « obsolète vers vide » — garde-fou contre les issues trompeuses. Si tous les outils restants sont à jour, le job passe sans créer d'issue. Sinon, l'issue suggère de jouer castor devcontainer:upgrade-tools pour appliquer le bump (version + hash SHA-256).

Pourquoi une issue plutôt qu'une MR auto (contrairement à security:composer-audit)

Chaque ARG *_VERSION du Dockerfile est apparié à un ARG *_SHA256 pour vérifier l'intégrité du binaire téléchargé au build. Calculer ce hash nécessite de télécharger chaque binaire dans un job dédié (un par outil — 4 jobs supplémentaires). La cible Castor castor devcontainer:upgrade-tools fait déjà ce boulot côté dev (download + sha256sum + bump du Dockerfile + MR via glab), couverte par ToolsUpgraderTest. Reproduire cette logique en CI dupliquerait du code testé unitairement sans bénéfice. Workflow assumé : la CI ouvre l'issue, l'humain joue castor devcontainer:upgrade-tools en local, push la MR.

Jobs container_scanning et security:container-issue (image de production)

Scan des CVE OS / libs système de l'image de production (Trivy, hérité du template natif Jobs/Container-Scanning.gitlab-ci.yml). container_scanning produit le rapport ; security:container-issue le parse et ouvre une issue GitLab uniquement si au moins une CVE de sévérité Critical ou High est remontée. Non bloquants (allow_failure: true) — une CVE de base image n'arrête jamais une release.

Particularité : deux déclencheurs distincts, et l'image scannée diffère selon le contexte. La sélection se fait via rules:variables (chaque branche de rules: pose sa propre valeur de CS_IMAGE / IMAGE_REF).

Déclencheur CS_IMAGE scannée par container_scanning IMAGE_REF utilisée par security:container-issue
Cron quotidien ($CI_PIPELINE_SOURCE == "schedule") $CI_REGISTRY_IMAGE/kirexo-prod:latest (image actuellement « courante » — implicitement celle déployée sauf rollback). latest (humanise le titre de l'issue).
Tag de release ($CI_COMMIT_TAG =~ /^[0-9]{12}$/) $CI_REGISTRY_IMAGE/kirexo-prod:$CI_COMMIT_TAG (image fraîchement poussée par docker:build-prod à la release). $CI_COMMIT_TAG.

Sur tag, container_scanning porte needs: [{ job: docker:build-prod, optional: true }] : il attend la fin du push de l'image qu'il va scanner. Sur schedule, l'image existe déjà, le needs reste optionnel.

security:container-issue ne casse jamais la pipeline (sort en 0). Si le rapport est absent (image inexistante, scan en échec), il log « rien à signaler » et termine. Sinon, il calcule un récap par sévérité (Critical / High / Medium / Low / Other / Total) et n'ouvre une issue que si Critical + High > 0. La liste détaillée dans le corps ne contient que les CVE Critical et High triées par sévérité décroissante ; les autres ne figurent qu'au compteur global. Labels : security, container. Titre : Failles image kirexo-prod:latest (N Critical/High) détectées le YYYY-MM-DD (cron) ou Failles image kirexo-prod:YYYYMMDDHHMM (N Critical/High) détectées le YYYY-MM-DD (release).

Authentification : GITLAB_TOKEN

security:container-issue (et security:container-issue:builders ci-dessous) ouvrent l'issue via POST /projects/:id/issues en s'authentifiant avec l'en-tête PRIVATE-TOKEN: ${GITLAB_TOKEN}pas JOB-TOKEN: ${CI_JOB_TOKEN}. C'est le même PAT que celui consommé par security:composer-audit, tools:version-check et dependencies:renovate ; il est déjà provisionné pour ces jobs et n'introduit donc pas de configuration supplémentaire (cf. Variables CI/CD GitLab).

Pourquoi pas CI_JOB_TOKEN ?

La whitelist de CI_JOB_TOKEN sur GitLab Free couvre Container Registry, Packages, Releases, Job artifacts et quelques autres routes — mais pas l'API REST des issues. Tenter un POST /projects/:id/issues avec JOB-TOKEN: ${CI_JOB_TOKEN} renvoie 401 Unauthorized (testé empiriquement sur le projet, cf. job 14721616686). Le réglage Settings → CI/CD → Token Access → « Allow projects to access this project via CI/CD job tokens » n'y change rien : il étend l'accès inter-projets du job token, pas la whitelist des endpoints. D'où le repli sur le PAT déjà disponible.

Aucun pré-requis Token Access à activer pour ces deux jobs — le PAT est rattaché à un compte, sa portée ne dépend pas du réglage Token Access du projet.

Pourquoi une issue et pas une MR (contrairement à security:composer-audit)

Corriger une CVE d'image revient à bumper le tag de l'image upstream du Dockerfile (ex. FROM dunglas/frankenphp:1-php8.5 → tag plus récent). C'est précisément le rôle de Renovate, manager dockerfile (cf. renovate.json) : il ouvre la MR de bump dès qu'un tag plus récent est publié. Reproduire ici cette logique (résolution de tag, contraintes semver, maintenance du lockfile) dupliquerait Renovate sans le fiabiliser. L'issue ouverte par security:container-issue reste utile comme signal pré-Renovate : entre l'instant où une CVE est publiée et celui où Renovate la voit, on a déjà une alerte humaine. La même logique s'applique aux jobs builders ci-dessous.

Pourquoi filtrer Critical / High

Une image de prod typique (Debian + extensions PHP + vendor/) accumule plusieurs centaines à plusieurs milliers de CVE Medium / Low / Unknown, soit non corrigeables côté Debian (« won't fix » / « postponed »), soit non exploitables dans le contexte d'usage. Sans filtrage, une issue était créée chaque jour avec ~1000 entrées de bruit, noyant les signaux exploitables. Limiter l'ouverture aux Critical + High garde le signal actionnable ; le récap complet par sévérité reste dans le corps de l'issue pour traçabilité, et l'artifact gl-container-scanning-report.json contient la liste exhaustive pour qui veut creuser.

Jobs container_scanning:builders et security:container-issue:builders (images CI)

Mêmes scanner et alerting que ci-dessus, mais ciblés sur les images qui alimentent la chaîne de build CI elle-même : kirexo-base et kirexo-dev. Une CVE critique dans leur OS / libs système (libc, openssl, …) compromettrait tous les jobs PHP et E2E. Cron quotidien uniquement — ces images ne sont pas livrées à l'utilisateur, elles n'ont pas de tag de release à scanner.

Implémentation : extends: container_scanning (réutilise l'image Trivy + le script du template), avec une parallel:matrix sur les deux images.

Caractéristique Valeur
Stage maintenance
parallel.matrix CS_BUILDER_IMAGE: [kirexo-base, kirexo-dev] → deux jobs en parallèle.
CS_IMAGE $CI_REGISTRY_IMAGE/$CS_BUILDER_IMAGE:php$PHP_VERSION — tag effectivement utilisé par les autres jobs CI (et non :latest, qui n'est qu'un alias poussé au tag de release).
needs: [] — détaché du DAG.
Déclencheur $CI_PIPELINE_SOURCE == "schedule" uniquement.
Allowed failure true (hérité de container_scanning).
Job d'alerting security:container-issue:builders — même matrix, lit gl-container-scanning-report.json, n'ouvre une issue que si Critical + High > 0 (même filtrage que security:container-issue). Authentification via PRIVATE-TOKEN: ${GITLAB_TOKEN} (cf. Authentification GITLAB_TOKEN). Titre : Failles image kirexo-base:php8.5 (N Critical/High) détectées le YYYY-MM-DD (ou kirexo-dev:…). Labels security, container. Le corps de l'issue mentionne explicitement que le bump du tag upstream du Dockerfile est porté par Renovate.

Container Scanning sur tier Free : on parse le rapport nous-mêmes

L'onglet Security et le widget MR sont réservés à GitLab Ultimate. En tier gratuit, les jobs container_scanning et container_scanning:builders produisent bien gl-container-scanning-report.json, mais ce sont security:container-issue et security:container-issue:builders qui le parsent pour décider d'ouvrir une issue — l'alerting est fait par nos soins.

Un seul schedule pour les jobs de cron quotidien

security:composer-audit, dependencies:renovate, tools:version-check, container_scanning, container_scanning:builders et leurs jobs d'alerting partagent le même déclencheur $CI_PIPELINE_SOURCE == "schedule". Une seule schedule quotidienne (ex. 0 5 * * * sur main) suffit à les lancer tous. Voir Configurer l'audit de sécurité quotidien.

Le job pages du stage release utilise lui aussi $CI_PIPELINE_SOURCE == "schedule", mais filtré en plus par $REBUILD_DOCS == "true" : sa schedule hebdo dédiée (cf. Configurer le rebuild hebdomadaire de la doc) ne réveille pas les jobs de cron quotidien, et inversement.

Pipeline de création de tag

Job unique tag:create (stage tag), déclenché par la variable CREATE_TAG=true. Voir le guide Créer un tag de release pour la procédure.

Caractéristique Valeur
Image alpine:3
Format du tag YYYYMMDDHHMM en UTC (ex. 202605231742)
Méthode POST /projects/:id/repository/tags via curl --fail
Authentification PRIVATE-TOKEN: $GITLAB_TOKEN
GIT_STRATEGY none (pas besoin de cloner — l'API agit côté serveur)
Rule $CREATE_TAG == "true" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHborné explicitement à main. Sans cette double condition, déclencher CREATE_TAG=true depuis une feature branch poserait quand même le tag sur main (via ref=$CI_DEFAULT_BRANCH du POST) — surprenant et facile à mal utiliser.
Garde-fou pipeline Avant de créer le tag, interroge GET /projects/:id/pipelines?sha=$CI_COMMIT_SHA&status=success et refuse de taguer si aucune pipeline success n'existe pour le commit ciblé (exit 1). Outrepassable par FORCE_TAG=true.
Garde-fou « tag déjà présent » Avant le POST, interroge GET /projects/:id/repository/tags/$TAG_NAME et capture le code HTTP. 200 → le tag existe déjà (deux clics dans la même minute, retry manuel après un succès partiel…) → le job sort en exit 1 avec un message clair (« abandon — attends la minute suivante ») plutôt que de laisser le POST échouer en 409 cryptique. 404 → tag libre, on continue. Tout autre code (5xx, token expiré…) → abandon : on ne tente pas aveuglément un POST.
Effet de bord GitLab détecte le tag créé et démarre automatiquement la pipeline $CI_COMMIT_TAG (quality + test + docker + release).

Garde-fou : pipeline verte exigée

Comme cette pipeline tourne sur main, $CI_COMMIT_SHA correspond au dernier commit de main. Avant de créer le tag, tag:create exige qu'au moins une pipeline success existe pour ce SHA (typiquement la pipeline de validation jouée au merge). Le but : ne jamais figer une release sur un commit dont la CI est rouge ou encore en cours.

Le contrôle est fail-safe : si l'appel API échoue (token invalide, réseau), le compteur de pipelines réussies retombe à 0 et le job bloque — il refuse de taguer plutôt que de risquer une release non validée.

Variable Valeur Effet sur le garde-fou
(absente) Garde-fou actif : pas de tag sans pipeline verte sur le commit.
FORCE_TAG true Garde-fou ignoré — le tag est créé sans vérifier le statut de pipeline. Réservé aux cas exceptionnels.

FORCE_TAG se passe en plus de CREATE_TAG=true, au déclenchement de la pipeline (UI ou glab, cf. Créer un tag de release). Elle n'a aucun effet hors de la pipeline CREATE_TAG=true.

Le format YYYYMMDDHHMM est :

  • stable : aucune collision tant qu'on ne crée pas deux tags dans la même minute UTC ;
  • trié naturellement : un git tag --sort=-version:refname donne l'ordre chronologique inverse ;
  • lisible : on devine quand le tag a été produit sans git show.

Release et déploiement de la documentation

Deux jobs dans .gitlab/ci/release.yml : release:create (création de la release GitLab) tourne exclusivement sur tag de release ; pages (déploiement de la doc) tourne sur tag de release et sur la schedule hebdo de rebuild (REBUILD_DOCS=true).

Job pages

Le job s'appelle obligatoirement pages pour que GitLab Pages collecte l'artifact public/ en tant que site déployé — c'est une convention GitLab non négociable.

Caractéristique Valeur
Image $KIREXO_DOCS_IMAGE (kirexo-docs:latest, pré-buildée par docker:build-docs à partir de mkdocs/Dockerfile). entrypoint: [""] neutralise l'entrypoint mkdocs hérité pour que GitLab CI lance le script via le shell.
Commande mkdocs build --strict -d public (préfixée, sur schedule uniquement, d'un git checkout --detach <dernier tag YYYYMMDDHHMM>).
Mode strict Activé — un lien cassé, un snippet manquant ou un avertissement de plugin échoue le build.
GIT_DEPTH 0 (histoire git complète, requise par git-revision-date-localized et par le git checkout <tag> du déclencheur schedule).
Pre-script Aucun. mkdocs-material et les plugins (imaging, git-revision-date) sont déjà installés dans l'image kirexo-docs — plus de apk add ni de pip install à chaque déploiement.
Artifact public/ (collecté automatiquement par GitLab Pages).
artifacts:expire_in 1 week — GitLab Pages conserve nativement le contenu du dernier pages réussi même après expiration de l'artifact ; la fenêtre courte évite simplement d'accumuler des artifacts obsolètes (un par tag + un par rebuild hebdo). Si la doc devait disparaître plus de 7 jours sans nouveau tag ni nouveau cron, la schedule weekly écrit de toute façon une nouvelle version avant.
Déclencheurs $CI_COMMIT_TAG =~ /^[0-9]{12}$/ (tag de release) ou $CI_PIPELINE_SOURCE == "schedule" && $REBUILD_DOCS == "true" (schedule hebdo de rebuild — cf. ci-dessous).

L'image kirexo-docs étant construite à partir du même mkdocs/Dockerfile que l'environnement local, le rendu de la doc en CI est à parité exacte avec un build local. Sur tag, docker:build-docs (stage docker) tourne avant pages (stage release) : l'image :latest est toujours fraîche au moment du déploiement.

Rebuild hebdomadaire (REBUILD_DOCS=true)

Une schedule GitLab dédiée (variable REBUILD_DOCS=true) déclenche pages une fois par semaine pour reconstruire la doc figée sur le dernier tag YYYYMMDDHHMM, sans attendre la prochaine release. Objectif : profiter d'une éventuelle mise à jour de l'image kirexo-docs — patch de mkdocs-material, bump de plugin (typiquement par Renovate sur l'image kirexo-docs) — entre deux releases. Le contenu publié reste celui de la version effectivement en production.

Mécanique du job sur ce déclencheur :

  1. git tag --list --sort=-creatordate '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' | head -1 récupère le dernier tag au format YYYYMMDDHHMM (le tri par creatordate reste robuste même si un tag annexe se glissait un jour dans le repo).
  2. Si aucun tag de ce format n'existe, le job sort en 0 (rien à régénérer).
  3. Sinon, git checkout --detach $LAST_TAG repositionne HEAD sur ce commit (HEAD détaché — aucune branche à toucher).
  4. mkdocs build --strict -d public produit le site, collecté ensuite par GitLab Pages comme un déploiement normal.

La configuration de cette schedule est documentée dans Configurer le rebuild hebdomadaire de la doc.

L'URL canonique cible est https://doc.kirexo.app/ — voir le guide Configurer le domaine GitLab Pages pour la configuration DNS et le certificat Let's Encrypt.

Job release:sbom

Génère sur chaque tag de release deux Software Bill of Materials de l'image kirexo-prod:$CI_COMMIT_TAG : un inventaire exhaustif des paquets et libs embarqués (OS Debian + extensions PHP + dépendances Composer présentes dans vendor/). Les deux formats coexistent car les écosystèmes consommateurs ne lisent pas le même :

  • SPDX JSON (Linux Foundation, format ISO 5962) — référence côté audit / juristes / conformité (OpenChain, NTIA Minimum Elements).
  • CycloneDX JSON (OWASP) — référence côté outillage de sécurité (Dependency-Track, Snyk, Trivy en mode SBOM, GitHub Advanced Security).
Caractéristique Valeur
Stage release
Image $KIREXO_SUPPLY_CHAIN_IMAGE (image custom Alpine + cosign + syft, cf. Image supply chain). Pas d'override d'entrypoint nécessaire — l'image embarque déjà sh.
needs: docker:build-prod avec artifacts: false (l'image est scannée via le registry, pas via une tarball locale — pas de download d'artifact) et docker:build-supply-chain avec artifacts: false (sans cette image, le job n'a pas de binaire syft).
Scan syft "registry:$CI_REGISTRY_IMAGE/kirexo-prod:$CI_COMMIT_TAG" — scan registry distant, pas de docker pull local.
Authentification registry SYFT_REGISTRY_AUTH_AUTHORITY / SYFT_REGISTRY_AUTH_USERNAME / SYFT_REGISTRY_AUTH_PASSWORD câblés sur les variables CI_REGISTRY* natives — pas de nouvelle variable à provisionner.
Artifacts produits sbom-kirexo-prod-$CI_COMMIT_TAG.spdx.json et sbom-kirexo-prod-$CI_COMMIT_TAG.cdx.json.
artifacts.expire_in 1 year — aligné sur le cycle d'audit annuel typique. Au-delà, le SBOM est de moins en moins représentatif des CVE connues et peut être régénéré à la demande depuis l'image taggée.
Déclencheur $CI_COMMIT_TAG =~ /^[0-9]{12}$/ uniquement.
interruptible false.

Job release:create

Caractéristique Valeur
Image registry.gitlab.com/gitlab-org/release-cli:v0.24.0 (version épinglée — pas de :latest ; Renovate suit ce tag).
Authentification CI_JOB_TOKEN (automatique — pas besoin de GITLAB_TOKEN).
needs: release:sbom avec artifacts: false — on attend que les artifacts SBOM soient publiés avant de créer la release pour que les assets.links pointent sur des URLs valides. Conséquence assumée : si release:sbom échoue, release:create ne tourne pas et la release GitLab n'est pas créée.
GIT_DEPTH 0 (requis par git describe --tags --abbrev=0 HEAD^ pour trouver le tag précédent).
Description Changelog généré à la volée — git log <prev>..<tag> au format * %s (%h). Si aucun tag précédent (première release), liste tous les commits depuis la racine.
release.tag_name $CI_COMMIT_TAG
release.name Release $CI_COMMIT_TAG
release.assets.links Deux entrées link_type: other pointant sur les artifacts de release:sbom via l'URL stable $CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_TAG/raw/<path>?job=release:sbom. Reste accessible tant que les artifacts n'ont pas expiré (1 an).

Les deux assets sont visibles dans le widget « Assets → Links » de la release GitLab dès qu'elle est publiée (cf. SBOM disponible en asset de la release).

Templates PHP réutilisables

Définis dans .gitlab-ci.yml, hérités par les jobs quality:* et test:*.

Template Image Services Particularité
.php_quality_job KIREXO_BASE_IMAGE aucun Léger. Style, lint statique, vérifs sans Postgres. Cache vendor/ en policy: pull (lecture seule) — le remplissage est délégué à build:vendor, seul job en pull-push (cf. Cache Composer).
.php_service_job KIREXO_BASE_IMAGE Postgres 16, Redis 7, Typesense 0.25 Étend .php_quality_job. Injecte les DSN applicatifs (DATABASE_URL, REDIS_URL, MESSENGER_TRANSPORT_DSN: sync://, etc.).
.php_phpstan_job KIREXO_BASE_IMAGE (idem .php_service_job) APP_ENV=dev + cache:warmup. Le containerXmlPath de phpstan.dist.neon pointe sur var/cache/dev/.
.php_test_job KIREXO_BASE_IMAGE (idem .php_service_job) Force POSTGRES_DB: app_test côté service Postgres pour matcher le suffixe _test ajouté par when@test dans config/packages/doctrine.yaml. before_script limité à composer install — la migration et le chargement des fixtures sont délégués à tests/bootstrap.php (cf. Préparation BDD côté tests).
.php_e2e_job KIREXO_DEV_IMAGE (idem .php_service_job) Étend .php_test_job (donc hérite aussi de la prise en charge automatique de la BDD par le bootstrap). Bascule sur l'image dev (Chromium + chromedriver). PANTHER_* configurés pour le mode headless CI.

Le MESSENGER_TRANSPORT_DSN: sync:// côté CI court-circuite RabbitMQ : les messages sont traités en process. C'est volontaire — la CI vérifie la logique métier, pas le transport AMQP.

Pourquoi pas de migrations:migrate ni fixtures:load dans before_script

Avant centralisation côté tests/bootstrap.php, le before_script du .php_test_job jouait explicitement les migrations puis les fixtures. La logique a été déplacée dans le bootstrap PHPUnit pour qu'un développeur local ait exactement la même séquence de préparation qu'en CI, sans avoir à recopier des étapes dans un script. Le service Postgres CI a juste besoin de la variable POSTGRES_DB: app_test pour que la base existe au bon nom dès le démarrage du conteneur.

Voir aussi