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-audit — tente 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 surmainet 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'imagekirexo-prod:$CI_COMMIT_TAGau moment de la release (cf. Jobscontainer_scanningetsecurity: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:vendor → quality: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ègleCLAUDE.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 :
- Lit cette première occurrence dans
cobertura-unit.xmlviased -n 's/.*branch-rate="\([0-9.]*\)".*/\1/p' | head -1. - Convertit en pourcentage à deux décimales avec
awk(pas debcdanskirexo-dev). - É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-base → dev / 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-basetourne automatiquement (on_success) ;devetprodattendent sa fin et pullent le base à jour → les couches base sont réutilisées au lieu d'être reconstruites ; - sur
main/ MR :docker:build-baseest manuel et n'est généralement pas déclenché ;optional: truelaisse alorsdevetproddé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 |
0 — git 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 :
- Audit initial —
composer 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é ». - Court-circuit dédup MR — interroge
GET /projects/:id/merge_requests?state=opened&labels=security,dependenciesfiltré sursource_branchpréfixée parsecurity/composer-. Si une MR de patch est déjà ouverte, le job sort en0sans créer de doublon (le cron quotidien ne refait pas la même MR tant qu'elle n'est pas mergée). - Tentative d'update —
composer update --no-interaction --with-all-dependencies $VULN_PACKAGES, où$VULN_PACKAGESest la liste dédupliquée extraite du rapport.--with-all-dependenciesremonte les transitives nécessaires pour satisfaire les nouvelles contraintes. - Ré-audit —
composer audit --no-interaction --lockedaprè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_BRANCH — borné 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:refnamedonne 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 :
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 -1récupère le dernier tag au formatYYYYMMDDHHMM(le tri parcreatordatereste robuste même si un tag annexe se glissait un jour dans le repo).- Si aucun tag de ce format n'existe, le job sort en
0(rien à régénérer). - Sinon,
git checkout --detach $LAST_TAGrepositionne HEAD sur ce commit (HEAD détaché — aucune branche à toucher). mkdocs build --strict -d publicproduit 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¶
- Bootstrapper la pipeline CI — séquence d'actions manuelles pour amorcer la pipeline sur une nouvelle instance GitLab (DNS, PAT, variable CI/CD, custom domain Pages, schedule, premier build des images).
- Variables d'environnement — Variables CI/CD GitLab — détail de
GITLAB_TOKENet des variables injectées par GitLab. - Créer un tag de release — procédure opérationnelle.
- Configurer l'audit de sécurité quotidien — création de la schedule GitLab qui déclenche
security:composer-audit,dependencies:renovate,tools:version-check,container_scanning(+:builders) etsecurity:container-issue(+:builders). - Configurer le rebuild hebdomadaire de la doc — schedule GitLab dédiée qui réveille
pagesavecREBUILD_DOCS=true. - Configurer les montées de version automatiques (Renovate) — réutilisation du
GITLAB_TOKEN,renovate.json, comportement des MRs. - Configurer le domaine GitLab Pages — DNS, certificat Let's Encrypt.
- Exécuter les tests — lancer la suite en local ; le double gate de couverture (lignes 100 % + branches 90 %) porté par
test:uniten CI. - Commandes Castor — cibles locales qui exécutent le même enchaînement que la CI (
castor all). - Supply chain et signatures — pourquoi signer
kirexo-prodet publier un SBOM, chaîne de confiance Fulcio / Rekor. - Déployer Kirexo en production — Vérifier la signature de l'image avant de la pull — procédure côté serveur de production.