dd-timer/docs/plans/2026-04-04-electron-updater-implementation.md
POL Mickaël 2893013093 docs: README, CLAUDE.md, changelog, plans de conception
- 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>
2026-04-06 05:43:38 +02:00

12 KiB

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

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" :

"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

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 :

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 :

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é :

// ─── 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 :

ipcMain.on('install-update', () => startDownload());

Par :

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 :

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

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-downloadingATTENTION : 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 :

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)

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

npm run build

Step 2: Vérifier que dist/latest.yml existe

ls dist/latest.yml
cat dist/latest.yml

Contenu attendu (exemple) :

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é

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) :

### 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

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 :
    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