Merge pull request #38 from cnumr/feat/2025-version #61
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ************************** | |
| # Release Electron app | |
| # Build and release on multiple OS (matrix) to generate installers | |
| # Triggered after PR merge to main with changesets | |
| # ************************** | |
| name: Release Electron app | |
| on: | |
| push: | |
| branches: | |
| - main | |
| # Ne se déclenche que si le commit contient "chore: version packages" | |
| # Cela évite de builder à chaque merge, seulement après la PR de version | |
| workflow_dispatch: | |
| concurrency: ${{ github.workflow }}-${{ github.ref }} | |
| jobs: | |
| check-release: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should-release: ${{ steps.check.outputs.should-release }} | |
| version: ${{ steps.check.outputs.version }} | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Check if version changed (should release) | |
| id: check | |
| run: | | |
| # Vérifier si le dernier commit contient "chore: version packages" (créé par changeset) | |
| # Ce commit est créé par le workflow changeset.yml après le merge de la PR de version | |
| LAST_COMMIT=$(git log -1 --pretty=%B) | |
| if echo "$LAST_COMMIT" | grep -q "chore: version packages"; then | |
| VERSION=$(node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); console.log(pkg.version);") | |
| echo "should-release=true" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "✅ Version détectée: $VERSION - Le build va être déclenché" | |
| else | |
| echo "should-release=false" >> $GITHUB_OUTPUT | |
| echo "version=" >> $GITHUB_OUTPUT | |
| echo "⏭️ Pas de version à publier (commit: $LAST_COMMIT)" | |
| echo "⏭️ Le workflow changeset.yml doit d'abord créer et merger une PR de version" | |
| fi | |
| build: | |
| needs: check-release | |
| if: needs.check-release.outputs.should-release == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: | |
| - { name: 'linux', image: 'ubuntu-latest' } | |
| - { name: 'windows', image: 'windows-latest' } | |
| - { name: 'macos-intel', image: 'macos-15-intel' } | |
| - { name: 'macos-arm', image: 'macos-15' } | |
| runs-on: ${{ matrix.os.image }} | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Setup Python (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Cache node modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: node_modules | |
| key: ${{ runner.os }}-modules-${{ hashFiles('package-lock.json') }} | |
| restore-keys: | | |
| ${{ runner.os }}-modules- | |
| - name: Install dependencies | |
| run: npm ci | |
| env: | |
| PUPPETEER_SKIP_DOWNLOAD: true | |
| - name: Debug APPLE_ID (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: echo ${APPLE_ID:0:3} | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| - name: Debug APPLE_APP_SPECIFIC_PASSWORD (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: echo ${APPLE_APP_SPECIFIC_PASSWORD:0:3} | |
| env: | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| - name: Debug APPLE_TEAM_ID (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: echo ${APPLE_TEAM_ID:0:3} | |
| env: | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| - name: Add macOS certificates (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: | | |
| if [ -f "add-osx-cert.sh" ] && [ -n "$CERTIFICATE_OSX_APPLICATION" ] && [ -n "$CERTIFICATE_PASSWORD" ]; then | |
| chmod +x add-osx-cert.sh && ./add-osx-cert.sh | |
| echo "✅ Certificats macOS installés" | |
| # Vérifier que le certificat est bien installé | |
| echo "Vérification des certificats installés..." | |
| security find-identity -v -p codesigning | grep "Developer ID" || echo "⚠️ Aucun certificat Developer ID trouvé" | |
| else | |
| echo "❌ ERREUR: Certificats macOS non configurés" | |
| echo "Les secrets suivants doivent être configurés dans GitHub:" | |
| echo " - APPLE_APPLICATION_CERT" | |
| echo " - APPLE_APPLICATION_CERT_PASSWORD" | |
| echo " - APPLE_IDENTITY" | |
| echo " - APPLE_ID" | |
| echo " - APPLE_APP_SPECIFIC_PASSWORD" | |
| echo " - APPLE_TEAM_ID" | |
| exit 1 | |
| fi | |
| env: | |
| CERTIFICATE_OSX_APPLICATION: ${{ secrets.APPLE_APPLICATION_CERT }} | |
| CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERT_PASSWORD }} | |
| - name: Verify signing configuration (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: | | |
| if [ -z "$APPLE_IDENTITY" ] || [ -z "$APPLE_ID" ] || [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then | |
| echo "❌ ERREUR: Configuration de signature macOS incomplète" | |
| echo "Les secrets suivants doivent être configurés:" | |
| echo " - APPLE_IDENTITY: ${APPLE_IDENTITY:+✅ défini}${APPLE_IDENTITY:-❌ manquant}" | |
| echo " - APPLE_ID: ${APPLE_ID:+✅ défini}${APPLE_ID:-❌ manquant}" | |
| echo " - APPLE_APP_SPECIFIC_PASSWORD: ${APPLE_APP_SPECIFIC_PASSWORD:+✅ défini}${APPLE_APP_SPECIFIC_PASSWORD:-❌ manquant}" | |
| echo " - APPLE_TEAM_ID: ${APPLE_TEAM_ID:+✅ défini}${APPLE_TEAM_ID:-❌ manquant}" | |
| exit 1 | |
| fi | |
| echo "✅ Configuration de signature macOS complète" | |
| echo "APPLE_IDENTITY complet: $APPLE_IDENTITY" | |
| echo "APPLE_TEAM_ID: $APPLE_TEAM_ID" | |
| # Vérifier que le certificat correspond exactement | |
| echo "Recherche du certificat correspondant..." | |
| CERT_FOUND=$(security find-identity -v -p codesigning | grep "$APPLE_IDENTITY" || echo "") | |
| if [ -z "$CERT_FOUND" ]; then | |
| echo "⚠️ ATTENTION: Le certificat '$APPLE_IDENTITY' n'a pas été trouvé exactement" | |
| echo "Certificats disponibles:" | |
| security find-identity -v -p codesigning | grep "Developer ID" | |
| echo "" | |
| echo "Vérifiez que APPLE_IDENTITY correspond EXACTEMENT au nom du certificat ci-dessus" | |
| else | |
| echo "✅ Certificat trouvé: $CERT_FOUND" | |
| fi | |
| env: | |
| APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| - name: Set macOS environment variables | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: | | |
| echo "APPLE_IDENTITY=${{ secrets.APPLE_IDENTITY }}" >> $GITHUB_ENV | |
| echo "APPLE_ID=${{ secrets.APPLE_ID }}" >> $GITHUB_ENV | |
| echo "APPLE_APP_SPECIFIC_PASSWORD=${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" >> $GITHUB_ENV | |
| echo "APPLE_TEAM_ID=${{ secrets.APPLE_TEAM_ID }}" >> $GITHUB_ENV | |
| echo "DEBUG=@electron/osx-sign,electron-forge:*" >> $GITHUB_ENV | |
| - name: Build application | |
| shell: bash | |
| run: | | |
| # Vérifications macOS uniquement | |
| if [ "${{ matrix.os.name }}" == "macos-intel" ] || [ "${{ matrix.os.name }}" == "macos-arm" ]; then | |
| # Vérifier que les variables d'environnement sont bien définies | |
| echo "Vérification des variables de signature..." | |
| echo "APPLE_IDENTITY complet: $APPLE_IDENTITY" | |
| echo "APPLE_ID: ${APPLE_ID:0:10}..." | |
| echo "APPLE_TEAM_ID: $APPLE_TEAM_ID" | |
| echo "APPLE_APP_SPECIFIC_PASSWORD: ${APPLE_APP_SPECIFIC_PASSWORD:+✅ défini}" | |
| # Vérifier que APPLE_IDENTITY contient "Developer ID" | |
| if [ -n "$APPLE_IDENTITY" ]; then | |
| if echo "$APPLE_IDENTITY" | grep -q "Developer ID"; then | |
| echo "✅ APPLE_IDENTITY contient 'Developer ID'" | |
| else | |
| echo "❌ ERREUR: APPLE_IDENTITY ne contient pas 'Developer ID'" | |
| echo " Valeur: $APPLE_IDENTITY" | |
| echo " Cela empêchera hasSigningConfig d'être true dans forge.config.js" | |
| fi | |
| else | |
| echo "❌ ERREUR: APPLE_IDENTITY n'est pas défini" | |
| fi | |
| # Vérifier que le certificat est accessible dans le keychain par défaut | |
| echo "Recherche du certificat dans le keychain..." | |
| security find-identity -v -p codesigning | |
| # Vérifier spécifiquement le certificat recherché | |
| if [ -n "$APPLE_IDENTITY" ]; then | |
| echo "Recherche du certificat exact: $APPLE_IDENTITY" | |
| CERT_MATCH=$(security find-identity -v -p codesigning | grep -i "$APPLE_IDENTITY" || echo "") | |
| if [ -z "$CERT_MATCH" ]; then | |
| echo "⚠️ ATTENTION: Le certificat '$APPLE_IDENTITY' n'a pas été trouvé exactement" | |
| echo "Certificats Developer ID disponibles:" | |
| security find-identity -v -p codesigning | grep "Developer ID" | |
| else | |
| echo "✅ Certificat trouvé: $CERT_MATCH" | |
| fi | |
| fi | |
| fi | |
| echo "" | |
| echo "🔍 Variables d'environnement avant npm run make:" | |
| echo " APPLE_IDENTITY: ${APPLE_IDENTITY:+✅ défini}${APPLE_IDENTITY:-❌ non défini}" | |
| if [ -n "$APPLE_IDENTITY" ]; then | |
| echo " APPLE_IDENTITY contient 'Developer ID': $(echo "$APPLE_IDENTITY" | grep -q "Developer ID" && echo "✅ oui" || echo "❌ non")" | |
| fi | |
| npm run make | |
| - name: Verify code signature (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: | | |
| echo "Vérification de la signature de l'application..." | |
| # Trouver l'application dans le répertoire de build | |
| # Electron Forge place les apps dans out/make/zip/darwin/{arch}/ | |
| # Chercher dans tous les répertoires d'architecture possibles | |
| APP_PATH="" | |
| TEMP_EXTRACT="" | |
| # Chercher dans tous les répertoires d'architecture | |
| for ARCH_DIR in arm64 x64; do | |
| ZIP_DIR="out/make/zip/darwin/${ARCH_DIR}" | |
| if [ -d "$ZIP_DIR" ]; then | |
| ZIP_FILE=$(find "$ZIP_DIR" -name "*.zip" -type f | head -1) | |
| if [ -n "$ZIP_FILE" ]; then | |
| echo "Extraction temporaire du ZIP pour vérification..." | |
| TEMP_EXTRACT=$(mktemp -d) | |
| ditto -xk "$ZIP_FILE" "$TEMP_EXTRACT" | |
| APP_PATH=$(find "$TEMP_EXTRACT" -name "*.app" -type d | head -1) | |
| if [ -n "$APP_PATH" ]; then | |
| break | |
| fi | |
| fi | |
| fi | |
| done | |
| # Si pas trouvé dans le ZIP, chercher directement | |
| if [ -z "$APP_PATH" ]; then | |
| APP_PATH=$(find out/make -name "*.app" -type d | head -1) | |
| fi | |
| if [ -z "$APP_PATH" ]; then | |
| echo "❌ Aucune application .app trouvée" | |
| echo "Contenu de out/make:" | |
| ls -la out/make/ || true | |
| echo "Contenu de out/make/zip/darwin:" | |
| ls -la out/make/zip/darwin/ || true | |
| exit 1 | |
| fi | |
| echo "Application trouvée: $APP_PATH" | |
| # Vérifier la signature (codesign retourne toujours un code de sortie, donc on capture la sortie) | |
| SIGNATURE_OUTPUT=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1) | |
| SIGNATURE_EXIT=$? | |
| echo "$SIGNATURE_OUTPUT" | |
| # Vérifier si la signature est "adhoc" (non signée) ou valide | |
| if echo "$SIGNATURE_OUTPUT" | grep -q "Signature=adhoc"; then | |
| echo "❌ ERREUR: L'application n'est pas signée (signature adhoc)" | |
| echo "Vérifiez que hasSigningConfig est true dans forge.config.js" | |
| echo "" | |
| echo "Debug: Variables d'environnement au moment du build:" | |
| echo " APPLE_IDENTITY était défini: ${APPLE_IDENTITY:+oui}${APPLE_IDENTITY:-non}" | |
| echo "" | |
| echo "Vérifiez les logs du step 'Build application' pour voir si hasSigningConfig était true" | |
| exit 1 | |
| elif echo "$SIGNATURE_OUTPUT" | grep -q "Signature=" && ! echo "$SIGNATURE_OUTPUT" | grep -q "Signature=adhoc"; then | |
| echo "✅ Signature vérifiée (non-adhoc)" | |
| elif [ $SIGNATURE_EXIT -eq 0 ]; then | |
| echo "✅ Signature vérifiée (code de sortie: 0)" | |
| else | |
| echo "⚠️ Avertissement: Impossible de vérifier la signature (code: $SIGNATURE_EXIT)" | |
| echo "Sortie complète:" | |
| echo "$SIGNATURE_OUTPUT" | |
| fi | |
| # Vérifier la notarisation (peut prendre quelques minutes après la notarisation) | |
| if spctl --assess --verbose --type execute "$APP_PATH" 2>&1; then | |
| echo "✅ Notarisation vérifiée" | |
| else | |
| echo "⚠️ Avertissement: La notarisation n'est pas vérifiée (peut prendre quelques minutes après la notarisation)" | |
| fi | |
| # Afficher les détails de la signature | |
| codesign -dvv "$APP_PATH" 2>&1 | head -20 | |
| # Nettoyer l'extraction temporaire | |
| if [ -n "$TEMP_EXTRACT" ] && [ -d "$TEMP_EXTRACT" ]; then | |
| rm -rf "$TEMP_EXTRACT" | |
| fi | |
| - name: Create DMG (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: | | |
| node scripts/create-dmg.js | |
| - name: Verify DMG signature (macOS only) | |
| if: matrix.os.name == 'macos-intel' || matrix.os.name == 'macos-arm' | |
| run: | | |
| echo "Vérification de la signature dans le DMG..." | |
| # Trouver le DMG créé | |
| DMG_PATH=$(find out/make -name "*.dmg" -type f | head -1) | |
| if [ -z "$DMG_PATH" ]; then | |
| echo "⚠️ Aucun DMG trouvé" | |
| exit 0 | |
| fi | |
| echo "DMG trouvé: $DMG_PATH" | |
| # Monter le DMG temporairement pour vérifier | |
| MOUNT_POINT=$(mktemp -d) | |
| hdiutil attach "$DMG_PATH" -mountpoint "$MOUNT_POINT" -quiet | |
| APP_IN_DMG=$(find "$MOUNT_POINT" -name "*.app" -type d | head -1) | |
| if [ -n "$APP_IN_DMG" ]; then | |
| echo "Vérification de la signature de l'application dans le DMG..." | |
| if codesign -dv --verbose=4 "$APP_IN_DMG" 2>&1; then | |
| echo "✅ Signature vérifiée dans le DMG" | |
| else | |
| echo "❌ ERREUR: La signature n'est pas valide dans le DMG" | |
| hdiutil detach "$MOUNT_POINT" -quiet | |
| exit 1 | |
| fi | |
| fi | |
| hdiutil detach "$MOUNT_POINT" -quiet | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.os.name }}-artifacts | |
| path: | | |
| out/**/*.deb | |
| out/**/*.dmg | |
| out/**/*setup.exe | |
| out/**/*.rpm | |
| out/**/*.zip | |
| retention-days: 7 | |
| release: | |
| needs: [check-release, build] | |
| if: needs.check-release.outputs.should-release == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Remove duplicate files | |
| run: | | |
| echo "Recherche des fichiers en double..." | |
| # Créer un dossier temporaire pour les fichiers uniques | |
| mkdir -p artifacts-unique | |
| # Trouver tous les fichiers et les copier dans artifacts-unique en évitant les doublons | |
| # Utiliser le nom de fichier comme clé unique | |
| find artifacts -type f \( -name "*.deb" -o -name "*.dmg" -o -name "*.exe" -o -name "*.rpm" -o -name "*.zip" \) | while read file; do | |
| filename=$(basename "$file") | |
| if [ ! -f "artifacts-unique/$filename" ]; then | |
| echo "Copie de $file vers artifacts-unique/$filename" | |
| cp "$file" "artifacts-unique/$filename" | |
| else | |
| echo "Fichier $filename déjà copié, ignoré: $file" | |
| fi | |
| done | |
| # Remplacer artifacts par artifacts-unique | |
| rm -rf artifacts | |
| mv artifacts-unique artifacts | |
| echo "Fichiers uniques dans artifacts:" | |
| find artifacts -type f | sort | |
| - name: Generate Release.txt | |
| run: echo ${{ github.sha }} > Release.txt | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| name: Release Electron-app ${{ needs.check-release.outputs.version }} | |
| tag_name: ${{ needs.check-release.outputs.version }} | |
| files: | | |
| Release.txt | |
| artifacts/**/*.deb | |
| artifacts/**/*.dmg | |
| artifacts/**/*setup.exe | |
| artifacts/**/*.rpm | |
| artifacts/**/*.zip | |
| generate_release_notes: true | |
| fail_on_unmatched_files: false | |
| overwrite_files: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Publish to npm (if configured) | |
| if: env.NPM_TOKEN != '' | |
| run: npm publish | |
| env: | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} |