# CI SIOP V2 (R0.12) — trois verrous : # ci-contract : la règle d'or (Zod → OpenAPI → clients typés) — échoue au # moindre diff entre le contrat committé et la génération ; # api : migration + tests Jest (couverture ≥ 70 % bloquante, jest.config) ; # web : typecheck + vitest + build de production. # lint : ESLint monorepo (dont la règle ADR-001 « pas d'import MinIO # hors FileStorage ») ; # e2e : parcours démo Playwright (connexion 1 clic → coquille → # bascule de rôle < 3 s → /design) contre l'API construite. name: CI on: push: branches: [main] pull_request: concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: lint: name: lint (ESLint monorepo) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm lint contract: name: ci-contract (règle d'or) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm --filter @siop/shared build - name: Régénérer la spec et les clients typés run: | pnpm --filter @siop/shared contract pnpm --filter @siop/web generate:client pnpm --filter @siop/mobile generate:client - name: Vérifier qu'aucun artefact ne dérive du contrat run: | if ! git diff --exit-code -- docs/openapi.json apps/web/src/api/schema.d.ts apps/mobile/src/api/schema.d.ts; then echo "::error::Contrat désynchronisé — régénérez spec et clients dans le même commit (pnpm contract && pnpm --filter @siop/web generate:client && pnpm --filter @siop/mobile generate:client)." exit 1 fi api: name: api (tests + couverture ≥ 70 %) runs-on: ubuntu-latest services: # PostgreSQL vient d'infra/postgres (pgvector + PostGIS — R5) : les # services ne savent pas builder, il démarre donc par étape ci-dessous. redis: image: redis:7.4-alpine ports: ['6379:6379'] options: >- --health-cmd "redis-cli ping" --health-interval 5s --health-timeout 3s --health-retries 10 env: DATABASE_URL: postgresql://siop:siop@localhost:5432/siop REDIS_URL: redis://localhost:6379 JWT_SECRET: ci-only-secret-0123456789abcdef MINIO_ENDPOINT: localhost MINIO_ACCESS_KEY: siop MINIO_SECRET_KEY: siop-minio steps: - uses: actions/checkout@v5 - name: Démarrer PostgreSQL (image infra — pgvector + PostGIS, comme partout) run: | docker build -t siop2/postgres infra/postgres docker run -d --name postgres -p 5432:5432 \ -e POSTGRES_USER=siop -e POSTGRES_PASSWORD=siop -e POSTGRES_DB=siop \ siop2/postgres for i in $(seq 1 30); do docker exec postgres pg_isready -U siop -d siop && break; sleep 2; done - name: Démarrer MinIO (l'image officielle exige une commande — pas un service) run: | docker run -d --name minio -p 9000:9000 \ -e MINIO_ROOT_USER=siop -e MINIO_ROOT_PASSWORD=siop-minio \ minio/minio:RELEASE.2025-09-07T16-13-09Z server /data - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm --filter @siop/shared build - run: pnpm --filter @siop/api prisma:generate - run: pnpm --filter @siop/api exec prisma migrate deploy - run: pnpm --filter @siop/api typecheck - run: pnpm --filter @siop/api test:cov web: name: web (typecheck + tests + build) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm --filter @siop/shared build - run: pnpm --filter @siop/web test - run: pnpm --filter @siop/web build mobile: name: mobile (typecheck + tests jest-expo) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm --filter @siop/shared build - run: pnpm --filter @siop/mobile typecheck - run: pnpm --filter @siop/mobile test ai: name: ai (pytest + ruff — uv) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: astral-sh/setup-uv@v5 - run: uv sync working-directory: apps/ai - run: uv run ruff check src tests working-directory: apps/ai - run: uv run pytest working-directory: apps/ai e2e: name: e2e (parcours démo Playwright) runs-on: ubuntu-latest services: # PostgreSQL vient d'infra/postgres (pgvector + PostGIS — R5) : les # services ne savent pas builder, il démarre donc par étape ci-dessous. redis: image: redis:7.4-alpine ports: ['6379:6379'] options: >- --health-cmd "redis-cli ping" --health-interval 5s --health-timeout 3s --health-retries 10 env: DATABASE_URL: postgresql://siop:siop@localhost:5432/siop REDIS_URL: redis://localhost:6379 JWT_SECRET: ci-only-secret-0123456789abcdef MINIO_ENDPOINT: localhost MINIO_ACCESS_KEY: siop MINIO_SECRET_KEY: siop-minio steps: - uses: actions/checkout@v5 - name: Démarrer PostgreSQL (image infra — pgvector + PostGIS, comme partout) run: | docker build -t siop2/postgres infra/postgres docker run -d --name postgres -p 5432:5432 \ -e POSTGRES_USER=siop -e POSTGRES_PASSWORD=siop -e POSTGRES_DB=siop \ siop2/postgres for i in $(seq 1 30); do docker exec postgres pg_isready -U siop -d siop && break; sleep 2; done - name: Démarrer MinIO (l'image officielle exige une commande — pas un service) run: | docker run -d --name minio -p 9000:9000 \ -e MINIO_ROOT_USER=siop -e MINIO_ROOT_PASSWORD=siop-minio \ minio/minio:RELEASE.2025-09-07T16-13-09Z server /data - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: node-version-file: .nvmrc cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm --filter @siop/shared build - run: pnpm --filter @siop/api prisma:generate - run: pnpm --filter @siop/api exec prisma migrate deploy - run: pnpm --filter @siop/api build - uses: astral-sh/setup-uv@v5 - name: Démarrer le service IA (R5 — embeddeur déterministe, seuils CI) working-directory: apps/ai env: AI_EMBEDDINGS: deterministe AI_SERVICE_TOKEN: dev-only-ai-token # L'embeddeur déterministe (tri-grammes hachés) produit des scores # bien plus bas que le vrai modèle : seuils calibrés sur mesures # réelles (vrai match ≈ 0,66 ; bruit de collisions ≈ 0,11 ; codes # de bilan pertinents ≈ 0,14-0,16 ; parasites ≈ 0,08). AI_SEUIL_PERTINENCE: '0.20' AI_SEUIL_SUGGESTION: '0.10' AI_SEUIL_CONFIANCE_FORTE: '0.30' run: | uv sync nohup uv run uvicorn siop_ai.app:app --host 127.0.0.1 --port 8000 > /tmp/siop-ai.log 2>&1 & for i in $(seq 1 30); do curl -fsS http://127.0.0.1:8000/healthz && break; sleep 1; done - run: pnpm --filter @siop/web exec playwright install --with-deps chromium - run: pnpm --filter @siop/web e2e - name: Journal du service IA en cas d'échec if: failure() run: cat /tmp/siop-ai.log || true - name: Traces Playwright en cas d'échec if: failure() uses: actions/upload-artifact@v4 with: name: playwright-report path: apps/web/playwright-report/ retention-days: 7 # Déploiement continu (runbook §4) : Dokploy reconstruit et redéploie le # compose UNIQUEMENT quand les 5 verrous sont verts sur main. Le webhook # vit dans le secret DOKPLOY_WEBHOOK_URL (Dokploy → service compose → # Deployments → Webhook URL) ; tant qu'il n'est pas configuré, le job # se termine en « skip » sans faire échouer le pipeline. deploy: name: deploy (Dokploy — siop2.apps.enset.top) if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: [lint, contract, api, web, mobile, ai, e2e] runs-on: ubuntu-latest environment: production steps: - name: Déclencher le déploiement Dokploy env: WEBHOOK_URL: ${{ secrets.DOKPLOY_WEBHOOK_URL }} run: | if [ -z "$WEBHOOK_URL" ]; then echo "::notice::DOKPLOY_WEBHOOK_URL absent — déploiement sauté (configurer le secret pour activer le CD)." exit 0 fi curl -fsS -X POST "$WEBHOOK_URL" echo "Déploiement déclenché — suivi : Dokploy → Deployments."