# Procédure de release > Sprint A9 du plan de remédiation institutionnelle > ([`docs/audits/remediation-plan-2026-05.md`](../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 ```bash # 32 bytes hex openssl rand -hex 32 # Persister dans le secret manager institutionnel : # - Vault : ``vault kv put secret/picarones csrf=`` # - 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`](../../.github/workflows/release.yml)). ## Procédure release standard ### Pré-requis (une fois) 1. **PyPI Trusted Publisher** : sur , 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 ```bash # 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` (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) : ```bash 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 : ```bash # 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==` fonctionne dans un venv frais. - [ ] `docker run ghcr.io/maribakulj/picarones:` 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:`. 3. GitHub Release : passer en draft + ajouter un README explicatif. 4. Tag git : `git push --delete origin v` puis nouveau tag `v+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.