After 48 hours of debugging, here are the non-negotiable truths about cross-platform code signing:
You'll need these values:
https://eus.codesigning.azure.net)Remove spaces from your product name everywhere:
// package.json
{
"productName": "YourAppName", // NO SPACES!
"build": {
"productName": "YourAppName",
"win": {
// DON'T use azureSignOptions from macOS - it's broken on Mac
// Build unsigned instead when building from Mac
// (azureSignOptions may work fine on Windows)
}
}
}
# .github/workflows/build-windows.yml
runs-on: windows-latest # MUST be x64, not ARM
steps:
- name: Install TrustedSigning Module
shell: powershell
run: Install-Module -Name TrustedSigning -Force
- name: Build Unsigned
run: npm run build:win:unsigned
- name: Sign with Azure
shell: powershell
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
$env:AZURE_TENANT_ID = "${{ secrets.AZURE_TENANT_ID }}"
$env:AZURE_CLIENT_ID = "${{ secrets.AZURE_CLIENT_ID }}"
$env:AZURE_CLIENT_SECRET = "${{ secrets.AZURE_CLIENT_SECRET }}"
Invoke-TrustedSigning `
-Endpoint "https://eus.codesigning.azure.net" `
-CodeSigningAccountName "YourAccount" `
-CertificateProfileName "YourProfile" `
-Files "dist/YourApp-Setup-*.exe"
<!-- entitlements.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.disable-library-validation</key>
<true/>
</dict>
</plist>
# .github/workflows/build-macos.yml
runs-on: macos-latest
steps:
- name: Import Certificates
run: |
echo "${{ secrets.MAC_CERTS_P12 }}" | base64 --decode > cert.p12
security create-keychain -p temp build.keychain
security import cert.p12 -k build.keychain -P "${{ secrets.MAC_CERTS_PASSWORD }}"
- name: Build and Sign
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npm run dist
Problem: You're on ARM64 Windows. SignTool is x64-only.
Solution: Use GitHub Actions with windows-latest (x64)
Problem: PowerShell quote escaping nightmare Solution: Use variables instead of inline strings:
# BAD
Write-Host "Path: $($env:PATH)"
# GOOD
$path = $env:PATH
Write-Host "Path: $path"
Problem: You're using AzureSignTool (wrong tool!) Solution: Use TrustedSigning PowerShell module instead
Problem: electron-builder can't parse --win.sign=false
Solution: Build with cleared environment variables:
delete process.env.AZURE_TENANT_ID;
delete process.env.AZURE_CLIENT_ID;
delete process.env.AZURE_CLIENT_SECRET;
// Now build unsigned
Problem: Hyperclay Local Setup 1.1.0.exe breaks PowerShell
Solution: Remove ALL spaces from productName in package.json
Problem: electron-builder spawns subprocess that loses env vars Solution: Build unsigned, sign separately with explicit env passing
Problem: electron-azure-trusted-signing package has hardcoded endpoint Solution: Don't use it. Use PowerShell module directly.
Problem: Writing credentials to .ps1 files Solution: Pass via environment variables only:
// Node.js
process.env.AZURE_CLIENT_SECRET_TEMP = secret;
// PowerShell reads from $env:AZURE_CLIENT_SECRET_TEMP
Problem: PowerShell here-strings are indent-sensitive Solution:
# BAD (indented terminator)
$text = @"
Hello
"@
# GOOD (terminator at column 0)
$text = @"
Hello
"@
The azureSignOptions in electron-builder doesn't work when building from macOS:
| Tool | Purpose | Service | Status |
|---|---|---|---|
| AzureSignTool | Azure Key Vault | Different service | Wrong tool |
| electron-azure-trusted-signing | Trusted Signing | Hardcoded endpoints | Broken |
| TrustedSigning PowerShell | Trusted Signing | Official Microsoft | Works on Windows |
| electron-builder azureSignOptions | Trusted Signing | Via PowerShell | Broken on Mac, may work on Windows |
undefined becomes the string "undefined"delete operator, not assignmentYour repository should have:
.github/workflows/
build-windows.yml # Builds on windows-latest
build-macos.yml # Builds on macos-latest
build-scripts/
build-unsigned.js # Builds without signing
notarize.js # macOS notarization
package.json # productName with NO SPACES
entitlements.plist # macOS entitlements
Skip the local VMs, skip the cross-platform attempts, skip the integrated signing. Build where the platform expects, sign with official tools, use the cloud.
Cross-platform code signing shouldn't take 48 hours, but if you're reading this because you searched for "SignTool.exe exit code 3" or "type.lastIndexOf is not a function" or "missing terminator PowerShell Azure", you're not alone. The ecosystem is fragmented, the tools make assumptions, and the error messages are cryptic.
Use GitHub Actions. Build unsigned. Sign separately. Remove spaces from filenames. Don't use ARM64 Windows for development. These aren't best practices—they're survival tactics.