Picarones / docs /operations /release-process.md
Claude
feat(sprint-S6)!: déploiement institutionnel — pin Tesseract, CSRF strict, logs JSON, observability
7d5b986 unverified

Procédure de release

Sprint A9 du plan de remédiation institutionnelle (docs/audits/remediation-plan-2026-05.md). Étendu au Sprint S6 (déploiement institutionnel BnF).

Pré-requis avant tout tag (Sprint S6)

Vérif Commande Cible
Tests verts pytest tests/ -q 0 failed
Lint propre ruff check picarones/ tests/ All checks passed
Type strict python -m mypy picarones/domain/ 0 erreur
Sécurité statique bandit -ll -r picarones/ 0 HIGH
CVEs deps pip-audit aucune CVE non-mitigée dans le runtime
CHANGELOG section ## [X.Y.Z] — YYYY-MM-DD présente
Compteurs doc python scripts/gen_readme_tables.py --check exit 0

Mode public vs institutionnel

Pour un déploiement BnF (mode institutionnel), s'assurer que les variables d'environnement de production sont prêtes avant de tagger. L'app refuse de démarrer sans (Sprint S6.9).

Variable Public (HF Space) Institutionnel
PICARONES_PUBLIC_MODE 1 non set
PICARONES_CSRF_REQUIRED non set 1
PICARONES_CSRF_SECRET non set OBLIGATOIRE
PICARONES_LOG_FORMAT non set json (recommandé pour ops)

Génération du CSRF secret

# 32 bytes hex
openssl rand -hex 32

# Persister dans le secret manager institutionnel :
#   - Vault : ``vault kv put secret/picarones csrf=<hex>``
#   - AWS Secrets Manager : ``aws secretsmanager create-secret``
#   - Kubernetes : ``kubectl create secret generic picarones-csrf``
#   - Docker Compose : ``.env`` non versionné

Ne JAMAIS committer ce secret dans un Dockerfile, un docker-compose.yml versionné, ou un dépôt git public.

Vue d'ensemble

Une release Picarones produit trois artefacts :

  1. Un wheel + sdist sur PyPI (pip install picarones==X.Y.Z).
  2. Une image Docker multi-arch sur ghcr.io (docker pull ghcr.io/maribakulj/picarones:X.Y.Z).
  3. Une GitHub Release avec le sdist/wheel attachés et les release notes extraites du CHANGELOG.md.

Le pipeline est entièrement automatisé : il suffit de pousser un tag v*.*.* pour déclencher l'enchaînement complet (workflow .github/workflows/release.yml).

Procédure release standard

Pré-requis (une fois)

  1. PyPI Trusted Publisher : sur https://pypi.org/manage/account/publishing/, ajouter ce repo + workflow release.yml + environnement pypi. Idem pour TestPyPI dans l'environnement testpypi.
  2. GitHub repo : créer les environnements pypi et testpypi dans Settings → Environments, et marquer pypi comme "required reviewers" si vous voulez une validation manuelle finale.
  3. GHCR : packages: write sur GITHUB_TOKEN est natif (rien à configurer).

Cycle de release

# 1. Vérifier que main est vert + à jour
git checkout main
git pull --ff-only

# 2. Mettre à jour le CHANGELOG.md (Keep a Changelog)
#    Ajouter une section ## [1.2.0] — YYYY-MM-DD avec les changes
git add CHANGELOG.md
git commit -m "docs(changelog): release 1.2.0"

# 3. Tag annoté + push
git tag -a v1.2.0 -m "Picarones 1.2.0"
git push origin main
git push origin v1.2.0

# 4. Surveiller le workflow Actions
gh run watch

Le workflow déroule automatiquement :

  1. build — sdist + wheel via setuptools_scm (version dérivée du tag), twine check, smoke test wheel install.
  2. publish-testpypi — upload TestPyPI via OIDC trust.
  3. testpypi-smoke — installation depuis TestPyPI dans un container vierge + picarones demo.
  4. publish-pypi — upload PyPI via OIDC trust (production).
  5. docker — build multi-arch (linux/amd64 + linux/arm64) avec QEMU, push ghcr.io, attestations SLSA + SBOM.
  6. github-release — création de la Release GitHub avec corps extrait depuis la section CHANGELOG correspondante.

Durée totale : ~15 min (multi-arch + 30s d'indexation TestPyPI).

Versionnement

Picarones suit Semantic Versioning 2.0.0 :

  • MAJOR.MINOR.PATCH — incompatibilité, ajout, fix.
  • Suffixes pré-release : -rc1, -beta1, -alpha1. Le workflow les détecte et coche prerelease=true sur la GitHub Release.

setuptools_scm dérive automatiquement la version du tag git :

Contexte Version produite
Tag v1.2.0 1.2.0
5 commits après v1.2.0 1.2.1.dev5+g<sha> (dev seulement)
v1.3.0-rc1 1.3.0rc1 (PEP 440)

Procédure d'urgence : hotfix sécurité

Pour un fix CVE qui doit sortir en < 72 h (politique GOVERNANCE.md) :

git checkout -b hotfix-cve-2026-XXXX main
# correctif minimal + test
git commit -m "fix(security): patch CVE-2026-XXXX"
# CHANGELOG bump
git commit -m "docs(changelog): release 1.2.1"
git tag -a v1.2.1 -m "Picarones 1.2.1 (security)"
git push origin hotfix-cve-2026-XXXX v1.2.1
# Le workflow release.yml gère le reste.
# Après merge : annonce sur SECURITY.md + courriel mainteneur.

Yanking d'une release publiée

PyPI permet de retirer (yank) une version compromise sans la supprimer. À utiliser si une release introduit une régression critique :

# Connexion à PyPI → Manage → version concernée → "Yank"
# Justification dans le commentaire (visible publiquement).

L'image ghcr.io reste — mais le tag :latest ne pointera plus vers la version yankée si on pousse une nouvelle release.

Validation post-release

Checklist 30 min après la fin du workflow :

  • pip install picarones==<version> fonctionne dans un venv frais.
  • docker run ghcr.io/maribakulj/picarones:<version> démarre sans erreur et expose /health.
  • La GitHub Release affiche bien les release notes attendues.
  • cffconvert --validate confirme que CITATION.cff cite la bonne version (Sprint A12+).

Annexe : rollback complet

Si la release est compromise et doit être retirée intégralement :

  1. PyPI : yank la version (cf. plus haut).
  2. ghcr.io : docker manifest rm ghcr.io/maribakulj/picarones:<version>.
  3. GitHub Release : passer en draft + ajouter un README explicatif.
  4. Tag git : git push --delete origin v<version> puis nouveau tag v<version>+1 corrigé (un tag git ne peut pas être réécrit sans casser tous les checkouts existants — préférer le bump).

Ne jamais force-push un tag déjà publié — les utilisateurs qui ont fait git fetch voient un conflit.