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:
parent
b353165f61
commit
a293bdcd4b
7 changed files with 79 additions and 6 deletions
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
|
|
@ -33,6 +33,25 @@ jobs:
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: npm ci
|
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
|
- name: Build and release
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
|
|
@ -43,6 +62,10 @@ jobs:
|
||||||
tagName: ${{ github.ref_name }}
|
tagName: ${{ github.ref_name }}
|
||||||
releaseName: "Simpl'Résultat ${{ github.ref_name }}"
|
releaseName: "Simpl'Résultat ${{ github.ref_name }}"
|
||||||
releaseBody: |
|
releaseBody: |
|
||||||
|
${{ steps.changelog.outputs.notes }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
**Windows** : Téléchargez le fichier `.exe` ci-dessous et lancez l'installation.
|
**Windows** : Téléchargez le fichier `.exe` ci-dessous et lancez l'installation.
|
||||||
|
|
@ -87,6 +110,22 @@ jobs:
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: npm ci
|
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
|
- name: Build and release
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
|
|
@ -97,6 +136,10 @@ jobs:
|
||||||
tagName: ${{ github.ref_name }}
|
tagName: ${{ github.ref_name }}
|
||||||
releaseName: "Simpl'Résultat ${{ github.ref_name }}"
|
releaseName: "Simpl'Résultat ${{ github.ref_name }}"
|
||||||
releaseBody: |
|
releaseBody: |
|
||||||
|
${{ steps.changelog.outputs.notes }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
**Windows** : Téléchargez le fichier `.exe` ci-dessous et lancez l'installation.
|
**Windows** : Téléchargez le fichier `.exe` ci-dessous et lancez l'installation.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.3.7
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.7]
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Remove MSI bundle to prevent updater install path conflict
|
- Remove MSI bundle to prevent updater install path conflict
|
||||||
|
|
|
||||||
|
|
@ -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/`
|
- 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.*`)
|
- 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
|
## Points d'attention RS&DE / CRIC
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type UpdateStatus =
|
||||||
interface UpdaterState {
|
interface UpdaterState {
|
||||||
status: UpdateStatus;
|
status: UpdateStatus;
|
||||||
version: string | null;
|
version: string | null;
|
||||||
|
body: string | null;
|
||||||
progress: number;
|
progress: number;
|
||||||
contentLength: number | null;
|
contentLength: number | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
@ -23,7 +24,7 @@ interface UpdaterState {
|
||||||
type UpdaterAction =
|
type UpdaterAction =
|
||||||
| { type: "CHECK_START" }
|
| { type: "CHECK_START" }
|
||||||
| { type: "UP_TO_DATE" }
|
| { type: "UP_TO_DATE" }
|
||||||
| { type: "AVAILABLE"; version: string }
|
| { type: "AVAILABLE"; version: string; body: string | null }
|
||||||
| { type: "DOWNLOAD_START" }
|
| { type: "DOWNLOAD_START" }
|
||||||
| { type: "DOWNLOAD_PROGRESS"; downloaded: number; contentLength: number | null }
|
| { type: "DOWNLOAD_PROGRESS"; downloaded: number; contentLength: number | null }
|
||||||
| { type: "READY_TO_INSTALL" }
|
| { type: "READY_TO_INSTALL" }
|
||||||
|
|
@ -33,6 +34,7 @@ type UpdaterAction =
|
||||||
const initialState: UpdaterState = {
|
const initialState: UpdaterState = {
|
||||||
status: "idle",
|
status: "idle",
|
||||||
version: null,
|
version: null,
|
||||||
|
body: null,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
contentLength: null,
|
contentLength: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
@ -45,7 +47,7 @@ function reducer(state: UpdaterState, action: UpdaterAction): UpdaterState {
|
||||||
case "UP_TO_DATE":
|
case "UP_TO_DATE":
|
||||||
return { ...state, status: "upToDate", error: null };
|
return { ...state, status: "upToDate", error: null };
|
||||||
case "AVAILABLE":
|
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":
|
case "DOWNLOAD_START":
|
||||||
return { ...state, status: "downloading", progress: 0, contentLength: null, error: null };
|
return { ...state, status: "downloading", progress: 0, contentLength: null, error: null };
|
||||||
case "DOWNLOAD_PROGRESS":
|
case "DOWNLOAD_PROGRESS":
|
||||||
|
|
@ -69,7 +71,7 @@ export function useUpdater() {
|
||||||
const update = await check();
|
const update = await check();
|
||||||
if (update) {
|
if (update) {
|
||||||
updateRef.current = update;
|
updateRef.current = update;
|
||||||
dispatch({ type: "AVAILABLE", version: update.version });
|
dispatch({ type: "AVAILABLE", version: update.version, body: update.body ?? null });
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: "UP_TO_DATE" });
|
dispatch({ type: "UP_TO_DATE" });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,8 @@
|
||||||
"installButton": "Install and restart",
|
"installButton": "Install and restart",
|
||||||
"installing": "Installing...",
|
"installing": "Installing...",
|
||||||
"error": "Update failed",
|
"error": "Update failed",
|
||||||
"retryButton": "Retry"
|
"retryButton": "Retry",
|
||||||
|
"releaseNotes": "What's New"
|
||||||
},
|
},
|
||||||
"dataManagement": {
|
"dataManagement": {
|
||||||
"title": "Data Management",
|
"title": "Data Management",
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,8 @@
|
||||||
"installButton": "Installer et redémarrer",
|
"installButton": "Installer et redémarrer",
|
||||||
"installing": "Installation en cours...",
|
"installing": "Installation en cours...",
|
||||||
"error": "Erreur lors de la mise à jour",
|
"error": "Erreur lors de la mise à jour",
|
||||||
"retryButton": "Réessayer"
|
"retryButton": "Réessayer",
|
||||||
|
"releaseNotes": "Nouveautés"
|
||||||
},
|
},
|
||||||
"dataManagement": {
|
"dataManagement": {
|
||||||
"title": "Gestion des données",
|
"title": "Gestion des données",
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,26 @@ export default function SettingsPage() {
|
||||||
<p>
|
<p>
|
||||||
{t("settings.updates.available", { version: state.version })}
|
{t("settings.updates.available", { version: state.version })}
|
||||||
</p>
|
</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
|
<button
|
||||||
onClick={downloadAndInstall}
|
onClick={downloadAndInstall}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-[var(--primary)] text-white rounded-lg hover:opacity-90 transition-opacity"
|
className="flex items-center gap-2 px-4 py-2 bg-[var(--primary)] text-white rounded-lg hover:opacity-90 transition-opacity"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue