Un pipeline CI/CD de SaaS B2B doit prouver que le code est correct, que ses dépendances et ses images ne portent pas de vulnérabilité connue, que l’artefact livré est bien celui qui a été construit, et qu’on peut revenir en arrière sans rien reconstruire. Dans saas-core, cette chaîne tient en dix workflows GitHub Actions, certains bloquants, d’autres informatifs. Cet article les décrit avec leurs déclencheurs réels et renvoie aux articles qui détaillent chaque outil.
Quels workflows composent la chaîne ?
Dix fichiers sous .github/workflows/, chacun avec un rôle unique :
| Workflow | Déclencheur | Rôle | Bloquant ? |
|---|---|---|---|
ci | push sur main, pull request, manuel | typecheck, lint, frontières, tests sur vrai PostgreSQL, build, contrat à jour, secrets, dépendances ; job images | oui pour la livraison |
e2e | push sur main, pull request, manuel | parcours navigateur Playwright contre la vraie API et le worker | oui pour la livraison |
sast | push sur main, pull request, manuel | analyse statique Semgrep | rouge sur tout résultat ; hors porte de livraison |
sbom | push sur main, manuel | SBOM CycloneDX de l’application et des images d’infrastructure | non, inventaire archivé |
image-scan | lundi 05:00 UTC, manuel | reconstruction et scan des images de main | rouge sur une critique corrigée en amont ; hors porte de livraison |
scorecard | lundi 06:00 UTC, manuel | OpenSSF Scorecard sur le dépôt | non, informatif |
load | étiquette v*, manuel | scénario k6 sur l’API compilée | non, avertissement si seuil franchi |
release-images | étiquette v*, manuel | images arm64, signature Cosign, provenance SLSA, OpenVEX, livraison | la livraison s’arrête si le journal des modifications manque ou si ci et e2e ne sont pas verts |
deploy | manuel uniquement | relivrer une étiquette existante : le retour arrière | sans objet |
doc-impact | pull request | pages de documentation à relire, référence générée à jour | rouge si la référence diffère ; non exigé à la fusion |
« Bloquant » vise la mise en ligne d’une version, gardée par une porte décrite plus bas. La protection de branche de main est aujourd’hui absente : un ci rouge n’empêche pas une fusion.
Que vérifie le workflow ci ?
Il vérifie, dans cet ordre, tout ce qui peut se prouver sans navigateur. Le job principal démarre un service PostgreSQL 17 épinglé par empreinte, puis enchaîne :
pnpm typecheck,pnpm lint(ESLint) etpnpm depcruise: dependency-cruiser interdit les imports qui franchissent une frontière, par exemple un écran qui importerait directement le fournisseur d’authentification, ou une dépendance circulaire ;pnpm check:cssetpnpm check:size, puis les tests des scripts d’outillage ;pnpm test --continue: les suites de tous les paquets contre le vrai PostgreSQL, méta-tests compris (isolation RLS, journal d’audit) ;--continueempêche un paquet rouge de masquer les suivants. Le détail est dans l’article sur Testcontainers et les tests sur un vrai PostgreSQL ;pnpm build, puis la vérification que le contrat OpenAPI et le SDK généré correspondent au code ;- gitleaks sur tout l’historique,
pnpm audit --audit-level=highet OSV-Scanner.
L’étape du contrat illustre la philosophie du fichier : ce qui se génère se régénère et se compare (extrait abrégé).
- name: Le contrat et le SDK sont a jour
run: |
pnpm --filter @saas-core/api generate:openapi
pnpm --filter @saas-core/sdk generate
if ! git diff --quiet -- apps/api/openapi.json packages/sdk/src/schema.d.ts; then
echo "::error::openapi.json ou schema.d.ts ne correspond plus au code."
exit 1
fi
Que fait le job images ?
Il construit les trois images livrées (API, worker, admin) à chaque PR, sans les pousser, puis démarre l’image de l’API et interroge /health. Il vérifie ensuite que les images d’exécution n’embarquent aucun outil de construction (npm, npx, corepack, yarn), et scanne les images avec syft (SBOM) et grype (vulnérabilités) via scripts/sbom/scan-images.mjs. Une vulnérabilité critique déjà corrigée en amont fait échouer le job, sauf exception motivée et datée dans image-scan-exceptions.json. Ce job appartient au workflow ci : son échec rend le run ci rouge.
Pourquoi séparer e2e de ci ?
Pour distinguer d’un coup d’œil une panne d’API d’une panne d’interface, et pour que la suite navigateur, plus lente, ne retarde pas le retour rapide des tests. Le workflow e2e démarre PostgreSQL, Redis et Mailpit en services, crée le rôle applicatif NOBYPASSRLS, applique les migrations, teste le worker d’un produit sur Redis réel, puis lance Playwright : un parcours d’invitation n’est prouvé que si l’e-mail est réellement relayé et reçu.
Comment la chaîne couvre-t-elle la sécurité des dépendances et du code ?
Par plusieurs outils complémentaires, chacun regardant une surface différente :
- Secrets : gitleaks parcourt tout l’historique git (
fetch-depth: 0), binaire officiel vérifié par somme SHA-256 ; voir l’article sur gitleaks. - Dépendances :
pnpm auditau niveauhigh, puis OSV-Scanner, épinglé par version et empreinte, avec les cas qualifiés « non affecté » consignés dansosv-scanner.toml; voir OSV-Scanner, pnpm audit et VEX. - Code écrit : Semgrep, avec les jeux de règles
p/typescript,p/nodejsetp/secretset l’option--error; voir l’article sur Semgrep. - Inventaire : le workflow
sbomproduit un SBOM CycloneDX de l’application avec cdxgen et des images PostgreSQL et Redis avec syft ; voir SBOM CycloneDX avec cdxgen et syft. L’analyse continue de ces SBOM se fait dans Dependency-Track, hors CI, avecdocker-compose.dtrack.yml; voir Dependency-Track. - Images : grype dans le job
imagesà chaque PR, et chaque lundi dansimage-scan, parce qu’une vulnérabilité publiée après la construction rend une image vulnérable sans qu’aucune ligne ne change. - Posture du dépôt : OpenSSF Scorecard chaque semaine, résultat archivé en artefact, sans effet sur la chaîne.
Chaque outil est épinglé par version et empreinte plutôt que latest : sinon le code du contrôle change entre deux exécutions, et un rouge devient indiscernable d’une régression réelle.
Comment une étiquette devient-elle une mise en ligne ?
Par le workflow release-images, qui enchaîne quatre étapes après une étiquette vX.Y.Z :
- Journal des modifications :
scripts/changelog-check.mjsrefuse une étiquette absente deCHANGELOG.mdetCHANGELOG.en.md. - Construction : les trois images sont construites nativement sur un runner auto-hébergé arm64, une par cible, puis réunies en un manifeste unique et poussées vers GHCR.
- Signature et attestations : signature Cosign keyless grâce au jeton OIDC du runner, provenance SLSA attachée par
cosign attest, document OpenVEX attaché comme attestation signée, puiscosign verifysur ce qui vient d’être signé. Le principe est détaillé dans l’article sur Cosign et SLSA. - Livraison : le job
livrerinterroge l’API GitHub sur la conclusion des runsciete2edu commit exact de l’étiquette.
La quatrième étape répond à un défaut constaté : ci ne se déclenche pas sur une étiquette, et une étiquette posée sur un commit rouge produisait une image signée, attestée et déployée. La signature atteste l’origine, jamais la qualité. La porte applique quatre verdicts (extrait simplifié) :
case "$verdict" in
success) break ;;
failure|timed_out)
echo "::error::livraison refusée — ${flux} sur ${GITHUB_SHA} : ${verdict}"
exit 1 ;;
cancelled)
# relance du run annulé, deux fois au plus
;;
*) sleep 30 ;;
esac
Un run en cours est attendu jusqu’à 30 minutes ; un run annulé par la concurrence est relancé deux fois au plus ; l’absence de run au bout du délai est refusée, car « aucun test n’a tourné » mérite le même refus que « les tests ont échoué ». La commande de livraison n’est pas dans le dépôt : le workflow appelle celle que déclare la variable DEPLOY_COMMAND, sans connaître la machine ni l’hébergeur. L’organisation côté serveur (Docker Compose, reverse proxy, outil comme Coolify) est discutée dans l’article sur la livraison avec Coolify, Traefik et Docker Compose.
Comment revenir en arrière ?
En relivrant une étiquette précédente avec le workflow deploy, déclenché à la main avec l’étiquette en paramètre. Il ne reconstruit rien : les images signées existent déjà. Il n’a volontairement aucun déclencheur sur étiquette : lancé par le même push que la construction, il chercherait des images pas encore construites. Les livraisons sont sérialisées sans annulation.
Que reste-t-il d’informatif, et pourquoi ?
Trois workflows signalent sans bloquer, chacun pour une raison écrite dans son fichier :
loadtourne sur chaque étiquette, en parallèle de la release. Ses seuils de latence se mesurent sur un runner partagé qui construit souvent des images au même moment ; un franchissement produit un avertissement et un résumé en artefact, pas un échec.scorecardest un outil d’hygiène hebdomadaire, sans déclencheur sur push.doc-impactcommente chaque PR avec les pages de documentation à relire et échoue si la référence générée ne correspond plus au code, mais il n’est pas exigé à la fusion : relire une page reste un geste humain.
Quels pièges d’exploitation éviter ?
Trois réglages du dépôt viennent d’incidents et valent pour toute chaîne similaire :
- Jeton en lecture seule :
ci,e2eetsast, qui exécutent le code d’une pull request, déclarentpermissions: contents: read. - Concurrence partagée :
ciete2epartagent le groupedb-service-sharedsur un runner où le démon Docker est commun. GitHub ne garde qu’un run en attente par groupe ; un nouveau run annule celui qui attendait. Un runcancelledn’est donc pas un verdict : il se relance, et la porte de livraison le fait elle-même. - Binaires vérifiés : gitleaks, k6, Scorecard et crane sont téléchargés depuis leur release officielle et vérifiés par une somme SHA-256 écrite dans le workflow, pas lue à côté de l’archive.
Comment saas-core l’intègre
- Les dix workflows sont dans
.github/workflows/:ci.yml,e2e.yml,sast.yml,sbom.yml,image-scan.yml,scorecard.yml,load.yml,release-images.yml,deploy.yml,doc-impact.yml. - La porte locale
pnpm run ci(package.json) rejoue typecheck, lint, frontières (.dependency-cruiser.cjs),check:css,check:sizeet tests ; la CI y ajoute build, contrat, gitleaks, audit et OSV. - Le scan des images est commun à
ciet àimage-scan(scripts/sbom/scan-images.mjs, exceptions dansimage-scan-exceptions.json) ; le document OpenVEX attaché aux images estsecurity/vex/saas-core.openvex.json. - Bloquant pour la livraison :
ci(jobimagescompris) ete2everts sur le commit exact, et l’entrée du journal des modifications. Rouge sans bloquer la livraison :sast,image-scan,doc-impact. Informatif :sbom,scorecard,load. - Absent : la protection de branche de
main. Aucune fusion n’est empêchée par la CI aujourd’hui ; c’est la livraison qui est gardée.
Pour aller plus loin
Ces workflows sont livrés avec le socle ; la livraison s’y appuie sur des variables de dépôt (runner, commande et environnement de livraison) plutôt que sur une infrastructure écrite en dur. Le contenu de chaque formule est présenté sur la page tarifs.