Templates de gates¶
Implémentation exécutable du skill Gates qualité & sécurité. Le skill porte le pourquoi et où passe la ligne plancher/jalon ; la liste consolidée des outils porte le quoi et son verdict de veto ; cette page porte le comment.
Des exemples, pas un livrable
Ces fichiers sont des templates dont on s'inspire, à réinstancier selon
les stacks réelles du projet — pas à copier tels quels. Ils ne tournent pas
sur ce dépôt : le suffixe .example garantit qu'aucun runner ne les
découvre comme de la CI active.
Les actions sont épinglées par SHA et les images par version à la date de rédaction. Les revérifier avant reprise — un SHA épinglé se périme, et sa mise à jour relève du cercle de réexamen de l'ADR.
GitHub Actions¶
Un workflow par stack. Les workflows gate-secrets et gate-container sont appelés par les autres via workflow_call — les copier tous les deux, quelle que soit la stack.
astro.yml
name: Astro — CI Foyer
# Structure identique à nextjs.yml (même écosystème npm) — seule la
# commande de build / le dossier de sortie changent.
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci
- run: npm run build
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file npm-sbom.cdx.json
- run: test -s npm-sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: npm-sbom
path: npm-sbom.cdx.json
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-eslint:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci
- run: npx eslint . --format json --output-file eslint-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: eslint-report
path: eslint-report.json
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
csharp.yml
name: C# / .NET — CI Foyer
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: "8.0.x"
- run: dotnet restore
- run: dotnet build -c Release --no-restore
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
# GATE 2 — SBOM applicatif (bloquant)
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: "8.0.x"
- run: dotnet tool install --global CycloneDX
- run: echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
# Une solution unique est attendue. En cas d'ambiguïté, échouer plutôt que
# prendre la première venue : un SBOM qui ne couvre qu'une partie du dépôt
# sans le dire est pire qu'un gate rouge.
- run: |
SLN_COUNT=$(find . -maxdepth 2 -name "*.sln" | wc -l)
if [ "$SLN_COUNT" -ne 1 ]; then
echo "::error::$SLN_COUNT fichiers .sln trouvés — préciser explicitement lequel scanner."
exit 1
fi
dotnet-CycloneDX "$(find . -maxdepth 2 -name '*.sln')" -o . --filename dotnet-sbom.cdx.json
- run: test -s dotnet-sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dotnet-sbom
path: dotnet-sbom.cdx.json
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-nuget-vulnerable:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: "8.0.x"
- run: dotnet restore
- run: dotnet list package --vulnerable --include-transitive | tee nuget-vulnerable-report.txt
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: nuget-vulnerable-report
path: nuget-vulnerable-report.txt
guidance-dotnet-format:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
with:
dotnet-version: "8.0.x"
- run: dotnet format --verify-no-changes --report dotnet-format-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: dotnet-format-report
path: dotnet-format-report.json
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/.*\.cs$'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
gate-container.yml
name: Gate — Conteneur
on:
workflow_call:
inputs:
image-ref:
required: true
type: string
description: "Référence de l'image à scanner (ex. ghcr.io/org/app:sha)"
jobs:
trivy:
name: Trivy (bloquant)
runs-on: ubuntu-latest
steps:
- name: SBOM image (CycloneDX) — informatif, ne bloque pas
uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # v0.28.0
with:
image-ref: ${{ inputs.image-ref }}
format: cyclonedx
output: trivy-sbom.cdx.json
- name: Scan vulnérabilités — bloquant sur CRITICAL/HIGH
uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # v0.28.0
with:
image-ref: ${{ inputs.image-ref }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: "1"
trivyignores: .trivyignore
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
with:
sarif_file: trivy-results.sarif
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: trivy-sbom
path: trivy-sbom.cdx.json
retention-days: 90
# Second avis, non bloquant. Nécessite les secrets DOCKER_HUB_USER /
# DOCKER_HUB_TOKEN. Déclenché manuellement (workflow_dispatch) tant que
# la question de l'altérité n'est pas tranchée pour ce dépôt — cf.
# TOOLS.md. Ne pas activer par défaut sur workflow_call.
docker-scout:
name: Docker Scout (informatif, manuel)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Docker Scout CVEs
uses: docker/scout-action@2688993af7bafd6ba8c6a74ec652442be91dd82b # v1.23.1
with:
command: cves
image: ${{ inputs.image-ref }}
sarif-file: scout.sarif
continue-on-error: true
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: docker-scout-report
path: scout.sarif
gate-secrets.yml
name: Gate — Secrets
on:
workflow_call:
jobs:
gitleaks:
name: Gitleaks (bloquant)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
env:
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "true"
GITLEAKS_ENABLE_SUMMARY: "true"
# L'action échoue (exit non nul) si un secret est détecté —
# c'est le comportement voulu, ne pas ajouter continue-on-error.
trufflehog-verified:
name: TruffleHog — sweep vérifié (planifié uniquement)
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- name: TruffleHog verified scan
uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.95.9
with:
extra_args: --results=verified --fail
java.yml
name: Java — CI Foyer
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
distribution: temurin
java-version: "21"
cache: maven
- run: mvn -B -DskipTests package
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
# GATE 2 — SBOM applicatif (bloquant)
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
distribution: temurin
java-version: "21"
cache: maven
- run: mvn -B org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
# makeAggregateBom produit DÉJÀ l'agrégat à la racine du reactor. Un find
# récursif ramassait en plus les BOM par module et, faute de -quit, chaque
# cp écrasait le précédent : c'était le BOM du dernier module parcouru qui
# était publié à la place de l'agrégat.
- run: cp target/bom.json maven-sbom.cdx.json
- run: test -s maven-sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: maven-sbom
path: maven-sbom.cdx.json
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-spotbugs:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
distribution: temurin
java-version: "21"
cache: maven
- run: mvn -B com.github.spotbugs:spotbugs-maven-plugin:check
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: spotbugs-report
path: "**/spotbugsXml.xml"
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/.*\.java$'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
nextjs.yml
name: Next.js — CI Foyer
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1" # sweep secrets vérifiés, chaque lundi
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci
- run: npm run build
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
# GATE 2 — SBOM applicatif (bloquant : job échoue si le fichier est vide)
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file npm-sbom.cdx.json
- run: test -s npm-sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: npm-sbom
path: npm-sbom.cdx.json
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-eslint:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci
- run: npx eslint . --format json --output-file eslint-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: eslint-report
path: eslint-report.json
# Rappel non bloquant, uniquement si des chemins IA sont touchés.
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- name: Vérifier les chemins modifiés
run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
php.yml
name: PHP (Symfony/Drupal) — CI Foyer
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: "8.3"
- run: composer install --no-dev --optimize-autoloader --no-interaction
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
# GATE 2 — SBOM applicatif (bloquant)
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: "8.3"
- run: composer global require cyclonedx/cyclonedx-php-composer --no-interaction
- run: |
echo "$(composer global config bin-dir --absolute)" >> "$GITHUB_PATH"
- run: composer CycloneDX:make-sbom --output-format=JSON --output-file=php-sbom.cdx.json --omit=dev
- run: test -s php-sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: php-sbom
path: php-sbom.cdx.json
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-composer-audit:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: "8.3"
- run: composer audit --locked --format=json > composer-audit-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: composer-audit-report
path: composer-audit-report.json
# phpcs — PSR-12 par défaut. Pour Drupal, remplacer --standard=PSR12
# par --standard=Drupal,DrupalPractice (nécessite drupal/coder en
# dépendance dev du projet).
guidance-phpcs:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: "8.3"
tools: cs2pr, phpcs
- run: phpcs --standard=PSR12 --report=json --report-file=phpcs-report.json src/
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: phpcs-report
path: phpcs-report.json
# Actif uniquement si composer.json référence Drupal.
guidance-drupal-check:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- id: is-drupal
run: |
if grep -q "drupal/core" composer.json 2>/dev/null; then
echo "drupal=true" >> "$GITHUB_OUTPUT"
else
echo "drupal=false" >> "$GITHUB_OUTPUT"
fi
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
if: steps.is-drupal.outputs.drupal == 'true'
with:
php-version: "8.3"
- if: steps.is-drupal.outputs.drupal == 'true'
run: |
composer global require mglaman/drupal-check --no-interaction
echo "$(composer global config bin-dir --absolute)" >> "$GITHUB_PATH"
drupal-check --deprecations web/modules/custom web/themes/custom
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/.*\.php$'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
postgresql-migrations.yml
name: PostgreSQL migrations — Gate Foyer
# Convention : migrations/<horodatage>_<nom>.up.sql / .down.sql
# Ajuster MIGRATIONS_DIR et le motif de nommage selon l'outil de
# migration utilisé (node-pg-migrate, sqlx, diesel, Flyway, Sqitch...).
on:
pull_request:
paths:
- "migrations/**"
push:
branches: [main]
paths:
- "migrations/**"
env:
MIGRATIONS_DIR: migrations
jobs:
# GATE — irréversibilité des migrations (bloquant)
# Une migration sans script de retour est un fait binaire, pas un
# jugement — et son absence devient irréversible une fois jouée en
# production. Même critère que les gates du document de cadrage.
gate-migrations-reversibility:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Vérifier la présence d'un script de retour pour chaque migration
run: |
missing=0
for up in $(find "$MIGRATIONS_DIR" -name "*.up.sql"); do
down="${up%.up.sql}.down.sql"
if [ ! -f "$down" ]; then
echo "::error::Manquant : $down (pour $up)"
missing=$((missing + 1))
fi
done
if [ "$missing" -gt 0 ]; then
echo "$missing migration(s) sans script de retour."
exit 1
fi
echo "Toutes les migrations ont un script de retour."
guidance-sqlfluff:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- run: pip install --no-cache-dir sqlfluff
- run: sqlfluff lint "$MIGRATIONS_DIR" --dialect postgres
# Dry-run réel sur une instance éphémère. Non bloquant (cf. TOOLS.md) :
# une migration cassée se corrige avant merge, ce n'est pas une perte
# d'information irréversible comme les quatre gates du cadrage.
guidance-migration-dry-run:
runs-on: ubuntu-latest
continue-on-error: true
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: ci
POSTGRES_DB: dryrun
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
PGHOST: localhost
PGUSER: postgres
PGPASSWORD: ci
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Appliquer puis annuler toutes les migrations
run: |
for up in $(find "$MIGRATIONS_DIR" -name "*.up.sql" | sort); do
echo "Application : $up"
psql -d dryrun -f "$up"
done
for down in $(find "$MIGRATIONS_DIR" -name "*.down.sql" | sort -r); do
echo "Retour : $down"
psql -d dryrun -f "$down"
done
powershell.yml
name: PowerShell — CI Foyer
# Un dépôt PowerShell n'est pas systématiquement conteneurisé — voir
# gitlab/.gitlab-ci.powershell.yml pour la justification complète.
# Le gate conteneur ne s'exécute que si un Dockerfile est présent.
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
has-dockerfile: ${{ steps.check.outputs.exists }}
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Vérifier la syntaxe des scripts
shell: pwsh
run: |
$errors = $null
Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object {
[System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$errors) | Out-Null
if ($errors) { throw "Erreur de syntaxe : $($_.FullName)" }
}
- id: check
run: |
if [ -f Dockerfile ]; then echo "exists=true" >> "$GITHUB_OUTPUT"; else echo "exists=false" >> "$GITHUB_OUTPUT"; fi
- name: Build image (si Dockerfile présent)
id: meta
if: steps.check.outputs.exists == 'true'
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
gate-container:
needs: build
if: needs.build.outputs.has-dockerfile == 'true'
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-psscriptanalyzer:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- shell: pwsh
run: |
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser -Repository PSGallery
$results = Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error
$results | ConvertTo-Json -Depth 4 | Out-File psscriptanalyzer-report.json
$results | Format-Table -AutoSize
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: psscriptanalyzer-report
path: psscriptanalyzer-report.json
guidance-pester:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- shell: pwsh
run: |
if (-not (Get-ChildItem -Recurse -Filter *.Tests.ps1 -ErrorAction SilentlyContinue)) {
Write-Host "Aucun test Pester trouvé."
exit 0
}
Install-Module -Name Pester -Force -Scope CurrentUser -MinimumVersion 5.0
Invoke-Pester -CI
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/.*\.ps1$'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
python.yml
name: Python — CI Foyer
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- run: pip install --no-cache-dir -r requirements.txt
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
# GATE 2 — SBOM applicatif (bloquant)
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- run: pip install --no-cache-dir cyclonedx-bom
- run: cyclonedx-py requirements -o python-sbom.cdx.json requirements.txt
- run: test -s python-sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: python-sbom
path: python-sbom.cdx.json
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-bandit:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- run: pip install --no-cache-dir "bandit[sarif]"
- run: bandit -r . -f sarif -o bandit.sarif
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: bandit-report
path: bandit.sarif
guidance-ruff:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- run: pip install --no-cache-dir ruff
- run: ruff check . --output-format=json > ruff-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: ruff-report
path: ruff-report.json
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/.*\.py$'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
rust.yml
name: Rust — CI Foyer
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# Exception d'épinglage assumée : chez cette action, la ref n'est pas une
# version mais le SÉLECTEUR de toolchain (stable/beta/nightly). L'épingler
# par SHA lui ferait perdre son sens. Cf. TOOLS.md § Épinglage.
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --release --locked
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
# GATE 2 — SBOM applicatif (bloquant)
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# Exception d'épinglage assumée : chez cette action, la ref n'est pas une
# version mais le SÉLECTEUR de toolchain (stable/beta/nightly). L'épingler
# par SHA lui ferait perdre son sens. Cf. TOOLS.md § Épinglage.
- uses: dtolnay/rust-toolchain@stable
- run: cargo install cargo-cyclonedx --locked
- run: cargo cyclonedx --format json --all --output-cdx
# Un workspace produit un SBOM PAR CRATE. Les collecter tous, en
# conservant l'arborescence : n'en garder qu'un (find … -quit) publierait
# la nomenclature d'un seul crate en la présentant comme celle du dépôt,
# ce qui vide le gate de sa fonction sans le signaler.
- run: |
mkdir -p sbom
find . -name "*.cdx.json" -not -path "./sbom/*" -exec cp --parents {} sbom/ \;
test -n "$(ls -A sbom)"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rust-sbom
path: sbom/
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-clippy:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# Exception d'épinglage assumée : chez cette action, la ref n'est pas une
# version mais le SÉLECTEUR de toolchain (stable/beta/nightly). L'épingler
# par SHA lui ferait perdre son sens. Cf. TOOLS.md § Épinglage.
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy --message-format=json --all-targets > clippy-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: clippy-report
path: clippy-report.json
guidance-cargo-audit:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# Exception d'épinglage assumée : chez cette action, la ref n'est pas une
# version mais le SÉLECTEUR de toolchain (stable/beta/nightly). L'épingler
# par SHA lui ferait perdre son sens. Cf. TOOLS.md § Épinglage.
- uses: dtolnay/rust-toolchain@stable
- run: cargo install cargo-audit --locked
- run: cargo audit --json > cargo-audit-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: cargo-audit-report
path: cargo-audit-report.json
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/.*\.rs$'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
svelte.yml
name: Svelte — CI Foyer
# Structure identique à nextjs.yml (même écosystème npm) — seule la
# commande de build / le dossier de sortie changent.
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * 1"
jobs:
gate-secrets:
uses: ./.github/workflows/gate-secrets.yml
build:
needs: gate-secrets
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci
- run: npm run build
- name: Build image
id: meta
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
docker build -t "$IMAGE" .
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
gate-sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file npm-sbom.cdx.json
- run: test -s npm-sbom.cdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: npm-sbom
path: npm-sbom.cdx.json
retention-days: 90
gate-container:
needs: build
uses: ./.github/workflows/gate-container.yml
with:
image-ref: ${{ needs.build.outputs.image }}
guidance-sast:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Opengrep scan
run: |
docker run --rm -v "$PWD:/src" opengrep/opengrep:v1.25.0 \
scan --config auto --sarif --output /src/opengrep.sarif /src
- uses: github/codeql-action/upload-sarif@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3.37.1
if: always()
with:
sarif_file: opengrep.sarif
guidance-eslint:
needs: build
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm ci
- run: npx eslint . --format json --output-file eslint-report.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: eslint-report
path: eslint-report.json
guidance-ai-checklist:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- run: |
if git diff --name-only origin/main...HEAD | grep -qE '(ai|llm)/'; then
echo "::notice::Chemin IA modifié — checklist de mise en service (TOOLS.md#ia) :"
echo "::notice::- version de modèle et endpoint consignés dans la config versionnée"
echo "::notice::- juridiction d'hébergement du modèle documentée"
echo "::notice::- accès au journal d'interactions restreint, finalité déclarée"
echo "::notice::- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
GitLab CI¶
.gitlab-ci.gates.yml porte les gates communs et les variables de version ; il s'inclut avec le fichier de la stack. .gitlab-ci.root.yml montre le .gitlab-ci.yml racine résultant.
.gitlab-ci.astro.yml
# Stack Astro — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.astro.yml'
#
# Structure identique au template Next.js (même écosystème npm) —
# seule la commande de build change. Voir .gitlab-ci.nextjs.yml pour les
# commentaires détaillés sur chaque job.
variables:
NODE_IMAGE: "node:22-slim"
build:astro:
stage: build
image: ${NODE_IMAGE}
script:
- npm ci
- npm run build # astro build
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
artifacts:
paths:
- dist/
expire_in: 1 day
gate:sbom-npm:
stage: gate-sbom
image: ${NODE_IMAGE}
script:
- npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file npm-sbom.cdx.json
- test -s npm-sbom.cdx.json
artifacts:
paths:
- npm-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
guidance:eslint:
stage: guidance
image: ${NODE_IMAGE}
script:
- npm ci
- npx eslint . --format json --output-file eslint-report.json || true
artifacts:
when: always
paths:
- eslint-report.json
allow_failure: true
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*"
- "**/llm/**/*"
allow_failure: true
.gitlab-ci.csharp.yml
# Stack C# / .NET — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.csharp.yml'
variables:
DOTNET_IMAGE: "mcr.microsoft.com/dotnet/sdk:8.0"
build:csharp:
stage: build
image: ${DOTNET_IMAGE}
script:
- dotnet restore
- dotnet build -c Release --no-restore
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
artifacts:
paths:
- "**/bin/Release/"
expire_in: 1 day
# GATE 2 — SBOM applicatif (bloquant)
gate:sbom-dotnet:
stage: gate-sbom
image: ${DOTNET_IMAGE}
script:
- dotnet tool install --global CycloneDX
- export PATH="$PATH:/root/.dotnet/tools"
# Une solution unique est attendue. En cas d'ambiguïté, échouer plutôt que
# prendre la première venue : un SBOM qui ne couvre qu'une partie du dépôt
# sans le dire est pire qu'un gate rouge.
- |
SLN_COUNT=$(find . -maxdepth 2 -name "*.sln" | wc -l)
if [ "$SLN_COUNT" -ne 1 ]; then
echo "$SLN_COUNT fichiers .sln trouvés — préciser explicitement lequel scanner." >&2
exit 1
fi
- dotnet-CycloneDX "$(find . -maxdepth 2 -name '*.sln')" -o . --filename dotnet-sbom.cdx.json
- test -s dotnet-sbom.cdx.json
artifacts:
paths:
- dotnet-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# GUIDANCE — SAST, non bloquant
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
# Vulnérabilités NuGet — commande native du SDK (base GitHub Advisory
# Database). Ne renvoie pas de code d'erreur par elle-même (limite connue
# de l'outil) ; capturée ici en rapport informatif plutôt que bloquante,
# cohérent avec le traitement des autres audits de dépendances de ce
# dossier (cargo-audit, composer audit) : triage humain requis.
guidance:nuget-vulnerable:
stage: guidance
image: ${DOTNET_IMAGE}
script:
- dotnet restore
- dotnet list package --vulnerable --include-transitive > nuget-vulnerable-report.txt || true
- cat nuget-vulnerable-report.txt
artifacts:
when: always
paths:
- nuget-vulnerable-report.txt
allow_failure: true
guidance:dotnet-format:
stage: guidance
image: ${DOTNET_IMAGE}
script:
- dotnet format --verify-no-changes --report dotnet-format-report.json || true
artifacts:
when: always
paths:
- dotnet-format-report.json
allow_failure: true
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*.cs"
- "**/llm/**/*.cs"
allow_failure: true
.gitlab-ci.gates.yml
# Gates communs — esprit Foyer
# À inclure dans le .gitlab-ci.yml du projet :
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.<stack>.yml'
#
# Ces jobs sont BLOQUANTS (allow_failure: false, valeur par défaut).
# Ne jamais les repasser en allow_failure: true sans repasser par la
# section 6 du document de cadrage — ce sont les seuls gates dont
# l'irréversibilité justifie le blocage.
stages:
- gate-secrets
- build
- gate-sbom
- gate-container
- guidance
- publish
variables:
TRIVY_VERSION: "0.56.2"
GITLEAKS_VERSION: "8.21.2"
OPENGREP_VERSION: "1.25.0"
TRUFFLEHOG_VERSION: "3.95.9"
# ---------------------------------------------------------------------------
# GATE 1 — Secrets (bloquant)
# ---------------------------------------------------------------------------
gate:secrets:
stage: gate-secrets
image: zricethezav/gitleaks:v${GITLEAKS_VERSION}
script:
- gitleaks detect --source . --report-format sarif --report-path gitleaks.sarif --exit-code 1
artifacts:
when: always
reports:
sast: gitleaks.sarif
paths:
- gitleaks.sarif
expire_in: 30 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
# Sweep périodique avec vérification de vivacité — job planifié (CI/CD
# Schedules), pas sur chaque commit : plus lent (appels réseau au
# fournisseur du secret pour confirmer qu'il est actif).
gate:secrets-verified:
stage: gate-secrets
image: trufflesecurity/trufflehog:v${TRUFFLEHOG_VERSION}
script:
- trufflehog git file://. --since-commit HEAD~200 --results=verified --fail
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
allow_failure: false
# ---------------------------------------------------------------------------
# GATE 3 — Vulnérabilités image conteneur (bloquant)
# Suppose que le job `build` du template de stack produit une image locale
# taggée $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA avant ce stage.
# ---------------------------------------------------------------------------
gate:container:
stage: gate-container
image: aquasec/trivy:${TRIVY_VERSION}
script:
# SBOM de l'image, format CycloneDX — alimente l'agrégateur SBOM
- trivy image --format cyclonedx --output trivy-sbom.cdx.json
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# Gate bloquant : échec si CRITICAL ou HIGH non couvert par une exception
- trivy image --severity CRITICAL,HIGH --exit-code 1
--ignorefile .trivyignore
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
artifacts:
when: always
paths:
- trivy-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# Second avis, non bloquant — base de vulnérabilités différente de Trivy.
# Nécessite DOCKER_SCOUT_HUB_USER / DOCKER_SCOUT_HUB_PASSWORD en variable
# CI protégée. Désactivé par défaut (rules: when: manual) tant que la
# question de l'altérité (image envoyée à un tiers) n'a pas été tranchée
# pour les dépôts contenant du code non public — cf. TOOLS.md.
guidance:docker-scout:
stage: guidance
image: docker:24
services:
- docker:24-dind
script:
- docker scout cves $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --format sarif --output scout.sarif || true
artifacts:
when: always
paths:
- scout.sarif
allow_failure: true
rules:
- when: manual
# Provenance IA — rappel de checklist, jamais bloquant. Aucun outil ne peut
# objectiver une dérogation art. 6§3 : c'est un jugement sur la finalité, pas
# un fait détectable dans un diff. Ce job affiche la checklist quand une MR
# touche un chemin IA ; c'est un humain qui la tient (cf. TOOLS.md#ia).
guidance:ai-checklist:
stage: guidance
image: alpine:3.20
variables:
GIT_DEPTH: 0 # le clone superficiel par défaut ne permet pas le diff à la base
before_script:
- apk add --no-cache git
script:
- |
if git diff --name-only "origin/$CI_DEFAULT_BRANCH...HEAD" | grep -qE '(ai|llm)/'; then
echo "Chemin IA modifié — checklist de mise en service :"
echo "- version de modèle et endpoint consignés dans la config versionnée"
echo "- juridiction d'hébergement du modèle documentée"
echo "- accès au journal d'interactions restreint, finalité déclarée"
echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
fi
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# ---------------------------------------------------------------------------
# Publication vers l'agrégateur central — désactivé tant que DefectDojo /
# Dependency-Track ne sont pas déployés (cf. TOOLS.md). Décommenter et
# renseigner DEFECTDOJO_URL / DEFECTDOJO_TOKEN en variable CI protégée.
# ---------------------------------------------------------------------------
# publish:aggregator:
# stage: publish
# image: curlimages/curl:8.10.1
# script:
# - >
# curl -sf -X POST "$DEFECTDOJO_URL/api/v2/import-scan/"
# -H "Authorization: Token $DEFECTDOJO_TOKEN"
# -F "scan_type=Trivy Scan"
# -F "file=@trivy-sbom.cdx.json"
# -F "engagement=$DEFECTDOJO_ENGAGEMENT_ID"
# rules:
# - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
.gitlab-ci.java.yml
# Stack Java / Maven — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.java.yml'
variables:
MAVEN_IMAGE: "maven:3.9-eclipse-temurin-21"
build:java:
stage: build
image: ${MAVEN_IMAGE}
script:
- mvn -B -DskipTests package
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
artifacts:
paths:
- target/*.jar
expire_in: 1 day
# GATE 2 — SBOM applicatif (bloquant)
gate:sbom-maven:
stage: gate-sbom
image: ${MAVEN_IMAGE}
script:
- mvn -B org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
# makeAggregateBom produit déjà l'agrégat à la racine du reactor ; le find
# récursif publiait le BOM d'un module au hasard à sa place.
- cp target/bom.json maven-sbom.cdx.json
- test -s maven-sbom.cdx.json
artifacts:
paths:
- maven-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# GUIDANCE — SAST, non bloquant
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
guidance:spotbugs:
stage: guidance
image: ${MAVEN_IMAGE}
script:
- mvn -B com.github.spotbugs:spotbugs-maven-plugin:check || true
artifacts:
when: always
paths:
- "**/spotbugsXml.xml"
allow_failure: true
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*.java"
- "**/llm/**/*.java"
allow_failure: true
.gitlab-ci.nextjs.yml
# Stack Next.js / Node — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.nextjs.yml'
variables:
NODE_IMAGE: "node:22-slim"
build:nextjs:
stage: build
image: ${NODE_IMAGE}
script:
- npm ci
- npm run build
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
# nécessite docker:dind si build d'image dans ce job ; sinon déplacer
# la construction d'image vers un job dédié avec services: [docker:dind]
artifacts:
paths:
- .next/
expire_in: 1 day
# GATE 2 — SBOM applicatif (bloquant : le job échoue si le fichier n'est
# pas produit, garantissant qu'aucun build ne sort sans nomenclature)
gate:sbom-npm:
stage: gate-sbom
image: ${NODE_IMAGE}
script:
- npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file npm-sbom.cdx.json
- test -s npm-sbom.cdx.json
artifacts:
paths:
- npm-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# GUIDANCE — SAST, non bloquant
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
guidance:eslint:
stage: guidance
image: ${NODE_IMAGE}
script:
- npm ci
- npx eslint . --format json --output-file eslint-report.json || true
artifacts:
when: always
paths:
- eslint-report.json
allow_failure: true
# GUIDANCE — checklist provenance IA, déclenché uniquement si des chemins
# liés à l'IA sont modifiés. Ne bloque pas le pipeline : dépose un rapport
# à faire viser en revue de merge request (cf. TOOLS.md#ia).
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*"
- "**/llm/**/*"
allow_failure: true
.gitlab-ci.php.yml
# Stack PHP / Composer (Symfony, Drupal) — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.php.yml'
#
# Commun aux deux frameworks : Composer est le gestionnaire de dépendances
# dans les deux cas, donc le SBOM et l'audit de sécurité sont identiques.
# Ce qui diffère est signalé en commentaire (standard de code, vérificateur
# de dépréciation Drupal).
variables:
PHP_IMAGE: "php:8.3-cli"
build:php:
stage: build
image: composer:2
script:
- composer install --no-dev --optimize-autoloader --no-interaction
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
artifacts:
paths:
- vendor/
expire_in: 1 day
# GATE 2 — SBOM applicatif (bloquant)
gate:sbom-composer:
stage: gate-sbom
image: composer:2
script:
- composer global require cyclonedx/cyclonedx-php-composer --no-interaction
- composer CycloneDX:make-sbom --output-format=JSON --output-file=php-sbom.cdx.json --omit=dev
- test -s php-sbom.cdx.json
artifacts:
paths:
- php-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# GUIDANCE — SAST, non bloquant
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
# Audit de dépendances — natif Composer depuis 2.4, base
# FriendsOfPHP/security-advisories. Même statut que cargo-audit /
# composer audit ailleurs dans ce dossier : une dépendance listée n'est
# pas nécessairement atteinte par le code, donc guidance et non gate.
guidance:composer-audit:
stage: guidance
image: composer:2
script:
- composer audit --locked --format=json > composer-audit-report.json || true
artifacts:
when: always
paths:
- composer-audit-report.json
allow_failure: true
# Standard de code — PSR-12 par défaut. Pour un projet Drupal, remplacer
# --standard=PSR12 par --standard=Drupal,DrupalPractice (paquet
# drupal/coder) : décommenter la ligne correspondante ci-dessous.
guidance:phpcs:
stage: guidance
image: ${PHP_IMAGE}
script:
- composer global require "squizlabs/php_codesniffer=*" --no-interaction
- export PATH="$PATH:$(composer global config bin-dir --absolute --quiet 2>/dev/null)"
- phpcs --standard=PSR12 --report=json --report-file=phpcs-report.json src/ || true
# Variante Drupal (nécessite composer require --dev drupal/coder dans le projet) :
# phpcs --standard=Drupal,DrupalPractice --report=json --report-file=phpcs-report.json web/modules/custom web/themes/custom
artifacts:
when: always
paths:
- phpcs-report.json
allow_failure: true
# Guidance spécifique Drupal — dépréciations d'API core/contrib, hors
# scope pour un projet Symfony pur. Actif uniquement si le paquet
# drupal/core-recommended est présent dans composer.json.
guidance:drupal-check:
stage: guidance
image: ${PHP_IMAGE}
script:
# Actif uniquement si composer.json référence Drupal — sort proprement
# (succès) sur un dépôt Symfony pur plutôt que d'échouer inutilement.
- grep -q "drupal/core" composer.json || { echo "Pas un projet Drupal — job ignoré."; exit 0; }
- composer global require mglaman/drupal-check --no-interaction
- export PATH="$PATH:$(composer global config bin-dir --absolute --quiet 2>/dev/null)"
- drupal-check --deprecations web/modules/custom web/themes/custom || true
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*.php"
- "**/llm/**/*.php"
allow_failure: true
.gitlab-ci.postgresql.yml
# PostgreSQL / migrations — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.postgresql.yml'
#
# Ce template ne compile rien : il porte sur le dossier de migrations
# (convention : migrations/<horodatage>_<nom>.up.sql /
# migrations/<horodatage>_<nom>.down.sql — ajuster GLOB_UP / GLOB_DOWN
# selon l'outil de migration utilisé : node-pg-migrate, sqlx, diesel,
# Flyway, Sqitch...).
#
# GATE — irréversibilité des migrations (bloquant)
# Justification Foyer directe : une migration appliquée sans script de
# retour n'est pas un jugement de qualité, c'est un fait binaire — le
# fichier .down.sql existe ou n'existe pas — et l'absence de retour est
# irréversible une fois la migration jouée en production. C'est le même
# critère "objectivable × irréversible" que les quatre gates du document
# de cadrage, appliqué au domaine base de données plutôt qu'au code.
variables:
MIGRATIONS_DIR: "migrations"
gate:migrations-reversibility:
stage: gate-sbom # même position dans le pipeline que les autres gates de fond
image: alpine:3.20
script:
- |
missing=0
for up in $(find "$MIGRATIONS_DIR" -name "*.up.sql" 2>/dev/null); do
down="${up%.up.sql}.down.sql"
if [ ! -f "$down" ]; then
echo "MANQUANT : $down (pour $up)"
missing=$((missing + 1))
fi
done
if [ "$missing" -gt 0 ]; then
echo "$missing migration(s) sans script de retour — irréversible, bloqué."
exit 1
fi
echo "Toutes les migrations ont un script de retour."
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "${MIGRATIONS_DIR}/**/*"
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
changes:
- "${MIGRATIONS_DIR}/**/*"
# GUIDANCE — lint SQL, non bloquant. Attrape les erreurs de style et
# certains anti-patterns (SELECT *, DROP sans garde-fou) avant revue.
guidance:sqlfluff:
stage: guidance
image: python:3.12-slim
script:
- pip install --no-cache-dir sqlfluff
- sqlfluff lint "$MIGRATIONS_DIR" --dialect postgres --format github-annotation > sqlfluff-report.json || true
artifacts:
when: always
paths:
- sqlfluff-report.json
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "${MIGRATIONS_DIR}/**/*"
# GUIDANCE — test d'application réelle up puis down sur une instance
# éphémère. Ne remplace pas le gate ci-dessus (qui vérifie l'existence
# du fichier) : ceci vérifie qu'il s'exécute sans erreur SQL. Adapter
# la commande d'application au client de migration réellement utilisé.
guidance:migration-dry-run:
stage: guidance
image: postgres:16-alpine
services:
- postgres:16-alpine
variables:
POSTGRES_PASSWORD: ci
POSTGRES_DB: dryrun
PGHOST: postgres
PGUSER: postgres
PGPASSWORD: ci
script:
- |
for up in $(find "$MIGRATIONS_DIR" -name "*.up.sql" | sort); do
echo "Application : $up"
psql -d dryrun -f "$up" || { echo "Échec sur $up"; exit 1; }
done
for down in $(find "$MIGRATIONS_DIR" -name "*.down.sql" | sort -r); do
echo "Retour : $down"
psql -d dryrun -f "$down" || { echo "Échec sur $down"; exit 1; }
done
allow_failure: true
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "${MIGRATIONS_DIR}/**/*"
.gitlab-ci.powershell.yml
# Stack PowerShell — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.powershell.yml'
#
# Particularité de ce stack : un dépôt PowerShell (scripts d'automatisation,
# module d'administration) n'est pas systématiquement conteneurisé. Les
# gates SBOM/conteneur du fichier gates.yml supposent une image Docker —
# ils ne s'exécutent donc ici que si un Dockerfile est présent. Aucun
# générateur CycloneDX dédié à l'écosystème PowerShell Gallery n'est
# aujourd'hui établi (voir TOOLS.md) : à défaut, on documente les modules
# requis plutôt que de produire un SBOM de façade.
variables:
PWSH_IMAGE: "mcr.microsoft.com/powershell:7.4-alpine-3.20"
build:powershell:
stage: build
image: ${PWSH_IMAGE}
script:
- pwsh -NoProfile -Command "Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object { \$null = [System.Management.Automation.Language.Parser]::ParseFile(\$_.FullName, [ref]\$null, [ref]\$errors); if (\$errors) { Write-Error \"Erreur de syntaxe : \$($_.FullName)\" } }"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
# Build/scan conteneur — uniquement si le dépôt en produit un (ex. tâche
# planifiée packagée, agent d'automatisation). Sans Dockerfile, ce job ne
# s'exécute pas : ni SBOM ni gate conteneur ne s'appliquent à un script
# exécuté directement sur un hôte, hors du périmètre couvert ici.
build:powershell-container:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
exists:
- Dockerfile
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
exists:
- Dockerfile
# GUIDANCE — PSScriptAnalyzer : lint et sécurité (détecte notamment
# ConvertTo-SecureString en clair, Invoke-Expression sur entrée non
# fiable, mots de passe en paramètre). Éditeur Microsoft, MIT, gratuit.
guidance:psscriptanalyzer:
stage: guidance
image: ${PWSH_IMAGE}
script:
- >
pwsh -NoProfile -Command "
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser -Repository PSGallery;
\$results = Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error;
\$results | ConvertTo-Json -Depth 4 | Out-File psscriptanalyzer-report.json;
\$results | Format-Table -AutoSize
"
artifacts:
when: always
paths:
- psscriptanalyzer-report.json
allow_failure: true
# GUIDANCE — Pester, si des tests sont présents (*.Tests.ps1).
guidance:pester:
stage: guidance
image: ${PWSH_IMAGE}
script:
- >
pwsh -NoProfile -Command "
if (-not (Get-ChildItem -Recurse -Filter *.Tests.ps1 -ErrorAction SilentlyContinue)) {
Write-Host 'Aucun test Pester trouvé — job ignoré.'; exit 0
}
Install-Module -Name Pester -Force -Scope CurrentUser -MinimumVersion 5.0;
Invoke-Pester -CI
"
allow_failure: true
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*.ps1"
- "**/llm/**/*.ps1"
allow_failure: true
.gitlab-ci.python.yml
# Stack Python — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.python.yml'
variables:
PYTHON_IMAGE: "python:3.12-slim"
build:python:
stage: build
image: ${PYTHON_IMAGE}
script:
- pip install --no-cache-dir -r requirements.txt
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
# GATE 2 — SBOM applicatif (bloquant)
gate:sbom-python:
stage: gate-sbom
image: ${PYTHON_IMAGE}
script:
- pip install --no-cache-dir cyclonedx-bom
- cyclonedx-py requirements -o python-sbom.cdx.json requirements.txt
- test -s python-sbom.cdx.json
artifacts:
paths:
- python-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# GUIDANCE — SAST, non bloquant
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
guidance:bandit:
stage: guidance
image: ${PYTHON_IMAGE}
script:
- pip install --no-cache-dir bandit[sarif]
- bandit -r . -f sarif -o bandit.sarif || true
artifacts:
when: always
paths:
- bandit.sarif
allow_failure: true
guidance:ruff:
stage: guidance
image: ${PYTHON_IMAGE}
script:
- pip install --no-cache-dir ruff
- ruff check . --output-format=json > ruff-report.json || true
artifacts:
when: always
paths:
- ruff-report.json
allow_failure: true
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*.py"
- "**/llm/**/*.py"
allow_failure: true
.gitlab-ci.root.yml
# Exemple de .gitlab-ci.yml pour un projet Next.js.
# Copier ce fichier à la racine du projet sous le nom .gitlab-ci.yml,
# ajuster le chemin des includes selon l'emplacement de ce dossier
# (sous-module, projet partagé "ci-templates", ou copie locale).
include:
- local: 'gitlab/.gitlab-ci.gates.yml'
- local: 'gitlab/.gitlab-ci.nextjs.yml'
# Remplacer par .gitlab-ci.java.yml ou .gitlab-ci.python.yml selon le stack.
# Un projet mixte (ex. frontend Next.js + backend Java) peut inclure
# les deux templates de stack : les stages sont partagés, les jobs
# portent des noms distincts (build:nextjs / build:java).
.gitlab-ci.rust.yml
# Stack Rust / Cargo — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.rust.yml'
variables:
RUST_IMAGE: "rust:1.82-slim-bookworm"
build:rust:
stage: build
image: ${RUST_IMAGE}
script:
- cargo build --release --locked
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
artifacts:
paths:
- target/release/
expire_in: 1 day
# GATE 2 — SBOM applicatif (bloquant)
gate:sbom-rust:
stage: gate-sbom
image: ${RUST_IMAGE}
script:
- cargo install cargo-cyclonedx --locked
- cargo cyclonedx --format json --all --output-cdx
# Un workspace produit un SBOM PAR CRATE — les collecter tous (cf. le
# commentaire du template GitHub équivalent).
- mkdir -p sbom
- find . -name "*.cdx.json" -not -path "./sbom/*" -exec cp --parents {} sbom/ \;
- test -n "$(ls -A sbom)"
artifacts:
paths:
- sbom/
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# GUIDANCE — SAST, non bloquant
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
guidance:clippy:
stage: guidance
image: ${RUST_IMAGE}
script:
- rustup component add clippy
- cargo clippy --message-format=json --all-targets > clippy-report.json || true
artifacts:
when: always
paths:
- clippy-report.json
allow_failure: true
# Base RustSec — complète Trivy, dont la couverture de l'écosystème Cargo
# est plus tardive que celle de RustSec. Non bloquant : une dépendance
# vulnérable listée n'est pas nécessairement atteinte par le code
# (triage requis, cf. TOOLS.md).
guidance:cargo-audit:
stage: guidance
image: ${RUST_IMAGE}
script:
- cargo install cargo-audit --locked
- cargo audit --json > cargo-audit-report.json || true
artifacts:
when: always
paths:
- cargo-audit-report.json
allow_failure: true
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*.rs"
- "**/llm/**/*.rs"
allow_failure: true
.gitlab-ci.svelte.yml
# Stack Svelte / SvelteKit — à inclure avec .gitlab-ci.gates.yml
# include:
# - local: 'gitlab/.gitlab-ci.gates.yml'
# - local: 'gitlab/.gitlab-ci.svelte.yml'
#
# Structure identique au template Next.js (même écosystème npm) —
# seule la commande de build change.
variables:
NODE_IMAGE: "node:22-slim"
build:svelte:
stage: build
image: ${NODE_IMAGE}
script:
- npm ci
- npm run build # vite build / svelte-kit build
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
artifacts:
paths:
- build/
expire_in: 1 day
gate:sbom-npm:
stage: gate-sbom
image: ${NODE_IMAGE}
script:
- npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file npm-sbom.cdx.json
- test -s npm-sbom.cdx.json
artifacts:
paths:
- npm-sbom.cdx.json
expire_in: 90 days
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
guidance:sast-opengrep:
stage: guidance
image: opengrep/opengrep:v${OPENGREP_VERSION}
script:
- opengrep scan --config auto --sarif --output opengrep.sarif .
artifacts:
when: always
reports:
sast: opengrep.sarif
allow_failure: true
guidance:eslint:
stage: guidance
image: ${NODE_IMAGE}
script:
- npm ci
- npx eslint . --format json --output-file eslint-report.json || true
artifacts:
when: always
paths:
- eslint-report.json
allow_failure: true
guidance:ai-provenance-checklist:
stage: guidance
image: alpine:3.20
script:
- echo "Rappel — vérifier avant merge (voir TOOLS.md#ia) :"
- echo "- version de modèle et endpoint consignés dans la config versionnée"
- echo "- juridiction d'hébergement du modèle documentée"
- echo "- accès au journal d'interactions restreint, finalité déclarée"
- echo "- évaluation art. 6§3 AI Act documentée si fonction annexe III"
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/ai/**/*"
- "**/llm/**/*"
allow_failure: true
Poste de travail¶
Seul le gate secrets est bloquant au commit (rapide, hors ligne). Le reste est délégué aux hooks natifs de chaque écosystème — voir install.sh dans le dépôt.
.pre-commit-config.yaml
# Gates de poste de travail — esprit Foyer.
# Installation : voir install.sh dans ce même dossier.
#
# Principe : seul le gate secrets est bloquant au commit (rapide, hors
# ligne). Tout le reste (lint, format) est laissé aux hooks natifs de
# chaque écosystème pour ne pas imposer d'outil — cf. README.md.
repos:
# ---------------------------------------------------------------------
# GATE — Secrets (bloquant, < 1s, aucun appel réseau)
# ---------------------------------------------------------------------
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
# ---------------------------------------------------------------------
# Garde-fous génériques, peu coûteux, consensuels — pas spécifiques
# à un écosystème, ne remplacent aucun gate CI.
# ---------------------------------------------------------------------
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: ["--maxkb=2000"]
- id: detect-private-key
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace
# ---------------------------------------------------------------------
# Guidance Python — non bloquant en soi (ruff --fix corrige plutôt que
# de rejeter), à activer uniquement dans les dépôts Python.
# ---------------------------------------------------------------------
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
args: ["--fix"]
# ---------------------------------------------------------------------
# Guidance Next.js / Astro / Svelte (npm) — délégué à la config ESLint
# du projet. Décommenter si le dépôt est un projet Node.
# ---------------------------------------------------------------------
# - repo: local
# hooks:
# - id: eslint
# name: eslint
# entry: npx eslint --fix
# language: system
# files: \.(js|jsx|ts|tsx|astro|svelte)$
# ---------------------------------------------------------------------
# Guidance Rust — clippy en local, rapide sur un dépôt déjà compilé au
# moins une fois (cache incrémental). Décommenter dans un dépôt Rust.
# ---------------------------------------------------------------------
# - repo: local
# hooks:
# - id: clippy
# name: cargo clippy
# entry: cargo clippy --all-targets -- -D warnings
# language: system
# pass_filenames: false
# ---------------------------------------------------------------------
# Guidance PHP — phpcs local. Décommenter dans un dépôt Symfony ou
# Drupal (ajuster --standard selon le cas, cf. TOOLS.md).
# ---------------------------------------------------------------------
# - repo: local
# hooks:
# - id: phpcs
# name: phpcs (PSR-12)
# entry: phpcs --standard=PSR12
# language: system
# files: \.php$
# ---------------------------------------------------------------------
# Guidance C# / .NET — dotnet format en mode vérification. Décommenter
# dans un dépôt .NET.
# ---------------------------------------------------------------------
# - repo: local
# hooks:
# - id: dotnet-format
# name: dotnet format
# entry: dotnet format --verify-no-changes
# language: system
# pass_filenames: false
# ---------------------------------------------------------------------
# Guidance PowerShell — PSScriptAnalyzer en local. Décommenter dans un
# dépôt de scripts/modules PowerShell.
# ---------------------------------------------------------------------
# - repo: local
# hooks:
# - id: psscriptanalyzer
# name: PSScriptAnalyzer
# entry: pwsh -NoProfile -Command "Invoke-ScriptAnalyzer -Path . -Recurse -Severity Warning,Error -EnableExit"
# language: system
# pass_filenames: false
# ---------------------------------------------------------------------
# Guidance PostgreSQL — lint des migrations. Décommenter dans un dépôt
# qui contient un dossier migrations/.
# ---------------------------------------------------------------------
# - repo: local
# hooks:
# - id: sqlfluff
# name: sqlfluff lint (postgres)
# entry: sqlfluff lint --dialect postgres
# language: system
# files: ^migrations/.*\.sql$