Claude
ci: remonter le budget pytest pour absorber Windows latest + Linux 3.11
8831e1d unverified
Raw
History Blame Contribute Delete
20.5 kB
# .github/workflows/ci.yml β€” Picarones CI/CD
#
# Pipeline GitHub Actions (mis Γ  jour Sprint A1 β€” Hardening CI) :
# - Tests sur Python 3.11 / 3.12 / 3.13 (3.13 informationnel, 6 mois)
# - Linux, macOS, Windows
# - Couverture exigΓ©e >= 85 % (--cov-fail-under, plancher 2 pts sous baseline 87 %)
# - Timeout pytest 5 min par test individuel (pytest-timeout, mode thread)
# - Type-check mypy (strict sur picarones/domain/, lax ailleurs β€” durci en A11)
# - Scanners sΓ©curitΓ© : bandit (statique) + pip-audit (CVE deps) + trivy (image)
# - Build de la distribution Python
# - VΓ©rification de l'exΓ©cutable demo
name: CI
on:
push:
branches: [main, develop, "feature/**", "claude/**"]
pull_request:
branches: [main, develop]
workflow_dispatch: # DΓ©clenchement manuel
permissions:
contents: read
# ──────────────────────────────────────────────────────────────────
# Job 1 : Tests unitaires et d'intΓ©gration
# ──────────────────────────────────────────────────────────────────
jobs:
tests:
name: Tests Python ${{ matrix.python-version }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
# ``CODECOV_TOKEN`` au niveau JOB plutΓ΄t que step : nΓ©cessaire
# pour que ``env.CODECOV_TOKEN`` soit visible dans le ``if:`` de
# l'Γ©tape Codecov (le ``env`` d'un step n'est PAS rΓ©solu avant
# l'Γ©valuation du ``if`` de ce mΓͺme step).
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# 3.13 ajoutΓ© Sprint A1 (item m-8). Reste informationnel
# (continue-on-error sur Linux uniquement) pendant 6 mois pour
# tracker la compat sans bloquer.
python-version: ["3.11", "3.12", "3.13"]
include:
- python-version: "3.13"
os: ubuntu-latest
experimental: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
# ── Tesseract ──────────────────────────────────────────────
- name: Install Tesseract (Ubuntu)
if: runner.os == 'Linux'
run: |
sudo apt-get update -qq
sudo apt-get install -y tesseract-ocr tesseract-ocr-fra tesseract-ocr-lat
- name: Install Tesseract (macOS)
if: runner.os == 'macOS'
run: |
brew install tesseract tesseract-lang
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
- name: Install Tesseract (Windows)
if: runner.os == 'Windows'
run: |
choco install tesseract -y
echo "C:\Program Files\Tesseract-OCR" >> $env:GITHUB_PATH
shell: pwsh
# ── DΓ©pendances Python ──────────────────────────────────────
# Mise Γ  jour pip/setuptools/wheel en dΓ©but de job (Sprint A1) pour
# Γ©viter les CVE qui dorment dans les images runner GitHub.
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -e ".[dev,web]"
# ── Tests ───────────────────────────────────────────────────
# Sprint A1 : --cov-fail-under=85 (baseline mesurΓ© 87 %, marge 2 pts).
# pytest-timeout est configurΓ© dans pyproject.toml [tool.pytest.ini_options].
#
# Garde-fous anti-hang :
#
# 1. ``timeout-minutes: 18`` au niveau step : cap dur GitHub si
# tout le reste Γ©choue. DimensionnΓ© pour Windows latest qui
# tournait Γ  ~10 min de wall-clock contre ~3 min sur Linux
# (filesystem 3-5x plus lent, pas de timeout intermΓ©diaire
# disponible cΓ΄tΓ© Git-Bash).
# 2. ``timeout`` GNU autour de pytest : SIGTERM Γ  12 minutes,
# SIGKILL 30s après si Python n'a pas obéi. Couvre
# spΓ©cifiquement le cas d'un hang de SHUTDOWN de
# l'interprΓ©teur Python 3.12+ (threads non-daemon, connexions
# sqlite non fermΓ©es, ResourceWarnings β€” observΓ© sur ubuntu
# 3.12 oΓΉ pytest finit en 3:21 et l'interprΓ©teur reste 12 min
# avant de rendre la main).
# 3. ``-X faulthandler`` : si le hang revient, on aura les stack
# traces de tous les threads dans le log avant le SIGKILL.
# 4. ``PYTHONFAULTHANDLER=1`` redondance ceinture-bretelles.
#
# Le code de retour 124 (SIGTERM par GNU timeout) ou 137 (SIGKILL)
# est traitΓ© comme un Γ©chec normal β€” on perd l'info pytest mais
# on prΓ©serve la latence de la CI.
#
# Γ‰volution du budget : `540s` -> `720s` GNU timeout +
# `12 min` -> `18 min` step cap (mai 2026) après que Windows
# latest a hit le cap 12 min et qu'Ubuntu 3.11 a hit les 9 min
# GNU sur la mΓͺme semaine. La cible reste Β« tests passent en
# 3-10 min selon plateforme Β» ; le cap +30s SIGKILL garde la
# CI faillible plutΓ΄t que de coller indΓ©finiment sur un hang.
- name: Run tests
# Sur Python 3.13, on continue malgrΓ© une erreur pour ne pas bloquer
# le merge pendant la fenΓͺtre informationnelle de 6 mois (m-8).
continue-on-error: ${{ matrix.python-version == '3.13' }}
timeout-minutes: 18
shell: bash
run: |
# ``timeout`` n'est pas standard sur macOS (BSD vs GNU) β€” on
# dΓ©tecte et on adapte. Sur Windows, le shell bash de
# Git-Bash n'a pas timeout : on retombe sur python direct.
if command -v timeout >/dev/null 2>&1; then
timeout --signal=SIGTERM --kill-after=30 720 \
python -X faulthandler -m pytest tests/ -q --tb=short --no-header \
--cov=picarones --cov-report=xml --cov-report=term-missing \
--cov-fail-under=85
elif command -v gtimeout >/dev/null 2>&1; then
# macOS Homebrew coreutils.
gtimeout --signal=SIGTERM --kill-after=30 720 \
python -X faulthandler -m pytest tests/ -q --tb=short --no-header \
--cov=picarones --cov-report=xml --cov-report=term-missing \
--cov-fail-under=85
else
python -X faulthandler -m pytest tests/ -q --tb=short --no-header \
--cov=picarones --cov-report=xml --cov-report=term-missing \
--cov-fail-under=85
fi
env:
PYTHONIOENCODING: utf-8
PYTHONUTF8: "1"
PYTHONFAULTHANDLER: "1"
# ── Couverture ──────────────────────────────────────────────
# Conditions :
# - ``always()`` : on remonte la couverture MÊME quand pytest a
# Γ©chouΓ© (utile pour suivre la dΓ©rive sur un build cassΓ©).
# - ``runner.os == 'Linux' && python-version == '3.11'`` : un seul
# upload par run pour ne pas saturer le rate limit Codecov.
# - ``env.CODECOV_TOKEN != ''`` : skip si le secret n'est pas
# dΓ©fini (fork PR, environnement de dev local).
#
# Garde-fous :
# - ``timeout-minutes: 5`` : codecov-action v4 a dΓ©jΓ  bloquΓ© la CI
# 50+ min en attendant un upload qui n'aboutissait pas.
# - ``fail_ci_if_error: false`` : un Γ©chec d'upload n'invalide
# pas un run de tests valide.
- name: Upload coverage to Codecov
if: always() && runner.os == 'Linux' && matrix.python-version == '3.11' && env.CODECOV_TOKEN != ''
timeout-minutes: 5
uses: codecov/codecov-action@v4
with:
token: ${{ env.CODECOV_TOKEN }}
files: coverage.xml
flags: unittests
name: picarones-coverage
fail_ci_if_error: false
# ──────────────────────────────────────────────────────────────────
# Job 2 : VΓ©rification du rapport demo
# ──────────────────────────────────────────────────────────────────
demo:
name: Demo end-to-end
runs-on: ubuntu-latest
needs: tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install Tesseract
run: |
sudo apt-get update -qq
sudo apt-get install -y tesseract-ocr tesseract-ocr-fra
- name: Install Picarones
run: pip install -e ".[web]"
- name: Run demo
run: |
picarones demo --docs 12 --output rapport_demo_ci.html \
--with-history --with-robustness
ls -lh rapport_demo_ci.html
# VΓ©rifier que le fichier est valide et contient les sections attendues
python -c "
content = open('rapport_demo_ci.html').read()
assert 'Picarones' in content, 'Picarones non trouvΓ© dans le rapport'
assert 'CER' in content, 'CER non trouvΓ© dans le rapport'
assert len(content) > 50000, f'Rapport trop petit : {len(content)} octets'
print(f'Rapport OK : {len(content):,} octets')
"
- name: Upload demo report as artifact
uses: actions/upload-artifact@v4
with:
name: rapport-demo
path: rapport_demo_ci.html
retention-days: 7
# ──────────────────────────────────────────────────────────────────
# Job 3 : Build de la distribution Python
# ──────────────────────────────────────────────────────────────────
build:
name: Build distribution
runs-on: ubuntu-latest
needs: tests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install build tools
run: pip install --upgrade build twine
- name: Build wheel and sdist
run: python -m build
- name: Check distribution
run: twine check dist/*
- name: Upload distribution as artifact
uses: actions/upload-artifact@v4
with:
name: dist-packages
path: dist/
retention-days: 30
# ──────────────────────────────────────────────────────────────────
# Job 4 : VΓ©rification de la qualitΓ© du code
# La config ruff est centralisΓ©e dans pyproject.toml ([tool.ruff]),
# donc CI, Makefile et invocations locales produisent exactement les
# mΓͺmes rΓ©sultats β€” pas de divergence possible entre flags.
# ──────────────────────────────────────────────────────────────────
lint:
name: Code quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install ruff
run: pip install ruff
- name: Run ruff
run: ruff check picarones/ tests/
# ──────────────────────────────────────────────────────────────────
# Job 4-ter : Validation Docker compose
#
# Sans ce check, une incohΓ©rence entre ``docker-compose.yml`` et
# ``docker-compose.prod.yml`` (variable non hΓ©ritΓ©e, override de
# port mal formΓ©, conflit de services) ne se voit qu'au
# dΓ©ploiement. ``docker compose config`` rΓ©sout les merges,
# substitue les variables d'env et Γ©choue si la composition est
# invalide β€” c'est exactement le linter manquant.
#
# Le secret CSRF est forcΓ© sur une valeur factice : ``prod.yml``
# exige ``${PICARONES_CSRF_SECRET:?...}`` qui ferait Γ©chouer
# ``config`` sans valeur. La valeur n'est jamais utilisΓ©e β€” on ne
# dΓ©marre aucun container, on valide uniquement le YAML.
# ──────────────────────────────────────────────────────────────────
compose-check:
name: Docker compose validation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate local compose
run: docker compose -f docker-compose.yml config > /dev/null
- name: Validate local + prod merge
env:
PICARONES_CSRF_SECRET: compose-check-not-a-real-secret
run: |
docker compose \
-f docker-compose.yml \
-f docker-compose.prod.yml \
config > /dev/null
# ──────────────────────────────────────────────────────────────────
# Job 4-bis : Sync compteurs README/CLAUDE.md β€” Phase 2.1 audit
# code-quality (2026-05). Le script gen_readme_tables.py reflète
# le code rΓ©el dans la prose des docs (tableaux Engines/CLI/API +
# compteur de tests). En CI : exit 1 si la doc dΓ©rive.
# ──────────────────────────────────────────────────────────────────
sync-counters:
name: Doc counters sync
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install Picarones (web extras pour ouvrir l'app FastAPI)
run: pip install -e ".[dev,web]"
- name: Vérifier que README.md / CLAUDE.md reflètent le code
run: python scripts/gen_readme_tables.py --check
# ──────────────────────────────────────────────────────────────────
# Job 5 : Type-checking β€” Sprint A1 (item M-4)
#
# mypy est configurΓ© dans pyproject.toml [tool.mypy] :
# - strict sur picarones.domain.* (couche 1 du rewrite, ex-picarones.core)
# - lax ailleurs (follow_imports=silent)
# Deux checks prΓ©-existants dΓ©sactivΓ©s (disallow_any_generics et
# warn_return_any), à ré-activer en Sprint A11 après fix des
# ~100 erreurs baseline.
# ──────────────────────────────────────────────────────────────────
typecheck:
name: Type checking (mypy)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -e ".[dev,web,stats]"
- name: Run mypy on picarones/domain (strict)
run: python -m mypy picarones/domain/
# ──────────────────────────────────────────────────────────────────
# Job 6 : SΓ©curitΓ© β€” Sprint A1 (item B-7)
#
# bandit : scan statique du code Python (HIGH/MEDIUM bloquants).
# pip-audit : CVEs des dΓ©pendances installΓ©es.
# trivy : scan du Dockerfile + image rΓ©sultante (HIGH/CRITICAL bloquants).
#
# Configuration bandit dans pyproject.toml [tool.bandit] avec exclusions
# documentΓ©es (B310, B608, B615, B701 β€” chacune avec sprint cible).
# ──────────────────────────────────────────────────────────────────
security:
name: Security scanners
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install scanners
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -e ".[dev,web]"
# bandit : -ll = LOW + above. La config pyproject.toml exclut les
# informationnels acceptΓ©s ; tout HIGH/MEDIUM nouveau bloque.
- name: Run bandit
run: python -m bandit -r picarones/ -ll -c pyproject.toml
# pip-audit : --skip-editable car picarones n'est pas sur PyPI.
# En CI, l'env est frais (pip Γ  jour grΓ’ce au step prΓ©cΓ©dent) donc
# les CVEs trouvΓ©es sont des vraies vulnΓ©rabilitΓ©s dΓ©clarΓ©es.
# NB : ``--strict`` + ``--skip-editable`` combinΓ©s transforment le
# skip en erreur (incompatibilitΓ© documentΓ©e du tool). On utilise
# uniquement ``--skip-editable`` ; pip-audit retourne dΓ©jΓ  exit 1
# sur CVE dΓ©tectΓ©e β€” le job Γ©chouera donc en cas de vraie vulnΓ©rabilitΓ©.
- name: Run pip-audit
run: python -m pip_audit --skip-editable
# trivy : scan du Dockerfile + image rΓ©sultante. Build local pour
# auditer ce qui sera rΓ©ellement dΓ©ployΓ© sur HuggingFace Space.
- name: Build Docker image for scan
run: docker build -t picarones:ci-scan .
- name: Run Trivy vulnerability scanner (image)
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: 'picarones:ci-scan'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'HIGH,CRITICAL'
vuln-type: 'os,library'
- name: Run Trivy on Dockerfile (config scan)
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: 'config'
scan-ref: 'Dockerfile'
exit-code: '1'
severity: 'HIGH,CRITICAL'
# ──────────────────────────────────────────────────────────────────
# Job 7 : CI/CD β€” DΓ©tection de rΓ©gression CER (optionnel)
# CommentΓ© par dΓ©faut β€” activer si vous avez un corpus de rΓ©fΓ©rence
# ──────────────────────────────────────────────────────────────────
# regression-check:
# name: Regression check
# runs-on: ubuntu-latest
# needs: tests
# if: github.event_name == 'pull_request'
#
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Install
# run: pip install -e .
#
# - name: Run benchmark on reference corpus
# run: |
# picarones run \
# --corpus ./tests/fixtures/reference_corpus/ \
# --engines tesseract \
# --output results_pr.json \
# --fail-if-cer-above 0.15 # fraction (0.15 = 15 %)