From a9599ba32aa2e43d045f0fca306e1299efac445c Mon Sep 17 00:00:00 2001 From: syoul Date: Mon, 23 Mar 2026 14:24:16 +0100 Subject: [PATCH] ci: refonte pipeline selon bonnes pratiques sonic --- .woodpecker.yml | 192 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 169 insertions(+), 23 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 22bab2b..9f68164 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,28 +1,40 @@ when: - branch: main - event: push + - branch: main + event: push steps: - test-backend: + + - name: security-check + image: alpine:3.20 + commands: + - | + if [ -f .env ]; then + echo "ERREUR: .env ne doit pas etre commite dans le depot" + exit 1 + fi + - 'grep -q "^\.env$" .gitignore || (echo "ERREUR: .env manquant dans .gitignore" && exit 1)' + - echo "Security check OK" + + - name: test-backend image: python:3.11-slim commands: - cd backend - pip install --no-cache-dir -r requirements.txt - pytest app/tests/ -v --tb=short - test-frontend: + - name: test-frontend image: node:20-slim commands: - cd frontend - npm ci - npm run build - docker-backend: + - name: docker-backend image: woodpeckerci/plugin-docker-buildx depends_on: - test-backend settings: - repo: ${CI_FORGE_URL}/${CI_REPO} + repo: ${CI_FORGE_URL}/${CI_REPO}/backend dockerfile: docker/backend.Dockerfile context: . tag: @@ -36,12 +48,12 @@ steps: password: from_secret: docker_password - docker-frontend: + - name: docker-frontend image: woodpeckerci/plugin-docker-buildx depends_on: - test-frontend settings: - repo: ${CI_FORGE_URL}/${CI_REPO} + repo: ${CI_FORGE_URL}/${CI_REPO}/frontend dockerfile: docker/frontend.Dockerfile context: . tag: @@ -55,21 +67,155 @@ steps: password: from_secret: docker_password - deploy: - image: appleboy/drone-ssh + # SBOM — inventaire des dépendances (filesystem scan, pas de registry auth requis) + - name: sbom-generate + image: alpine:3.20 depends_on: - docker-backend - docker-frontend - settings: - host: - from_secret: deploy_host - username: - from_secret: deploy_username - key: - from_secret: deploy_key - port: 22 - script: - - cd /opt/libredecision - - docker compose -f docker/docker-compose.yml pull - - docker compose -f docker/docker-compose.yml up -d --remove-orphans - - docker image prune -f + commands: + - apk add --no-cache curl + - curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin latest + - mkdir -p .reports + - syft dir:backend -o cyclonedx-json --file .reports/sbom-backend.cyclonedx.json + - syft dir:frontend -o cyclonedx-json --file .reports/sbom-frontend.cyclonedx.json + - echo "SBOM genere" + + # NOTE: volumes + pas de from_secret : compatible + - name: sbom-scan + image: aquasec/trivy:latest + depends_on: + - sbom-generate + volumes: + - /home/syoul/trivy-cache:/root/.cache/trivy + commands: + - trivy sbom --format json --output .reports/trivy-backend.json .reports/sbom-backend.cyclonedx.json + - trivy sbom --format json --output .reports/trivy-frontend.json .reports/sbom-frontend.cyclonedx.json + - echo "Scan CVE termine" + + # NOTE: from_secret + pas de volumes : compatible + - name: sbom-publish + image: alpine/curl:latest + depends_on: + - sbom-scan + environment: + DTRACK_TOKEN: + from_secret: dependency_track_token + DTRACK_DOMAIN: + from_secret: dtrack_domain + commands: + - | + VERSION=$(date +%Y-%m-%d)-$(echo "$CI_COMMIT_SHA" | cut -c1-8) + for COMPONENT in backend frontend; do + HTTP=$(curl -s -o /tmp/dtrack-resp.txt -w "%{http_code}" -X POST "https://$DTRACK_DOMAIN/api/v1/bom" \ + -H "X-Api-Key: $DTRACK_TOKEN" \ + -F "autoCreate=true" \ + -F "projectName=libredecision-$COMPONENT" \ + -F "projectVersion=$VERSION" \ + -F "bom=@.reports/sbom-$COMPONENT.cyclonedx.json") + echo "HTTP $HTTP ($COMPONENT) : $(cat /tmp/dtrack-resp.txt)" + [ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ] || exit 1 + done + + # NOTE: from_secret + pas de volumes : compatible + - name: write-env + image: alpine:3.20 + depends_on: + - sbom-publish + environment: + APP_DOMAIN: + from_secret: app_domain + POSTGRES_PASSWORD: + from_secret: postgres_password + SECRET_KEY: + from_secret: secret_key + commands: + - echo "DOMAIN=$APP_DOMAIN" > .env.deploy + - echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" >> .env.deploy + - echo "SECRET_KEY=$SECRET_KEY" >> .env.deploy + - OWNER=$(echo "$CI_REPO_OWNER" | tr 'A-Z' 'a-z') && REPO=$(echo "$CI_REPO_NAME" | tr 'A-Z' 'a-z') && BRANCH=$(echo "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-') && echo "COMPOSE_PROJECT_NAME=$OWNER-$REPO-$BRANCH" >> .env.deploy + - echo ".env.deploy cree ($(wc -c < .env.deploy) octets)" + + - name: test-env + image: alpine:3.20 + depends_on: + - write-env + commands: + - | + [ -f .env.deploy ] || { echo "FAIL: .env.deploy introuvable"; exit 1; } + echo "PASS: .env.deploy present" + - | + VAL=$(grep '^COMPOSE_PROJECT_NAME=' .env.deploy | cut -d= -f2) + [ -z "$VAL" ] && echo "FAIL: COMPOSE_PROJECT_NAME vide" && exit 1 + echo "PASS: COMPOSE_PROJECT_NAME = $VAL" + - | + VAL=$(grep '^DOMAIN=' .env.deploy | cut -d= -f2) + [ -z "$VAL" ] && echo "FAIL: DOMAIN vide" && exit 1 + echo "PASS: DOMAIN = $VAL" + + # NOTE: volumes + pas de from_secret : compatible + # Fabio/Traefik routing géré via labels du docker-compose.yml + - name: deploy + image: docker:27-cli + depends_on: + - test-env + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /opt/libredecision:/opt/libredecision + commands: + - cp .env.deploy /opt/libredecision/.env + - chmod 600 /opt/libredecision/.env + - cp docker/docker-compose.yml /opt/libredecision/docker-compose.yml + - cd /opt/libredecision && docker compose pull + - cd /opt/libredecision && docker compose up -d --remove-orphans + - cd /opt/libredecision && docker compose ps + + # Vérification que les containers sont running + - name: test-deploy + image: docker:27-cli + depends_on: + - deploy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /opt/libredecision:/opt/libredecision + commands: + - | + PROJECT=$(grep '^COMPOSE_PROJECT_NAME=' /opt/libredecision/.env | cut -d= -f2) + for SVC in backend frontend; do + STATUS=$(docker inspect --format '{{.State.Status}}' "$PROJECT-$SVC-1" 2>/dev/null || echo "absent") + echo "$PROJECT-$SVC-1 : $STATUS" + [ "$STATUS" = "running" ] || { echo "FAIL: $PROJECT-$SVC-1 non running"; exit 1; } + done + echo "PASS: tous les containers running" + + - name: healthcheck + image: alpine:3.20 + depends_on: + - test-deploy + commands: + - apk add --no-cache --quiet curl + - | + SITE=$(grep '^DOMAIN=' .env.deploy | cut -d= -f2) + TARGET="https://$SITE" + echo "Healthcheck $TARGET..." + MAX=60 + i=0 + until [ $i -ge $MAX ]; do + CODE=$(curl -sSo /dev/null -w "%{http_code}" "$TARGET" 2>/dev/null) + echo "Tentative $((i+1))/$MAX - HTTP $CODE" + if [ "$CODE" = "200" ] || [ "$CODE" = "301" ] || [ "$CODE" = "302" ]; then + echo "PASS: app repond sur $TARGET" + exit 0 + fi + i=$((i+1)) + sleep 10 + done + echo "FAIL: app ne repond pas apres 10 minutes" + exit 1 + + - name: notify-failure + image: alpine:3.20 + commands: + - 'echo "ECHEC pipeline #$CI_BUILD_NUMBER sur $CI_COMMIT_BRANCH ($CI_COMMIT_SHA)"' + when: + - status: failure