dd-timer/docs/plans/2026-04-04-electron-updater-implementation.md
POL Mickaël 3e485fd09b chore: normalise fins de ligne CRLF → LF dans tout le repo
Applique .gitattributes sur tous les fichiers existants.
Élimine les différences fantômes entre WSL et Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 08:55:10 +02:00

391 lines
12 KiB
Markdown
Executable File

# Migration electron-updater + Gitea — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Remplacer le système de mise à jour custom (download HTTP + script batch) par `electron-updater`, en gardant Gitea comme source de releases.
**Architecture:** Approche hybride — appel API Gitea pour découvrir la dernière version (car Gitea n'a pas d'URL fixe "latest"), puis `electron-updater` (generic provider) pour le cycle download → vérification sha512 → installation NSIS → restart. Les IPC channels restent identiques pour que `UpdateBanner.ts` et `preload.ts` n'aient aucun changement.
**Tech Stack:** electron-updater 6.x, Electron 32.x, TypeScript
---
### Task 1: Installer electron-updater et configurer package.json
**Files:**
- Modify: `package.json`
**Step 1: Installer la dépendance**
```bash
npm install electron-updater
```
> `electron-updater` doit être dans `dependencies` (pas `devDependencies`) car il tourne dans l'app packagée.
**Step 2: Ajouter la config `publish` dans la section `build`**
Dans `package.json`, ajouter dans `"build"` :
```json
"publish": {
"provider": "generic",
"url": "https://gitea.mickael-pol.fr/mickael/dd-timer/releases/download/latest"
}
```
> Cette URL est un placeholder — au runtime, on la remplace dynamiquement via `setFeedURL()`.
> Son seul rôle est de déclencher la génération de `latest.yml` par electron-builder.
**Step 3: Commit**
```bash
git add package.json package-lock.json
git commit -m "chore: add electron-updater dependency + publish config"
```
---
### Task 2: Réécrire la section mise à jour dans main.ts
**Files:**
- Modify: `src/infrastructure/electron/main.ts:1-16` (imports)
- Modify: `src/infrastructure/electron/main.ts:37-48` (UpdateInfo + state vars)
- Delete: `src/infrastructure/electron/main.ts:254-450` (tout le bloc update custom)
- Add: nouveau bloc update avec electron-updater (~60 lignes)
**Step 1: Ajouter l'import electron-updater, supprimer les imports inutiles**
Remplacer les imports en haut du fichier :
```typescript
import {
app,
BrowserWindow,
Tray,
Menu,
nativeImage,
ipcMain,
Notification,
dialog,
} from 'electron';
import path from 'path';
import fs from 'fs';
import { autoUpdater } from 'electron-updater';
```
> Supprimer : `import https from 'https'`, `import http from 'http'`, `import os from 'os'`, `import { spawn } from 'child_process'` — ils ne sont plus nécessaires pour les mises à jour.
>
> **Attention** : `https` et `http` sont encore utilisés par le bloc ntfy (lignes 214-243). Vérifier si d'autres usages existent avant de supprimer. Si ntfy les utilise → garder `https` et `http`. Supprimer seulement `os` et `spawn` si plus aucun usage.
**Step 2: Simplifier les variables d'état**
Remplacer l'interface `UpdateInfo` et les variables `updateInfo` / `downloading` :
```typescript
interface UpdateInfo {
version: string;
}
let updateInfo: UpdateInfo | null = null;
```
> On supprime `downloadUrl`, `assetName`, `releaseNotes` — electron-updater gère tout ça.
> On supprime `downloading` — electron-updater gère l'état.
**Step 3: Supprimer tout le bloc update custom**
Supprimer entièrement (lignes 254-450) :
- `compareVersions()`
- Interfaces `GiteaAsset`, `GiteaRelease`
- `checkForUpdates()`
- `startDownload()`
- `sendUpdateError()`
- `launchUpdater()`
**Step 4: Écrire le nouveau système de mise à jour**
Ajouter à la place du code supprimé :
```typescript
// ─── COMPARAISON DE VERSIONS ────────────────────────────────────────────────
function compareVersions(a: string, b: string): number {
const pa = a.replace(/^v/, '').split('.').map(Number);
const pb = b.replace(/^v/, '').split('.').map(Number);
for (let i = 0; i < 3; i++) {
if ((pb[i] || 0) > (pa[i] || 0)) return 1;
if ((pb[i] || 0) < (pa[i] || 0)) return -1;
}
return 0;
}
// ─── ELECTRON-UPDATER : CONFIGURATION ───────────────────────────────────────
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = false;
// ─── ELECTRON-UPDATER : EVENTS ──────────────────────────────────────────────
autoUpdater.on('update-available', (info) => {
updateInfo = { version: info.version };
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-available', updateInfo);
}
fireNotification(
`Mise a jour v${info.version} disponible !`,
'Téléchargement en cours...'
);
rebuildTrayMenu();
});
autoUpdater.on('download-progress', (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-progress', { percent: Math.round(progress.percent) });
}
});
autoUpdater.on('update-downloaded', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-ready', updateInfo ?? {});
}
});
autoUpdater.on('error', (err) => {
console.error('Update error:', err.message);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-error', { message: err.message });
}
});
// ─── VÉRIFICATION VIA API GITEA ─────────────────────────────────────────────
function checkForUpdates(silent = false): void {
const https = require('https') as typeof import('https');
const options: import('https').RequestOptions = {
hostname: GITEA_HOST,
port: 443,
path: `/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases?limit=1`,
method: 'GET',
headers: {
'User-Agent': `MinuteurDragodinde/${CURRENT_VERSION}`,
'Accept': 'application/json',
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk: string) => { data += chunk; });
res.on('end', () => {
try {
const releases = JSON.parse(data);
if (!Array.isArray(releases) || releases.length === 0) return;
const release = releases[0];
const latestVersion = release.tag_name;
if (!latestVersion || compareVersions(CURRENT_VERSION, latestVersion) <= 0) {
if (!silent && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-not-available');
}
return;
}
// Pointer electron-updater vers le tag de la release
const tag = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
autoUpdater.setFeedURL({
provider: 'generic',
url: `https://${GITEA_HOST}/${GITEA_USER}/${GITEA_REPO}/releases/download/${tag}`,
});
// electron-updater prend le relais : lit latest.yml, télécharge, vérifie
autoUpdater.checkForUpdates();
} catch (e: unknown) {
console.error('Update check parse error:', (e as Error).message);
}
});
});
req.on('error', (e: Error) => console.error('Update check error:', e.message));
req.end();
}
```
**Step 5: Mettre à jour le handler `install-update`**
Remplacer :
```typescript
ipcMain.on('install-update', () => startDownload());
```
Par :
```typescript
ipcMain.on('install-update', () => {
autoUpdater.quitAndInstall(true, true);
});
```
> `quitAndInstall(isSilent, isForceRunAfter)` : installe en silencieux et relance l'app.
**Step 6: Mettre à jour le tray menu**
Dans `rebuildTrayMenu()`, remplacer le click du menu update :
```typescript
if (updateInfo) {
items.push({ type: 'separator' });
items.push({
label: `⬆ Mise a jour v${updateInfo.version} disponible !`,
click: () => autoUpdater.quitAndInstall(true, true),
});
}
```
**Step 7: Simplifier le bloc did-finish-load**
Le bloc `did-finish-load` (ligne 115-133) reste identique — il envoie déjà `update-available` si `updateInfo` existe.
**Step 8: Commit**
```bash
git add src/infrastructure/electron/main.ts
git commit -m "feat: migrate update system to electron-updater"
```
---
### Task 3: Vérifier que preload.ts et UpdateBanner.ts n'ont pas besoin de changement
**Files:**
- Read: `src/infrastructure/electron/preload.ts`
- Read: `src/presentation/components/UpdateBanner.ts`
**Step 1: Vérifier les IPC channels**
Les channels IPC n'ont pas changé :
- `update-available``{ version }`
- `update-downloading`**ATTENTION** : l'ancien code envoyait `update-downloading`, mais electron-updater n'a pas cet event. Il passe directement de `update-available` à `download-progress`. Le banner state `downloading` est activé par `onUpdateDownloading` OU `onUpdateProgress`. Vérifier que `UpdateBanner` gère bien la transition.
**Step 2: Envoyer `update-downloading` explicitement**
Dans les events autoUpdater, ajouter après `update-available` :
```typescript
autoUpdater.on('update-available', (info) => {
updateInfo = { version: info.version };
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-available', updateInfo);
// Envoyer aussi downloading car autoDownload = true
mainWindow.webContents.send('update-downloading', { version: info.version });
}
fireNotification(
`Mise a jour v${info.version} disponible !`,
'Téléchargement en cours...'
);
rebuildTrayMenu();
});
```
> Cela garantit que `UpdateBanner` passe bien en état `downloading` comme avant.
**Step 3: Commit (si changement)**
```bash
git add src/infrastructure/electron/main.ts
git commit -m "fix: send update-downloading event for banner compatibility"
```
---
### Task 4: Vérifier la génération de latest.yml
**Step 1: Lancer un build**
```bash
npm run build
```
**Step 2: Vérifier que `dist/latest.yml` existe**
```bash
ls dist/latest.yml
cat dist/latest.yml
```
Contenu attendu (exemple) :
```yaml
version: 1.1.5
files:
- url: Minuteur-Dragodinde-Setup-1.1.5.exe
sha512: <hash>
size: <size>
path: Minuteur-Dragodinde-Setup-1.1.5.exe
sha512: <hash>
releaseDate: '2026-04-04T...'
```
**Step 3: Vérifier que le .exe est aussi généré**
```bash
ls dist/Minuteur-Dragodinde-Setup-*.exe
```
---
### Task 5: Mettre à jour le CHANGELOG
**Files:**
- Modify: `CHANGELOG.md`
**Step 1: Ajouter l'entrée**
Ajouter sous `## v1.1.6` (ou créer `## v1.1.7` si nouvelle version) :
```markdown
### Mise à jour automatique
- **Migration electron-updater** : remplacement du système custom (download HTTP + script batch) par `electron-updater` (generic provider)
- Vérification sha512 automatique des mises à jour
- Installation NSIS native (plus de script batch hack)
- Restart automatique après installation
- Code simplifié (~40 lignes vs ~200)
- Compatible Gitea : découverte via API + `latest.yml` uploadé en asset de release
```
**Step 2: Commit**
```bash
git add CHANGELOG.md
git commit -m "docs: add electron-updater migration to CHANGELOG"
```
---
### Task 6: Mettre à jour le workflow de release
**Step 1: Documenter le nouveau process**
Le nouveau workflow de release est :
1. Mettre à jour `version` dans `package.json`
2. `npm run build`
3. Deux fichiers sont générés dans `dist/` :
- `Minuteur-Dragodinde-Setup-X.Y.Z.exe`
- `latest.yml`
4. Commit + tag + push :
```bash
git add -A && git commit -m "vX.Y.Z"
git tag vX.Y.Z && git push && git push --tags
```
5. Sur Gitea → Releases → Créer release avec le tag
6. **Uploader les 2 fichiers** : le `.exe` ET `latest.yml`
> **IMPORTANT** : `latest.yml` DOIT être uploadé à chaque release, sinon electron-updater ne pourra pas vérifier l'intégrité du fichier.
---
## Récapitulatif des fichiers modifiés
| Fichier | Action |
|---------|--------|
| `package.json` | Ajout dep `electron-updater` + config `publish` |
| `package-lock.json` | Mis à jour automatiquement |
| `src/infrastructure/electron/main.ts` | Réécriture section update (~200 lignes → ~60 lignes) |
| `src/infrastructure/electron/preload.ts` | **Aucun changement** |
| `src/presentation/components/UpdateBanner.ts` | **Aucun changement** |
| `CHANGELOG.md` | Entrée migration |