fix(version): BUG-047 la version suit chaque livraison (source unique VERSION + bump SemVer automatique)
VERSION (racine) devient la source unique de verite MAJEUR.MINEUR.CORRECTIF, incrementee a chaque commit par le hook prepare-commit-msg (BREAKING -> majeur, feat -> mineur, sinon correctif) ; post-commit rattache les fichiers derives au commit et cree le tag vX.Y.Z, publie au push (push.followTags). Tous les derives sont resynchronises : package.json, desktop Tauri, README.md/README.fr.md, docs/ROADMAP.md, CHANGELOG.md. Backend, image Docker et desktop lisent le meme fichier (plus de numero code en dur). Outils : scripts/bump_version.py, scripts/install-hooks.sh. Tests : tests/test_version.py (46, garde-fou de coherence globale).
This commit is contained in:
@@ -126,10 +126,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Generate VERSION file
|
- name: Version livrée
|
||||||
run: |
|
# VERSION (racine du dépôt) est copié dans l'image par le Dockerfile :
|
||||||
VERSION=$(git describe --tags --dirty 2>/dev/null | sed 's/^v//' || echo "0.0.0-dev")
|
# plus aucun numéro généré ni codé en dur dans le pipeline.
|
||||||
echo "$VERSION" > backend/VERSION
|
run: echo "Version livree = $(cat VERSION)"
|
||||||
|
|
||||||
- name: Configure DNS (workaround flaky 127.0.0.11 resolver)
|
- name: Configure DNS (workaround flaky 127.0.0.11 resolver)
|
||||||
# GitHub Actions runners occasionally fail to resolve auth.docker.io via
|
# GitHub Actions runners occasionally fail to resolve auth.docker.io via
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ObsiGate — rattache l'incrément de version au commit qui vient d'être créé,
|
||||||
|
# puis crée le tag `vX.Y.Z` de la version livrée.
|
||||||
|
#
|
||||||
|
# Deux situations :
|
||||||
|
# 1. `prepare-commit-msg` a incrémenté ./VERSION et mis à jour les fichiers
|
||||||
|
# dérivés (package.json, desktop, READMEs, ROADMAP, CHANGELOG) : on les
|
||||||
|
# rattache ici au commit via `--amend` (le commit n'est pas encore poussé) ;
|
||||||
|
# 2. le message venait d'un éditeur : l'incrément a été différé, il est
|
||||||
|
# calculé ici à partir du message final (`COMMIT_EDITMSG`).
|
||||||
|
#
|
||||||
|
# Résultat : la version livrée, le contenu du commit et le tag correspondent
|
||||||
|
# toujours. Le tag est publié avec la branche à chaque push (`push.followTags`,
|
||||||
|
# posé par scripts/install-hooks.sh).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
[ "${SKIP_VERSION_BUMP:-}" = "" ] || exit 0
|
||||||
|
[ "${OBSIGATE_VERSION_AMENDING:-}" = "" ] || exit 0 # garde anti-récursion
|
||||||
|
|
||||||
|
root="$(git rev-parse --show-toplevel)"
|
||||||
|
version_file="$root/VERSION"
|
||||||
|
[ -f "$version_file" ] || exit 0
|
||||||
|
|
||||||
|
# Opérations en cours (rebase, merge, cherry-pick…) : ne jamais amender ici.
|
||||||
|
for state in rebase-merge rebase-apply CHERRY_PICK_HEAD REVERT_HEAD MERGE_HEAD BISECT_LOG; do
|
||||||
|
if [ -e "$(git rev-parse --git-path "$state")" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
list="$(git rev-parse --git-path obsigate-version-files)"
|
||||||
|
defer="$(git rev-parse --git-path obsigate-version-defer)"
|
||||||
|
|
||||||
|
# Cas 2 : l'incrément avait été différé (commit rédigé dans un éditeur).
|
||||||
|
if [ -f "$defer" ]; then
|
||||||
|
rm -f "$defer"
|
||||||
|
rm -f "$list"
|
||||||
|
tool="$root/scripts/bump_version.py"
|
||||||
|
py=""
|
||||||
|
for cand in python3 python; do
|
||||||
|
if command -v "$cand" >/dev/null 2>&1; then py="$cand"; break; fi
|
||||||
|
done
|
||||||
|
if [ -n "$py" ] && [ -f "$tool" ]; then
|
||||||
|
"$py" "$tool" --from-message-file "$(git rev-parse --git-path COMMIT_EDITMSG)" \
|
||||||
|
--files-out "$list" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rattachement des fichiers synchronisés au commit qui vient d'être créé.
|
||||||
|
if [ -s "$list" ]; then
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
git add -- $(tr -d '\r' < "$list")
|
||||||
|
OBSIGATE_VERSION_AMENDING=1 \
|
||||||
|
git commit --amend --no-edit --no-verify --quiet
|
||||||
|
rm -f "$list"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ver="$(tr -d ' \t\r\n' < "$version_file")"
|
||||||
|
[ -n "$ver" ] || exit 0
|
||||||
|
|
||||||
|
if git rev-parse -q --verify "refs/tags/v$ver" >/dev/null 2>&1; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag -a "v$ver" -m "ObsiGate v$ver"
|
||||||
|
echo "ObsiGate version : tag v$ver créé (poussé avec la branche)."
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ObsiGate — incrément automatique de la version livrée (SemVer) à chaque commit.
|
||||||
|
#
|
||||||
|
# Source unique de vérité : ./VERSION (MAJEUR.MINEUR.CORRECTIF)
|
||||||
|
#
|
||||||
|
# `!:` / `BREAKING CHANGE:` -> MAJEUR (x.0.0)
|
||||||
|
# `feat:` -> MINEUR (x.y.0)
|
||||||
|
# tout le reste (fix, perf…) -> CORRECTIF (x.y.z)
|
||||||
|
#
|
||||||
|
# Aucun incrément pour : merge, squash, revert, `chore(release)`, amend.
|
||||||
|
# Commit rédigé dans un éditeur (message pas encore saisi) : l'incrément est
|
||||||
|
# différé à `post-commit`, qui dispose du message final.
|
||||||
|
#
|
||||||
|
# `scripts/bump_version.py` incrémente VERSION et resynchronise les fichiers
|
||||||
|
# dérivés (package.json, desktop, READMEs, ROADMAP, CHANGELOG) ; le `git add` est
|
||||||
|
# fait ici, par le hook lui-même. La liste des fichiers est laissée à
|
||||||
|
# `.githooks/post-commit`, qui rattache l'incrément au commit (git fige l'arbre
|
||||||
|
# avant `prepare-commit-msg` : un `git add` à cet instant ne serait pris en
|
||||||
|
# compte qu'au commit suivant).
|
||||||
|
#
|
||||||
|
# Neutraliser ponctuellement : SKIP_VERSION_BUMP=1 git commit ...
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
[ "${SKIP_VERSION_BUMP:-}" = "" ] || exit 0
|
||||||
|
|
||||||
|
msg_file="${1:-}"
|
||||||
|
source_kind="${2:-message}"
|
||||||
|
|
||||||
|
case "$source_kind" in
|
||||||
|
merge|squash|commit) exit 0 ;; # merge / squash / amend (-c, -C, --amend)
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -n "$msg_file" ] && [ -f "$msg_file" ] || exit 0
|
||||||
|
|
||||||
|
root="$(git rev-parse --show-toplevel)"
|
||||||
|
tool="$root/scripts/bump_version.py"
|
||||||
|
[ -f "$tool" ] || exit 0
|
||||||
|
|
||||||
|
py=""
|
||||||
|
for cand in python3 python; do
|
||||||
|
if command -v "$cand" >/dev/null 2>&1; then py="$cand"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "$py" ]; then
|
||||||
|
echo "⚠ version : aucun interpréteur Python trouvé — version non incrémentée." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
list="$(git rev-parse --git-path obsigate-version-files)"
|
||||||
|
defer="$(git rev-parse --git-path obsigate-version-defer)"
|
||||||
|
|
||||||
|
# Message pas encore rédigé (ouverture d'un éditeur) : on diffère l'incrément.
|
||||||
|
if ! grep -qvE '^[[:space:]]*(#|$)' "$msg_file"; then
|
||||||
|
: > "$defer"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$defer" # marqueur d'un commit précédent avorté
|
||||||
|
|
||||||
|
out=""
|
||||||
|
if out="$("$py" "$tool" --from-message-file "$msg_file" --files-out "$list" 2>&1)"; then
|
||||||
|
printf '%s\n' "$out"
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
if [ "$status" = "3" ]; then
|
||||||
|
rm -f "$list"
|
||||||
|
exit 0 # 3 = version inchangée (revert, release…) : pas une erreur
|
||||||
|
fi
|
||||||
|
echo "✗ version : échec de l'incrément automatique de ./VERSION" >&2
|
||||||
|
printf '%s\n' "$out" >&2
|
||||||
|
echo " → corriger, ou passer outre avec : SKIP_VERSION_BUMP=1 git commit ..." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ajout des fichiers synchronisés (chemins explicites).
|
||||||
|
if [ -s "$list" ]; then
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
git add -- $(tr -d '\r' < "$list")
|
||||||
|
fi
|
||||||
+33
-5
@@ -5,13 +5,17 @@ Toutes les modifications notables d'ObsiGate sont documentées dans ce fichier.
|
|||||||
Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements non publiés sont dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version publiée est **2.2.1**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.3.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.3.0] — 2026-09-16
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|
||||||
- **#89 Drag & Drop complet de fichiers/dossiers & intégration Assistant IA** — solution
|
- **#89 Drag & Drop complet de fichiers/dossiers & intégration Assistant IA** — solution
|
||||||
@@ -87,9 +91,13 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
lecture (bandeau, métadonnées, tags et contenu). L'overlay reste monté mais neutralisé
|
lecture (bandeau, métadonnées, tags et contenu). L'overlay reste monté mais neutralisé
|
||||||
(transparent, sans capture des clics, `z-index` conservé) car le ruban d'édition mobile y est
|
(transparent, sans capture des clics, `z-index` conservé) car le ruban d'édition mobile y est
|
||||||
ancré. Retour à la lecture par ✓ (sauvegarde), ✕, Échap ou l'ouverture d'un autre fichier —
|
ancré. Retour à la lecture par ✓ (sauvegarde), ✕, Échap ou l'ouverture d'un autre fichier —
|
||||||
un garde-fou dans `renderFile()` libère proprement la session (destruction CodeMirror/Yjs,
|
trois garde-fous libèrent proprement la session (destruction CodeMirror/Yjs, retrait de
|
||||||
retrait de l'iframe Forge) ; l'overlay n'est conservé qu'en repli, quand la cible n'est pas le
|
l'iframe Forge) avant qu'un rendu concurrent n'efface son DOM : `renderFile()`, l'activation
|
||||||
document affiché (ex. fichier créé depuis la palette). Conséquence directe : l'éditeur ne
|
d'onglet (`TabManager.activate`, `PaneTabManager.activate`, qui vident la zone avant de
|
||||||
|
rendre) et l'événement SSE `index_updated` (un fichier modifié hors de l'app recharge le
|
||||||
|
**tampon de l'éditeur** au lieu de re-rendre la vue lecture). L'overlay n'est conservé qu'en
|
||||||
|
repli, quand la cible n'est pas le document affiché (ex. fichier créé depuis la palette).
|
||||||
|
Conséquence directe : l'éditeur ne
|
||||||
recouvre plus le panneau de l'assistant IA, qui peut donc mettre le document à jour **sous
|
recouvre plus le panneau de l'assistant IA, qui peut donc mettre le document à jour **sous
|
||||||
les yeux de l'utilisateur** — `app_context.editing` annonce au modèle le document en cours
|
les yeux de l'utilisateur** — `app_context.editing` annonce au modèle le document en cours
|
||||||
d'édition (et sa surface), et toute écriture d'outil (`edit_file`, `append_to_file`,
|
d'édition (et sa surface), et toute écriture d'outil (`edit_file`, `append_to_file`,
|
||||||
@@ -101,6 +109,26 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
Tests : `tests/frontend/editor-inline.test.mjs` (19), `tests/test_bookslm.py::TestGeneralPrompt` (3 nouveaux).
|
Tests : `tests/frontend/editor-inline.test.mjs` (19), `tests/test_bookslm.py::TestGeneralPrompt` (3 nouveaux).
|
||||||
|
|
||||||
### Corrigé
|
### Corrigé
|
||||||
|
|
||||||
|
- **BUG-047 — La version affichée ne suivait pas les livraisons** : la version
|
||||||
|
provenait du **dernier tag git**, or aucun tag n'était créé lors des livraisons
|
||||||
|
→ l'application restait bloquée sur `2.2.1` alors que 66 commits avaient été
|
||||||
|
livrés (et les numéros codés en dur divergeaient : `package.json` 1.0.0,
|
||||||
|
desktop Tauri 2.0.0, `Dockerfile` 2.2.1, README 1.7.0). Le fichier **`VERSION`
|
||||||
|
(racine du dépôt) devient la source unique de vérité** (`MAJEUR.MINEUR.CORRECTIF`),
|
||||||
|
incrémenté **automatiquement à chaque commit** par le hook versionné
|
||||||
|
`.githooks/prepare-commit-msg` (`!:`/`BREAKING CHANGE` → MAJEUR, `feat` → MINEUR, sinon
|
||||||
|
CORRECTIF) ; `.githooks/post-commit` crée le tag `vX.Y.Z` et `push.followTags`
|
||||||
|
le publie à chaque push. Tous les dérivés sont resynchronisés dans le même
|
||||||
|
commit : `package.json`, desktop Tauri (`tauri.conf.json`, `Cargo.toml`,
|
||||||
|
`Cargo.lock`), `docs/ROADMAP.md`, `README.md`/`README.fr.md` et rotation de
|
||||||
|
`CHANGELOG.md` (`[Unreleased]` → `[X.Y.Z] — date`). Le backend
|
||||||
|
(`backend/version.py`), l'image Docker (`COPY VERSION`) et le desktop Tauri
|
||||||
|
lisent ce même fichier : plus aucun numéro codé en dur dans le pipeline.
|
||||||
|
Outils : `scripts/bump_version.py` (+ `scripts/bump_version.sh`),
|
||||||
|
`scripts/install-hooks.sh`. Tests : `tests/test_version.py`, dont un garde-fou
|
||||||
|
de cohérence globale (VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔
|
||||||
|
README). Vérifié : `curl /api/health` sur l'instance de test → `2.3.0`.
|
||||||
- **BUG-045 — Double barre de défilement dans l'éditeur « Editer »** : sur les
|
- **BUG-045 — Double barre de défilement dans l'éditeur « Editer »** : sur les
|
||||||
documents longs, `#editor-body` (qui gardait `overflow:auto`) et le scroller
|
documents longs, `#editor-body` (qui gardait `overflow:auto`) et le scroller
|
||||||
CodeMirror débordaient simultanément — la barre d'outils IA empilée sous un
|
CodeMirror débordaient simultanément — la barre d'outils IA empilée sous un
|
||||||
|
|||||||
+4
-7
@@ -15,7 +15,6 @@ RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
LABEL maintainer="Bruno Beloeil" \
|
LABEL maintainer="Bruno Beloeil" \
|
||||||
version="1.4.0" \
|
|
||||||
description="ObsiGate — lightweight web interface for Obsidian vaults"
|
description="ObsiGate — lightweight web interface for Obsidian vaults"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -33,12 +32,10 @@ RUN apt-get update \
|
|||||||
COPY backend/ ./backend/
|
COPY backend/ ./backend/
|
||||||
COPY frontend/ ./frontend/
|
COPY frontend/ ./frontend/
|
||||||
|
|
||||||
# Bake version: build.sh/CI pre-generate backend/VERSION; backend/VERSION est
|
# Version livrée : `VERSION` (racine du dépôt) est la source unique de vérité —
|
||||||
# exclu du .dockerignore pour que le build utilise TOUJOURS l'ARG ci-dessous
|
# copié dans l'image, jamais un numéro codé en dur. `backend/version.py` le lit
|
||||||
# (jamais un fichier stale "1.8.0-...-dirty" du contexte build qui afficherait
|
# et /api/health l'expose (header + boîte À propos).
|
||||||
# une vieille version au lieu du tag courant).
|
COPY VERSION ./VERSION
|
||||||
ARG VERSION=2.2.1
|
|
||||||
RUN test -f backend/VERSION || echo "$VERSION" > backend/VERSION
|
|
||||||
|
|
||||||
# Create non-root user for security + data directory for auth persistence
|
# Create non-root user for security + data directory for auth persistence
|
||||||
# Using explicit UID/GID 1000 to match common host user and docker-compose settings
|
# Using explicit UID/GID 1000 to match common host user and docker-compose settings
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -916,8 +916,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.0.0-dev).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.3.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.0.0-dev | Dernière mise à jour : Juin 2026*
|
*Projet : ObsiGate | Version : 2.3.0 | Dernière mise à jour : Juin 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1085,8 +1085,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v1.7.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.3.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 1.7.0 | Last updated: May 2026*
|
*Project: ObsiGate | Version: 2.3.0 | Last updated: May 2026*
|
||||||
|
|||||||
+49
-19
@@ -1,23 +1,34 @@
|
|||||||
"""
|
"""
|
||||||
Version management for ObsiGate.
|
Version management for ObsiGate.
|
||||||
|
|
||||||
The canonical version is the numeric SemVer of the LATEST release tag
|
Source unique de vérité : le fichier `VERSION` à la racine du dépôt, au format
|
||||||
(MAJOR.MINOR.PATCH). get_version() always returns a clean "x.y.z" string so
|
`MAJEUR.MINEUR.CORRECTIF` (incrémenté à chaque livraison par
|
||||||
the UI never shows "-dev", "0.0.0-dev" or a "-N-gHASH" suffix — the same
|
`scripts/bump_version.py`, hook git `commit-msg`). get_version() retourne
|
||||||
number appears in the header badge, the About modal and the API health.
|
toujours une chaîne propre "x.y.z" — jamais "-dev", "0.0.0-dev" ou "-N-gHASH" :
|
||||||
|
le même numéro s'affiche dans le badge d'en-tête, la boîte À propos et /api/health.
|
||||||
|
|
||||||
Examples:
|
Les sources sont consultées dans cet ordre :
|
||||||
tag v2.0.0 -> "2.0.0"
|
1. variable d'environnement OBSIGATE_VERSION (surcharge explicite / tests) ;
|
||||||
HEAD 31 commits after -> "2.0.0" (release version, no dev clutter)
|
2. `VERSION` à la racine du dépôt (source unique, copiée dans l'image Docker) ;
|
||||||
no git / no VERSION -> "0.0.0"
|
3. `backend/VERSION` (ancien emplacement, encore produit par certains builds) ;
|
||||||
|
4. dernier tag git (`git describe --tags --abbrev=0`) ;
|
||||||
|
5. "0.0.0".
|
||||||
|
|
||||||
|
Exemples :
|
||||||
|
VERSION = 2.3.0 -> "2.3.0"
|
||||||
|
tag v2.3.0, 4 commits -> "2.3.0"
|
||||||
|
aucun VERSION / git -> "0.0.0"
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_ROOT = Path(__file__).resolve().parent.parent # ObsiGate repo root
|
_ROOT = Path(__file__).resolve().parent.parent # racine du dépôt ObsiGate
|
||||||
_VERSION_FILE = Path(__file__).resolve().parent / "VERSION"
|
VERSION_FILE = _ROOT / "VERSION" # source unique de vérité
|
||||||
|
LEGACY_VERSION_FILE = Path(__file__).resolve().parent / "VERSION" # backend/VERSION
|
||||||
|
_ENV_VAR = "OBSIGATE_VERSION"
|
||||||
|
|
||||||
|
|
||||||
def _run_git(args: list[str]) -> str:
|
def _run_git(args: list[str]) -> str:
|
||||||
@@ -57,6 +68,16 @@ def _clean_base(raw: str) -> str:
|
|||||||
return ".".join(nums)
|
return ".".join(nums)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_version_file(path: Path) -> str:
|
||||||
|
"""Clean base of a VERSION file, or '' when absent/unreadable/invalid."""
|
||||||
|
try:
|
||||||
|
if not path.exists():
|
||||||
|
return ""
|
||||||
|
return _clean_base(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def get_git_describe() -> str:
|
def get_git_describe() -> str:
|
||||||
"""Full `git describe` string (e.g. "2.0.0-31-gabc1234") or '' if no git."""
|
"""Full `git describe` string (e.g. "2.0.0-31-gabc1234") or '' if no git."""
|
||||||
return _run_git(["describe", "--tags", "--dirty=-dirty"]).lstrip("v")
|
return _run_git(["describe", "--tags", "--dirty=-dirty"]).lstrip("v")
|
||||||
@@ -68,23 +89,32 @@ def get_git_commit() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
"""Return the clean release version x.y.z (latest tag) — never a -suffix.
|
"""Return the clean release version x.y.z — never a -suffix.
|
||||||
|
|
||||||
Priority: latest git tag -> backend/VERSION file -> "0.0.0".
|
Priority: OBSIGATE_VERSION -> ./VERSION -> backend/VERSION -> latest git tag
|
||||||
|
-> "0.0.0".
|
||||||
"""
|
"""
|
||||||
# 1) Latest tag from git (works even with commits beyond the tag)
|
# 1) Explicit override (docker-compose, tests, builds hors dépôt)
|
||||||
tag = _run_git(["describe", "--tags", "--abbrev=0"])
|
override = _clean_base(os.environ.get(_ENV_VAR, ""))
|
||||||
base = _clean_base(tag)
|
if override:
|
||||||
|
return override
|
||||||
|
|
||||||
|
# 2) Source unique de vérité : VERSION à la racine du dépôt
|
||||||
|
base = _read_version_file(VERSION_FILE)
|
||||||
if base:
|
if base:
|
||||||
return base
|
return base
|
||||||
|
|
||||||
# 2) backend/VERSION file (baked at build time by build.sh / CI / Docker)
|
# 3) Ancien emplacement (backend/VERSION, baké par certains builds)
|
||||||
if _VERSION_FILE.exists():
|
base = _read_version_file(LEGACY_VERSION_FILE)
|
||||||
base = _clean_base(_VERSION_FILE.read_text(encoding="utf-8"))
|
|
||||||
if base:
|
if base:
|
||||||
return base
|
return base
|
||||||
|
|
||||||
# 3) Nothing available
|
# 4) Dernier tag git
|
||||||
|
base = _clean_base(_run_git(["describe", "--tags", "--abbrev=0"]))
|
||||||
|
if base:
|
||||||
|
return base
|
||||||
|
|
||||||
|
# 5) Rien d'exploitable
|
||||||
return "0.0.0"
|
return "0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -193,10 +193,10 @@ else
|
|||||||
info "Construction de l'image Docker (avec cache)..."
|
info "Construction de l'image Docker (avec cache)..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate VERSION file from git before Docker build
|
# Version livrée : le fichier VERSION (racine du dépôt) est la source unique de
|
||||||
VERSION=$(git describe --tags --dirty 2>/dev/null | sed 's/^v//' || echo "0.0.0-dev")
|
# vérité ; le Dockerfile le copie dans l'image. Aucun numéro codé en dur ici.
|
||||||
echo "$VERSION" > backend/VERSION
|
VERSION=$(cat VERSION 2>/dev/null | tr -d '[:space:]' | sed 's/^v//' || true)
|
||||||
# Export so docker compose substitutes ${VERSION} in the build arg
|
[ -n "$VERSION" ] || VERSION="0.0.0"
|
||||||
export VERSION
|
export VERSION
|
||||||
info "Version: $VERSION"
|
info "Version: $VERSION"
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.0.0"
|
version = "2.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.0.0"
|
version = "2.3.0"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
+14
-3
@@ -1,12 +1,22 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
// Get version from git describe (same as backend/version.py)
|
// Version livrée : `VERSION` (racine du dépôt) est la source unique de
|
||||||
let version = std::process::Command::new("git")
|
// vérité (même fichier que backend/version.py et l'image Docker).
|
||||||
|
// Repli : `git describe` quand le fichier n'est pas dans le contexte de build.
|
||||||
|
let version = std::fs::read_to_string(
|
||||||
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../VERSION"),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
std::process::Command::new("git")
|
||||||
.args(["describe", "--tags", "--dirty=-dirty"])
|
.args(["describe", "--tags", "--dirty=-dirty"])
|
||||||
.output()
|
.output()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
.map(|s| s.trim().trim_start_matches('v').to_string())
|
.map(|s| s.trim().trim_start_matches('v').to_string())
|
||||||
.unwrap_or_else(|| "0.0.0-dev".to_string());
|
.unwrap_or_else(|| "0.0.0".to_string())
|
||||||
|
});
|
||||||
|
|
||||||
// Extract short hash for display
|
// Extract short hash for display
|
||||||
let short_hash = std::process::Command::new("git")
|
let short_hash = std::process::Command::new("git")
|
||||||
@@ -23,6 +33,7 @@ fn main() {
|
|||||||
println!("cargo:rustc-env=GIT_VERSION={}", version);
|
println!("cargo:rustc-env=GIT_VERSION={}", version);
|
||||||
println!("cargo:rustc-env=GIT_HASH={}", short_hash);
|
println!("cargo:rustc-env=GIT_HASH={}", short_hash);
|
||||||
println!("cargo:rustc-env=SEMVER={}", semver_base);
|
println!("cargo:rustc-env=SEMVER={}", semver_base);
|
||||||
|
println!("cargo:rerun-if-changed=../VERSION");
|
||||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||||
println!("cargo:rerun-if-changed=.git/refs/heads/main");
|
println!("cargo:rerun-if-changed=.git/refs/heads/main");
|
||||||
println!("cargo:rerun-if-changed=.git/refs/tags");
|
println!("cargo:rerun-if-changed=.git/refs/tags");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.0.0",
|
"version": "2.3.0",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -11,12 +11,6 @@ services:
|
|||||||
obsigate:
|
obsigate:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
|
||||||
# VERSION est injecte par build.sh/CI via `git describe`.
|
|
||||||
# En `docker compose build` direct (sans build.sh), on retombe sur le
|
|
||||||
# DERNIER TAG (v2.2.1) et non 0.0.0-dev, pour ne jamais afficher "0.0.0"
|
|
||||||
# dans le header / la boîte À propos. Packagé par backend/VERSION.
|
|
||||||
VERSION: ${VERSION:-2.2.1}
|
|
||||||
image: obsigate:latest
|
image: obsigate:latest
|
||||||
container_name: obsigate
|
container_name: obsigate
|
||||||
user: "1000:1000"
|
user: "1000:1000"
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ la checklist du §5 n'est pas entièrement verte, **CI compris**.
|
|||||||
|
|
||||||
| Fichier | Rôle | Quand le mettre à jour |
|
| Fichier | Rôle | Quand le mettre à jour |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| [`VERSION`](../VERSION) (racine) | **Version livrée** — source unique de vérité (SemVer `MAJEUR.MINEUR.CORRECTIF`) | Automatiquement à chaque commit (hook `.githooks/prepare-commit-msg`) |
|
||||||
| [`docs/ROADMAP.md`](./ROADMAP.md) | Travail **à venir** (🔵 En cours + ⚪ Backlog) + index du complété | Au début (statut) et à la fin (index) |
|
| [`docs/ROADMAP.md`](./ROADMAP.md) | Travail **à venir** (🔵 En cours + ⚪ Backlog) + index du complété | Au début (statut) et à la fin (index) |
|
||||||
| [`CHANGELOG.md`](../CHANGELOG.md) | Historique officiel par version (Keep a Changelog) | À chaque livraison, dans `[Unreleased]` |
|
| [`CHANGELOG.md`](../CHANGELOG.md) | Historique officiel par version (Keep a Changelog) | À chaque livraison : écrire dans `[Unreleased]` (publié en `[X.Y.Z] — date` par le bump) |
|
||||||
| [`docs/features/<slug>.md`](./features/) | **Conception / spec détaillée** d'une grosse feature | Quand l'item est livré |
|
| [`docs/features/<slug>.md`](./features/) | **Conception / spec détaillée** d'une grosse feature | Quand l'item est livré |
|
||||||
| [`docs/archive/COMPLETED_v1-v2.md`](./archive/COMPLETED_v1-v2.md) | Détail des items courts livrés | Quand l'item est livré |
|
| [`docs/archive/COMPLETED_v1-v2.md`](./archive/COMPLETED_v1-v2.md) | Détail des items courts livrés | Quand l'item est livré |
|
||||||
| [`docs/ISSUES_TODOLIST.md`](./ISSUES_TODOLIST.md) | Registre des **bugs / TODO** | À chaque bug (statut + correctif/commit) |
|
| [`docs/ISSUES_TODOLIST.md`](./ISSUES_TODOLIST.md) | Registre des **bugs / TODO** | À chaque bug (statut + correctif/commit) |
|
||||||
@@ -52,8 +53,11 @@ la checklist du §5 n'est pas entièrement verte, **CI compris**.
|
|||||||
4. **Vérifier en local** — exécuter les commandes du §6.
|
4. **Vérifier en local** — exécuter les commandes du §6.
|
||||||
5. **Documenter** — CHANGELOG `[Unreleased]`, Roadmap / ISSUES, fiche feature ou archive, guide
|
5. **Documenter** — CHANGELOG `[Unreleased]`, Roadmap / ISSUES, fiche feature ou archive, guide
|
||||||
utilisateur + i18n FR/EN, README si besoin, OpenAPI si API.
|
utilisateur + i18n FR/EN, README si besoin, OpenAPI si API.
|
||||||
6. **Commit** — message conventionnel (`feat:`, `fix:`…) référençant `#NN` / `BUG-NNN`.
|
6. **Commit** — message conventionnel (`feat:`, `fix:`…) référençant `#NN` / `BUG-NNN` : le
|
||||||
7. **Push** puis **vérifier le CI vert** (jobs `lint`, `test`, `security`, `build`, `e2e`).
|
préfixe détermine l'incrément SemVer de `VERSION` (MAJEUR / MINEUR / CORRECTIF), appliqué
|
||||||
|
automatiquement par le hook `.githooks/prepare-commit-msg`.
|
||||||
|
7. **Push** puis **vérifier le CI vert** (jobs `lint`, `test`, `security`, `build`, `e2e`) ; le
|
||||||
|
tag `vX.Y.Z` de la version livrée est publié avec la branche (`push.followTags`).
|
||||||
8. **Clôturer** — statut `🟢 corrigé` / index `✅` posé par l'IA ; l'utilisateur valide (`✅ vérifié`).
|
8. **Clôturer** — statut `🟢 corrigé` / index `✅` posé par l'IA ; l'utilisateur valide (`✅ vérifié`).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -74,6 +78,7 @@ la checklist du §5 n'est pas entièrement verte, **CI compris**.
|
|||||||
- [ ] E2E Playwright si flow UI critique touché
|
- [ ] E2E Playwright si flow UI critique touché
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
- [ ] `VERSION` incrémenté et dérivés synchronisés (automatique via le hook ; `tests/test_version.py` vert)
|
||||||
- [ ] `CHANGELOG.md` → `[Unreleased]` (section Ajouté / Modifié / Corrigé)
|
- [ ] `CHANGELOG.md` → `[Unreleased]` (section Ajouté / Modifié / Corrigé)
|
||||||
- [ ] `docs/ROADMAP.md` → statut mis à jour + ligne dans l'index « Complété »
|
- [ ] `docs/ROADMAP.md` → statut mis à jour + ligne dans l'index « Complété »
|
||||||
- [ ] Fiche `docs/features/<slug>.md` **ou** `docs/archive/` si l'item est livré
|
- [ ] Fiche `docs/features/<slug>.md` **ou** `docs/archive/` si l'item est livré
|
||||||
@@ -113,9 +118,22 @@ npx playwright test
|
|||||||
|
|
||||||
## 7. Versionnement & release
|
## 7. Versionnement & release
|
||||||
|
|
||||||
- **SemVer** `MAJOR.MINOR.PATCH` ; tags Git `vX.Y.Z` ; source de vérité = `CHANGELOG.md`.
|
- **Source unique de vérité : le fichier [`VERSION`](../VERSION)** (racine du dépôt), au format
|
||||||
- Lors d'une publication : déplacer `[Unreleased]` vers `[X.Y.Z] — date`, tagger, builder et
|
`MAJEUR.MINEUR.CORRECTIF` (SemVer). Il est incrémenté **automatiquement à chaque commit** par
|
||||||
publier (procédure détaillée dans [DEVELOPMENT_AND_RELEASES.md](./DEVELOPMENT_AND_RELEASES.md)).
|
le hook versionné `.githooks/prepare-commit-msg` — `!:` / `BREAKING CHANGE` → **MAJEUR**, `feat` →
|
||||||
|
**MINEUR**, tout le reste → **CORRECTIF** — puis tagué `vX.Y.Z` par `.githooks/post-commit`
|
||||||
|
(tag publié au push grâce à `push.followTags`). Installation une fois par clone :
|
||||||
|
`scripts/install-hooks.sh`.
|
||||||
|
- Le même commit resynchronise les dérivés : `package.json`, desktop Tauri (`tauri.conf.json`,
|
||||||
|
`Cargo.toml`, `Cargo.lock`), `README.md`/`README.fr.md`, `docs/ROADMAP.md`, et publie la
|
||||||
|
section `[Unreleased]` du `CHANGELOG.md` en `[X.Y.Z] — date`.
|
||||||
|
- Le backend (`backend/version.py`), l'image Docker (`COPY VERSION`) et le desktop Tauri lisent
|
||||||
|
ce fichier : la version affichée par l'UI (`/api/health` → header, boîte À propos) suit donc
|
||||||
|
chaque livraison.
|
||||||
|
- **Garde-fou** : `tests/test_version.py::TestRepoVersionAlignment` échoue dès qu'un dérivé
|
||||||
|
diverge de `VERSION` (numéro codé en dur, CHANGELOG sans section, README non resynchronisé).
|
||||||
|
- Bump manuel si besoin : `scripts/bump_version.py --dry-run|--minor|--set X.Y.Z|--push`.
|
||||||
|
Commit sans incrément (cas exceptionnel) : `SKIP_VERSION_BUMP=1 git commit …`.
|
||||||
- **Ne jamais** réécrire une version déjà publiée dans le CHANGELOG.
|
- **Ne jamais** réécrire une version déjà publiée dans le CHANGELOG.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -164,40 +164,67 @@ Document produit (URLs construites vers les assets de la release) :
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Commit, Push & Tagging Git
|
## 3. Version, Commit & Push
|
||||||
|
|
||||||
Une fois les modifications testées et validées :
|
### Source unique de vérité : `VERSION`
|
||||||
|
|
||||||
|
Le fichier **`VERSION`** à la racine du dépôt contient la version livrée au format
|
||||||
|
`MAJEUR.MINEUR.CORRECTIF`. C'est la **seule** source : elle est incrémentée à chaque
|
||||||
|
commit et tout le reste en découle.
|
||||||
|
|
||||||
|
| Dérivé | Contenu |
|
||||||
|
|---|---|
|
||||||
|
| `backend/version.py` | lit `VERSION` (priorité : `OBSIGATE_VERSION` > `./VERSION` > `backend/VERSION` > tag git) et l'expose via `/api/health` |
|
||||||
|
| `Dockerfile` | `COPY VERSION ./VERSION` — plus aucun numéro codé en dur dans l'image |
|
||||||
|
| `build.sh` / CI | affichent la version lue dans `VERSION` |
|
||||||
|
| `desktop/build.rs` | injecte `VERSION` dans `GIT_VERSION` (repli `git describe`) |
|
||||||
|
| `package.json`, `desktop/tauri.conf.json`, `desktop/Cargo.{toml,lock}`, `README.md`, `README.fr.md`, `docs/ROADMAP.md`, `CHANGELOG.md` | resynchronisés automatiquement à chaque incrément |
|
||||||
|
|
||||||
|
### Incrément automatique (hooks git versionnés)
|
||||||
|
|
||||||
|
`scripts/install-hooks.sh` (une seule fois par clone) pose `core.hooksPath=.githooks`
|
||||||
|
et `push.followTags=true`. Ensuite, chaque commit incrémente la version selon son
|
||||||
|
message (Conventional Commits) :
|
||||||
|
|
||||||
|
| Message de commit | Incrément |
|
||||||
|
|---|---|
|
||||||
|
| `!:` ou `BREAKING CHANGE:` | MAJEUR — `x.0.0` |
|
||||||
|
| `feat:` / `feat(scope):` | MINEUR — `x.y.0` |
|
||||||
|
| `fix:`, `perf:`, `docs:`, … | CORRECTIF — `x.y.z` |
|
||||||
|
|
||||||
|
`.githooks/prepare-commit-msg` incrémente `VERSION`, resynchronise les fichiers dérivés
|
||||||
|
et publie la section `[Unreleased]` du CHANGELOG en `[X.Y.Z] — date`. `.githooks/post-commit`
|
||||||
|
rattache ces fichiers au commit qui vient d'être créé (git fige l'arbre avant
|
||||||
|
`prepare-commit-msg` : un `git add` à cet instant ne serait repris qu'au commit suivant —
|
||||||
|
d'où un `--amend` immédiat, le commit n'étant pas encore poussé) puis crée le tag `vX.Y.Z`,
|
||||||
|
publié automatiquement au push (`push.followTags`). Aucun incrément pour un merge, un
|
||||||
|
revert, un `chore(release)` ou un `--amend`.
|
||||||
|
|
||||||
|
Livraison type :
|
||||||
|
|
||||||
1. **Commit et Push du code source** :
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add <chemins explicites> # pas de `git add -A` (test_vault = brouillon utilisateur)
|
||||||
git commit -m "feat: préparation release v2.0.0"
|
git commit -m "fix(ai): BUG-047 …" # → VERSION incrémentée + tag vX.Y.Z créé
|
||||||
git push origin main
|
git push origin main # → branche + tag publiés
|
||||||
```
|
|
||||||
*Ceci déclenchera la vérification CI standard sur Gitea (lint, tests unitaires, build Docker).*
|
|
||||||
|
|
||||||
2. **Création du Tag de Version** :
|
|
||||||
```bash
|
|
||||||
git tag -a v2.0.0 -m "Release v2.0.0"
|
|
||||||
git push origin v2.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Bump automatisé (recommandé)
|
Contournement ponctuel (commit sans incrément) : `SKIP_VERSION_BUMP=1 git commit …`.
|
||||||
|
|
||||||
`scripts/bump_version.sh` calcule la prochaine version depuis les commits
|
### Bump manuel / outillage
|
||||||
(Conventional Commits), **synchronise la version desktop**
|
|
||||||
(`tauri.conf.json`, `Cargo.toml`, `Cargo.lock`), crée un commit de release puis
|
|
||||||
le tag :
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/bump_version.sh --dry-run # prévisualiser la prochaine version
|
scripts/bump_version.py --print-version # version courante
|
||||||
scripts/bump_version.sh # bump + commit + tag (local)
|
scripts/bump_version.py --dry-run # prochaine version, sans rien écrire
|
||||||
scripts/bump_version.sh --push # idem + push branche & tag
|
scripts/bump_version.py --minor # incrément forcé + resynchronisation
|
||||||
|
scripts/bump_version.py --set 3.0.0 # version imposée
|
||||||
|
scripts/bump_version.py --major --commit --tag --push
|
||||||
|
scripts/bump_version.sh … # même outil (wrapper historique)
|
||||||
```
|
```
|
||||||
|
|
||||||
> La version desktop doit être synchronisée avec le tag pour que l'updater Tauri
|
> La version desktop doit être synchronisée avec le tag pour que l'updater Tauri
|
||||||
> détecte les mises à jour (`latest.json` reprend la version de
|
> détecte les mises à jour (`latest.json` reprend la version de `tauri.conf.json`) :
|
||||||
> `tauri.conf.json`). `--no-files` conserve l'ancien comportement (tag seul).
|
> l'incrément automatique s'en charge.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
- **Projet** : ObsiGate — Porte d'entrée web pour vaults Obsidian
|
- **Projet** : ObsiGate — Porte d'entrée web pour vaults Obsidian
|
||||||
- **Stack** : Python 3.11+ (backend FastAPI) · JavaScript/Vanilla (frontend) · Tauri/Rust (desktop)
|
- **Stack** : Python 3.11+ (backend FastAPI) · JavaScript/Vanilla (frontend) · Tauri/Rust (desktop)
|
||||||
- **Dernière mise à jour** : 2026-09-15
|
- **Dernière mise à jour** : 2026-09-16
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -156,6 +156,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| *BUG-044* | [🟡 IMPORTANT] Capacités des modèles IA erronées : aucun modèle Mistral n'est détecté « Vision capable » (et des modèles texte sont annoncés « Embeddings ») | 🟢 corrigé | P1 | ⚙️ backend + 🔌 api | IA | `backend/model_capabilities.py`, `backend/provider_capabilities.py`, `backend/main.py`, `backend/ai_routes.py` | `curl -H "Authorization: Bearer $MISTRAL_API_KEY" https://api.mistral.ai/v1/models` puis `GET /api/ai/model-capabilities?provider=mistral&model=mistral-small-latest` | Nouveau calque **déclaratif** (`backend/provider_capabilities.py`) : les capacités publiées par le fournisseur (Mistral `capabilities`, OpenRouter `architecture`) sont lues et mises en cache par `GET /api/config/ai-models` ; elles **priment** sur la table statique, qui ne comble plus que les drapeaux non déclarés (et sert de repli hors ligne / sans clé). Table statique corrigée : familles vision Mistral (`ministral`, `magistral`, `mistral-small`, `mistral-medium`, `mistral-vibe-cli`, `labs-leanstral`), `mistral-ocr` = vision sans chat, défaut fournisseur Mistral sans `embeddings`. Tests : `tests/test_provider_capabilities.py`, `tests/test_model_capabilities.py::TestMistralFamilies`, `tests/test_ai_models.py::TestDeclaredCapabilities` | Le panneau de modèle par défaut et la bulle ⓘ n'affichaient **aucun** Mistral vision alors que l'API en déclare 28 ; seul le motif `pixtral` (retiré de l'API) matchait. Effet secondaire corrigé : `mistral-large-latest` / `codestral-latest` étaient annoncés « Embeddings » (défaut fournisseur). La porte vision de l'assistant (`bookslm_routes.py`) accepte désormais les images avec un modèle Mistral vision |
|
| *BUG-044* | [🟡 IMPORTANT] Capacités des modèles IA erronées : aucun modèle Mistral n'est détecté « Vision capable » (et des modèles texte sont annoncés « Embeddings ») | 🟢 corrigé | P1 | ⚙️ backend + 🔌 api | IA | `backend/model_capabilities.py`, `backend/provider_capabilities.py`, `backend/main.py`, `backend/ai_routes.py` | `curl -H "Authorization: Bearer $MISTRAL_API_KEY" https://api.mistral.ai/v1/models` puis `GET /api/ai/model-capabilities?provider=mistral&model=mistral-small-latest` | Nouveau calque **déclaratif** (`backend/provider_capabilities.py`) : les capacités publiées par le fournisseur (Mistral `capabilities`, OpenRouter `architecture`) sont lues et mises en cache par `GET /api/config/ai-models` ; elles **priment** sur la table statique, qui ne comble plus que les drapeaux non déclarés (et sert de repli hors ligne / sans clé). Table statique corrigée : familles vision Mistral (`ministral`, `magistral`, `mistral-small`, `mistral-medium`, `mistral-vibe-cli`, `labs-leanstral`), `mistral-ocr` = vision sans chat, défaut fournisseur Mistral sans `embeddings`. Tests : `tests/test_provider_capabilities.py`, `tests/test_model_capabilities.py::TestMistralFamilies`, `tests/test_ai_models.py::TestDeclaredCapabilities` | Le panneau de modèle par défaut et la bulle ⓘ n'affichaient **aucun** Mistral vision alors que l'API en déclare 28 ; seul le motif `pixtral` (retiré de l'API) matchait. Effet secondaire corrigé : `mistral-large-latest` / `codestral-latest` étaient annoncés « Embeddings » (défaut fournisseur). La porte vision de l'assistant (`bookslm_routes.py`) accepte désormais les images avec un modèle Mistral vision |
|
||||||
| *BUG-045* | [🟡 IMPORTANT] Éditeur « Editer » : deux barres de défilement superposées sur les documents longs | 🟢 corrigé | P1 | 📱 frontend | Éditeur | `frontend/style.css` | Ouvrir un fichier long (ex. IT/Docker Guide.md), cliquer Editer, mesurer `#editor-body` et `.cm-scroller` | `.editor-body-cm` gardait `overflow:auto` et un `.cm-editor{height:100%}` sous la rangée barre d'outils IA → le corps (toolbar+éditeur) ET le scroller CodeMirror débordaient simultanément. L'override global legacy `.cm-scroller{min-height:100%;overflow-y:auto!important}` aggravait. Passé en flex column : corps `overflow:hidden`, toolbar `flex:0 0 auto`, `.cm-editor` `flex:1 1 auto; height:auto`, seul le scroller défile ; override legacy retiré. Test : `tests/frontend/editor-inline.test.mjs` (+1) ; vérifié Playwright sur l'instance de test (un seul conteneur scrollable). |
|
| *BUG-045* | [🟡 IMPORTANT] Éditeur « Editer » : deux barres de défilement superposées sur les documents longs | 🟢 corrigé | P1 | 📱 frontend | Éditeur | `frontend/style.css` | Ouvrir un fichier long (ex. IT/Docker Guide.md), cliquer Editer, mesurer `#editor-body` et `.cm-scroller` | `.editor-body-cm` gardait `overflow:auto` et un `.cm-editor{height:100%}` sous la rangée barre d'outils IA → le corps (toolbar+éditeur) ET le scroller CodeMirror débordaient simultanément. L'override global legacy `.cm-scroller{min-height:100%;overflow-y:auto!important}` aggravait. Passé en flex column : corps `overflow:hidden`, toolbar `flex:0 0 auto`, `.cm-editor` `flex:1 1 auto; height:auto`, seul le scroller défile ; override legacy retiré. Test : `tests/frontend/editor-inline.test.mjs` (+1) ; vérifié Playwright sur l'instance de test (un seul conteneur scrollable). |
|
||||||
| *BUG-046* | [🔴 BLOQUANT] Assistant IA : « Échec de l'action : [object Object] » à l'application d'un ajout de texte au document courant | 🟢 corrigé | P0 | 📱 frontend + ⚙️ backend | IA | `frontend/js/bookslm.js`, `backend/bookslm.py`, `backend/bookslm_routes.py` | Mode agent : demander d'ajouter du texte au document ouvert puis cliquer « Appliquer » | Trois causes : (1) continuation de confirmation avec `payload:null` → second « Appliquer » sans `message` → 422 ; (2) `new Error(detail)` sur un `detail` tableau d'objets FastAPI → « [object Object] » ; (3) prompt documents/directory sans nom de vault → le modèle inventait `"vault":"test"` → échec silencieux de l'outil. Nouveau `_responseError()` (aplatit tableau/objet), payload porté à la continuation, bloc « Ces documents appartiennent au vault « X » » + consigne outils d'écriture (`build_system_prompt(vault_name=...)`). `SW_VERSION` v20. Tests : `tests/test_bookslm.py::test_vault_name_guidance`, `tests/frontend/ai.test.mjs` (+2). |
|
| *BUG-046* | [🔴 BLOQUANT] Assistant IA : « Échec de l'action : [object Object] » à l'application d'un ajout de texte au document courant | 🟢 corrigé | P0 | 📱 frontend + ⚙️ backend | IA | `frontend/js/bookslm.js`, `backend/bookslm.py`, `backend/bookslm_routes.py` | Mode agent : demander d'ajouter du texte au document ouvert puis cliquer « Appliquer » | Trois causes : (1) continuation de confirmation avec `payload:null` → second « Appliquer » sans `message` → 422 ; (2) `new Error(detail)` sur un `detail` tableau d'objets FastAPI → « [object Object] » ; (3) prompt documents/directory sans nom de vault → le modèle inventait `"vault":"test"` → échec silencieux de l'outil. Nouveau `_responseError()` (aplatit tableau/objet), payload porté à la continuation, bloc « Ces documents appartiennent au vault « X » » + consigne outils d'écriture (`build_system_prompt(vault_name=...)`). `SW_VERSION` v20. Tests : `tests/test_bookslm.py::test_vault_name_guidance`, `tests/frontend/ai.test.mjs` (+2). |
|
||||||
|
| *BUG-047* | [🔴 BLOQUANT] La version affichée par l'application ne suit pas les livraisons : 66 commits livrés depuis v2.2.1 et l'UI/API restent bloquées sur `2.2.1` (et les numéros codés en dur divergent : `package.json` 1.0.0, desktop Tauri 2.0.0, `Dockerfile` 2.2.1, README 1.7.0) | 🟢 corrigé | P0 | ⚙️ build + 📄 docs | IA | `VERSION` (nouveau), `scripts/bump_version.py` (nouveau), `.githooks/prepare-commit-msg` + `.githooks/post-commit` (nouveaux), `scripts/install-hooks.sh` (nouveau), `backend/version.py`, `Dockerfile`, `docker-compose.yml`, `build.sh`, `.gitea/workflows/ci.yml`, `desktop/build.rs`, `tests/test_version.py` (nouveau) | `git tag -l \| tail -1` puis `python scripts/bump_version.py --print-version` ; `curl -s http://localhost:2020/api/health \| jq .version` | Le numéro provenait du **dernier tag git** et aucun tag n'était créé aux livraisons (`bump_version.sh` jamais appelé) → version figée, plus quatre numéros codés en dur ailleurs. Corrigé : **`VERSION` (racine) = source unique de vérité**, incrémentée automatiquement à chaque commit par le hook versionné `prepare-commit-msg` (SemVer : `!:`/`BREAKING CHANGE` → MAJEUR, `feat` → MINEUR, sinon CORRECTIF), tag `vX.Y.Z` créé par `post-commit` et publié au push (`push.followTags`) ; `bump_version.py` resynchronise `package.json`, desktop Tauri, ROADMAP, READMEs et fait la rotation du CHANGELOG dans le même commit ; backend, image Docker (`COPY VERSION`) et desktop lisent ce fichier. Contournement ponctuel : `SKIP_VERSION_BUMP=1`. | Garde-fou : `tests/test_version.py::TestRepoVersionAlignment` échoue dès qu'un dérivé diverge de `VERSION`. Vérifié : pytest complet vert, ruff/mypy 0, `/api/health` → `2.3.0` sur l'instance de test. |
|
||||||
| | | | | | | | | | | |
|
| | | | | | | | | | | |
|
||||||
|
|
||||||
### TODOs techniques (améliorations / nouvelles tâches)
|
### TODOs techniques (améliorations / nouvelles tâches)
|
||||||
@@ -199,6 +200,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| 2026-09-14 | BUG-043, #82 | Correction | `frontend/js/ai.js`, `frontend/js/bookslm.js`, `frontend/js/config.js`, `tests/frontend/ai.test.mjs`, `docs/features/ai-provider-picker.md` | La liste des fournisseurs de la barre latérale de l'assistant suit désormais la configuration du projet : nouveau `refreshAIPickers()` (`ai.js`) qui reconstruit chaque picker monté dans son emplacement `.ai-picker-slot` (host conservé dans la barre de l'assistant, montage par `replaceChildren`), appelé après `saveAIKeys()` et `deleteAIKey()` (`config.js`) ; slot préservé même sans fournisseur configuré (le premier fournisseur ajouté s'y monte) ; sélection persistée d'un fournisseur non configuré purgée de `obsigate_ai_picker` (retour au défaut, modèle effacé). Vérifié : tests frontend 66/66 (IA) + 9 suites JSDOM vertes, validate-imports 36 modules, pytest 963 passed / 6 skipped, ruff OK, et vérification en navigateur (Playwright) sur l'instance de test : ajout de `nvidia` → fournisseur visible sans rechargement, retrait → disparition + sélection réinitialisée (`{"provider":null,"model":null}`), un seul montage du picker dans son host. CI Gitea verte (lint, test, security, build, e2e). |
|
| 2026-09-14 | BUG-043, #82 | Correction | `frontend/js/ai.js`, `frontend/js/bookslm.js`, `frontend/js/config.js`, `tests/frontend/ai.test.mjs`, `docs/features/ai-provider-picker.md` | La liste des fournisseurs de la barre latérale de l'assistant suit désormais la configuration du projet : nouveau `refreshAIPickers()` (`ai.js`) qui reconstruit chaque picker monté dans son emplacement `.ai-picker-slot` (host conservé dans la barre de l'assistant, montage par `replaceChildren`), appelé après `saveAIKeys()` et `deleteAIKey()` (`config.js`) ; slot préservé même sans fournisseur configuré (le premier fournisseur ajouté s'y monte) ; sélection persistée d'un fournisseur non configuré purgée de `obsigate_ai_picker` (retour au défaut, modèle effacé). Vérifié : tests frontend 66/66 (IA) + 9 suites JSDOM vertes, validate-imports 36 modules, pytest 963 passed / 6 skipped, ruff OK, et vérification en navigateur (Playwright) sur l'instance de test : ajout de `nvidia` → fournisseur visible sans rechargement, retrait → disparition + sélection réinitialisée (`{"provider":null,"model":null}`), un seul montage du picker dans son host. CI Gitea verte (lint, test, security, build, e2e). |
|
||||||
| 2026-09-15 | BUG-044 | Correction | `backend/provider_capabilities.py` (nouveau), `backend/model_capabilities.py`, `backend/main.py`, `backend/ai_routes.py`, `tests/test_provider_capabilities.py` (nouveau), `tests/test_model_capabilities.py`, `tests/test_ai_models.py`, `docs/features/ai-provider-picker.md`, `CHANGELOG.md` | BUG-044 : les capacités des modèles IA sont désormais lues chez le fournisseur quand il les publie (Mistral `capabilities`, OpenRouter `architecture`, détection par forme du payload), mises en cache par `GET /api/config/ai-models` (aucune requête supplémentaire) et prioritaires sur la table statique qui ne comble plus que les drapeaux non déclarés (repli hors ligne / sans clé). Table statique corrigée : familles vision Mistral (`ministral`, `magistral`, `mistral-small`, `mistral-medium`, `mistral-vibe-cli`, `labs-leanstral`), `mistral-ocr` = vision sans chat, défaut fournisseur Mistral sans `embeddings` (mistral-large / codestral n'étaient plus des « embedders »). Vérifié : diagnostic live avant/après sur l'API Mistral (28 modèles vision déclarés, **0** détectés avant → **28** après, 0 écart dans les deux sens), pytest 1007 passed / 6 skipped, ruff 0 (backend), mypy 0 (68 fichiers), tests frontend (unit 9/9, IA 66/66, validate-imports 37 modules), et vérification sur l'instance de test reconstruite (`obsigate-test`, http://localhost:2020) via les deux endpoints du panneau : `mistral-small/medium-latest`, `ministral-8b-latest`, `magistral-small-latest` → `chat,vision` ; `mistral-ocr-latest` → `vision` seul ; `mistral-large-latest`/`codestral-latest` → `chat` ; `mistral-embed` → `embeddings` seul ; `deepseek-chat` (fournisseur muet) inchangé. | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-09-15 | BUG-044 | Correction | `backend/provider_capabilities.py` (nouveau), `backend/model_capabilities.py`, `backend/main.py`, `backend/ai_routes.py`, `tests/test_provider_capabilities.py` (nouveau), `tests/test_model_capabilities.py`, `tests/test_ai_models.py`, `docs/features/ai-provider-picker.md`, `CHANGELOG.md` | BUG-044 : les capacités des modèles IA sont désormais lues chez le fournisseur quand il les publie (Mistral `capabilities`, OpenRouter `architecture`, détection par forme du payload), mises en cache par `GET /api/config/ai-models` (aucune requête supplémentaire) et prioritaires sur la table statique qui ne comble plus que les drapeaux non déclarés (repli hors ligne / sans clé). Table statique corrigée : familles vision Mistral (`ministral`, `magistral`, `mistral-small`, `mistral-medium`, `mistral-vibe-cli`, `labs-leanstral`), `mistral-ocr` = vision sans chat, défaut fournisseur Mistral sans `embeddings` (mistral-large / codestral n'étaient plus des « embedders »). Vérifié : diagnostic live avant/après sur l'API Mistral (28 modèles vision déclarés, **0** détectés avant → **28** après, 0 écart dans les deux sens), pytest 1007 passed / 6 skipped, ruff 0 (backend), mypy 0 (68 fichiers), tests frontend (unit 9/9, IA 66/66, validate-imports 37 modules), et vérification sur l'instance de test reconstruite (`obsigate-test`, http://localhost:2020) via les deux endpoints du panneau : `mistral-small/medium-latest`, `ministral-8b-latest`, `magistral-small-latest` → `chat,vision` ; `mistral-ocr-latest` → `vision` seul ; `mistral-large-latest`/`codestral-latest` → `chat` ; `mistral-embed` → `embeddings` seul ; `deepseek-chat` (fournisseur muet) inchangé. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
| 2026-09-16 | BUG-045, BUG-046 | Correction | `frontend/style.css`, `frontend/js/bookslm.js`, `backend/bookslm.py`, `backend/bookslm_routes.py`, `frontend/sw.js`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md` | BUG-045 : fin de la double barre de défilement de l'éditeur « Editer » — `#editor-body` en flex column `overflow:hidden`, `.cm-editor` `flex:1 1 auto`, seul le scroller CodeMirror défile ; suppression de l'override global `.cm-scroller` (min-height/overflow-y !important). BUG-046 : « Échec de l'action : [object Object] » à l'application d'un ajout de texte au document courant — (1) `_responseError()` aplatit le `detail` 422 (tableau d'objets FastAPI) en message lisible, (2) la continuation de confirmation hérite du payload d'origine (le second « Appliquer » ne POSTe plus sans `message`), (3) le prompt documents/dossier nomme le vault et enjoint les outils d'écriture d'utiliser ce nom exact (le modèle inventait `"vault":"test"` → échec silencieux). `SW_VERSION` v20. Vérifié : pytest 1038 passed / 6 skipped, ruff 0 (backend), tests frontend IA 84/84, editor-inline 25/25, validate-imports 38 modules ; reproduction et validation Playwright sur l'instance de test (un seul conteneur scrollable après correctif). | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-09-16 | BUG-045, BUG-046 | Correction | `frontend/style.css`, `frontend/js/bookslm.js`, `backend/bookslm.py`, `backend/bookslm_routes.py`, `frontend/sw.js`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md` | BUG-045 : fin de la double barre de défilement de l'éditeur « Editer » — `#editor-body` en flex column `overflow:hidden`, `.cm-editor` `flex:1 1 auto`, seul le scroller CodeMirror défile ; suppression de l'override global `.cm-scroller` (min-height/overflow-y !important). BUG-046 : « Échec de l'action : [object Object] » à l'application d'un ajout de texte au document courant — (1) `_responseError()` aplatit le `detail` 422 (tableau d'objets FastAPI) en message lisible, (2) la continuation de confirmation hérite du payload d'origine (le second « Appliquer » ne POSTe plus sans `message`), (3) le prompt documents/dossier nomme le vault et enjoint les outils d'écriture d'utiliser ce nom exact (le modèle inventait `"vault":"test"` → échec silencieux). `SW_VERSION` v20. Vérifié : pytest 1038 passed / 6 skipped, ruff 0 (backend), tests frontend IA 84/84, editor-inline 25/25, validate-imports 38 modules ; reproduction et validation Playwright sur l'instance de test (un seul conteneur scrollable après correctif). | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
| 2026-09-16 | BUG-047 | Correction + build | `VERSION` (nouveau), `scripts/bump_version.py` (nouveau), `.githooks/prepare-commit-msg` + `.githooks/post-commit` (nouveaux), `scripts/install-hooks.sh` (nouveau), `scripts/bump_version.sh`, `backend/version.py`, `Dockerfile`, `docker-compose.yml`, `build.sh`, `.gitea/workflows/ci.yml`, `desktop/build.rs`, `package.json`, `README.md`, `README.fr.md`, `docs/ROADMAP.md`, `docs/DELIVERY_WORKFLOW.md`, `docs/DEVELOPMENT_AND_RELEASES.md`, `tests/test_version.py` (nouveau) | **Version alignée de bout en bout.** Cause : la version affichée venait du dernier **tag git** et aucun tag n'était créé aux livraisons → 66 commits livrés mais UI/API figées sur `2.2.1` ; quatre autres numéros codés en dur dérivaient (`package.json` 1.0.0, desktop 2.0.0, `Dockerfile` 2.2.1, README 1.7.0). Nouveau modèle : **`VERSION` (racine) = source unique de vérité** (`MAJEUR.MINEUR.CORRECTIF`), incrémentée **automatiquement au commit** par le hook versionné `prepare-commit-msg` (SemVer depuis le message : `!:`/`BREAKING CHANGE` → MAJEUR, `feat` → MINEUR, sinon CORRECTIF ; aucun bump pour merge/revert/`chore(release)`/amend) qui resynchronise `package.json`, desktop Tauri, ROADMAP, READMEs et publie la section `[Unreleased]` du CHANGELOG en `[X.Y.Z] — date` ; `post-commit` rattache ces fichiers au commit qui vient d'être créé (amend immédiat, le commit n'étant pas encore poussé) puis crée le tag `vX.Y.Z`, publié au push (`push.followTags`). `bump_version.py` (avec `--dry-run`, `--set`, `--major|--minor|--patch`, `--print-version`) reste utilisable à la main ; `scripts/install-hooks.sh` installe les hooks. Côté build : Docker `COPY VERSION` (plus d'`ARG VERSION` ni d'env compose codés en dur), `build.sh` et CI lisent `VERSION`, `desktop/build.rs` aussi. Vérifié : `tests/test_version.py` (46 tests, dont le garde-fou `TestRepoVersionAlignment` : VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔ READMEs ↔ pipeline sans numéro codé en dur), pytest complet, ruff/mypy 0, `/api/health` → `2.3.0` sur l'instance de test reconstruite. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+61
-2
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.3.0-dev | **Dernière mise à jour :** 2026-09-15
|
> **Version :** 2.3.0 | **Dernière mise à jour :** 2026-09-16
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -94,6 +94,63 @@
|
|||||||
- [ ] Transverse : `tenacity` (backoff), cache SQLite des résultats web avec TTL, secrets via Infisical
|
- [ ] Transverse : `tenacity` (backoff), cache SQLite des résultats web avec TTL, secrets via Infisical
|
||||||
- [ ] Chaque outil : libellé `labels.py` + clés i18n `ai.step.*` FR/EN + tests (httpx mocké)
|
- [ ] Chaque outil : libellé `labels.py` + clés i18n `ai.step.*` FR/EN + tests (httpx mocké)
|
||||||
|
|
||||||
|
### 94. Assistant IA — Bouton de soumission arrondi et icône renouvelée
|
||||||
|
|
||||||
|
- **Effort :** 0,5 jour | **Impact :** 🟢 | **Zone :** frontend (`style.css`, `bookslm.js`)
|
||||||
|
- **Description :** remplacer le bouton de soumission du panneau Assistant IA par un bouton rond
|
||||||
|
et changer l'icône avion (`✈`) par une icône plus moderne (par ex. flèche vers le haut ou
|
||||||
|
icône séduisante).
|
||||||
|
- **Sous-tâches :**
|
||||||
|
- [ ] Remplacer l'icône avion par une icône alternatif (flèche, paper plane stylisée, etc.)
|
||||||
|
- [ ] Rendre le bouton de soumission circulaire (taille fixe, border-radius 50%)
|
||||||
|
- [ ] Ajuster le positionnement (aligné en bas à droite de la zone de saisie)
|
||||||
|
- [ ] i18n FR/EN + tests frontend
|
||||||
|
|
||||||
|
### 95. Assistant IA — Historique permanent des conversations (persistance côté backend)
|
||||||
|
|
||||||
|
- **Effort :** 2-3 jours | **Impact :** 🟡 | **Zone :** backend + frontend
|
||||||
|
- **Description :** rendre la gestion de l'historique des conversations avec l'assistant IA
|
||||||
|
permanente en persistant les échanges côté backend (et non uniquement en session/navigateur).
|
||||||
|
L'historique doit survivre aux rechargements de page et être consultable à tout moment.
|
||||||
|
- **Sous-tâches :**
|
||||||
|
- [ ] Persister les conversations dans un stockage backend (SQLite ou JSON chiffré)
|
||||||
|
- [ ] API CRUD pour les conversations (liste, récupération, suppression)
|
||||||
|
- [ ] Synchroniser l'historique côté frontend au chargement du panneau IA
|
||||||
|
- [ ] Nettoyage automatique de l'historique (rétention configurable, purge des anciennes)
|
||||||
|
- [ ] Tests unitaires backend + tests frontend
|
||||||
|
|
||||||
|
### 96. Assistant IA — Accès rapide à l'historique depuis le panneau de navigation
|
||||||
|
|
||||||
|
- **Effort :** 1 jour | **Impact :** 🟡 | **Zone :** frontend (`nav.js`, `bookslm.js`)
|
||||||
|
- **Description :** ajouter un icône dédié dans le panneau de navigation gauche (à côté de
|
||||||
|
Vaultes, Tags, Récent, Sauvegarde) permettant d'ouvrir directement la liste des
|
||||||
|
conversations de l'historique de l'assistant IA.
|
||||||
|
- **Sous-tâches :**
|
||||||
|
- [ ] Ajouter l'icône « Historique IA » dans la sidebar de navigation
|
||||||
|
- [ ] Au clic : afficher un panneau latéral avec la liste chronologique des conversations
|
||||||
|
- [ ] Sélection d'une conversation → ouverture dans le panneau Assistant IA
|
||||||
|
- [ ] Indicateur visuel (badge) si de nouvelles conversations sont présentes
|
||||||
|
- [ ] i18n FR/EN + tests frontend
|
||||||
|
|
||||||
|
### 97. Assistant IA — Panneau « + » extensible (fichiers, Deep Research, contextes, skills, etc.)
|
||||||
|
|
||||||
|
- **Effort :** 2-3 jours | **Impact :** 🟡 | **Zone :** frontend (`bookslm.js`, `style.css`)
|
||||||
|
- **Description :** remplacer le bouton « Attach an image » par un bouton « + » qui ouvre
|
||||||
|
un panneau d'extensions contenant diverses actions : téléverser des fichiers, Deep Research,
|
||||||
|
ajouter des contextes, ajouter des skills, recherche sur Internet, intégration Canva, et
|
||||||
|
d'autres options à venir. Ce panneau est extensible et modulaire.
|
||||||
|
- **Sous-tâches :**
|
||||||
|
- [ ] Remplacer le bouton « Attach an image » par un bouton « + » (icône universelle)
|
||||||
|
- [ ] Panneau overlay / dropdown listant les options disponibles
|
||||||
|
- [ ] Téléverser des fichiers (drag & drop + sélecteur)
|
||||||
|
- [ ] Deep Research (lancement d'une recherche approfondie via les outils IA)
|
||||||
|
- [ ] Ajouter contextes (fichiers, répertoires, URL)
|
||||||
|
- [ ] Ajouter skills (catalogue de skills disponibles)
|
||||||
|
- [ ] Recherche sur Internet (accès direct à `web_search`)
|
||||||
|
- [ ] Intégration Canva (ou outil externe similaire)
|
||||||
|
- [ ] Architecture modulaire : chaque option est un plugin/extensible facilement
|
||||||
|
- [ ] i18n FR/EN + tests frontend
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚪ Backlog — Sécurité, architecture & performance (P0/P1)
|
## ⚪ Backlog — Sécurité, architecture & performance (P0/P1)
|
||||||
@@ -151,6 +208,7 @@
|
|||||||
|
|
||||||
| # | Domaine / fonctionnalité | Version | Détails |
|
| # | Domaine / fonctionnalité | Version | Détails |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| BUG-047 | Versionnage — source unique `VERSION` + bump SemVer automatique au commit (hooks + tag) | 2.3.0 | [DEVELOPMENT_AND_RELEASES.md](./DEVELOPMENT_AND_RELEASES.md) |
|
||||||
| 90 | Barre d'actions du document — regroupement fonctionnel + spacers | 2.3.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
| 90 | Barre d'actions du document — regroupement fonctionnel + spacers | 2.3.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
||||||
| 89 | Drag & drop complet de fichiers/dossiers & intégration Assistant IA | 2.3.0 | [features/drag-and-drop-ai.md](./features/drag-and-drop-ai.md) |
|
| 89 | Drag & drop complet de fichiers/dossiers & intégration Assistant IA | 2.3.0 | [features/drag-and-drop-ai.md](./features/drag-and-drop-ai.md) |
|
||||||
| 1–8 | Fondations (FastAPI, TF-IDF + stemming, watchdog, SPA, sécurité, path traversal, gzip, PWA) | 1.0.0→1.4.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
| 1–8 | Fondations (FastAPI, TF-IDF + stemming, watchdog, SPA, sécurité, path traversal, gzip, PWA) | 1.0.0→1.4.0 | [archive](./archive/COMPLETED_v1-v2.md) |
|
||||||
@@ -200,8 +258,9 @@
|
|||||||
| 🔵 P2 restant | #77 Desktop : signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour |
|
| 🔵 P2 restant | #77 Desktop : signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour |
|
||||||
| ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours |
|
| ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours |
|
||||||
| ⚪ P2 restant | #92 Assistant IA — écosystème d'outils phase 2 (web étendu, sources connectées, documents) | 3-5 jours |
|
| ⚪ P2 restant | #92 Assistant IA — écosystème d'outils phase 2 (web étendu, sources connectées, documents) | 3-5 jours |
|
||||||
|
| ⚪ P2 restant | #94–97 Assistant IA — améliorations UI (soumission, historique, navigation, panneau +) | ~6-7 jours |
|
||||||
| ⚪ P0/P1 restant | #85-87 Refonte architecturale, performance, CI/CD (issues BUG-035 → BUG-040) | ~15-23 jours |
|
| ⚪ P0/P1 restant | #85-87 Refonte architecturale, performance, CI/CD (issues BUG-035 → BUG-040) | ~15-23 jours |
|
||||||
| **Total restant** | **7 items + finitions** | **~31-47 jours** |
|
| **Total restant** | **11 items + finitions** | **~37-54 jours** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "1.0.0",
|
"version": "2.3.0",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -0,0 +1,576 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""ObsiGate — outil de versionnage SemVer (source unique de vérité : `VERSION`).
|
||||||
|
|
||||||
|
Le fichier `VERSION` (racine du dépôt) contient la version **livrée** au format
|
||||||
|
`MAJEUR.MINEUR.CORRECTIF`. C'est la seule source de vérité : tout le reste en
|
||||||
|
découle (backend `backend/version.py`, image Docker, desktop Tauri, `package.json`,
|
||||||
|
`CHANGELOG.md`, `docs/ROADMAP.md`).
|
||||||
|
|
||||||
|
Incrément (Conventional Commits) :
|
||||||
|
|
||||||
|
`!:` ou `BREAKING CHANGE:` -> MAJEUR x.0.0
|
||||||
|
`feat:` -> MINEUR x.y.0
|
||||||
|
tout le reste (fix, perf…) -> CORRECTIF x.y.z
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
|
||||||
|
scripts/bump_version.py # bump auto (commits depuis le dernier tag)
|
||||||
|
scripts/bump_version.py --from-message-file .git/COMMIT_EDITMSG --stage
|
||||||
|
scripts/bump_version.py --minor --dry-run
|
||||||
|
scripts/bump_version.py --set 3.0.0
|
||||||
|
scripts/bump_version.py --print-version
|
||||||
|
scripts/bump_version.py --major --commit --tag --push
|
||||||
|
|
||||||
|
Options :
|
||||||
|
|
||||||
|
--bump {auto,major,minor,patch} incrément explicite
|
||||||
|
--major | --minor | --patch raccourcis de --bump
|
||||||
|
--set X.Y.Z fixe la version (sans incrément)
|
||||||
|
--from-message-file PATH détecte l'incrément depuis un message de commit
|
||||||
|
--message TEXT détecte l'incrément depuis un message donné
|
||||||
|
--dry-run affiche sans rien écrire
|
||||||
|
--sync / --no-files synchroniser (ou non) les fichiers dérivés
|
||||||
|
--changelog / --no-changelog rotation de CHANGELOG.md (ou non)
|
||||||
|
--stage `git add` des fichiers modifiés (utilisé par le hook)
|
||||||
|
--commit crée un commit `chore(release): vX.Y.Z`
|
||||||
|
--tag crée le tag annoté `vX.Y.Z` s'il n'existe pas
|
||||||
|
--push pousse la branche et les tags (implique --tag)
|
||||||
|
--print-version affiche la version courante et sort
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
VERSION_FILE = "VERSION" # source unique de vérité
|
||||||
|
LEGACY_VERSION_FILES = ("backend/VERSION",) # anciens emplacements (repli)
|
||||||
|
|
||||||
|
PACKAGE_JSON = "package.json"
|
||||||
|
ROADMAP = "docs/ROADMAP.md"
|
||||||
|
CHANGELOG = "CHANGELOG.md"
|
||||||
|
DESKTOP_CONF = "desktop/tauri.conf.json"
|
||||||
|
DESKTOP_CARGO = "desktop/Cargo.toml"
|
||||||
|
DESKTOP_LOCK = "desktop/Cargo.lock"
|
||||||
|
|
||||||
|
BUMP_KINDS = ("major", "minor", "patch")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Version ────────────────────────────────────────────────────────────────
|
||||||
|
def parse_version(raw: str) -> tuple[int, int, int] | None:
|
||||||
|
"""Extrait (majeur, mineur, correctif) d'une chaîne quelconque.
|
||||||
|
|
||||||
|
Accepte `2.3.0`, `v2.3.0`, `2.3.0-dev`, `2.2.1-35-g314e37c`.
|
||||||
|
Retourne ``None`` si aucun triplet numérique n'est présent.
|
||||||
|
"""
|
||||||
|
match = re.search(r"(\d+)\.(\d+)\.(\d+)", str(raw or ""))
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
||||||
|
|
||||||
|
|
||||||
|
def format_version(version: tuple[int, int, int]) -> str:
|
||||||
|
"""Rend `MAJEUR.MINEUR.CORRECTIF`."""
|
||||||
|
return f"{version[0]}.{version[1]}.{version[2]}"
|
||||||
|
|
||||||
|
|
||||||
|
def bump_version(version: tuple[int, int, int], kind: str) -> tuple[int, int, int]:
|
||||||
|
"""Applique un incrément SemVer à ``version``."""
|
||||||
|
major, minor, patch = version
|
||||||
|
if kind == "major":
|
||||||
|
return (major + 1, 0, 0)
|
||||||
|
if kind == "minor":
|
||||||
|
return (major, minor + 1, 0)
|
||||||
|
if kind == "patch":
|
||||||
|
return (major, minor, patch + 1)
|
||||||
|
raise ValueError(f"incrément inconnu : {kind!r}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Git ────────────────────────────────────────────────────────────────────
|
||||||
|
def git(*args: str, root: Path = REPO_ROOT) -> str | None:
|
||||||
|
"""Exécute git dans ``root`` ; retourne stdout (strippé) ou ``None``."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", *args],
|
||||||
|
cwd=str(root),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||||
|
return None
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def last_tag_version(root: Path = REPO_ROOT) -> str | None:
|
||||||
|
"""Version (sans `v`) du dernier tag de release, ou ``None``."""
|
||||||
|
tag = git("describe", "--tags", "--abbrev=0", root=root)
|
||||||
|
if not tag:
|
||||||
|
return None
|
||||||
|
parsed = parse_version(tag)
|
||||||
|
return format_version(parsed) if parsed else None
|
||||||
|
|
||||||
|
|
||||||
|
def read_version(root: Path = REPO_ROOT) -> str:
|
||||||
|
"""Version courante : `VERSION` → `backend/VERSION` → dernier tag → `0.0.0`."""
|
||||||
|
for rel in (VERSION_FILE, *LEGACY_VERSION_FILES):
|
||||||
|
path = root / rel
|
||||||
|
if path.exists():
|
||||||
|
parsed = parse_version(path.read_text(encoding="utf-8"))
|
||||||
|
if parsed:
|
||||||
|
return format_version(parsed)
|
||||||
|
tag = last_tag_version(root)
|
||||||
|
return tag or "0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
def head_version(root: Path = REPO_ROOT) -> str | None:
|
||||||
|
"""Version contenue dans `VERSION` au commit HEAD, ou ``None``."""
|
||||||
|
raw = git("show", f"HEAD:{VERSION_FILE}", root=root)
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
parsed = parse_version(raw)
|
||||||
|
return format_version(parsed) if parsed else None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Détection de l'incrément ───────────────────────────────────────────────
|
||||||
|
def detect_bump_from_message(message: str, default: str | None = None) -> str | None:
|
||||||
|
"""Type d'incrément déduit d'un message de commit.
|
||||||
|
|
||||||
|
Retourne ``None`` quand le commit ne doit **pas** déclencher de version
|
||||||
|
(merge, revert, commit de release, message vide) — sauf si ``default`` est
|
||||||
|
fourni : il s'applique alors à un message vide (commit rédigé dans un
|
||||||
|
éditeur, où le hook s'exécute avant la saisie du message).
|
||||||
|
"""
|
||||||
|
text = (message or "").strip()
|
||||||
|
if not text:
|
||||||
|
return default
|
||||||
|
lines = [line for line in text.splitlines() if not line.lstrip().startswith("#")]
|
||||||
|
if not lines or not "\n".join(lines).strip():
|
||||||
|
return default
|
||||||
|
subject = lines[0].strip()
|
||||||
|
if re.match(r"^(Merge|Revert)\b", subject):
|
||||||
|
return None
|
||||||
|
if re.match(r"^chore\(release\)", subject):
|
||||||
|
return None
|
||||||
|
if "BREAKING CHANGE" in text or re.match(r"^[a-zA-Z][a-zA-Z0-9-]*(\([^)]*\))?!:", subject):
|
||||||
|
return "major"
|
||||||
|
if re.match(r"^(feat|feature)(\([^)]*\))?:", subject):
|
||||||
|
return "minor"
|
||||||
|
return "patch"
|
||||||
|
|
||||||
|
|
||||||
|
def detect_bump_from_commits(root: Path = REPO_ROOT) -> str | None:
|
||||||
|
"""Incrément déduit des commits depuis le dernier tag (ou des 20 derniers)."""
|
||||||
|
tag = git("describe", "--tags", "--abbrev=0", root=root)
|
||||||
|
rev_range = f"{tag}..HEAD" if tag else "HEAD~20..HEAD"
|
||||||
|
log = git("log", "--format=%s%n%b", rev_range, root=root)
|
||||||
|
if log is None:
|
||||||
|
log = git("log", "-1", "--format=%s%n%b", root=root) or ""
|
||||||
|
if not log.strip():
|
||||||
|
log = git("log", "-1", "--format=%s%n%b", root=root) or ""
|
||||||
|
subjects = [line for line in log.splitlines() if line.strip()]
|
||||||
|
if not subjects:
|
||||||
|
return None
|
||||||
|
if "BREAKING CHANGE" in log or any(
|
||||||
|
re.match(r"^[a-zA-Z][a-zA-Z0-9-]*(\([^)]*\))?!:", line) for line in subjects
|
||||||
|
):
|
||||||
|
return "major"
|
||||||
|
if any(re.match(r"^(feat|feature)(\([^)]*\))?:", line) for line in subjects):
|
||||||
|
return "minor"
|
||||||
|
return "patch"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Écriture de fichiers (préserve les fins de ligne) ──────────────────────
|
||||||
|
def write_preserving_newlines(path: Path, text: str) -> None:
|
||||||
|
"""Écrit ``text`` en conservant le style de fin de ligne du fichier."""
|
||||||
|
original = path.read_bytes() if path.exists() else b""
|
||||||
|
crlf = b"\r\n" in original
|
||||||
|
data = text.replace("\r\n", "\n")
|
||||||
|
if crlf:
|
||||||
|
data = data.replace("\n", "\r\n")
|
||||||
|
path.write_bytes(data.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def sync_package_json(root: Path, version: str) -> bool:
|
||||||
|
"""Aligne `package.json` (`version`)."""
|
||||||
|
path = root / PACKAGE_JSON
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
original = path.read_text(encoding="utf-8")
|
||||||
|
data = json.loads(original)
|
||||||
|
if data.get("version") == version:
|
||||||
|
return False
|
||||||
|
data["version"] = version
|
||||||
|
text = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
||||||
|
write_preserving_newlines(path, text)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def sync_desktop(root: Path, version: str) -> list[str]:
|
||||||
|
"""Aligne les fichiers desktop Tauri ; retourne les fichiers réellement modifiés."""
|
||||||
|
changed: list[str] = []
|
||||||
|
|
||||||
|
conf = root / DESKTOP_CONF
|
||||||
|
if conf.exists():
|
||||||
|
text = conf.read_text(encoding="utf-8")
|
||||||
|
new_text, count = re.subn(
|
||||||
|
r'("version"\s*:\s*")[^"]*(")',
|
||||||
|
lambda m: f"{m.group(1)}{version}{m.group(2)}",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
if count and new_text != text:
|
||||||
|
write_preserving_newlines(conf, new_text)
|
||||||
|
changed.append(DESKTOP_CONF)
|
||||||
|
|
||||||
|
cargo = root / DESKTOP_CARGO
|
||||||
|
if cargo.exists():
|
||||||
|
text = cargo.read_text(encoding="utf-8")
|
||||||
|
new_text, count = re.subn(
|
||||||
|
r'(?m)^version\s*=\s*"[^"]*"',
|
||||||
|
f'version = "{version}"',
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
if count and new_text != text:
|
||||||
|
write_preserving_newlines(cargo, new_text)
|
||||||
|
changed.append(DESKTOP_CARGO)
|
||||||
|
|
||||||
|
lock = root / DESKTOP_LOCK
|
||||||
|
if lock.exists():
|
||||||
|
text = lock.read_text(encoding="utf-8")
|
||||||
|
new_text, count = re.subn(
|
||||||
|
r'(?m)^(name = "obsigate-desktop"\r?\nversion = ")[^"]*(")',
|
||||||
|
lambda m: f"{m.group(1)}{version}{m.group(2)}",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
if count and new_text != text:
|
||||||
|
write_preserving_newlines(lock, new_text)
|
||||||
|
changed.append(DESKTOP_LOCK)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def sync_readmes(root: Path, version: str) -> list[str]:
|
||||||
|
"""Aligne les badges et pieds de page des README (FR/EN)."""
|
||||||
|
changed: list[str] = []
|
||||||
|
patterns = (
|
||||||
|
(r"(badge/Version-)[^)]*(-blue)", rf"\g<1>{version}\g<2>"),
|
||||||
|
(r"(\| Version\s?:\s?)[\d.]+(?:-dev)?", rf"\g<1>{version}"),
|
||||||
|
(r"\(v1\.0\.0 → v[\d.]+(?:-dev)?\)", f"(v1.0.0 → v{version})"),
|
||||||
|
)
|
||||||
|
for rel in ("README.md", "README.fr.md"):
|
||||||
|
path = root / rel
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
new_text = text
|
||||||
|
for pattern, replacement in patterns:
|
||||||
|
new_text = re.sub(pattern, replacement, new_text)
|
||||||
|
if new_text != text:
|
||||||
|
write_preserving_newlines(path, new_text)
|
||||||
|
changed.append(rel)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def sync_roadmap(root: Path, version: str, when: date | None = None) -> bool:
|
||||||
|
"""Aligne l'en-tête de `docs/ROADMAP.md` (version + date de mise à jour)."""
|
||||||
|
path = root / ROADMAP
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
today = (when or date.today()).isoformat()
|
||||||
|
new_text, count = re.subn(
|
||||||
|
r"(> \*\*Version :\*\* )\S+( \| \*\*Dernière mise à jour :\*\* )\S+",
|
||||||
|
lambda m: f"{m.group(1)}{version}{m.group(2)}{today}",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
if not count:
|
||||||
|
return False
|
||||||
|
if new_text == text:
|
||||||
|
return False
|
||||||
|
write_preserving_newlines(path, new_text)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_changelog(root: Path, version: str, when: date | None = None) -> bool:
|
||||||
|
"""Publie `[Unreleased]` en `[X.Y.Z] — date` et ouvre une nouvelle section vide.
|
||||||
|
|
||||||
|
Idempotent : si la version est déjà présente dans le fichier, rien n'est fait.
|
||||||
|
"""
|
||||||
|
path = root / CHANGELOG
|
||||||
|
if not path.exists():
|
||||||
|
return False
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
if re.search(rf"(?m)^## \[{re.escape(version)}\]", text):
|
||||||
|
return False
|
||||||
|
heading = re.search(r"(?m)^## \[Unreleased\][^\n]*\n", text)
|
||||||
|
if not heading:
|
||||||
|
return False
|
||||||
|
today = (when or date.today()).isoformat()
|
||||||
|
replacement = (
|
||||||
|
"## [Unreleased]\n"
|
||||||
|
"\n"
|
||||||
|
"---\n"
|
||||||
|
"\n"
|
||||||
|
f"## [{version}] — {today}\n"
|
||||||
|
)
|
||||||
|
text = text[: heading.start()] + replacement + text[heading.end() :]
|
||||||
|
|
||||||
|
# Phrase d'en-tête : dernière version livrée
|
||||||
|
text = re.sub(
|
||||||
|
r"(?s)> \*\*En cours de développement\*\* : les changements non publiés sont dans la section\n"
|
||||||
|
r"> \[Unreleased\]\(#unreleased\)\. La dernière version publiée est \*\*[\d.]+\*\*\.",
|
||||||
|
"> **En cours de développement** : les changements à venir sont listés dans la section\n"
|
||||||
|
"> [Unreleased](#unreleased). La dernière version livrée est "
|
||||||
|
f"**{version}**.",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
text = re.sub(
|
||||||
|
r"(La dernière version (?:publiée|livrée) est \*\*)[\d.]+(\*\*)",
|
||||||
|
rf"\g<1>{version}\g<2>",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
write_preserving_newlines(path, text)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def apply_version(
|
||||||
|
root: Path,
|
||||||
|
version: str,
|
||||||
|
*,
|
||||||
|
sync: bool = True,
|
||||||
|
changelog: bool = True,
|
||||||
|
when: date | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Écrit `VERSION` et synchronise tous les fichiers dérivés.
|
||||||
|
|
||||||
|
Retourne la liste (relative au dépôt) des fichiers modifiés.
|
||||||
|
"""
|
||||||
|
touched: list[str] = []
|
||||||
|
version_path = root / VERSION_FILE
|
||||||
|
previous = read_version(root)
|
||||||
|
if previous != version or not version_path.exists():
|
||||||
|
write_preserving_newlines(version_path, version + "\n")
|
||||||
|
touched.append(VERSION_FILE)
|
||||||
|
|
||||||
|
if sync:
|
||||||
|
if sync_package_json(root, version):
|
||||||
|
touched.append(PACKAGE_JSON)
|
||||||
|
touched.extend(sync_desktop(root, version))
|
||||||
|
touched.extend(sync_readmes(root, version))
|
||||||
|
if sync_roadmap(root, version, when):
|
||||||
|
touched.append(ROADMAP)
|
||||||
|
|
||||||
|
if changelog and rotate_changelog(root, version, when):
|
||||||
|
touched.append(CHANGELOG)
|
||||||
|
|
||||||
|
return touched
|
||||||
|
|
||||||
|
|
||||||
|
def stage(root: Path, files: list[str]) -> None:
|
||||||
|
"""`git add` des fichiers modifiés (les fichiers absents sont ignorés)."""
|
||||||
|
existing = [rel for rel in dict.fromkeys(files) if (root / rel).exists()]
|
||||||
|
if existing:
|
||||||
|
git("add", "--", *existing, root=root)
|
||||||
|
|
||||||
|
|
||||||
|
# ── CLI ────────────────────────────────────────────────────────────────────
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="bump_version.py",
|
||||||
|
description="ObsiGate — version SemVer (source unique de vérité : VERSION).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bump",
|
||||||
|
choices=("auto", *BUMP_KINDS),
|
||||||
|
default="auto",
|
||||||
|
help="incrément à appliquer (défaut : auto = commits depuis le dernier tag)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--major", action="store_true", help="raccourci --bump major")
|
||||||
|
parser.add_argument("--minor", action="store_true", help="raccourci --bump minor")
|
||||||
|
parser.add_argument("--patch", action="store_true", help="raccourci --bump patch")
|
||||||
|
parser.add_argument("--set", metavar="X.Y.Z", help="fixe la version sans incrément")
|
||||||
|
parser.add_argument(
|
||||||
|
"--from-message-file",
|
||||||
|
metavar="PATH",
|
||||||
|
help="détecte l'incrément depuis un fichier de message de commit",
|
||||||
|
)
|
||||||
|
parser.add_argument("--message", metavar="TEXT", help="détecte l'incrément depuis un message")
|
||||||
|
parser.add_argument(
|
||||||
|
"--default-kind",
|
||||||
|
choices=BUMP_KINDS,
|
||||||
|
help="incrément appliqué quand le message est vide (commit via éditeur)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="affiche sans écrire")
|
||||||
|
parser.add_argument(
|
||||||
|
"--sync",
|
||||||
|
dest="sync",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="synchroniser les fichiers dérivés (défaut)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-files",
|
||||||
|
"--no-sync",
|
||||||
|
dest="sync",
|
||||||
|
action="store_false",
|
||||||
|
help="ne pas toucher aux fichiers dérivés",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--changelog",
|
||||||
|
dest="changelog",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="rotation de CHANGELOG.md (défaut)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-changelog",
|
||||||
|
dest="changelog",
|
||||||
|
action="store_false",
|
||||||
|
help="ne pas toucher à CHANGELOG.md",
|
||||||
|
)
|
||||||
|
parser.add_argument("--stage", action="store_true", help="`git add` des fichiers modifiés")
|
||||||
|
parser.add_argument(
|
||||||
|
"--files-out",
|
||||||
|
metavar="PATH",
|
||||||
|
help="écrit la liste des fichiers modifiés (un par ligne) — utilisée par les hooks git",
|
||||||
|
)
|
||||||
|
parser.add_argument("--commit", action="store_true", help="commit `chore(release): vX.Y.Z`")
|
||||||
|
parser.add_argument("--tag", action="store_true", help="créer le tag annoté vX.Y.Z")
|
||||||
|
parser.add_argument("--push", action="store_true", help="pousser la branche et les tags")
|
||||||
|
parser.add_argument(
|
||||||
|
"--print-version", action="store_true", help="afficher la version courante"
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_bump(args: argparse.Namespace, root: Path) -> str | None:
|
||||||
|
"""Détermine l'incrément à appliquer (``None`` = aucun bump)."""
|
||||||
|
for flag, kind in (("major", "major"), ("minor", "minor"), ("patch", "patch")):
|
||||||
|
if getattr(args, flag):
|
||||||
|
return kind
|
||||||
|
if args.bump != "auto":
|
||||||
|
return args.bump
|
||||||
|
if args.from_message_file:
|
||||||
|
message = Path(args.from_message_file).read_text(encoding="utf-8", errors="replace")
|
||||||
|
return detect_bump_from_message(message, default=args.default_kind)
|
||||||
|
if args.message:
|
||||||
|
return detect_bump_from_message(args.message, default=args.default_kind)
|
||||||
|
return detect_bump_from_commits(root)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
root = REPO_ROOT
|
||||||
|
|
||||||
|
if args.print_version:
|
||||||
|
print(read_version(root))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
current = read_version(root)
|
||||||
|
|
||||||
|
# Version déjà fixée explicitement
|
||||||
|
if args.set:
|
||||||
|
parsed = parse_version(args.set)
|
||||||
|
if not parsed:
|
||||||
|
print(f"✗ Version invalide : {args.set!r} (attendu MAJEUR.MINEUR.CORRECTIF)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
next_version = format_version(parsed)
|
||||||
|
kind = "set"
|
||||||
|
else:
|
||||||
|
# Un incrément est-il déjà préparé dans l'arbre de travail (commit avorté,
|
||||||
|
# version posée à la main, premier passage avant tout commit) ?
|
||||||
|
committed = head_version(root)
|
||||||
|
last_tag = last_tag_version(root)
|
||||||
|
prepared = (committed is not None and current != committed) or (
|
||||||
|
committed is None and last_tag is not None and current != last_tag
|
||||||
|
)
|
||||||
|
if prepared:
|
||||||
|
kind = "none"
|
||||||
|
next_version = current
|
||||||
|
else:
|
||||||
|
kind = resolve_bump(args, root)
|
||||||
|
if kind is None:
|
||||||
|
print("Version inchangée (commit sans bump : merge, revert ou release).")
|
||||||
|
return 3
|
||||||
|
next_version = format_version(bump_version(parse_version(current) or (0, 0, 0), kind))
|
||||||
|
|
||||||
|
print(f"ObsiGate version : {current} → {next_version} ({kind})")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("── DRY RUN ─────────────────────────────")
|
||||||
|
for rel in (
|
||||||
|
VERSION_FILE,
|
||||||
|
PACKAGE_JSON,
|
||||||
|
DESKTOP_CONF,
|
||||||
|
DESKTOP_CARGO,
|
||||||
|
DESKTOP_LOCK,
|
||||||
|
ROADMAP,
|
||||||
|
CHANGELOG,
|
||||||
|
"README.md",
|
||||||
|
"README.fr.md",
|
||||||
|
):
|
||||||
|
if (root / rel).exists():
|
||||||
|
print(f" à synchroniser : {rel}")
|
||||||
|
print(" (aucune modification, aucun tag créé)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if kind == "none" and (root / VERSION_FILE).exists():
|
||||||
|
# Rien à incrémenter : on resynchronise seulement les satellites.
|
||||||
|
touched = apply_version(root, current, sync=args.sync, changelog=False)
|
||||||
|
else:
|
||||||
|
touched = apply_version(root, next_version, sync=args.sync, changelog=args.changelog)
|
||||||
|
|
||||||
|
if touched:
|
||||||
|
print(" fichiers mis à jour : " + ", ".join(dict.fromkeys(touched)))
|
||||||
|
|
||||||
|
if args.files_out:
|
||||||
|
list_path = Path(args.files_out)
|
||||||
|
if not list_path.is_absolute():
|
||||||
|
list_path = root / list_path
|
||||||
|
list_path.write_text(
|
||||||
|
"".join(f"{rel}\n" for rel in dict.fromkeys(touched)), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.stage and touched:
|
||||||
|
stage(root, touched)
|
||||||
|
|
||||||
|
if args.commit:
|
||||||
|
stage(root, touched or [VERSION_FILE])
|
||||||
|
git("commit", "-m", f"chore(release): v{next_version}", root=root)
|
||||||
|
print(f"✓ Commit de release créé : v{next_version}")
|
||||||
|
|
||||||
|
if args.tag or args.push:
|
||||||
|
tag = f"v{next_version}"
|
||||||
|
if git("rev-parse", "-q", "--verify", f"refs/tags/{tag}", root=root) is None:
|
||||||
|
if git("tag", "-a", tag, "-m", f"ObsiGate {tag}", root=root) is None:
|
||||||
|
print(f"✗ Impossible de créer le tag {tag}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"✓ Tag créé : {tag}")
|
||||||
|
else:
|
||||||
|
print(f"ℹ Tag {tag} déjà présent.")
|
||||||
|
|
||||||
|
if args.push:
|
||||||
|
branch = git("rev-parse", "--abbrev-ref", "HEAD", root=root) or "main"
|
||||||
|
git("push", "origin", branch, "--follow-tags", root=root)
|
||||||
|
print(f"✓ Branche {branch} et tags poussés.")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+19
-150
@@ -1,165 +1,34 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# ObsiGate — version bump helper (SemVer MAJOR.MINOR.PATCH)
|
# ObsiGate — version bump helper (SemVer MAJEUR.MINEUR.CORRECTIF)
|
||||||
#
|
#
|
||||||
# The canonical version is the latest release tag. This tool inspects the
|
# Wrapper historique : toute la logique vit dans `scripts/bump_version.py`
|
||||||
# commits since that tag and decides the next version from their type
|
# (source unique de vérité : `./VERSION`, incrément inspiré des
|
||||||
# (Conventional Commits), then:
|
# Conventional Commits).
|
||||||
# 1. synchronise la version desktop (tauri.conf.json, Cargo.toml, Cargo.lock) ;
|
|
||||||
# 2. commit ce bump (`chore(release): vX.Y.Z`) ;
|
|
||||||
# 3. crée le tag annoté (et pousse branche + tag avec --push).
|
|
||||||
#
|
#
|
||||||
# breaking change / `!:` / "BREAKING CHANGE" -> MAJOR bump (x.0.0)
|
# breaking (`!:` / BREAKING CHANGE) -> MAJEUR (x.0.0)
|
||||||
# feat / feature -> MINOR bump (x.y.0)
|
# `feat` -> MINEUR (x.y.0)
|
||||||
# anything else (fix, perf, refactor, ...) -> PATCH bump (x.y.z)
|
# tout le reste (fix, perf, …) -> CORRECTIF (x.y.z)
|
||||||
#
|
#
|
||||||
# Usage :
|
# Usage :
|
||||||
# scripts/bump_version.sh [--major|--minor|--patch] [--dry-run] [--push] [--no-files]
|
# scripts/bump_version.sh --dry-run # prévisualiser
|
||||||
|
# scripts/bump_version.sh # bump + synchro des fichiers
|
||||||
|
# scripts/bump_version.sh --minor --commit --tag --push
|
||||||
|
# scripts/bump_version.sh --print-version
|
||||||
#
|
#
|
||||||
# --dry-run : affiche la prochaine version sans rien modifier ni tagger.
|
# Voir `scripts/bump_version.py --help` pour la liste complète des options.
|
||||||
# --push : pousse la branche courante ET le nouveau tag vers origin.
|
|
||||||
# --no-files : ne met pas à jour les fichiers desktop (tag seul).
|
|
||||||
# --major|--minor|--patch : force l'incrément, ignore la détection.
|
|
||||||
#
|
|
||||||
# Examples:
|
|
||||||
# scripts/bump_version.sh --dry-run # preview next version
|
|
||||||
# scripts/bump_version.sh # bump + commit + tag (local)
|
|
||||||
# scripts/bump_version.sh --push # idem + push branche & tag
|
|
||||||
# scripts/bump_version.sh --patch --push # force patch + push
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel)" # run from repo root regardless of cwd
|
root="$(git rev-parse --show-toplevel)"
|
||||||
BRANCH="$(git branch --show-current)"
|
|
||||||
|
|
||||||
# ── Parse args ────────────────────────────────────────────────────────────
|
py=""
|
||||||
FORCE=""; DRY_RUN=0; DO_PUSH=0; DO_FILES=1
|
for cand in python3 python; do
|
||||||
for a in "$@"; do
|
if command -v "$cand" >/dev/null 2>&1; then py="$cand"; break; fi
|
||||||
case "$a" in
|
|
||||||
--major) FORCE=major ;;
|
|
||||||
--minor) FORCE=minor ;;
|
|
||||||
--patch) FORCE=patch ;;
|
|
||||||
--dry-run) DRY_RUN=1 ;;
|
|
||||||
--push) DO_PUSH=1 ;;
|
|
||||||
--no-files) DO_FILES=0 ;;
|
|
||||||
*) echo "✗ Argument inconnu: $a"; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
if [ -z "$py" ]; then
|
||||||
# ── Current version = latest tag base ─────────────────────────────────────
|
echo "✗ Aucun interpréteur Python trouvé (python3 / python)." >&2
|
||||||
CURRENT="$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")"
|
|
||||||
CURRENT="${CURRENT#v}"
|
|
||||||
IFS='.' read -r MAJ MIN PAT <<< "$CURRENT"
|
|
||||||
MAJ="${MAJ:-0}"; MIN="${MIN:-0}"; PAT="${PAT:-0}"
|
|
||||||
|
|
||||||
echo "Version actuelle : $CURRENT (branche: $BRANCH)"
|
|
||||||
|
|
||||||
# ── Detect bump type from commits since last tag ──────────────────────────
|
|
||||||
if [ -z "$FORCE" ]; then
|
|
||||||
# Use HEAD~1..HEAD when no tag exists yet (all commits "since" nothing).
|
|
||||||
RANGE="${CURRENT#0.0.0}" # if 0.0.0 fallback there may be no tag at all
|
|
||||||
if git rev-parse "v${CURRENT}" >/dev/null 2>&1; then
|
|
||||||
RANGE="v${CURRENT}..HEAD"
|
|
||||||
else
|
|
||||||
RANGE="HEAD~10..HEAD"
|
|
||||||
echo "⚠ Aucun tag v${CURRENT} trouvé — analyse des 10 derniers commits."
|
|
||||||
fi
|
|
||||||
LOG="$(git log --format='%s%n%b' "$RANGE" 2>/dev/null || true)"
|
|
||||||
|
|
||||||
BUMP="patch" # default
|
|
||||||
if echo "$LOG" | grep -qE '^[a-zA-Z]+\([^)]*\)!:|\!:\s|BREAKING CHANGE:'; then
|
|
||||||
BUMP="major"
|
|
||||||
elif echo "$LOG" | grep -qiE '^(feat|feature)(\(|:|\s)'; then
|
|
||||||
BUMP="minor"
|
|
||||||
fi
|
|
||||||
# If nothing meaningful in range, keep patch
|
|
||||||
echo "Détection (${RANGE}) : changement ${BUMP}"
|
|
||||||
else
|
|
||||||
BUMP="$FORCE"
|
|
||||||
echo "Incrément forcé : ${BUMP}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Compute next version ──────────────────────────────────────────────────
|
|
||||||
case "$BUMP" in
|
|
||||||
major) MAJ=$((MAJ + 1)); MIN=0; PAT=0 ;;
|
|
||||||
minor) MIN=$((MIN + 1)); PAT=0 ;;
|
|
||||||
*) PAT=$((PAT + 1)) ;;
|
|
||||||
esac
|
|
||||||
NEXT="${MAJ}.${MIN}.${PAT}"
|
|
||||||
|
|
||||||
# ── Guard: tag already exists? ────────────────────────────────────────────
|
|
||||||
if git rev-parse "v${NEXT}" >/dev/null 2>&1; then
|
|
||||||
echo "✗ Le tag v${NEXT} existe déjà. Corrigez l'incrément ou retirez le tag."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Fichiers desktop à synchroniser ───────────────────────────────────────
|
exec "$py" "$root/scripts/bump_version.py" "$@"
|
||||||
DESKTOP_CONF="desktop/tauri.conf.json"
|
|
||||||
DESKTOP_CARGO="desktop/Cargo.toml"
|
|
||||||
DESKTOP_LOCK="desktop/Cargo.lock"
|
|
||||||
|
|
||||||
# Met à jour la version dans les fichiers desktop (in-place, portable GNU sed).
|
|
||||||
sync_desktop_version() {
|
|
||||||
local ver="$1"
|
|
||||||
if [ -f "$DESKTOP_CONF" ]; then
|
|
||||||
sed -i -E 's/^([[:space:]]*"version"[[:space:]]*:[[:space:]]*")[^"]*(")/\1'"$ver"'\2/' "$DESKTOP_CONF"
|
|
||||||
fi
|
|
||||||
if [ -f "$DESKTOP_CARGO" ]; then
|
|
||||||
# Première ligne `version = "..."` = version du [package].
|
|
||||||
sed -i -E '0,/^version = "[^"]*"/s//version = "'"$ver"'"/' "$DESKTOP_CARGO"
|
|
||||||
fi
|
|
||||||
if [ -f "$DESKTOP_LOCK" ]; then
|
|
||||||
# Bloc `name = "obsigate-desktop"` -> ligne `version` suivante.
|
|
||||||
sed -i -E '/^name = "obsigate-desktop"$/{n;s/^version = "[^"]*"/version = "'"$ver"'"/;}' "$DESKTOP_LOCK"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$DRY_RUN" = "1" ]; then
|
|
||||||
echo "── DRY RUN ─────────────────────────────"
|
|
||||||
echo " Prochain tag : v${NEXT}"
|
|
||||||
if [ "$DO_FILES" = "1" ]; then
|
|
||||||
echo " Fichiers desktop à synchroniser :"
|
|
||||||
echo " - $DESKTOP_CONF"
|
|
||||||
echo " - $DESKTOP_CARGO"
|
|
||||||
echo " - $DESKTOP_LOCK (obsigate-desktop)"
|
|
||||||
fi
|
|
||||||
echo " (aucune modification, aucun tag créé)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Sync + commit des versions desktop ────────────────────────────────────
|
|
||||||
if [ "$DO_FILES" = "1" ]; then
|
|
||||||
echo "─ Synchronisation de la version desktop → ${NEXT}"
|
|
||||||
sync_desktop_version "$NEXT"
|
|
||||||
|
|
||||||
CHANGED_FILES=()
|
|
||||||
for f in "$DESKTOP_CONF" "$DESKTOP_CARGO" "$DESKTOP_LOCK"; do
|
|
||||||
[ -f "$f" ] && CHANGED_FILES+=("$f")
|
|
||||||
done
|
|
||||||
|
|
||||||
if git diff --quiet -- "${CHANGED_FILES[@]}"; then
|
|
||||||
echo "ℹ Aucun changement de version desktop à committer."
|
|
||||||
else
|
|
||||||
git add -- "${CHANGED_FILES[@]}"
|
|
||||||
git commit -m "chore(release): v${NEXT}" \
|
|
||||||
-m "Synchronise la version desktop (tauri.conf.json, Cargo.toml, Cargo.lock)."
|
|
||||||
echo "✓ Commit de release créé : v${NEXT}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "ℹ --no-files : fichiers desktop inchangés."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Create tag ────────────────────────────────────────────────────────────
|
|
||||||
git tag -a "v${NEXT}" -m "Release v${NEXT} (${BUMP} bump depuis v${CURRENT})"
|
|
||||||
echo "✓ Tag créé localement : v${NEXT}"
|
|
||||||
|
|
||||||
if [ "$DO_PUSH" = "1" ]; then
|
|
||||||
git push origin "$BRANCH"
|
|
||||||
git push origin "v${NEXT}"
|
|
||||||
echo "✓ Branche '$BRANCH' et tag v${NEXT} poussés."
|
|
||||||
else
|
|
||||||
echo "ℹ Tag local uniquement. Poussez avec:"
|
|
||||||
echo " git push origin $BRANCH && git push origin v${NEXT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Done."
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ObsiGate — installe les hooks git versionnés (.githooks).
|
||||||
|
#
|
||||||
|
# 1. `core.hooksPath = .githooks` -> hooks partagés par tout le monde (versionnés)
|
||||||
|
# 2. `push.followTags = true` -> le tag de version créé au commit est publié
|
||||||
|
# automatiquement à chaque `git push`
|
||||||
|
#
|
||||||
|
# À exécuter une fois par clone : scripts/install-hooks.sh
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
if [ ! -d .githooks ]; then
|
||||||
|
echo "✗ Dossier .githooks introuvable." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
git config push.followTags true
|
||||||
|
chmod +x .githooks/* 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "✓ Hooks installés :"
|
||||||
|
echo " core.hooksPath = $(git config --get core.hooksPath)"
|
||||||
|
echo " push.followTags = $(git config --get push.followTags)"
|
||||||
|
echo " → La version (./VERSION) est incrémentée à chaque commit et taguée"
|
||||||
|
echo " (vX.Y.Z), puis publiée avec la branche à chaque push."
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
"""Tests du versionnage ObsiGate (source unique de vérité : `VERSION`).
|
||||||
|
|
||||||
|
Deux niveaux :
|
||||||
|
|
||||||
|
1. `backend/version.py` — résolution de la version exposée par /api/health.
|
||||||
|
2. `scripts/bump_version.py` — incrément SemVer, détection depuis le message de
|
||||||
|
commit, synchronisation des fichiers dérivés et rotation du CHANGELOG.
|
||||||
|
3. Un test de cohérence globale : la version affichée doit être **la même**
|
||||||
|
partout (VERSION, package.json, desktop Tauri, CHANGELOG, ROADMAP) — c'est le
|
||||||
|
garde-fou contre la dérive qui a motivé BUG-047.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from backend import version as version_module
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# ── scripts/bump_version.py (outil hors package) ───────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _load_bump_tool():
|
||||||
|
"""Charge `scripts/bump_version.py` par chemin (scripts/ n'est pas un package)."""
|
||||||
|
path = REPO_ROOT / "scripts" / "bump_version.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("obsigate_bump_version", path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec.loader is not None
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
bump_tool = _load_bump_tool()
|
||||||
|
|
||||||
|
|
||||||
|
# ── backend/version.py ────────────────────────────────────────────────────
|
||||||
|
class TestVersionResolution:
|
||||||
|
def test_clean_base(self):
|
||||||
|
assert version_module._clean_base("v2.3.0") == "2.3.0"
|
||||||
|
assert version_module._clean_base("2.3.0-12-gabc1234") == "2.3.0"
|
||||||
|
assert version_module._clean_base("2.3.0-dev") == "2.3.0"
|
||||||
|
assert version_module._clean_base(" v1.2.3-dirty ") == "1.2.3"
|
||||||
|
assert version_module._clean_base("pas une version") == ""
|
||||||
|
assert version_module._clean_base("") == ""
|
||||||
|
|
||||||
|
def test_root_version_file_wins(self, monkeypatch, tmp_path):
|
||||||
|
root_version = tmp_path / "VERSION"
|
||||||
|
root_version.write_text("2.3.0\n", encoding="utf-8")
|
||||||
|
legacy = tmp_path / "backend" / "VERSION"
|
||||||
|
legacy.parent.mkdir()
|
||||||
|
legacy.write_text("1.9.9\n", encoding="utf-8")
|
||||||
|
|
||||||
|
monkeypatch.delenv("OBSIGATE_VERSION", raising=False)
|
||||||
|
monkeypatch.setattr(version_module, "VERSION_FILE", root_version)
|
||||||
|
monkeypatch.setattr(version_module, "LEGACY_VERSION_FILE", legacy)
|
||||||
|
|
||||||
|
assert version_module.get_version() == "2.3.0"
|
||||||
|
assert version_module.get_version_tuple() == (2, 3, 0)
|
||||||
|
|
||||||
|
def test_env_override_wins(self, monkeypatch, tmp_path):
|
||||||
|
root_version = tmp_path / "VERSION"
|
||||||
|
root_version.write_text("2.3.0\n", encoding="utf-8")
|
||||||
|
monkeypatch.setattr(version_module, "VERSION_FILE", root_version)
|
||||||
|
monkeypatch.setenv("OBSIGATE_VERSION", "v9.9.9-dirty")
|
||||||
|
|
||||||
|
assert version_module.get_version() == "9.9.9"
|
||||||
|
|
||||||
|
def test_falls_back_to_legacy_then_zero(self, monkeypatch, tmp_path):
|
||||||
|
missing = tmp_path / "VERSION"
|
||||||
|
legacy = tmp_path / "backend" / "VERSION"
|
||||||
|
legacy.parent.mkdir()
|
||||||
|
legacy.write_text("1.8.0-35-g314e37c\n", encoding="utf-8")
|
||||||
|
|
||||||
|
monkeypatch.delenv("OBSIGATE_VERSION", raising=False)
|
||||||
|
monkeypatch.setattr(version_module, "VERSION_FILE", missing)
|
||||||
|
monkeypatch.setattr(version_module, "LEGACY_VERSION_FILE", legacy)
|
||||||
|
assert version_module.get_version() == "1.8.0"
|
||||||
|
|
||||||
|
legacy.unlink()
|
||||||
|
monkeypatch.setattr(version_module, "_run_git", lambda args: "")
|
||||||
|
assert version_module.get_version() == "0.0.0"
|
||||||
|
|
||||||
|
def test_invalid_version_file_is_ignored(self, monkeypatch, tmp_path):
|
||||||
|
broken = tmp_path / "VERSION"
|
||||||
|
broken.write_text("dev\n", encoding="utf-8")
|
||||||
|
monkeypatch.delenv("OBSIGATE_VERSION", raising=False)
|
||||||
|
monkeypatch.setattr(version_module, "VERSION_FILE", broken)
|
||||||
|
monkeypatch.setattr(version_module, "LEGACY_VERSION_FILE", tmp_path / "absent")
|
||||||
|
monkeypatch.setattr(version_module, "_run_git", lambda args: "")
|
||||||
|
assert version_module.get_version() == "0.0.0"
|
||||||
|
|
||||||
|
def test_no_dev_suffix_ever_returned(self, monkeypatch, tmp_path):
|
||||||
|
"""Le numéro affiché ne doit jamais contenir -dev / -N-gHASH."""
|
||||||
|
root_version = tmp_path / "VERSION"
|
||||||
|
root_version.write_text("2.3.0-12-gabc1234\n", encoding="utf-8")
|
||||||
|
monkeypatch.delenv("OBSIGATE_VERSION", raising=False)
|
||||||
|
monkeypatch.setattr(version_module, "VERSION_FILE", root_version)
|
||||||
|
assert version_module.get_version() == "2.3.0"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Incrément SemVer ──────────────────────────────────────────────────────
|
||||||
|
class TestBumpMath:
|
||||||
|
def test_major_resets_minor_and_patch(self):
|
||||||
|
assert bump_tool.bump_version((2, 3, 4), "major") == (3, 0, 0)
|
||||||
|
|
||||||
|
def test_minor_resets_patch(self):
|
||||||
|
assert bump_tool.bump_version((2, 3, 4), "minor") == (2, 4, 0)
|
||||||
|
|
||||||
|
def test_patch(self):
|
||||||
|
assert bump_tool.bump_version((2, 3, 4), "patch") == (2, 3, 5)
|
||||||
|
|
||||||
|
def test_unknown_kind_raises(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
bump_tool.bump_version((2, 3, 4), "banana")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("raw", "expected"),
|
||||||
|
[
|
||||||
|
("v2.3.0", (2, 3, 0)),
|
||||||
|
("2.3.0-dev", (2, 3, 0)),
|
||||||
|
("2.2.1-35-g314e37c", (2, 2, 1)),
|
||||||
|
("n'importe quoi", None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_version(self, raw, expected):
|
||||||
|
assert bump_tool.parse_version(raw) == expected
|
||||||
|
|
||||||
|
def test_format_version(self):
|
||||||
|
assert bump_tool.format_version((2, 3, 0)) == "2.3.0"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Détection depuis le message de commit ─────────────────────────────────
|
||||||
|
class TestDetectBumpFromMessage:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("message", "expected"),
|
||||||
|
[
|
||||||
|
("feat(editeur): edition inline", "minor"),
|
||||||
|
("feature: x", "minor"),
|
||||||
|
("fix(ai): erreur lisible", "patch"),
|
||||||
|
("perf: rapide", "patch"),
|
||||||
|
("docs: readme", "patch"),
|
||||||
|
("chore: menage", "patch"),
|
||||||
|
("refactor: nettoyage", "patch"),
|
||||||
|
("feat: x\n\nBREAKING CHANGE: api", "major"),
|
||||||
|
("feat(api)!: rupture", "major"),
|
||||||
|
("fix!: rupture", "major"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_types(self, message, expected):
|
||||||
|
assert bump_tool.detect_bump_from_message(message) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"message",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
" \n",
|
||||||
|
"# commentaire de git seulement\n",
|
||||||
|
"Merge branch 'main' into dev",
|
||||||
|
"Revert \"feat: x\"",
|
||||||
|
"chore(release): v2.3.0",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_no_bump(self, message):
|
||||||
|
assert bump_tool.detect_bump_from_message(message) is None
|
||||||
|
|
||||||
|
def test_commit_message_comment_is_ignored(self):
|
||||||
|
message = "fix: correction\n# Please enter the commit message\n"
|
||||||
|
assert bump_tool.detect_bump_from_message(message) == "patch"
|
||||||
|
|
||||||
|
def test_empty_message_uses_default_kind(self):
|
||||||
|
"""Commit rédigé dans un éditeur : incrément différé, message pas encore saisi."""
|
||||||
|
template = "\n# Please enter the commit message for your changes. Lines starting\n# with '#'\n"
|
||||||
|
assert bump_tool.detect_bump_from_message(template) is None
|
||||||
|
assert bump_tool.detect_bump_from_message(template, default="patch") == "patch"
|
||||||
|
assert bump_tool.detect_bump_from_message("", default="patch") == "patch"
|
||||||
|
# Un vrai message garde la priorité sur le défaut.
|
||||||
|
assert bump_tool.detect_bump_from_message("feat: x", default="patch") == "minor"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Synchronisation des fichiers dérivés ──────────────────────────────────
|
||||||
|
@pytest.fixture
|
||||||
|
def fake_repo(tmp_path: Path) -> Path:
|
||||||
|
"""Dépôt minimal avec tous les fichiers dérivés du versionnage."""
|
||||||
|
(tmp_path / "docs").mkdir()
|
||||||
|
(tmp_path / "desktop").mkdir()
|
||||||
|
(tmp_path / "scripts").mkdir()
|
||||||
|
|
||||||
|
(tmp_path / "VERSION").write_text("2.2.1\n", encoding="utf-8")
|
||||||
|
(tmp_path / "package.json").write_text(
|
||||||
|
json.dumps({"name": "obsigate", "version": "1.0.0", "type": "commonjs"}, indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(tmp_path / "desktop" / "tauri.conf.json").write_text(
|
||||||
|
json.dumps({"productName": "ObsiGate", "version": "2.0.0"}, indent=2) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(tmp_path / "desktop" / "Cargo.toml").write_text(
|
||||||
|
'[package]\nname = "obsigate-desktop"\nversion = "2.0.0"\nedition = "2021"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(tmp_path / "desktop" / "Cargo.lock").write_text(
|
||||||
|
'[[package]]\nname = "autre"\nversion = "1.0.0"\n\n'
|
||||||
|
'[[package]]\nname = "obsigate-desktop"\nversion = "2.0.0"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(tmp_path / "docs" / "ROADMAP.md").write_text(
|
||||||
|
"# ObsiGate — Roadmap\n\n"
|
||||||
|
"> **Version :** 2.3.0-dev | **Dernière mise à jour :** 2026-09-15\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(tmp_path / "CHANGELOG.md").write_text(
|
||||||
|
"# Changelog — ObsiGate\n\n"
|
||||||
|
"> **En cours de développement** : les changements non publiés sont dans la section\n"
|
||||||
|
"> [Unreleased](#unreleased). La dernière version publiée est **2.2.1**.\n\n"
|
||||||
|
"---\n\n"
|
||||||
|
"## [Unreleased]\n\n"
|
||||||
|
"### Corrigé\n\n"
|
||||||
|
"- **BUG-047 — version** : correctif.\n\n"
|
||||||
|
"---\n\n"
|
||||||
|
"## [2.2.1] — 2026-09-11\n\n"
|
||||||
|
"### Corrigé\n\n"
|
||||||
|
"- ancien\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
class TestSyncDerivedFiles:
|
||||||
|
def test_apply_version_writes_everything(self, fake_repo):
|
||||||
|
touched = bump_tool.apply_version(fake_repo, "2.3.0", when=date(2026, 9, 16))
|
||||||
|
|
||||||
|
assert (fake_repo / "VERSION").read_text(encoding="utf-8").strip() == "2.3.0"
|
||||||
|
assert json.loads((fake_repo / "package.json").read_text(encoding="utf-8"))["version"] == "2.3.0"
|
||||||
|
assert (
|
||||||
|
json.loads((fake_repo / "desktop" / "tauri.conf.json").read_text(encoding="utf-8"))[
|
||||||
|
"version"
|
||||||
|
]
|
||||||
|
== "2.3.0"
|
||||||
|
)
|
||||||
|
cargo = (fake_repo / "desktop" / "Cargo.toml").read_text(encoding="utf-8")
|
||||||
|
assert 'version = "2.3.0"' in cargo
|
||||||
|
lock = (fake_repo / "desktop" / "Cargo.lock").read_text(encoding="utf-8")
|
||||||
|
assert 'name = "autre"\nversion = "1.0.0"' in lock # bloc non touché
|
||||||
|
assert re.search(r'name = "obsigate-desktop"\nversion = "2\.3\.0"', lock)
|
||||||
|
assert "2.3.0" in (fake_repo / "docs" / "ROADMAP.md").read_text(encoding="utf-8")
|
||||||
|
assert "-dev" not in (fake_repo / "docs" / "ROADMAP.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert set(touched) == {
|
||||||
|
"VERSION",
|
||||||
|
"package.json",
|
||||||
|
"desktop/tauri.conf.json",
|
||||||
|
"desktop/Cargo.toml",
|
||||||
|
"desktop/Cargo.lock",
|
||||||
|
"docs/ROADMAP.md",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_changelog_rotation(self, fake_repo):
|
||||||
|
bump_tool.rotate_changelog(fake_repo, "2.3.0", when=date(2026, 9, 16))
|
||||||
|
text = (fake_repo / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# La nouvelle section vide est en tête, la version livrée juste après.
|
||||||
|
assert re.search(
|
||||||
|
r"## \[Unreleased\]\n\n---\n\n## \[2\.3\.0\] — 2026-09-16\n", text
|
||||||
|
), text
|
||||||
|
# Le contenu précédemment « Unreleased » appartient désormais à 2.3.0.
|
||||||
|
assert text.index("## [2.3.0] — 2026-09-16") < text.index("- **BUG-047 — version**")
|
||||||
|
# La phrase d'en-tête suit la version livrée.
|
||||||
|
assert "La dernière version livrée est **2.3.0**" in text
|
||||||
|
assert "La dernière version publiée est **2.2.1**" not in text
|
||||||
|
|
||||||
|
def test_changelog_rotation_is_idempotent(self, fake_repo):
|
||||||
|
bump_tool.rotate_changelog(fake_repo, "2.3.0", when=date(2026, 9, 16))
|
||||||
|
once = (fake_repo / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||||
|
assert bump_tool.rotate_changelog(fake_repo, "2.3.0", when=date(2026, 9, 16)) is False
|
||||||
|
assert (fake_repo / "CHANGELOG.md").read_text(encoding="utf-8") == once
|
||||||
|
|
||||||
|
def test_apply_version_is_idempotent(self, fake_repo):
|
||||||
|
bump_tool.apply_version(fake_repo, "2.3.0", when=date(2026, 9, 16))
|
||||||
|
assert bump_tool.apply_version(fake_repo, "2.3.0", when=date(2026, 9, 16)) == []
|
||||||
|
|
||||||
|
def test_crlf_is_preserved(self, fake_repo):
|
||||||
|
path = fake_repo / "VERSION"
|
||||||
|
path.write_bytes(b"2.2.1\r\n")
|
||||||
|
bump_tool.write_preserving_newlines(path, "2.4.0\n")
|
||||||
|
assert path.read_bytes() == b"2.4.0\r\n"
|
||||||
|
|
||||||
|
def test_readmes_badges_and_footers(self, fake_repo):
|
||||||
|
(fake_repo / "README.md").write_text(
|
||||||
|
"[]()\n"
|
||||||
|
"...\n"
|
||||||
|
"See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v1.7.0).\n"
|
||||||
|
"*Project: ObsiGate | Version: 1.7.0 | Last updated: May 2026*\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(fake_repo / "README.fr.md").write_text(
|
||||||
|
"[]()\n"
|
||||||
|
"Consultez le [CHANGELOG.md](./CHANGELOG.md) (v1.0.0 → v2.0.0-dev).\n"
|
||||||
|
"*Projet : ObsiGate | Version : 2.0.0-dev | Dernière mise à jour : Juin 2026*\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
changed = bump_tool.sync_readmes(fake_repo, "2.3.0")
|
||||||
|
|
||||||
|
assert sorted(changed) == ["README.fr.md", "README.md"]
|
||||||
|
readme = (fake_repo / "README.md").read_text(encoding="utf-8")
|
||||||
|
assert "badge/Version-2.3.0-blue.svg" in readme
|
||||||
|
assert "| Version: 2.3.0 |" in readme
|
||||||
|
assert "(v1.0.0 → v2.3.0)" in readme
|
||||||
|
readme_fr = (fake_repo / "README.fr.md").read_text(encoding="utf-8")
|
||||||
|
assert "badge/Version-2.3.0-blue.svg" in readme_fr
|
||||||
|
assert "| Version : 2.3.0 |" in readme_fr
|
||||||
|
assert "(v1.0.0 → v2.3.0)" in readme_fr
|
||||||
|
# Idempotent
|
||||||
|
assert bump_tool.sync_readmes(fake_repo, "2.3.0") == []
|
||||||
|
|
||||||
|
|
||||||
|
# ── Cohérence globale du dépôt (garde-fou anti-dérive) ────────────────────
|
||||||
|
class TestRepoVersionAlignment:
|
||||||
|
def _repo_version(self) -> str:
|
||||||
|
raw = (REPO_ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
||||||
|
assert re.fullmatch(r"\d+\.\d+\.\d+", raw), f"VERSION invalide : {raw!r}"
|
||||||
|
return raw
|
||||||
|
|
||||||
|
def test_version_file_is_semver(self):
|
||||||
|
assert self._repo_version()
|
||||||
|
|
||||||
|
def test_package_json_matches(self):
|
||||||
|
data = json.loads((REPO_ROOT / "package.json").read_text(encoding="utf-8"))
|
||||||
|
assert data["version"] == self._repo_version()
|
||||||
|
|
||||||
|
def test_desktop_files_match(self):
|
||||||
|
version = self._repo_version()
|
||||||
|
conf = json.loads((REPO_ROOT / "desktop" / "tauri.conf.json").read_text(encoding="utf-8"))
|
||||||
|
assert conf["version"] == version
|
||||||
|
cargo = (REPO_ROOT / "desktop" / "Cargo.toml").read_text(encoding="utf-8")
|
||||||
|
assert re.search(rf'^version = "{re.escape(version)}"$', cargo, re.M), cargo[:200]
|
||||||
|
lock = (REPO_ROOT / "desktop" / "Cargo.lock").read_text(encoding="utf-8")
|
||||||
|
assert re.search(rf'name = "obsigate-desktop"\r?\nversion = "{re.escape(version)}"', lock)
|
||||||
|
|
||||||
|
def test_changelog_has_a_section_for_the_version(self):
|
||||||
|
version = self._repo_version()
|
||||||
|
text = (REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||||
|
assert re.search(rf"(?m)^## \[{re.escape(version)}\]", text), (
|
||||||
|
"CHANGELOG.md doit contenir une section pour la version livrée"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_roadmap_header_matches(self):
|
||||||
|
version = self._repo_version()
|
||||||
|
text = (REPO_ROOT / "docs" / "ROADMAP.md").read_text(encoding="utf-8")
|
||||||
|
assert re.search(rf"> \*\*Version :\*\* {re.escape(version)} ", text)
|
||||||
|
|
||||||
|
def test_no_hardcoded_version_left_in_build_pipeline(self):
|
||||||
|
"""Dockerfile / compose / CI ne doivent plus coder de numéro de version."""
|
||||||
|
dockerfile = (REPO_ROOT / "Dockerfile").read_text(encoding="utf-8")
|
||||||
|
assert "COPY VERSION ./VERSION" in dockerfile
|
||||||
|
assert not re.search(r"ARG VERSION=\d+\.\d+\.\d+", dockerfile)
|
||||||
|
|
||||||
|
compose = (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
|
assert not re.search(r"VERSION:\s*\$\{VERSION:-[\d.]+\}", compose)
|
||||||
|
|
||||||
|
ci = (REPO_ROOT / ".gitea" / "workflows" / "ci.yml").read_text(encoding="utf-8")
|
||||||
|
assert "git describe --tags --dirty" not in ci
|
||||||
|
|
||||||
|
def test_hooks_are_versioned_and_documented(self):
|
||||||
|
assert (REPO_ROOT / ".githooks" / "prepare-commit-msg").exists()
|
||||||
|
assert (REPO_ROOT / ".githooks" / "post-commit").exists()
|
||||||
|
assert (REPO_ROOT / "scripts" / "install-hooks.sh").exists()
|
||||||
|
|
||||||
|
# post-commit rattache les fichiers au commit (amend) puis tague.
|
||||||
|
post = (REPO_ROOT / ".githooks" / "post-commit").read_text(encoding="utf-8")
|
||||||
|
assert "--amend" in post
|
||||||
|
assert "git tag -a" in post
|
||||||
|
assert "rebase-merge" in post # garde : jamais d'amend en rebase/merge
|
||||||
|
|
||||||
|
# prepare-commit-msg incrémente et laisse la liste des fichiers au post-commit.
|
||||||
|
prep = (REPO_ROOT / ".githooks" / "prepare-commit-msg").read_text(encoding="utf-8")
|
||||||
|
assert "--from-message-file" in prep
|
||||||
|
assert "obsigate-version-files" in prep
|
||||||
Reference in New Issue
Block a user