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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## 0.3.7
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.7]
|
||||
|
||||
### Fixes
|
||||
- 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/`
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue