Build and Release #55
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
| name: Build and Release | |
| on: | |
| push: | |
| branches: | |
| - master | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Tag to build (e.g., v1.5.0)' | |
| required: true | |
| type: string | |
| jobs: | |
| build: | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-latest, windows-2022, ubuntu-latest] | |
| include: | |
| - os: macos-latest | |
| platform: mac | |
| artifact_name: mac | |
| - os: windows-2022 | |
| platform: win | |
| artifact_name: win | |
| - os: ubuntu-latest | |
| platform: linux | |
| artifact_name: linux | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| if: matrix.platform != 'win' | |
| run: npm ci | |
| timeout-minutes: 10 | |
| - name: Install dependencies (Windows) | |
| if: matrix.platform == 'win' | |
| run: npm ci --no-audit --no-fund | |
| timeout-minutes: 30 | |
| shell: pwsh | |
| - name: Preflight macOS signing secrets | |
| if: matrix.platform == 'mac' | |
| shell: bash | |
| run: | | |
| missing=0 | |
| check() { | |
| name="$1" | |
| val="$2" | |
| if [ -z "$val" ]; then | |
| echo "Missing required secret: $name" | |
| missing=1 | |
| fi | |
| } | |
| check "MACOS_CERT_P12_BASE64" "${{ secrets.MACOS_CERT_P12_BASE64 }}" | |
| check "MACOS_CERT_P12_PASSWORD" "${{ secrets.MACOS_CERT_P12_PASSWORD }}" | |
| check "APPLE_TEAM_ID" "${{ secrets.APPLE_TEAM_ID }}" | |
| check "APPLE_API_KEY_ID" "${{ secrets.APPLE_API_KEY_ID }}" | |
| check "APPLE_API_ISSUER" "${{ secrets.APPLE_API_ISSUER }}" | |
| check "APPLE_API_KEY_P8_BASE64" "${{ secrets.APPLE_API_KEY_P8_BASE64 }}" | |
| if [ "$missing" -ne 0 ]; then | |
| echo "One or more required secrets are missing. Go to Settings → Secrets and variables → Actions and verify the names exactly." | |
| exit 1 | |
| fi | |
| echo "macOS signing secrets appear present." | |
| # Check for MAS provisioning profile (optional - will warn if missing) | |
| if [ -z "${{ secrets.MAS_PROVISIONING_PROFILE_BASE64 }}" ]; then | |
| echo "⚠️ Warning: MAS_PROVISIONING_PROFILE_BASE64 not set. MAS build may fail." | |
| else | |
| echo "✅ MAS provisioning profile secret found." | |
| fi | |
| - name: Build for mac (macOS, signed + notarized) | |
| if: matrix.platform == 'mac' | |
| timeout-minutes: 45 | |
| run: | | |
| # Unset GitHub tokens to prevent electron-builder from auto-publishing | |
| unset GH_TOKEN | |
| unset GITHUB_TOKEN | |
| # Write App Store Connect API key to disk for notarytool | |
| echo "${{ secrets.APPLE_API_KEY_P8_BASE64 }}" | base64 --decode > /tmp/AuthKey.p8 | |
| chmod 600 /tmp/AuthKey.p8 | |
| export APPLE_API_KEY=/tmp/AuthKey.p8 | |
| # Create an entitlements plist for hardened runtime (Electron helpers need JIT allowances) | |
| cat > /tmp/entitlements.mac.plist <<'PLIST' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>com.apple.security.cs.allow-jit</key> | |
| <true/> | |
| <key>com.apple.security.cs.allow-unsigned-executable-memory</key> | |
| <true/> | |
| <key>com.apple.security.cs.disable-library-validation</key> | |
| <true/> | |
| </dict> | |
| </plist> | |
| PLIST | |
| set -o pipefail | |
| npm run build:desktop:mac -- \ | |
| -c.mac.hardenedRuntime=true \ | |
| -c.mac.entitlements=/tmp/entitlements.mac.plist \ | |
| -c.mac.entitlementsInherit=/tmp/entitlements.mac.plist \ | |
| 2>&1 | tee /tmp/electron-builder-mac.log | |
| exit ${PIPESTATUS[0]} | |
| env: | |
| # Enable signing/notarization for mac builds | |
| CI: true | |
| CSC_IDENTITY_AUTO_DISCOVERY: true | |
| CSC_LINK: ${{ secrets.MACOS_CERT_P12_BASE64 }} | |
| CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERT_P12_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_API_KEY: /tmp/AuthKey.p8 | |
| - name: Build for MAS (Mac App Store) | |
| if: matrix.platform == 'mac' | |
| timeout-minutes: 45 | |
| run: | | |
| # Unset GitHub tokens to prevent electron-builder from auto-publishing | |
| unset GH_TOKEN | |
| unset GITHUB_TOKEN | |
| # Unset notarization variables - MAS apps are validated by App Store Connect, not notarized | |
| unset APPLE_API_KEY | |
| unset APPLE_API_KEY_ID | |
| unset APPLE_API_ISSUER | |
| # CRITICAL: Unset APPLE_TEAM_ID to prevent electron-builder from auto-adding | |
| # com.apple.developer.team-identifier to entitlements. For MAS apps, team-identifier | |
| # must come ONLY from the provisioning profile, not from entitlements file. | |
| unset APPLE_TEAM_ID | |
| # Check if MAS certificates are available | |
| if [ -z "${{ secrets.MAS_CERT_P12_BASE64 }}" ]; then | |
| echo "❌ ERROR: MAS_CERT_P12_BASE64 secret is not set!" | |
| echo "Please add MAS_CERT_P12_BASE64 and MAS_CERT_P12_PASSWORD to GitHub Secrets" | |
| echo "The regular MACOS_CERT_P12_BASE64 only contains Developer ID certificates, not MAS certificates" | |
| exit 1 | |
| fi | |
| echo "✅ MAS certificate secret found" | |
| # Extract and install provisioning profile if provided | |
| if [ -n "${{ secrets.MAS_PROVISIONING_PROFILE_BASE64 }}" ]; then | |
| echo "Installing MAS provisioning profile..." | |
| mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles | |
| echo "${{ secrets.MAS_PROVISIONING_PROFILE_BASE64 }}" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/profile.provisionprofile | |
| echo "✅ Provisioning profile installed" | |
| else | |
| echo "⚠️ No provisioning profile provided - electron-builder will try to auto-discover" | |
| fi | |
| set -o pipefail | |
| # Build MAS package (notarization disabled by unsetting APPLE_API_KEY* vars above) | |
| # APPLE_TEAM_ID is also unset to prevent auto-adding team-identifier to entitlements | |
| npm run build:desktop:mas 2>&1 | tee /tmp/electron-builder-mas.log | |
| BUILD_EXIT=$? | |
| # Verify and fix PKG signing if needed | |
| PKG_FILE=$(find dist -name "*.pkg" -type f | head -1) | |
| if [ -n "$PKG_FILE" ] && [ -f "$PKG_FILE" ]; then | |
| echo "Checking PKG signature: $PKG_FILE" | |
| # Check if PKG is signed with installer certificate | |
| if ! pkgutil --check-signature "$PKG_FILE" 2>&1 | grep -q "3rd Party Mac Developer Installer"; then | |
| echo "⚠️ PKG not signed with installer certificate, re-signing..." | |
| # Find installer identity | |
| INSTALLER_IDENTITY=$(security find-identity -v -p basic | grep "3rd Party Mac Developer Installer" | head -1 | sed 's/.*"\(.*\)".*/\1/') | |
| if [ -n "$INSTALLER_IDENTITY" ]; then | |
| echo "Found installer identity: $INSTALLER_IDENTITY" | |
| # Re-sign the PKG with installer certificate | |
| TEMP_PKG="${PKG_FILE}.temp" | |
| productsign --sign "$INSTALLER_IDENTITY" "$PKG_FILE" "$TEMP_PKG" | |
| if [ -f "$TEMP_PKG" ]; then | |
| mv "$TEMP_PKG" "$PKG_FILE" | |
| echo "✅ PKG re-signed with installer certificate" | |
| # Verify the signature | |
| pkgutil --check-signature "$PKG_FILE" | |
| else | |
| echo "❌ Failed to re-sign PKG" | |
| exit 1 | |
| fi | |
| else | |
| echo "❌ Could not find 3rd Party Mac Developer Installer certificate" | |
| exit 1 | |
| fi | |
| else | |
| echo "✅ PKG already signed with installer certificate" | |
| fi | |
| else | |
| echo "⚠️ No PKG file found" | |
| fi | |
| exit $BUILD_EXIT | |
| env: | |
| # Enable signing for MAS builds (but NOT notarization) | |
| CI: true | |
| CSC_IDENTITY_AUTO_DISCOVERY: true | |
| # MUST use MAS-specific P12 (contains 3rd Party Mac Developer certificates) | |
| # Regular MACOS_CERT_P12_BASE64 only has Developer ID certificates which won't work for MAS | |
| CSC_LINK: ${{ secrets.MAS_CERT_P12_BASE64 }} | |
| CSC_KEY_PASSWORD: ${{ secrets.MAS_CERT_P12_PASSWORD }} | |
| # NOTE: APPLE_TEAM_ID is intentionally NOT set here - it causes electron-builder | |
| # to add com.apple.developer.team-identifier to entitlements, which breaks MAS validation. | |
| # The team-identifier must come ONLY from the provisioning profile. | |
| # Do NOT set APPLE_API_KEY* variables - they trigger notarization which MAS apps don't need | |
| # electron-builder should auto-discover the installer certificate from the P12 | |
| - name: Build for Linux | |
| if: matrix.platform == 'linux' | |
| timeout-minutes: 30 | |
| run: | | |
| # Unset GitHub tokens to prevent electron-builder from auto-publishing | |
| unset GH_TOKEN | |
| unset GITHUB_TOKEN | |
| npm run build:desktop:linux | |
| env: | |
| CSC_IDENTITY_AUTO_DISCOVERY: false | |
| CI: true | |
| - name: Build for Windows | |
| if: matrix.platform == 'win' | |
| timeout-minutes: 45 | |
| continue-on-error: true | |
| run: npm run build:desktop:win | |
| shell: pwsh | |
| env: | |
| # Disable electron-builder auto-publish and code signing | |
| CSC_IDENTITY_AUTO_DISCOVERY: false | |
| CI: true | |
| - name: Prepare artifacts for upload | |
| if: matrix.platform != 'win' | |
| run: | | |
| # Create a filtered artifacts directory with only final distribution files | |
| mkdir -p artifacts-to-upload | |
| # Copy only final distribution files (not intermediate builds or debug files) | |
| # Exclude: extension/, mas-arm64/, mac-arm64/ (intermediate builds) | |
| # Include: *.dmg, *.zip, *.pkg, *.blockmap, *.AppImage, *.deb files | |
| find dist -type f \( \ | |
| -name "*.dmg" -o \ | |
| -name "*.zip" -o \ | |
| -name "*.pkg" -o \ | |
| -name "*.blockmap" -o \ | |
| -name "*.AppImage" -o \ | |
| -name "*.deb" \ | |
| \) -not -path "*/extension/*" \ | |
| -not -path "*/mas-arm64/*" \ | |
| -not -path "*/mac-arm64/*" \ | |
| -not -name "builder-debug.yml" \ | |
| -not -name "builder-effective-config.yaml" \ | |
| -not -name "latest-mac.yml" \ | |
| -exec cp {} artifacts-to-upload/ \; | |
| # Show what we're uploading | |
| echo "📦 Artifacts to upload (final distribution files only):" | |
| ls -lh artifacts-to-upload/ || echo "⚠️ No artifacts found!" | |
| # Show size reduction | |
| ORIGINAL_SIZE=$(du -sh dist 2>/dev/null | awk '{print $1}' || echo "unknown") | |
| FILTERED_SIZE=$(du -sh artifacts-to-upload 2>/dev/null | awk '{print $1}' || echo "0") | |
| echo "📊 Size: Original dist/=$ORIGINAL_SIZE, Filtered=$FILTERED_SIZE" | |
| # Fail if no artifacts found for macOS (Linux is OK to be empty) | |
| if [ "${{ matrix.platform }}" = "mac" ] && [ ! "$(ls -A artifacts-to-upload)" ]; then | |
| echo "❌ ERROR: No distribution files found for macOS build!" | |
| echo " Checked for: *.dmg, *.zip, *.pkg, *.blockmap" | |
| exit 1 | |
| fi | |
| shell: bash | |
| - name: Upload artifacts (macOS/Linux) | |
| if: matrix.platform != 'win' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }}-build | |
| path: artifacts-to-upload/ | |
| retention-days: 30 | |
| if-no-files-found: ${{ matrix.platform == 'linux' && 'warn' || 'error' }} | |
| - name: Upload artifacts (Windows) | |
| if: matrix.platform == 'win' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }}-build | |
| path: dist/ | |
| retention-days: 30 | |
| if-no-files-found: warn | |
| release: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| # Only run release job on tag pushes or workflow_dispatch (not regular branch pushes) | |
| if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Prepare release files | |
| run: | | |
| mkdir -p release-assets | |
| # Find and copy all build artifacts (installers, zips, blockmaps) | |
| find artifacts -type f \( \ | |
| -name "*.dmg" -o \ | |
| -name "*.zip" -o \ | |
| -name "*.exe" -o \ | |
| -name "*.AppImage" -o \ | |
| -name "*.deb" -o \ | |
| -name "*.pkg" -o \ | |
| -name "*.blockmap" \ | |
| \) -not -path "*/node_modules/*" -exec cp {} release-assets/ \; | |
| echo "Found release assets:" | |
| ls -lh release-assets/ || echo "Warning: No release assets found!" | |
| # Don't fail if no assets - Windows might have failed | |
| if [ ! "$(ls -A release-assets)" ]; then | |
| echo "Warning: No release assets to upload! (This is OK if Windows build failed)" | |
| fi | |
| - name: Get tag name | |
| id: tag | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| TAG="${{ github.event.inputs.tag }}" | |
| elif [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| else | |
| echo "Error: Cannot determine tag name for non-tag push" | |
| exit 1 | |
| fi | |
| echo "tag=${TAG}" >> $GITHUB_OUTPUT | |
| echo "Tag to use: ${TAG}" | |
| - name: Check release assets | |
| run: | | |
| echo "Release assets to upload:" | |
| ls -lh release-assets/ || echo "No release-assets directory found" | |
| find release-assets -type f || echo "No files found in release-assets" | |
| - name: Delete existing release assets | |
| run: | | |
| TAG="${{ steps.tag.outputs.tag }}" | |
| # Get release ID | |
| RELEASE_ID=$(gh api repos/${{ github.repository }}/releases/tags/${TAG} --jq '.id' 2>/dev/null || echo "") | |
| if [ -n "$RELEASE_ID" ]; then | |
| echo "Found existing release, deleting old assets..." | |
| # Get all asset IDs and delete them | |
| gh api repos/${{ github.repository }}/releases/${RELEASE_ID}/assets --jq '.[].id' | while read asset_id; do | |
| if [ -n "$asset_id" ]; then | |
| echo "Deleting asset ID: $asset_id" | |
| gh api -X DELETE repos/${{ github.repository }}/releases/assets/$asset_id || true | |
| fi | |
| done | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| continue-on-error: true | |
| - name: Update or Create Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| files: release-assets/* | |
| draft: false | |
| prerelease: false | |
| make_latest: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |