Ajoute le mode d'authentification PRONOTE_AUTH_MODE=qr_token comme alternative au mode password pour les instances Pronote utilisant HubEduConnect/EduConnect où l'authentification par mot de passe échoue (CAPTCHA, MFA, flux SAML). Nouveaux éléments : - PronoteSettings : auth_mode, qr_code_file, qr_pin (SecretStr) - PronoteAuthState : persistance du token rotatif dans .pronote_auth_state.json (écriture atomique, permissions 0600, symlink-safe via O_EXCL|O_NOFOLLOW) - PronoteClient._connect_qr_token() : token_login avec creds persistés, qrcode_login pour l'enrôlement initial, export_credentials persisté après chaque login réussi - PronoteAuthRotationError : levée en cas d'échec de rotation du token, propagée sans wrapping à travers PronoteFetcher et fetch_step jusqu'à PipelineRunner.run() qui notifie via XMPP (si canal disponible et dry_run inactif) - _is_pronotepy_configured() mode-aware : qr_token ne requiert que PRONOTE_URL - _collect_auth_secrets() : redaction des secrets explicites (token, PIN, jeton QR) dans tous les logs du chemin d'authentification Documentation : - .env.example : PRONOTE_AUTH_MODE, PRONOTE_QR_CODE_FILE, PRONOTE_QR_PIN - AGENTS.md : contrat d'authentification QR code / token - Wiki GuidePronote : section enrôlement, exécutions suivantes, ré-enrôlement Tests (686 passés, couverture 94.87%) : - 5 tests config QR, 9 tests auth_state, 10 tests client QR, 3 tests propagation, 4 tests intégration rotation end-to-end, 4 tests fallback mode-aware - Tests de non-fuite : sentinelles distinctes pour token, PIN, jeton QR Co-authored-by: coder/litellm/coder <coder@agents.invalid>
398 lines
18 KiB
Markdown
398 lines
18 KiB
Markdown
# Guide pour les agents et contributeurs — `pronote-sync`
|
|
|
|
> **Langue de travail** : Français (ce fichier et toute la documentation projet).
|
|
|
|
---
|
|
|
|
## 1. Description du projet
|
|
`pronote-sync` est un pipeline Python qui synchronise les données de **Pronote** (système de gestion scolaire) vers **CalDAV** (agendas) et **XMPP** (notifications).
|
|
|
|
> **Spécification complète** : Voir [`GUIDE_DEV_PYTHON.md`](./GUIDE_DEV_PYTHON.md).
|
|
|
|
---
|
|
|
|
## 2. Stack technique
|
|
|
|
### Langage et dépendances
|
|
- **Python** : ≥ 3.13.5 (actuellement 3.14 dans `.venv/`)
|
|
- **Dépendances principales** :
|
|
`pydantic>=2.0`, `pydantic-settings`, `icalendar`, `caldav`, `slixmpp`, `pronotepy`,
|
|
`feedparser`, `beautifulsoup4`, `requests`, `httpx`, `openai`
|
|
|
|
### Outils de développement
|
|
- **Linter** : `ruff` (longueur de ligne : 100)
|
|
- **Typage** : `mypy` (mode `strict=true`)
|
|
- **Tests** : `pytest` (couverture ≥ 90%), `pytest-mock`, `responses`, `aioresponses`
|
|
- **Sécurité** : `bandit`
|
|
- **Pre-commit** : hooks configurés pour `ruff`, `mypy`, `bandit`
|
|
|
|
### Environnement
|
|
- **Virtualenv** : `.venv/` (à activer avant toute opération)
|
|
- **Fichier de configuration** : `.env` (à partir de `.env.example`)
|
|
|
|
---
|
|
|
|
## 3. Structure du projet
|
|
|
|
```
|
|
pronote_sync/
|
|
├── config/ # Configuration et paramètres
|
|
├── errors.py # Hiérarchie canonique des erreurs du pipeline
|
|
├── models/ # Modèles de données (Pydantic v2)
|
|
├── sources/ # Connecteurs (Pronote, iCal, etc.)
|
|
├── sync/ # Logique de synchronisation
|
|
├── synthesis/ # Génération de synthèses (IA)
|
|
├── channels/ # Canaux de sortie (CalDAV, XMPP)
|
|
├── pipeline/ # Orchestration du pipeline
|
|
├── utils/ # Utilitaires (redaction, logging, etc.)
|
|
├── cli/ # Interface en ligne de commande
|
|
└── __init__.py
|
|
|
|
tests/
|
|
├── fixtures/ # Données de test anonymisées
|
|
├── unit/ # Tests unitaires
|
|
└── integration/ # Tests d'intégration
|
|
```
|
|
|
|
> **Plan de développement** : Voir [`TODO.md`](./TODO.md) pour les 15 jalons (M1-M15).
|
|
|
|
---
|
|
|
|
## 4. Commandes essentielles
|
|
|
|
### Environnement
|
|
```bash
|
|
# Activer le virtualenv
|
|
source .venv/bin/activate
|
|
|
|
# Installer les dépendances de développement
|
|
pip install -e ".[dev]"
|
|
```
|
|
|
|
### Qualité de code
|
|
```bash
|
|
# Linter (ruff)
|
|
ruff check .
|
|
|
|
# Formattage (ruff)
|
|
ruff format .
|
|
|
|
# Vérification des types (mypy)
|
|
mypy .
|
|
|
|
# Analyse de sécurité (bandit)
|
|
bandit -r pronote_sync/
|
|
|
|
# Pre-commit (tous les hooks)
|
|
pre-commit run --all-files
|
|
```
|
|
|
|
### Tests
|
|
```bash
|
|
# Exécuter les tests
|
|
pytest
|
|
|
|
# Exécuter les tests avec couverture
|
|
pytest --cov
|
|
```
|
|
|
|
### CLI
|
|
```bash
|
|
# Exécuter en mode dry-run (simulation)
|
|
pronote-sync --dry-run
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Conventions de code
|
|
|
|
### Typage
|
|
- **Strict** : `mypy` en mode `strict=true`. **Tous** les paramètres, retours et variables locales doivent être typés.
|
|
- **Pydantic v2** : Tous les modèles de données héritent de `BaseModel`. Les modèles de contrat utilisent `frozen=True`.
|
|
|
|
### Architecture
|
|
- **Injection de dépendances** : Utiliser `typing.Protocol` et une **composition root** dans `pipeline/run.py`. **Interdiction** des singletons globaux.
|
|
- **Erreurs** : Conserver une seule hiérarchie dans `pronote_sync/errors.py` ; ne pas créer de
|
|
doublon dans `pipeline/steps/errors.py`.
|
|
|
|
### Style
|
|
- **Longueur de ligne** : 100 caractères maximum (configuré dans `ruff`).
|
|
- **Imports** : Triés automatiquement par `ruff` (intègre `isort`).
|
|
|
|
### Tests
|
|
- **Sans réseau** : Utiliser **uniquement** des mocks (`pytest-mock`, `responses`, `aioresponses`).
|
|
- **Fixtures** : Les données de test doivent être anonymisées et stockées dans `tests/fixtures/`.
|
|
|
|
### Comportement
|
|
- **Idempotence** : Deux exécutions identiques sans changement externe doivent produire le **même résultat**.
|
|
- **Mode dégradé** :
|
|
- Si l'IA échoue → retourner un message **sans synthèse**.
|
|
- En mode source `auto`, essayer iCal puis utiliser `pronotepy` uniquement si iCal lève une
|
|
exception.
|
|
- En mode source explicite (`ical` ou `pronotepy`), ne pas changer silencieusement de source.
|
|
- En mode `auto`, si iCal et `pronotepy` échouent → lever une erreur critique explicite.
|
|
- Une liste vide est un succès valide ; elle ne doit pas être assimilée à une panne.
|
|
|
|
### Contrat des sources Pronote
|
|
- `PRONOTE_URL` (connexion API) et `PRONOTE_ICAL_URL` (flux iCal sensible) sont deux paramètres
|
|
distincts ; ne pas déduire l'un de l'autre.
|
|
- Le compte actuellement visé est un compte parent : utiliser
|
|
`pronotepy.ParentClient(pronote_url, username, password, ent=ent_function)`.
|
|
- Résoudre le slug `PRONOTE_ENT` vers une fonction de `pronotepy.ent` au moyen d'une liste fermée.
|
|
- Exposer séparément les cours et les devoirs dans le client ; filtrer les devoirs `pronotepy` sur
|
|
la date cible.
|
|
- Les récupérations critiques agenda/devoirs propagent une erreur expurgée ; seuls les messages et
|
|
informations non critiques peuvent se dégrader en liste vide avec warning.
|
|
- Réutiliser un téléchargement/parsing iCal pour l'agenda et les devoirs pendant un même run, sans
|
|
cache global ni persistant.
|
|
|
|
### Contrat d'authentification QR code / token
|
|
|
|
- Le mode d'authentification est sélectionné par `PRONOTE_AUTH_MODE` :
|
|
- `password` (défaut) : authentification classique via URL, identifiant, mot de passe et ENT.
|
|
- `qr_token` : authentification par QR code puis token persistant (pour les instances Pronote
|
|
utilisant HubEduConnect/EduConnect où l'authentification par mot de passe échoue).
|
|
- En mode `qr_token`, le premier login utilise `pronotepy.qrcode_login(qr_code, pin, uuid)` avec
|
|
les paramètres `PRONOTE_QR_CODE_FILE` (chemin du JSON QR) et `PRONOTE_QR_PIN` (PIN SecretStr).
|
|
- Après chaque login réussi, les credentials exportées par `pronotepy.export_credentials()` sont
|
|
persistées dans `.pronote_auth_state.json` (permissions `0600`, format JSON versionné, écriture
|
|
atomique). Le token rotate à chaque session — le fichier doit être mis à jour après chaque run.
|
|
- Les logins suivants utilisent `pronotepy.token_login(**credentials)` avec le token persisté.
|
|
- En cas d'échec de `token_login` (token expiré/invalide), une `PronoteAuthRotationError` est levée.
|
|
Cette erreur se propage sans wrapping à travers `PronoteFetcher` et `fetch_step` jusqu'à
|
|
`PipelineRunner.run()`, qui :
|
|
- journalise l'erreur (expurgée) ;
|
|
- envoie une notification XMPP actionnable si le canal est disponible et `dry_run` est inactif ;
|
|
- retourne un résultat dégradé `(None, errors)`.
|
|
- `PronoteAuthRotationError` est re-levée telle quelle (`except PronoteAuthRotationError: raise`)
|
|
dans toutes les couches d'enveloppement du chemin critique (fetch_agenda, fetch_homework,
|
|
fetch_step). Ne pas l'attraper avec `except Exception` sans la re-léver d'abord.
|
|
- Le fichier `.pronote_auth_state.json` ne doit jamais être committé (couvert par `.gitignore`).
|
|
Son contenu (token vivant) ne doit jamais apparaître dans les logs, les messages d'erreur ou
|
|
les notifications XMPP.
|
|
|
|
### Contrat du provider `openai-compatible`
|
|
- Le provider `openai-compatible` réutilise `OpenAISynthesisProvider` avec un `base_url` personnalisé ; aucun nouveau provider n'est créé.
|
|
- `AI_BASE_URL` et `AI_MODEL` sont requis ; `AI_API_KEY` est requis (MVP).
|
|
- L'URL doit utiliser `https` sauf si `AI_ALLOW_INSECURE_HTTP=true`.
|
|
- Les credentials dans l'URL (`user:pass@host`) sont refusés.
|
|
- Les paramètres sensibles dans la *query string* sont refusés, y compris ceux sans valeur (`?token`).
|
|
- Les URL malformées ou sans hostname sont rejetées (`ValueError` catché).
|
|
- Aucune manipulation automatique de `/v1` n'est effectuée.
|
|
- Configuration incomplète ou invalide → `None` avec avertissement (mode dégradé) ; la factory ne lève jamais d'exception.
|
|
- La factory ne fait aucun appel réseau ; les avertissements utilisent `redact_url()`.
|
|
|
|
### Documentation (docstrings)
|
|
- **Obligatoire** : **Toute** fonction, méthode et classe publique doit avoir une docstring.
|
|
- **Format** : Utiliser le format **Sphinx/reST** (pas Google ou NumPy) pour une compatibilité native avec Sphinx.
|
|
- **Priorité** : Les blocs historiques de `GUIDE_DEV_PYTHON.md` utilisant `Args:`/`Returns:` sont
|
|
illustratifs ; le format Sphinx/reST défini ici prévaut pour le code de production.
|
|
- **Contenu** :
|
|
- Une ligne de résumé courte (une phrase).
|
|
- Une description étendue optionnelle.
|
|
- Les paramètres avec `:param nom:`.
|
|
- Le retour avec `:return:` et `:rtype:`.
|
|
- Les exceptions avec `:raises TypeException:`.
|
|
- **Modules** : Chaque module doit avoir une docstring au niveau module.
|
|
- **Objectif** : Générer une **documentation PDF via LaTeX** avec Sphinx.
|
|
|
|
Exemple :
|
|
```python
|
|
def fetch_ical(url: str) -> str:
|
|
"""Récupère le contenu d'un flux iCal Pronote.
|
|
|
|
:param url: URL du flux iCal (avec token ``icalsecurise``).
|
|
:return: Contenu brut du flux iCal.
|
|
:rtype: str
|
|
:raises requests.RequestException: Si la requête HTTP échoue.
|
|
"""
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Sécurité et secrets
|
|
|
|
### Règles absolues
|
|
- **Aucun secret en clair** : Ni dans le code, ni dans les logs, ni dans les erreurs, ni dans les fixtures.
|
|
- **Masquage** : Utiliser systématiquement `redact_url()`, `redact_secrets()`, et `redact_exception()` depuis `utils/redaction.py`.
|
|
- **Chaînage d'exceptions** : Ne jamais conserver comme `__cause__` ou `__context__` une exception
|
|
externe brute susceptible de contenir un secret. Journaliser la version expurgée puis utiliser
|
|
`raise ... from None`, ou chaîner une cause elle-même expurgée.
|
|
- **Tests de non-fuite** : Vérifier les messages, les logs, `__cause__`, `__context__` et le
|
|
traceback complet avec des sentinelles distinctes pour chaque secret.
|
|
|
|
### Bonnes pratiques
|
|
- **Types sécurisés** : Les mots de passe et clés API doivent utiliser `pydantic.SecretStr`.
|
|
- **Fichier `.env`** : **Jamais** committé (couvert par `.gitignore`). Utiliser `.env.example` comme template.
|
|
|
|
---
|
|
|
|
## 7. Plan de développement
|
|
|
|
Le projet suit un plan séquentiel en **15 jalons** (M1-M15) décrits dans [`TODO.md`](./TODO.md).
|
|
|
|
- Chaque jalon a des **critères d'acceptation** à valider avant de passer au suivant.
|
|
- Ordre logique :
|
|
**scaffolding → config → modèles → sources → sync → synthèse → canaux → pipeline → CLI → tests → déploiement → docs**
|
|
|
|
---
|
|
|
|
## 8. Référence
|
|
|
|
| Document | Rôle |
|
|
|----------|------|
|
|
| [`GUIDE_DEV_PYTHON.md`](./GUIDE_DEV_PYTHON.md) | Spécification complète (architecture, modèles, parsing, déploiement) |
|
|
| [`TODO.md`](./TODO.md) | Plan de développement détaillé (15 jalons) |
|
|
|
|
---
|
|
|
|
## 9. Agents OpenCode
|
|
|
|
Cette section s'applique uniquement lorsque le travail est exécuté avec le système d'agents d'OpenCode.
|
|
|
|
Les rôles d'agents disponibles pour ce projet sont les suivants :
|
|
|
|
- `@architect` : Arbitrages d'architecture et choix techniques structurants. **Ne produit pas de code.**
|
|
- `@coder` : Écrit et modifie du code, de la configuration et des scripts. **Ne valide pas** (ruff, mypy, pytest) — c'est le rôle de `@verifier`. **Ne diagnostique pas** — c'est le rôle de `@debugger`.
|
|
- `@debugger` : Reproduit un symptôme et établit sa cause profonde. **Ne modifie pas le code.**
|
|
- `@explorer` : Explore le dépôt en lecture seule. **Ne modifie rien, n'exécute pas de commandes.**
|
|
- `@orchestrator` : Compréhension globale, définition des jalons, coordination et garantie du résultat. **N'écrit pas de code.**
|
|
- `@planner` : Transforme une demande complexe en unités exécutables. **Ne dirige aucun technicien.**
|
|
- `@reviewer` : Revues indépendantes de correction, régression, contrats et maintenabilité. **Ne modifie pas le code.**
|
|
- `@security-auditor` : Audit indépendant d'une surface de sécurité. **Ne modifie pas le code.**
|
|
- `@tech-writer` : Rédige et maintient la documentation. **N'écrit pas de code applicatif.**
|
|
- `@test-engineer` : Conçoit, écrit et exécute des tests ciblés. **N'écrit pas de code de production.**
|
|
- `@ui-designer` : Conçoit et implémente les interfaces Web et terminal.
|
|
- `@verifier` : Vérifie indépendamment le comportement livré, les régressions et le respect des conventions (ruff, mypy, pytest, bandit, idempotence, mode dégradé). **Ne modifie pas le code.**
|
|
- `@web-explorer` : Recherche et extrait des sources Web vérifiables. **Ne modifie pas le dépôt.**
|
|
|
|
> **Note** : Ne pas utiliser `@coder` pour les tâches de documentation (`@tech-writer`) ni pour les tests (`@test-engineer`).
|
|
|
|
### Séparation des rôles
|
|
|
|
| Type de tâche | Agent responsable | Ne pas confier à |
|
|
|---|---|---|
|
|
| Écrire/modifier du code | `@coder` | `@verifier`, `@explorer` |
|
|
| Valider (ruff, mypy, pytest, bandit) | `@verifier` | `@coder` |
|
|
| Diagnostiquer un bug | `@debugger` | `@coder` |
|
|
| Écrire un test | `@test-engineer` | `@coder` |
|
|
| Rédiger de la documentation | `@tech-writer` | `@coder` |
|
|
| Explorer le dépôt (lecture) | `@explorer` | `@coder`, `@verifier` |
|
|
| Arbitrage technique structurant | `@architect` | `@coder`, `@planner` |
|
|
| Revue de code | `@reviewer` | `@coder`, `@verifier` |
|
|
| Audit de sécurité | `@security-auditor` | `@coder`, `@verifier` |
|
|
| Recherche web | `@web-explorer` | `@explorer` |
|
|
| Découpage de travail complexe | `@planner` | `@coder` |
|
|
|
|
---
|
|
|
|
## 10. Workflow de modification
|
|
|
|
1. Lire la demande, [`TODO.md`](./TODO.md), les sections pertinentes de
|
|
[`GUIDE_DEV_PYTHON.md`](./GUIDE_DEV_PYTHON.md), `git status` et les fichiers concernés.
|
|
2. Préserver les changements existants de l'utilisateur.
|
|
3. Pour une correction, reproduire d'abord le défaut avec un test automatisé lorsque c'est raisonnable.
|
|
4. Faire une modification étroite et cohérente, en respectant les conventions du projet
|
|
(idempotence, modes explicites stricts et repli iCal → `pronotepy` uniquement en mode `auto`).
|
|
5. Faire vérifier le comportement par `@verifier` (ruff, mypy, pytest, bandit) et les cas d'erreur, notamment :
|
|
- Succès de la synchronisation Pronote → CalDAV/XMPP.
|
|
- Repli d'iCal vers `pronotepy` en mode `auto`, sans repli dans les modes explicites.
|
|
- Distinction entre résultat vide et échec des sources.
|
|
- Gestion des erreurs explicites sans fuite dans les causes ou tracebacks.
|
|
6. Mettre à jour la documentation et les exemples dans le même changement si leur comportement public évolue.
|
|
7. Cocher dans [`TODO.md`](./TODO.md) uniquement les éléments entièrement réalisés et validés.
|
|
8. Terminer avec un *handoff* concis : fichiers modifiés, validations exécutées, limites et prochaine étape.
|
|
|
|
> **Pour les changements larges ou risqués** : Produire d'abord un audit ou un aperçu.
|
|
> **Règle de commit** : Ne pas committer sans autorisation explicite. Une autorisation de commit ne vaut pas autorisation de push.
|
|
|
|
---
|
|
|
|
## 11. Branches et commits
|
|
|
|
- Une évolution cohérente se fait sur une **branche dédiée**.
|
|
- Nommer les branches selon le format `<type>/<sujet-en-kebab-case>`, où `<type>` est l'un des suivants :
|
|
`feature`, `fix`, `docs`, `chore` ou `refactor`.
|
|
- Partir de l'état validé de la branche principale (`main` ou `dev`), sauf demande explicite.
|
|
- Garder **un commit atomique par objectif vérifiable**. Utiliser les préfixes conventionnels pour les messages de commit :
|
|
- `feat:` pour une nouvelle fonctionnalité.
|
|
- `fix:` pour une correction de bug.
|
|
- `docs:` pour une mise à jour de documentation.
|
|
- `chore:` pour une tâche de maintenance.
|
|
- `refactor:` pour une refactorisation de code.
|
|
- Quand un agent a contribué au changement, ajouter un *trailer* Git standard au commit :
|
|
```
|
|
Co-authored-by: <harness>/<modèle> <adresse@agents.invalid>
|
|
```
|
|
- Avant un commit autorisé, vérifier :
|
|
- `git status` (fichiers modifiés attendus).
|
|
- Le diff complet (`git diff`).
|
|
- L'absence de secret ou de configuration locale dans le diff.
|
|
- Les validations pertinentes (`ruff`, `mypy`, `pytest`, `bandit`).
|
|
- `git diff --check` (pas de problèmes d'espaces blancs).
|
|
- Après le commit, rapporter :
|
|
- Le hash du commit.
|
|
- Le contenu du commit.
|
|
- Les validations exécutées.
|
|
|
|
> **Règle absolue** : Ne jamais pousser (`git push`) sans demande distincte et explicite.
|
|
|
|
---
|
|
|
|
## 12. Définition de terminé
|
|
|
|
Un changement est considéré comme **terminé** lorsque :
|
|
|
|
- Le cas nominal et les échecs pertinents sont testés (ex. : synchronisation réussie, repli
|
|
iCal → `pronotepy` en mode `auto`, modes explicites stricts, erreurs explicites).
|
|
- Les outils de validation (`ruff`, `mypy`, `pytest`, `bandit`) passent sans erreur.
|
|
- Aucun secret ni configuration locale n'apparaît dans le diff ou les fichiers suivis.
|
|
- La documentation reste cohérente avec le code (ex. : mise à jour des exemples, des contrats ou des décisions d'architecture).
|
|
- Le *handoff* distingue clairement :
|
|
- Ce qui a été vérifié localement (ex. : tests unitaires, linter).
|
|
- Ce qui nécessite encore une vérification manuelle (ex. : tests d'intégration avec un serveur CalDAV réel).
|
|
|
|
---
|
|
|
|
## 13. Versionnage et releases
|
|
|
|
### Politique de versionnage
|
|
|
|
Le projet suit **Semantic Versioning** (semver.org v2.0.0). Phase actuelle : `0.x` (pré-`1.0.0`).
|
|
|
|
| Changement | Incrément |
|
|
|------------|----------|
|
|
| Défaut constaté au déploiement | Patch (`0.1.Z`) — correction rétrocompatible |
|
|
| Ajout ou cassure en phase `0.x` | Minor (`0.Y.0`) |
|
|
| Déploiement réel validé | `1.0.0` |
|
|
|
|
### Règle absolue de validation
|
|
|
|
**Aucune montée de version (tag + release) ne peut être effectuée
|
|
sans validation préalable en environnement réel.** Les tests automatisés et la revue de code
|
|
ne suffisent pas ; le correctif ou la fonctionnalité doit avoir été testé avec succès
|
|
sur le serveur de production (ou un environnement équivalent) avant de tagger.
|
|
|
|
### Procédure de release
|
|
|
|
1. **Valider en environnement réel** : le correctif ou la fonctionnalité est testé
|
|
sur le serveur de production.
|
|
2. **Mettre à jour `pyproject.toml`** : incrémenter le champ `version` à la nouvelle version.
|
|
3. **Mettre à jour `CHANGELOG.md`** : ajouter une entrée sous le format
|
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) avec la nouvelle version et la date.
|
|
4. **Committer** : un commit `chore: monter en version x.y.z` regroupe
|
|
les mises à jour de `pyproject.toml` et `CHANGELOG.md`.
|
|
5. **Tagger** : créer un tag annoté `vx.y.z` sur le commit de version.
|
|
6. **Pousser le tag** : `git push origin vx.y.z`.
|
|
7. **Créer la release** sur Gitea avec le changelog correspondant.
|
|
|
|
### Cohérence des versions
|
|
|
|
Les trois sources de version doivent toujours être synchronisées au moment d'un tag :
|
|
- Le tag Git (`vx.y.z`)
|
|
- `pyproject.toml` (`version = "x.y.z"`)
|
|
- `CHANGELOG.md` (`## [x.y.z] - YYYY-MM-DD`)
|
|
|
|
> **Rappel** : Ne jamais créer un tag sans avoir d'abord mis à jour
|
|
> `pyproject.toml` et `CHANGELOG.md`.
|