- README : fonctionnalités, installation, build, tests (302 + 20 E2E), couverture 94%, workflow mise à jour latest.yml, changelog v1.1.6 - CLAUDE.md : règles de collaboration, architecture, conventions - Plans de conception : DDD, electron-updater, accouplement, toast Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
391 lines
12 KiB
Markdown
391 lines
12 KiB
Markdown
# 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 |
|