feat: display release notes from CHANGELOG in GitHub releases and in-app updater

Extract changelog sections in CI to populate GitHub release bodies dynamically.
Expose the update body from tauri-plugin-updater and render it in the settings
page when an update is available. Add CHANGELOG discipline rule to CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-02-21 10:12:04 -05:00
parent b353165f61
commit a293bdcd4b
7 changed files with 79 additions and 6 deletions

View file

@ -33,6 +33,25 @@ jobs:
- name: Install frontend dependencies
run: npm ci
- name: Extract changelog
id: changelog
shell: bash
run: |
TAG="${GITHUB_REF_NAME#v}"
# Extract section between ## [TAG] (or ## TAG) and next ## header
NOTES=$(sed -n "/^## \[${TAG}\]/,/^## /{/^## \[${TAG}\]/d;/^## /d;p}" CHANGELOG.md)
if [ -z "$NOTES" ]; then
NOTES=$(sed -n "/^## ${TAG}/,/^## /{/^## ${TAG}/d;/^## /d;p}" CHANGELOG.md)
fi
# Trim leading/trailing blank lines
NOTES=$(echo "$NOTES" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba}')
# Write to multiline output
{
echo "notes<<CHANGELOG_EOF"
echo "$NOTES"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and release
uses: tauri-apps/tauri-action@v0
env:
@ -43,6 +62,10 @@ jobs:
tagName: ${{ github.ref_name }}
releaseName: "Simpl'Résultat ${{ github.ref_name }}"
releaseBody: |
${{ steps.changelog.outputs.notes }}
---
## Installation
**Windows** : Téléchargez le fichier `.exe` ci-dessous et lancez l'installation.
@ -87,6 +110,22 @@ jobs:
- name: Install frontend dependencies
run: npm ci
- name: Extract changelog
id: changelog
shell: bash
run: |
TAG="${GITHUB_REF_NAME#v}"
NOTES=$(sed -n "/^## \[${TAG}\]/,/^## /{/^## \[${TAG}\]/d;/^## /d;p}" CHANGELOG.md)
if [ -z "$NOTES" ]; then
NOTES=$(sed -n "/^## ${TAG}/,/^## /{/^## ${TAG}/d;/^## /d;p}" CHANGELOG.md)
fi
NOTES=$(echo "$NOTES" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba}')
{
echo "notes<<CHANGELOG_EOF"
echo "$NOTES"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and release
uses: tauri-apps/tauri-action@v0
env:
@ -97,6 +136,10 @@ jobs:
tagName: ${{ github.ref_name }}
releaseName: "Simpl'Résultat ${{ github.ref_name }}"
releaseBody: |
${{ steps.changelog.outputs.notes }}
---
## Installation
**Windows** : Téléchargez le fichier `.exe` ci-dessous et lancez l'installation.

View file

@ -1,6 +1,8 @@
# Changelog
## 0.3.7
## [Unreleased]
## [0.3.7]
### Fixes
- Remove MSI bundle to prevent updater install path conflict

View file

@ -134,6 +134,10 @@ La documentation technique est centralisée dans `docs/` :
- Décision technique structurante (choix de librairie, pattern architectural, changement de stratégie) → créer un nouvel ADR dans `docs/adr/`
- Changement affectant l'utilisation de l'app → mettre à jour `docs/guide-utilisateur.md` et les traductions i18n correspondantes (`src/i18n/locales/fr.json`, `src/i18n/locales/en.json`, clés sous `docs.*`)
**Règle CHANGELOG :** tout changement affectant le comportement utilisateur → ajouter une entrée sous `## [Unreleased]` dans `CHANGELOG.md`
- Catégories : Added, Changed, Fixed, Removed
- Format [Keep a Changelog](https://keepachangelog.com/). Le contenu est extrait automatiquement par le CI pour les release notes GitHub et affiché dans l'app.
---
## Points d'attention RS&DE / CRIC

View file

@ -15,6 +15,7 @@ type UpdateStatus =
interface UpdaterState {
status: UpdateStatus;
version: string | null;
body: string | null;
progress: number;
contentLength: number | null;
error: string | null;
@ -23,7 +24,7 @@ interface UpdaterState {
type UpdaterAction =
| { type: "CHECK_START" }
| { type: "UP_TO_DATE" }
| { type: "AVAILABLE"; version: string }
| { type: "AVAILABLE"; version: string; body: string | null }
| { type: "DOWNLOAD_START" }
| { type: "DOWNLOAD_PROGRESS"; downloaded: number; contentLength: number | null }
| { type: "READY_TO_INSTALL" }
@ -33,6 +34,7 @@ type UpdaterAction =
const initialState: UpdaterState = {
status: "idle",
version: null,
body: null,
progress: 0,
contentLength: null,
error: null,
@ -45,7 +47,7 @@ function reducer(state: UpdaterState, action: UpdaterAction): UpdaterState {
case "UP_TO_DATE":
return { ...state, status: "upToDate", error: null };
case "AVAILABLE":
return { ...state, status: "available", version: action.version, error: null };
return { ...state, status: "available", version: action.version, body: action.body, error: null };
case "DOWNLOAD_START":
return { ...state, status: "downloading", progress: 0, contentLength: null, error: null };
case "DOWNLOAD_PROGRESS":
@ -69,7 +71,7 @@ export function useUpdater() {
const update = await check();
if (update) {
updateRef.current = update;
dispatch({ type: "AVAILABLE", version: update.version });
dispatch({ type: "AVAILABLE", version: update.version, body: update.body ?? null });
} else {
dispatch({ type: "UP_TO_DATE" });
}

View file

@ -382,7 +382,8 @@
"installButton": "Install and restart",
"installing": "Installing...",
"error": "Update failed",
"retryButton": "Retry"
"retryButton": "Retry",
"releaseNotes": "What's New"
},
"dataManagement": {
"title": "Data Management",

View file

@ -382,7 +382,8 @@
"installButton": "Installer et redémarrer",
"installing": "Installation en cours...",
"error": "Erreur lors de la mise à jour",
"retryButton": "Réessayer"
"retryButton": "Réessayer",
"releaseNotes": "Nouveautés"
},
"dataManagement": {
"title": "Gestion des données",

View file

@ -125,6 +125,26 @@ export default function SettingsPage() {
<p>
{t("settings.updates.available", { version: state.version })}
</p>
{state.body && (
<div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--foreground)]">
{t("settings.updates.releaseNotes")}
</h3>
<div className="max-h-48 overflow-y-auto rounded-lg bg-[var(--background)] border border-[var(--border)] p-3 text-sm text-[var(--muted-foreground)] space-y-1">
{state.body.split("\n").map((line, i) => {
const trimmed = line.trim();
if (!trimmed) return <div key={i} className="h-2" />;
if (trimmed.startsWith("### "))
return <p key={i} className="font-semibold text-[var(--foreground)] mt-2">{trimmed.slice(4)}</p>;
if (trimmed.startsWith("## "))
return <p key={i} className="font-bold text-[var(--foreground)] mt-2">{trimmed.slice(3)}</p>;
if (trimmed.startsWith("- "))
return <p key={i} className="pl-3">{"\u2022 "}{trimmed.slice(2).replace(/\*\*(.+?)\*\*/g, "$1")}</p>;
return <p key={i}>{trimmed}</p>;
})}
</div>
</div>
)}
<button
onClick={downloadAndInstall}
className="flex items-center gap-2 px-4 py-2 bg-[var(--primary)] text-white rounded-lg hover:opacity-90 transition-opacity"