- Prerequisites
- macOS Electron -- Signing and Notarization
- Windows Electron -- Signing
- Linux Electron
- iOS -- App Store
- Android -- Play Store
- GitHub Actions Secrets (Complete List)
- Local Development Signing
- Cutting a Release
- Troubleshooting
- Node.js >= 22 and bun installed
- Apple Developer Program membership ($99/year) -- required for macOS notarization and iOS distribution
- Google Play Developer account ($25 one-time) -- required for Android distribution
- (Optional) Windows code signing certificate -- EV or OV cert from DigiCert, Sectigo, etc.
Without signing and notarization, macOS Gatekeeper blocks the app with: "macOS cannot verify that this app is free from malware."
The config in apps/app/electron/electron-builder.config.json is already set up with:
hardenedRuntime: trueentitlements: "entitlements.mac.plist"notarize: true- Targets: DMG + ZIP (zip is required for Electron auto-updater)
All you need to do is set up the certificate and add GitHub secrets.
- Open Keychain Access on your Mac
- Go to Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority
- Fill in your email (
shawmakesmusic@gmail.com) and name, select "Saved to disk", save the CSR - Log into https://developer.apple.com/account
- Go to Certificates, Identifiers & Profiles > Certificates > +
- Choose Developer ID Application (NOT "Mac App Distribution")
- Upload your CSR and download the certificate
- Double-click to install it in your Keychain
- In Keychain Access, find the "Developer ID Application: ..." certificate
- Expand it to reveal the private key
- Select BOTH the certificate AND the private key
- Right-click > Export 2 items... > save as
milady-mac-cert.p12 - Set a strong password (this becomes
CSC_KEY_PASSWORD)
base64 -i milady-mac-cert.p12 | tr -d '\n' > milady-mac-cert.b64
cat milady-mac-cert.b64
# Copy the entire output -- this is your CSC_LINK value- Go to https://appleid.apple.com > Sign-In and Security > App-Specific Passwords
- Click Generate and name it "Milady Notarization"
- Copy the generated password (format:
xxxx-xxxx-xxxx-xxxx)
Go to your repo Settings > Secrets and variables > Actions > New repository secret:
CSC_LINK-- the base64-encoded.p12from Step 3CSC_KEY_PASSWORD-- the password you set when exporting the.p12APPLE_ID--shawmakesmusic@gmail.comAPPLE_APP_SPECIFIC_PASSWORD-- the app-specific password from Step 4APPLE_TEAM_ID--25877RY2EH
Unsigned Windows apps trigger SmartScreen warnings ("Windows protected your PC").
- ~$200-400/year from DigiCert, Sectigo, GlobalSign, or SSL.com
- Immediate SmartScreen trust (no reputation building)
- Usually ships on a hardware token; cloud signing options available
- ~$70-200/year
- Requires building SmartScreen reputation over time
- ~$10/month via Azure
- Microsoft's cloud-based signing service
- Purchase and export the certificate as
.pfx(PKCS#12) - Base64-encode it:
base64 -i milady-win-cert.pfx | tr -d '\n' > milady-win-cert.b64- Add GitHub secrets:
WIN_CSC_LINK-- base64-encoded.pfxWIN_CSC_KEY_PASSWORD-- password for the.pfx
No code signing required. AppImage and .deb targets work as-is.
The bundle ID com.miladyai.milady is already registered in the Apple Developer portal.
The Xcode project is configured with DEVELOPMENT_TEAM = 25877RY2EH and automatic signing.
# Build web assets and sync to iOS
cd apps/app
bun run build:ios
# Open in Xcode
npx cap open iosIn Xcode:
- Product > Archive
- Distribute App > App Store Connect
- Follow the prompts to upload
- Go to https://appstoreconnect.apple.com
- Click + > New App
- Fill in:
- Platform: iOS
- Name: Milady
- Bundle ID:
com.miladyai.milady - SKU:
milady
- Set up your app listing (description, screenshots, etc.)
keytool -genkey -v \
-keystore milady-release.keystore \
-alias milady \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass <your-store-password> \
-keypass <your-key-password> \
-dname "CN=Milady, O=milady-ai"BACK UP THIS KEYSTORE. If you lose it, you cannot push updates to the same Play Store listing.
base64 -i milady-release.keystore | tr -d '\n' > milady-release.b64ANDROID_KEYSTORE-- base64-encoded keystoreANDROID_KEYSTORE_PASSWORD-- the storepassANDROID_KEY_ALIAS--miladyANDROID_KEY_PASSWORD-- the keypass
Add a signing config to apps/app/android/app/build.gradle inside the android block:
signingConfigs {
release {
def ksPath = System.getenv("ANDROID_KEYSTORE_PATH")
if (ksPath) {
storeFile file(ksPath)
storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD")
keyAlias System.getenv("ANDROID_KEY_ALIAS") ?: "milady"
keyPassword System.getenv("ANDROID_KEY_PASSWORD")
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (signingConfigs.release.storeFile) {
signingConfig signingConfigs.release
}
}
}- Go to https://play.google.com/console and create a developer account ($25)
- Create a new app with package name
com.miladyai.milady - Enable Play App Signing (recommended)
- Build and upload your first AAB:
cd apps/app
bun run build:android
npx cap open android
# In Android Studio: Build > Generate Signed Bundle / APK > Android App BundleAdd these at Settings > Secrets and variables > Actions in the milady-ai/milady repo.
| Secret | Value |
|---|---|
CSC_LINK |
Base64 .p12 Developer ID Application certificate |
CSC_KEY_PASSWORD |
Password for the .p12 |
APPLE_ID |
shawmakesmusic@gmail.com |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password from appleid.apple.com |
APPLE_TEAM_ID |
25877RY2EH |
| Secret | Value |
|---|---|
WIN_CSC_LINK |
Base64 .pfx code signing certificate |
WIN_CSC_KEY_PASSWORD |
Password for the .pfx |
| Secret | Value |
|---|---|
ANDROID_KEYSTORE |
Base64 keystore file |
ANDROID_KEYSTORE_PASSWORD |
Keystore password |
ANDROID_KEY_ALIAS |
milady |
ANDROID_KEY_PASSWORD |
Key password |
| Secret | Value |
|---|---|
IOS_CERTIFICATE_P12 |
Base64 Apple Distribution certificate |
IOS_CERTIFICATE_PASSWORD |
Certificate password |
IOS_PROVISIONING_PROFILE |
Base64 provisioning profile |
APPSTORE_CONNECT_API_KEY_ID |
App Store Connect API key ID |
APPSTORE_CONNECT_API_ISSUER_ID |
API issuer ID |
APPSTORE_CONNECT_API_KEY |
Base64 .p8 private key |
To test signed macOS builds locally:
# Set env vars (or add to .env / shell profile)
export CSC_LINK="$(base64 -i ~/path/to/milady-mac-cert.p12 | tr -d '\n')"
export CSC_KEY_PASSWORD="your-password"
export APPLE_ID="shawmakesmusic@gmail.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="25877RY2EH"
# Build and package
cd apps/app
bun run build:electron
cd electron
bun install
bunx electron-builder build --mac -c ./electron-builder.config.jsonThe DMG in electron/dist/ will be signed and notarized.
Option A: Tag push
# Bump version in package.json first, then:
git tag v2.0.0-alpha.3
git push origin v2.0.0-alpha.3The Build & Release workflow triggers automatically. It builds for macOS (Intel + Apple Silicon), Windows, and Linux, signs/notarizes the macOS builds, and creates a GitHub Release with all artifacts and SHA256 checksums.
Option B: Manual dispatch
- Go to Actions > Build & Release > Run workflow
- Enter the tag (e.g.
v2.0.0-alpha.3) - Check "Create as draft release" if you want to review before publishing
Each release includes:
Milady-X.Y.Z-arm64.dmg-- macOS Apple Silicon (signed + notarized)Milady-X.Y.Z.dmg-- macOS Intel (signed + notarized)Milady-X.Y.Z-arm64-mac.zip-- macOS Apple Silicon (for auto-updater)Milady-X.Y.Z-mac.zip-- macOS Intel (for auto-updater)Milady-Setup-X.Y.Z.exe-- Windows installerMilady-X.Y.Z.AppImage-- Linux AppImagemilady_X.Y.Z_amd64.deb-- Debian/Ubuntu packageSHA256SUMS.txt-- checksums for all files
The install scripts at milady-ai.github.io/milady/ point to these releases. Users can install with:
curl -fsSL https://milady-ai.github.io/milady/install.sh | bashThe app was signed but not notarized. Check APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID.
The app is not signed at all. Check CSC_LINK and CSC_KEY_PASSWORD. The certificate must be a Developer ID Application certificate.
Missing entitlements. Verify entitlements.mac.plist exists in apps/app/electron/ and hardenedRuntime is true in electron-builder config.
Log into https://developer.apple.com and accept any pending license agreements.
In CI, CSC_LINK must be the raw base64 string with no line breaks (base64 | tr -d '\n'). Locally, make sure the cert is in your login keychain.
Without an EV cert, SmartScreen reputation builds over time. Users click "More info > Run anyway". An EV cert gives immediate trust.
Verify ANDROID_KEY_ALIAS matches the alias from keytool (default: milady).
Open Xcode > Preferences > Accounts, ensure your Apple ID is added and team 25877RY2EH is visible. Click Manage Certificates.
Check that all required secrets are set. Missing secrets result in empty env vars which cause electron-builder to skip signing silently (macOS builds will still produce DMGs, just unsigned).