documentation
Kohinos v3 Doc générée le commit 0138e6198

CI/CD — Pipelines Woodpecker Kohinos

Le moteur d'intégration continue de Kohinos est Woodpecker CI self-host (cf. ADR 0028-ci-engine-woodpecker.md) — souveraineté, priorité 2 du projet. Le serveur Woodpecker et ses agents tournent sur l'infrastructure de la Fédération (VPS 4 Go ou Raspberry Pi 5), sans aucun service cloud propriétaire dans le path critique. Les pipelines vivent dans .woodpecker/*.yml et sont déclenchés par événement (pull request, push main, tag, cron, manuel). Aucun déploiement production sans pipeline verte et action manuelle explicite.

📌 Source de vérité

Cette page est une vue d'ensemble. Les références détaillées sont : l'ADR 0028-ci-engine-woodpecker.md (choix du moteur), docs/ci/woodpecker-architecture.md (server + agents), docs/ci/woodpecker-workflows.md (les pipelines, fichier par fichier) et docs/ci/secrets-management.md (secrets & rotation). Les pipelines eux-mêmes (.woodpecker/*.yml) restent la référence exécutable.

🛈 Historique — GitLab CI n'est plus le moteur

Une configuration .gitlab-ci.yml a existé (qualité + tests, sans déploiement). Depuis l'ADR 0028 (2026-05-23), le moteur retenu est Woodpecker : .gitlab-ci.yml est désormais gitignoré (.gitignore:24, reliquat local inerte qui ne déclenche rien). Le code vit sur deux forges officielles miroirs — GitLab gitlab.com/federation-kohinos/kohinos (remote origin) et GitHub github.com/jujote/kohinos — mais l'orchestration CI/CD passe entièrement par Woodpecker. Détail du raisonnement (RAM, licence, portabilité forge) → section Historique GitLab CI → Woodpecker.

Vue d'ensemble — 6 pipelines par événement

Woodpecker ne raisonne pas en « stages » globaux mais en fichiers de pipeline déclenchés par événement. Six pipelines couvrent le cycle de vie :

flowchart TB PR[pull_request] --> P1[".woodpecker/pr.yml
< 10 min — feedback rapide"] PUSH[push main/master] --> P2[".woodpecker/main.yml
< 25 min — PR + finance + MSI + deploy staging"] CRON[cron 02:00 UTC] --> P3[".woodpecker/nightly.yml
< 60 min — full + E2E + audit"] TAG[tag v*] --> P4[".woodpecker/tag.yml
note de release"] MAN1[manuel + CONFIRM] --> P5[".woodpecker/cutover.yml
v2→v3 prod orchestré SSH"] MAN2[manuel + CONFIRM] --> P6[".woodpecker/rollback.yml
restore dump + tag précédent"] P2 --> S[deploy:staging via SSH] P5 --> PROD[prod — gates parité + invariants]

Pipelines détaillés

PipelineDéclencheurCibleSteps principaux
pr.yml event: pull_request < 10 min composer-install · php-cs-fixer · phpstan · phpunit-unit · phpunit-functional (SQLite) · npm-build · assets-freshness. Saute les suites longues SSA/SEPA/MLC.
main.yml event: push, branche main/master < 25 min Reprend pr.yml + phpunit-financial (SSA/SEPA/MLC) + infection-msi-gate (--min-msi via secret, palier A2) + deploy-staging (SSH).
nightly.yml event: cron (nightly = 0 2 * * *, UI Woodpecker) < 60 min phpunit-full · e2e-playwright · security-audit (composer audit + checks CSRF/headers) · migrate-parity (rejeu ETL synthétique). Diagnostic, non bloquant.
tag.yml event: tag, ref: refs/tags/v* < 5 min changelog-aggregate → var/release/${CI_COMMIT_TAG}.md. Pas de gh release create auto (souveraineté : pas de token GitHub longue durée en CI).
cutover.yml event: manual + CONFIRM=I_UNDERSTAND_THIS_TOUCHES_PROD manuel Orchestration SSH v2→v3 : validate-inputs → parité pré-vol → backup DB → maintenance ON → deploy → migrate → chiffrement IBAN (dry-run puis réel) → gates parité + invariants → smoke → maintenance OFF → notif Mattermost.
rollback.yml event: manual + CONFIRM=I_UNDERSTAND_THIS_IS_A_ROLLBACK manuel Maintenance ON → restore-db.sh (dump étiqueté) → checkout tag précédent → cache clear → smoke → maintenance OFF → notif.
📦 La CI ne build pas les assets front

Les bundles Webpack Encore sont pré-compilés et versionnés dans public/build/ (+ public/sw.js) : ils sont committés au dépôt. Aucun Node/npm/yarn n'est requis sur le serveur de prod ni pour déployer. Le step npm-build (image node:22-alpine) reste présent dans pr.yml/main.yml à titre de filet, mais n'est pas un prérequis de déploiement. Le vrai garde-fou est le step assets-freshness (image alpine:3.20, 100 % git, sans Node, via scripts/ci-assets-freshness.sh) : il échoue si une source front (assets/, webpack.config.js, package.json…) a changé sans que public/build/ ait été republié — c'est au développeur de committer le build à jour avant de pousser.

⚠ Tests financiers = bloquants

Les testsuites ssa, sepa et mlc sont obligatoirement vertes dans main.yml avant tout déploiement staging, et le gate infection-msi-gate (Infection sur src/Service/Sepa, src/Service/Ssa, src/Flux, src/Entity/Operation.php) doit franchir le palier MSI. La priorité 1 du projet est l'intégrité financière — un test rouge ou un MSI insuffisant bloque la chaîne (pas de deploy-staging). Le hook GrumPHP local rejoue les mêmes garde-fous en amont.

Cache & volumes

Pour limiter la pression sur l'agent (objectif : PR < 10 min, même sur Pi 5 8 Go), Woodpecker monte des volumes nommés persistants partagés entre tous les runs et tous les pipelines (PR + main + nightly). Pas de purge automatique.

RessourceVolume montéPortée
Cache Composer /cache/composer → $COMPOSER_CACHE_DIR incrémental, partagé entre runs et pipelines
Cache npm /cache/npm → ~/.npm step npm-build (filet, non bloquant pour le deploy)
Navigateurs Playwright /cache/playwright-browsers → $PLAYWRIGHT_BROWSERS_PATH step e2e-playwright (nightly)
public/build/ aucun — versionné dans le dépôt pré-compilé et committé, pas un artefact CI
# Extrait .woodpecker/pr.yml — cache Composer via volume nommé
  - name: composer-install
    image: composer:2.8
    commands:
      - composer install --prefer-dist --no-progress --no-scripts --no-interaction
    volumes:
      - /cache/composer:/tmp/composer-cache
    environment:
      COMPOSER_CACHE_DIR: /tmp/composer-cache
      COMPOSER_ALLOW_SUPERUSER: '1'

# Purge manuelle au cas par cas (pas d'expiration auto) :
#   docker volume rm kohinos-ci-composer-cache

Secrets Woodpecker

Aucun secret n'est versionné. Les credentials sensibles vivent dans les secrets Woodpecker (chiffrés au repos, masqués dans les logs, scoppés au dépôt) et sont injectés par from_secret. Les secrets de prod côté serveur restent dans le Symfony secrets vault (config/secrets/prod/, déchiffré au boot).

SecretInjectionUtilisé par
min_msifrom_secret: min_msimain.yml step infection-msi-gate (palier MSI A2)
ssh_deploy_keyfrom_secret: ssh_deploy_keymain.yml deploy-staging, cutover.yml, rollback.yml
staging_hostfrom_secret: staging_hostmain.yml step deploy-staging
staging_urlfrom_secret: staging_urlnightly.yml step security-audit
mattermost_webhookfrom_secret: mattermost_webhookcutover.yml / rollback.yml step notify
Secrets de prod (DB, SMTP, gateways, IBAN)Symfony vaultconfig/secrets/prod/ chiffré, déchiffré au boot serveur
⛔ Jamais de secrets en clair dans .woodpecker/*.yml

Toute valeur sensible (clé SSH de déploiement, webhook Mattermost, seuil MSI) passe par un secret Woodpecker injecté via from_secret — jamais en clair dans le YAML versionné. Le hook GrumPHP git_blacklist rejette les commits contenant PAYUM_ ou SECRET en clair, mais ce filet est faillible — la vraie protection est l'humain. Détail rotation (6 mois) → docs/ci/secrets-management.md.

Agents Woodpecker

Un woodpecker-server + un ou plusieurs woodpecker-agent suffisent. Chaque step tourne dans un conteneur OCI isolé (image épinglée). La sélection d'agent se fait par label : aujourd'hui tous les pipelines épinglent linux/amd64.

ComposantRôleSouveraineté
woodpecker-server UI, webhooks forge (OAuth), orchestration, métriques Prometheus self-host Fédération — < 600 Mo RAM avec 1 agent
Agent linux/amd64 exécute tous les steps actuels (lint, PHPStan, PHPUnit, Infection, npm, deploy) self-host — VPS 4 Go, données de test isolées
Agent linux/arm64 (Pi 5) inutilisé tant qu'aucun pipeline ne demande platform: linux/arm64 évolution future : nightly-arm64.yml pour valider le build sur ARM
# docker-compose (extrait) — agent ARM64 sur Raspberry Pi 5
services:
  woodpecker-agent-arm64:
    image: woodpeckerci/woodpecker-agent:v3
    environment:
      WOODPECKER_SERVER: woodpecker.kohinos.example:9000
      WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
      WOODPECKER_BACKEND: docker
      WOODPECKER_FILTER_LABELS: "platform=linux/arm64"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
🥧 Pi 5 comme agent CI

Un Raspberry Pi 5 (8 Go RAM, SSD NVMe via HAT) tient confortablement les steps php-cs-fixer, phpstan et phpunit-unit. Pour phpunit-financial avec SQLite, prévoir un peu de marge ou offload sur un agent x86. Le low-tech est une priorité, pas une religion — si un step CI met > 5 min sur Pi, le déporter sur un agent plus costaud sans culpabiliser.

Boucle commit recommandée

Reproduit localement, dans l'ordre, ce que la CI vérifiera ensuite. Évite les aller-retours pipeline rouge → fix → re-push.

  1. Mettre à jour CHANGELOG.md (technique) et CHANGELOG_USER.md (fonctionnel).
  2. Lancer les tests ciblés — jamais vendor/bin/phpunit seul.
  3. Formater le code avec php-cs-fixer.
  4. Lancer la pipeline locale via GrumPHP (mêmes hooks que la CI).
  5. Committer (le hook pre-commit GrumPHP rejouera tout).
# 1. CHANGELOG (manuel, dans l'éditeur)
$EDITOR CHANGELOG.md CHANGELOG_USER.md

# 2. Tests ciblés (cf. CLAUDE.md)
vendor/bin/phpunit --testsuite=ssa
vendor/bin/phpunit tests/Service/Sepa/SepaProcessorTest.php

# 3. Format
vendor/bin/php-cs-fixer fix

# 4. Pipeline locale
vendor/bin/grumphp run

# 5. Commit + push
git add -A
git commit -m "feat(ssa): nouvelle stratégie de redistribution proportionnelle"
git push origin feature/ssa-proportional

Règles dures

⛔ Interdictions absolues — pas de tolérance
  • Jamais de déploiement production sans pipeline CI verte (incluant phpunit-financial et infection-msi-gate).
  • Jamais de merge vers main avec un test rouge, même un test « flaky » — soit on le stabilise, soit on le marque @group flaky et on l'isole dans un step séparé non bloquant.
  • Jamais de secret en clair dans .woodpecker/*.yml, composer.json, ou un fichier versionné. Tout passe par les secrets Woodpecker (from_secret) ou le Symfony secrets vault.
  • Jamais de git push --force sur main ni sur les tags v*.
  • Jamais de skip de tests « temporaire » — un markTestSkipped() sans ticket associé et sans date de réactivation est une dette qui dure 6 mois. Préférer markTestIncomplete() avec message explicite, ou supprimer le test.
  • Jamais de composer update dans la CI : uniquement composer install --prefer-dist à partir du composer.lock versionné.

GrumPHP local — pre-commit hook

GrumPHP rejoue dans le hook pre-commit les mêmes contrôles que la CI, de sorte qu'un push ne franchisse jamais la frontière du laptop sans avoir passé les filtres. Les tâches actives sont configurées dans grumphp.yml à la racine. Alignement détaillé pre-commit / CI → docs/ci/pre-commit-vs-ci.md.

# grumphp.yml (extrait)
grumphp:
  tasks:
    git_blacklist:
      keywords:
        - "die("
        - "var_dump("
        - "dump("
        - "exit;"
      whitelist_patterns:
        - /^scripts\//
        - /^docs\//
    yamllint: ~
    composer: ~
    jsonlint: ~
    phpcsfixer:
      config: .php-cs-fixer.dist.php
    phpunit:
      config_file: phpunit.xml
      testsuite: unit

Activation locale :

composer install        # GrumPHP s'installe automatiquement
vendor/bin/grumphp git:init   # registre le hook .git/hooks/pre-commit
vendor/bin/grumphp run        # exécution manuelle
💡 GrumPHP ≠ remplacement de la CI

Le hook local peut être contourné (git commit --no-verify) et ne tourne pas phpunit-financial ni infection-msi-gate par défaut (trop long). La CI reste le filet de sécurité final ; GrumPHP est le filet amont qui économise un round-trip agent.

Historique GitLab CI → Woodpecker

Le code est hébergé sur deux forges miroirs — GitLab (gitlab.com/federation-kohinos/kohinos, remote origin) et GitHub (github.com/jujote/kohinos). Une configuration .gitlab-ci.yml de qualité/tests a existé, sans déploiement. Le choix d'un moteur CI auto-hébergé a tranché en faveur de Woodpecker (ADR 0028, 2026-05-23). Comparaison des options :

CritèreWoodpecker CI (retenu)GitLab CI
Licence Apache-2.0 (OSI), pas de risque de bascule propriétaire cœur libre, mais SaaS GitLab.com propriétaire dans le path
RAM minimale ~600 Mo (server + 1 agent) — tient sur Pi 5 / VPS 4 Go runner ~4 Go+, impose la stack GitLab complète
Souveraineté (prio 2) self-host, OAuth multi-forge, aucun SaaS imposé tire vers GitLab.com SaaS ou un GitLab CE lourd
Sobriété (prio 3) ARM64 first-class, conteneurs OCI légers runner gourmand, peu adapté Raspberry Pi
Déploiement deploy-staging + cutover + rollback (SSH) aucun — qualité/test uniquement
Portabilité forge GitHub / Gitea / Forgejo / GitLab via OAuth couplé à GitLab

État actuel :

ÉlémentÉtat
.woodpecker/{pr,main,nightly,tag,cutover,rollback}.yml✅ actifs — moteur CI/CD réel
.gitlab-ci.ymlgitignoré (.gitignore:24) — reliquat local inerte, ne déclenche rien
Forges officielles (miroirs)GitLab gitlab.com/federation-kohinos/kohinos (origin, déploiement) + GitHub github.com/jujote/kohinos (backend OAuth du CI). Branche par défaut main ; liens doc paramétrables via KOHINOS_FORGE=gitlab|github
Dependabotremplacé par Renovate self-host

Renovate self-host

Renovate (bot open-source de mise à jour des dépendances) tourne en self-host sur le serveur Fédération Kohinos, via cron. Remplace Dependabot (GitHub-only) et reste forge-agnostique (GitLab aujourd'hui, portable demain).

# renovate.json (à la racine du dépôt)
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "schedule": ["before 6am on monday"],
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "groupName": "patches",
      "automerge": false
    },
    {
      "matchPackagePatterns": ["doctrine/dbal"],
      "allowedVersions": "<4.0.0",
      "description": "Pin DBAL 3.x — DBAL 4.x incompatible avec ORM 2.14"
    },
    {
      "matchPackagePatterns": ["sonata-project/admin-bundle"],
      "allowedVersions": "<5.0.0",
      "description": "Pin Sonata 4.x — Sonata 5.x non testé"
    }
  ],
  "vulnerabilityAlerts": { "enabled": true, "labels": ["security"] }
}
⚠ Exclusions critiques

doctrine/dbal est épinglé à 3.x : la 4.x introduit des breaking changes incompatibles avec Doctrine ORM 2.14. sonata/admin reste en 4.x tant que la migration vers 5.x n'est pas planifiée. Renovate doit respecter ces bornes — sans quoi une MR auto-mergée peut casser la prod.

Voir aussi