Skip to content

Build and Release

Build and Release #55

Workflow file for this run

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