# libreDecision v3 — le produit (Rust + Svelte) sur decision.librodrome.org. # Une seule image : la construction rejoue toute la boucle (fmt, clippy -D warnings, # tests Rust, svelte-check, vitest) — un contrôle rouge, pas d'image. # Données runtime : volume nommé lpk-libredecision-app_data (SQLite, clone du dépôt # des textes, clé de publication). Jamais `down -v`, jamais recréé. when: - branch: main event: push # La maquette libère d'abord la racine (elle part sur maquette.decision.librodrome.org) ; # le produit se déploie ensuite, même si la maquette a échoué. depends_on: - maquette runs_on: [success, failure] steps: - 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" # NOTE: volumes + pas de from_secret : compatible - name: build-app image: docker:27-cli depends_on: - security-check volumes: - /var/run/docker.sock:/var/run/docker.sock commands: - BUILD=$(echo "$CI_COMMIT_SHA" | cut -c1-8) - docker build -t libredecision-app:latest -f docker/app.Dockerfile --target production --build-arg LD_BUILD=$BUILD . - echo "Image app construite ($BUILD)" # NOTE: volumes + pas de from_secret : compatible - name: sbom-generate image: alpine:3.20 depends_on: - build-app volumes: - /var/run/docker.sock:/var/run/docker.sock 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 # spec 1.5 épinglée : syft « latest » émet plus récent que ce que le trivy du runner décode - syft libredecision-app:latest -o cyclonedx-json@1.5 --file .reports/sbom-app.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-app.json .reports/sbom-app.cyclonedx.json - echo "Scan CVE termine" # NOTE: from_secret + pas de volumes : compatible # failure: ignore — télémétrie, ne bloque pas le déploiement (cf. maquette.yml). - name: sbom-publish image: alpine/curl:latest failure: ignore 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) 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-app" \ -F "projectVersion=$VERSION" \ -F "bom=@.reports/sbom-app.cyclonedx.json") echo "HTTP $HTTP (app) : $(cat /tmp/dtrack-resp.txt)" [ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ] || exit 1 # NOTE: volumes + pas de from_secret : compatible # Fabio route via les labels SERVICE_* de docker/app-compose.yml. - name: deploy image: docker:27-cli depends_on: - sbom-publish volumes: - /var/run/docker.sock:/var/run/docker.sock - /opt/libredecision-app:/opt/libredecision-app commands: - DOMAIN=decision.librodrome.org - echo "APP_DOMAIN=$DOMAIN" > /opt/libredecision-app/.env - cp docker/app-compose.yml /opt/libredecision-app/docker-compose.yml # Tout conteneur qui revendique encore la racine (maquette pas encore déplacée, # ou en échec) est arrêté : la racine est au produit, et fabio partagerait le trafic. - | for TAG in "urlprefix-$DOMAIN/*" "urlprefix-$DOMAIN/api/*"; do for C in $(docker ps -q --filter "label=SERVICE_3000_TAGS=$TAG") $(docker ps -q --filter "label=SERVICE_8002_TAGS=$TAG"); do NAME=$(docker inspect --format '{{.Name}}' "$C") [ "$NAME" = "/lpk-libredecision-app" ] && continue echo "Arret de $NAME (revendique $TAG)" docker stop "$C" done done # Arrêter avant le challenge ACME : la route hôte /* masquerait le webroot de sonic-acme-1. - cd /opt/libredecision-app && docker compose stop - | ACME_EXIT=0 docker exec sonic-acme-1 /app/acme.sh \ --home /etc/acme.sh \ --issue -d "$DOMAIN" \ --webroot /usr/share/nginx/html \ --server letsencrypt \ --accountemail support+acme@asycn.io || ACME_EXIT=$? if [ "$ACME_EXIT" -eq 0 ] || [ "$ACME_EXIT" -eq 2 ]; then docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/fullchain.cer /host/certs/$DOMAIN-cert.pem docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/$DOMAIN.key /host/certs/$DOMAIN-key.pem echo "TLS OK (acme exit $ACME_EXIT)" elif docker exec sonic-acme-1 test -f "/host/certs/$DOMAIN-cert.pem"; then echo "AVERTISSEMENT: emission ACME en echec (exit $ACME_EXIT) - certificat existant conserve" else echo "ERREUR: acme.sh a echoue (exit $ACME_EXIT) et aucun certificat existant pour $DOMAIN" exit 1 fi # Image construite localement dans la pipeline : pas de docker compose pull - cd /opt/libredecision-app && docker compose up -d --remove-orphans - cd /opt/libredecision-app && docker compose ps - name: test-deploy image: docker:27-cli depends_on: - deploy volumes: - /var/run/docker.sock:/var/run/docker.sock commands: - | STATUS=$(docker inspect --format '{{.State.Status}}' lpk-libredecision-app 2>/dev/null || echo "absent") echo "lpk-libredecision-app : $STATUS" [ "$STATUS" = "running" ] || { docker logs --tail 50 lpk-libredecision-app 2>&1; echo "FAIL: non running"; exit 1; } echo "PASS: conteneur running" # Le produit répond, et c'est bien ce commit qui répond. - name: healthcheck image: alpine:3.20 depends_on: - test-deploy commands: - apk add --no-cache --quiet curl - | BUILD=$(echo "$CI_COMMIT_SHA" | cut -c1-8) TARGET="https://decision.librodrome.org/api/health" MAX=60 i=0 until [ $i -ge $MAX ]; do BODY=$(curl -sS -m 10 "$TARGET" 2>/dev/null || true) echo "Tentative $((i+1))/$MAX - $BODY" case "$BODY" in *'"ok":true'*"\"build\":\"$BUILD\""*|*"\"build\":\"$BUILD\""*'"ok":true'*) echo "PASS: $BUILD repond sur $TARGET"; exit 0 ;; esac i=$((i+1)) sleep 10 done echo "FAIL: $BUILD ne repond pas apres 10 minutes" exit 1 - name: notify-failure image: alpine:3.20 commands: - 'echo "ECHEC pipeline app #$CI_PIPELINE_NUMBER sur $CI_COMMIT_BRANCH ($CI_COMMIT_SHA)"' when: - status: failure