feat: fait reposer le deploiement sur des versions taguees
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Configuration du déploiement — copier en .env (non versionné) :
|
||||||
|
# cp .env.example .env
|
||||||
|
|
||||||
|
# Version de l'image déployée, telle que publiée par la CI à partir d'un tag
|
||||||
|
# git. Un tag v0.2.0 publie les images 0.2.0, 0.2 et 0 : épingler 0.2.0 fige
|
||||||
|
# le déploiement à l'octet près, 0.2 laisse entrer les correctifs de la série.
|
||||||
|
#
|
||||||
|
# Monter de version ou revenir en arrière = changer ce numéro puis `make docker`.
|
||||||
|
PLESNA_VERSION=0.1.0
|
||||||
@@ -13,8 +13,47 @@ env:
|
|||||||
NAMESPACE: ${{ secrets.REGISTRY_NAMESPACE }}
|
NAMESPACE: ${{ secrets.REGISTRY_NAMESPACE }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# Une image publiee est une image deployable : rien ne part au registre sans
|
||||||
|
# que la suite soit passee. Les tests golden se sautent d'eux-memes ici (le
|
||||||
|
# corpus PDF n'est pas versionne), le reste de la suite tourne.
|
||||||
|
test:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
|
||||||
|
- name: Install backend dependencies
|
||||||
|
run: uv sync
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: uv run ruff check .
|
||||||
|
|
||||||
|
- name: Backend tests
|
||||||
|
run: uv run pytest
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Frontend tests
|
||||||
|
working-directory: frontend
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm test
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build App Image
|
name: Build App Image
|
||||||
|
needs: [test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -78,14 +117,16 @@ jobs:
|
|||||||
name: Build Summary
|
name: Build Summary
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build]
|
needs: [build]
|
||||||
if: always()
|
# Pas de `always()` : un resume qui annonce un succes apres un build rate
|
||||||
|
# est pire que pas de resume du tout.
|
||||||
steps:
|
steps:
|
||||||
- name: Build summary
|
- name: Build summary
|
||||||
run: |
|
run: |
|
||||||
echo "## 🐳 Docker Image Built Successfully"
|
echo "## 🐳 Docker Image Built Successfully"
|
||||||
echo ""
|
echo ""
|
||||||
echo "- Registry: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/plesna-gerance"
|
echo "- Registry: ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/plesna-gerance"
|
||||||
echo "- Tags: latest, ${{ gitea.ref_name }}"
|
echo "- Ref: ${{ gitea.ref_name }}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "### 🚀 Deployment"
|
echo "### 🚀 Deployment"
|
||||||
echo "docker compose up -d"
|
echo "Epingler la version dans .env (PLESNA_VERSION), puis :"
|
||||||
|
echo "docker compose pull && docker compose up -d"
|
||||||
|
|||||||
14
Makefile
14
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: install dev_back dev_front dev docker docker-build
|
.PHONY: install dev_back dev_front dev test docker docker-build release
|
||||||
|
|
||||||
# Installe les dependances backend + frontend (a refaire par worktree)
|
# Installe les dependances backend + frontend (a refaire par worktree)
|
||||||
install:
|
install:
|
||||||
@@ -17,6 +17,12 @@ dev_front:
|
|||||||
dev:
|
dev:
|
||||||
$(MAKE) dev_back & $(MAKE) dev_front & wait
|
$(MAKE) dev_back & $(MAKE) dev_front & wait
|
||||||
|
|
||||||
|
# Ce que la CI verifie avant de publier une image (cf. .gitea/workflows)
|
||||||
|
test:
|
||||||
|
uv run ruff check .
|
||||||
|
uv run pytest
|
||||||
|
cd frontend && npm test
|
||||||
|
|
||||||
# Tire l'image publiee (registre Gitea) et lance via podman-compose
|
# Tire l'image publiee (registre Gitea) et lance via podman-compose
|
||||||
docker:
|
docker:
|
||||||
podman-compose pull
|
podman-compose pull
|
||||||
@@ -25,3 +31,9 @@ docker:
|
|||||||
# Construit l'image en local (test avant publication)
|
# Construit l'image en local (test avant publication)
|
||||||
docker-build:
|
docker-build:
|
||||||
podman build -f backend.Dockerfile -t plesna-gerance:local .
|
podman build -f backend.Dockerfile -t plesna-gerance:local .
|
||||||
|
|
||||||
|
# Pose une version : aligne les fichiers, commite, tague (sans pousser).
|
||||||
|
# Usage : make release VERSION=0.2.0
|
||||||
|
release:
|
||||||
|
@test -n "$(VERSION)" || { echo "Usage : make release VERSION=0.2.0"; exit 1; }
|
||||||
|
uv run python scripts/release.py $(VERSION)
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -35,6 +35,80 @@ make dev
|
|||||||
|
|
||||||
Ouvrir **http://localhost:5173** — le proxy Vite redirige `/api` vers le backend.
|
Ouvrir **http://localhost:5173** — le proxy Vite redirige `/api` vers le backend.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # ruff check + pytest + vitest, soit exactement ce que la CI verifie
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests de non-regression des parseurs comparent l'extraction de PDF reels a
|
||||||
|
des references figees. PDF (`data/`) et references (`tests/golden/`) contiennent
|
||||||
|
des donnees personnelles et ne sont pas versionnes : ces tests se **sautent**
|
||||||
|
d'eux-memes la ou le corpus est absent, CI comprise. Les lancer pour de vrai
|
||||||
|
suppose donc un corpus local (cf. `tests/test_parsers_golden.py`).
|
||||||
|
|
||||||
|
## Versions et cycle de vie
|
||||||
|
|
||||||
|
La production heberge des donnees reelles : elle suit des **versions**, pas la
|
||||||
|
pointe de `main`.
|
||||||
|
|
||||||
|
### Ce qu'est une version
|
||||||
|
|
||||||
|
Un tag git `vMAJEUR.MINEUR.CORRECTIF` (semver), pose sur `main`. Le meme numero
|
||||||
|
est ecrit dans quatre fichiers, que l'outillage tient d'accord entre eux :
|
||||||
|
`pyproject.toml`, `src/plesna_gerance/__init__.py`, `frontend/package.json` et
|
||||||
|
`packaging/installer.iss` (version affichee par l'installeur Windows).
|
||||||
|
|
||||||
|
Quand incrementer quoi :
|
||||||
|
|
||||||
|
| Segment | Quand | Exemple |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| CORRECTIF (`0.1.1`) | correction sans changement d'usage | un montant mal lu |
|
||||||
|
| MINEUR (`0.2.0`) | nouvelle fonctionnalite, donnees existantes intactes | une nouvelle page |
|
||||||
|
| MAJEUR (`1.0.0`) | rupture : migration de base ou changement d'usage a annoncer | refonte du referentiel |
|
||||||
|
|
||||||
|
### Publier une version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # ce que la CI verifiera de toute facon
|
||||||
|
make release VERSION=0.2.0 # aligne les 4 fichiers, commite, pose le tag
|
||||||
|
git push origin main v0.2.0 # <- c'est ce push qui publie
|
||||||
|
```
|
||||||
|
|
||||||
|
`make release` **ne pousse pas** : le tag reste local tant qu'on ne l'a pas
|
||||||
|
envoye, ce qui laisse le temps de relire le commit de version. Le script
|
||||||
|
(`scripts/release.py`) refuse d'avancer hors de `main`, sur un arbre sale, ou
|
||||||
|
si le tag existe deja — un tag publie ne se reecrit pas.
|
||||||
|
|
||||||
|
### Ce que declenche le push d'un tag
|
||||||
|
|
||||||
|
- **`.gitea/workflows/docker-publish.yml`** : lance d'abord les tests
|
||||||
|
(`ruff`, `pytest`, `vitest`), et seulement s'ils passent construit et publie
|
||||||
|
l'image sous **trois** tags — `0.2.0`, `0.2` et `0`. Epingler `0.2.0` fige le
|
||||||
|
deploiement a l'octet pres ; `0.2` laisse entrer les correctifs de la serie.
|
||||||
|
- **`.github/workflows/build-windows.yml`** : construit l'executable et
|
||||||
|
l'installeur Windows (necessite un runner `windows-latest`).
|
||||||
|
|
||||||
|
Un push sur `main` **sans tag** publie `latest` et `main` : utile pour essayer
|
||||||
|
la pointe, jamais pour la production.
|
||||||
|
|
||||||
|
### Deployer une version
|
||||||
|
|
||||||
|
La version deployee est epinglee dans `.env`, lu par `docker-compose.yml` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # une seule fois
|
||||||
|
# editer PLESNA_VERSION=0.2.0
|
||||||
|
make docker # pull + up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Revenir en arriere, c'est la meme manoeuvre : remettre le numero precedent dans
|
||||||
|
`.env` et relancer `make docker`. Les donnees vivent dans le volume nomme
|
||||||
|
`plesna-data`, independamment de l'image — un retour arriere d'image ne les
|
||||||
|
touche pas. En revanche une version qui a **migre le schema** de la base ne se
|
||||||
|
defait pas en changeant le numero : sauvegarder le volume avant une montee de
|
||||||
|
version majeure.
|
||||||
|
|
||||||
## Production (Docker)
|
## Production (Docker)
|
||||||
|
|
||||||
Un **conteneur unique** : l'image embarque le frontend Vue builde, servi par
|
Un **conteneur unique** : l'image embarque le frontend Vue builde, servi par
|
||||||
@@ -42,7 +116,7 @@ le backend FastAPI (API + interface web sur le meme port). Pas de nginx.
|
|||||||
|
|
||||||
L'image est **publiee par la CI Gitea** (`.gitea/workflows/docker-publish.yml`)
|
L'image est **publiee par la CI Gitea** (`.gitea/workflows/docker-publish.yml`)
|
||||||
sur `git.opytex.org/lafrite/plesna-gerance`. Le `docker-compose.yml` la **tire**
|
sur `git.opytex.org/lafrite/plesna-gerance`. Le `docker-compose.yml` la **tire**
|
||||||
directement (pas de build local) :
|
directement (pas de build local), a la version epinglee dans `.env` :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make docker # podman-compose pull && podman-compose up -d
|
make docker # podman-compose pull && podman-compose up -d
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
# Image publiée par la CI Gitea (pas de build local).
|
# Image publiée par la CI Gitea (pas de build local). La version déployée
|
||||||
image: git.opytex.org/lafrite/plesna-gerance:latest
|
# est épinglée dans .env (voir .env.example) : en production on suit un
|
||||||
|
# numéro de version, pas la pointe de main. Sans .env, on retombe sur
|
||||||
|
# `latest`, qui suit main et n'a donc pas de garantie de stabilité.
|
||||||
|
image: git.opytex.org/lafrite/plesna-gerance:${PLESNA_VERSION:-latest}
|
||||||
ports:
|
ports:
|
||||||
# hôte:conteneur — l'app (API + interface web) écoute sur 8000
|
# hôte:conteneur — l'app (API + interface web) écoute sur 8000
|
||||||
- "8080:8000"
|
- "8080:8000"
|
||||||
|
|||||||
155
scripts/release.py
Normal file
155
scripts/release.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""Pose une version : aligne les fichiers, commite, tague.
|
||||||
|
|
||||||
|
Le numéro de version est écrit à quatre endroits qui doivent rester d'accord
|
||||||
|
(paquet Python, module, frontend, installeur Windows). Les tenir à jour à la
|
||||||
|
main, c'est publier tôt ou tard un tag `v0.3.0` sur un code qui se déclare
|
||||||
|
`0.1.0`. Ce script fait la mise à jour d'un bloc et refuse d'avancer au moindre
|
||||||
|
doute plutôt que de produire une version à moitié cohérente.
|
||||||
|
|
||||||
|
uv run python scripts/release.py 0.2.0
|
||||||
|
|
||||||
|
Il s'arrête avant le `git push` : le tag reste local tant qu'il n'est pas
|
||||||
|
poussé, et c'est le push qui déclenche la CI (images Docker + build Windows).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
RACINE = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Un fichier, le motif qui y porte la version, et le remplacement. Le motif doit
|
||||||
|
# capturer la version dans le groupe 1 pour que l'on puisse relire l'ancienne
|
||||||
|
# valeur, et matcher **exactement une fois** (vérifié plus bas).
|
||||||
|
PORTEURS_DE_VERSION = [
|
||||||
|
("pyproject.toml", re.compile(r'^version = "([^"]+)"$', re.M), 'version = "{v}"'),
|
||||||
|
(
|
||||||
|
"src/plesna_gerance/__init__.py",
|
||||||
|
re.compile(r'^__version__ = "([^"]+)"$', re.M),
|
||||||
|
'__version__ = "{v}"',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"frontend/package.json",
|
||||||
|
re.compile(r'^ "version": "([^"]+)",$', re.M),
|
||||||
|
' "version": "{v}",',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"packaging/installer.iss",
|
||||||
|
re.compile(r'^#define AppVersion "([^"]+)"$', re.M),
|
||||||
|
'#define AppVersion "{v}"',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
SEMVER = re.compile(r"^\d+\.\d+\.\d+$")
|
||||||
|
|
||||||
|
|
||||||
|
class Refus(Exception):
|
||||||
|
"""Condition non remplie : on s'arrête sans rien modifier."""
|
||||||
|
|
||||||
|
|
||||||
|
def git(*args: str) -> str:
|
||||||
|
"""Lance une commande git dans le dépôt et renvoie sa sortie."""
|
||||||
|
resultat = subprocess.run(
|
||||||
|
["git", "-C", str(RACINE), *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if resultat.returncode != 0:
|
||||||
|
raise Refus(f"git {' '.join(args)} a échoué :\n{resultat.stderr.strip()}")
|
||||||
|
return resultat.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_le_depot(version: str, autoriser_hors_main: bool) -> None:
|
||||||
|
"""Refuse de poser une version depuis un état de dépôt douteux."""
|
||||||
|
branche = git("rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
if branche != "main" and not autoriser_hors_main:
|
||||||
|
raise Refus(
|
||||||
|
f"branche courante « {branche} » : une version se pose sur main.\n"
|
||||||
|
"Fusionner d'abord, ou forcer avec --autoriser-hors-main."
|
||||||
|
)
|
||||||
|
|
||||||
|
if git("status", "--porcelain"):
|
||||||
|
raise Refus(
|
||||||
|
"l'arbre de travail n'est pas propre : commiter ou remiser d'abord.\n"
|
||||||
|
"Une version doit correspondre à un état de code identifiable."
|
||||||
|
)
|
||||||
|
|
||||||
|
if git("tag", "--list", f"v{version}"):
|
||||||
|
raise Refus(
|
||||||
|
f"le tag v{version} existe déjà. Choisir un numéro supérieur "
|
||||||
|
"(un tag publié ne se réécrit pas)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def appliquer_la_version(version: str) -> list[str]:
|
||||||
|
"""Écrit la version dans les fichiers porteurs. Renvoie ceux qui ont changé."""
|
||||||
|
modifies = []
|
||||||
|
for chemin_relatif, motif, remplacement in PORTEURS_DE_VERSION:
|
||||||
|
chemin = RACINE / chemin_relatif
|
||||||
|
contenu = chemin.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
occurrences = motif.findall(contenu)
|
||||||
|
if len(occurrences) != 1:
|
||||||
|
raise Refus(
|
||||||
|
f"{chemin_relatif} : {len(occurrences)} ligne(s) de version trouvée(s), "
|
||||||
|
"une seule attendue. Le fichier a changé de forme : "
|
||||||
|
"corriger le motif dans scripts/release.py."
|
||||||
|
)
|
||||||
|
|
||||||
|
if occurrences[0] == version:
|
||||||
|
continue
|
||||||
|
|
||||||
|
chemin.write_text(
|
||||||
|
motif.sub(remplacement.format(v=version), contenu), encoding="utf-8"
|
||||||
|
)
|
||||||
|
modifies.append(chemin_relatif)
|
||||||
|
|
||||||
|
return modifies
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parseur = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parseur.add_argument("version", help="numéro de version, sans le v (ex. 0.2.0)")
|
||||||
|
parseur.add_argument(
|
||||||
|
"--autoriser-hors-main",
|
||||||
|
action="store_true",
|
||||||
|
help="pose la version depuis une autre branche (rattrapage)",
|
||||||
|
)
|
||||||
|
arguments = parseur.parse_args()
|
||||||
|
|
||||||
|
version = arguments.version.lstrip("v")
|
||||||
|
if not SEMVER.match(version):
|
||||||
|
print(
|
||||||
|
f"Version « {arguments.version} » invalide : attendu MAJEUR.MINEUR.CORRECTIF "
|
||||||
|
"(ex. 0.2.0).",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
verifier_le_depot(version, arguments.autoriser_hors_main)
|
||||||
|
modifies = appliquer_la_version(version)
|
||||||
|
|
||||||
|
if modifies:
|
||||||
|
git("add", *modifies)
|
||||||
|
git("commit", "-m", f"chore: passe en version {version}")
|
||||||
|
print(f"Version écrite dans : {', '.join(modifies)}")
|
||||||
|
else:
|
||||||
|
print(f"Les fichiers déclarent déjà {version} : aucun commit de version.")
|
||||||
|
|
||||||
|
git("tag", "-a", f"v{version}", "-m", f"Version {version}")
|
||||||
|
except Refus as refus:
|
||||||
|
print(f"Publication interrompue : {refus}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"Tag v{version} posé sur {git('rev-parse', '--short', 'HEAD')}.")
|
||||||
|
print("\nRien n'est publié tant que le tag n'est pas poussé :")
|
||||||
|
print(f" git push origin main v{version}")
|
||||||
|
print("\nLe push déclenche la CI : images Docker taguées puis build Windows.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user