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`](../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=<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`](../../.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
```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<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) :
```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==<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.