Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable MacOS code signing and notarization in GitHub Actions #1267

Merged
merged 6 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 184 additions & 10 deletions .github/actions/build-electron/action.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
name: "Build Electron App"
description: "Builds and packages the Electron app for different platforms"

inputs:
os:
description: "One of the supported platforms: macos, linux, windows"
Expand All @@ -8,13 +11,45 @@ inputs:
extension:
description: "Platform specific extensions to copy in the output: dmg, deb, rpm, exe, zip"
required: true

runs:
using: composite
steps:
- name: Set up Python for appdmg to be installed
# Certificate setup
- name: Import Apple certificates
if: inputs.os == 'macos'
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
keychain: build
keychain-password: ${{ github.run_id }}

- name: Install Installer certificate
if: inputs.os == 'macos'
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
keychain: build
keychain-password: ${{ github.run_id }}
# We don't need to create a keychain here because we're using the build keychain that was created in the previous step
create-keychain: false

- name: Verify certificates
if: inputs.os == 'macos'
shell: bash
run: |
echo "Available signing identities:"
security find-identity -v -p codesigning build.keychain

- name: Set up Python and other macOS dependencies
if: ${{ inputs.os == 'macos' }}
shell: bash
run: brew install python-setuptools
run: |
brew install python-setuptools
brew install create-dmg

- name: Install dependencies for RPM and Flatpak package building
if: ${{ inputs.os == 'linux' }}
shell: bash
Expand All @@ -24,21 +59,160 @@ runs:
FLATPAK_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi)
FLATPAK_VERSION='24.08'
flatpak install --user --no-deps --arch $FLATPAK_ARCH --assumeyes runtime/org.freedesktop.Platform/$FLATPAK_ARCH/$FLATPAK_VERSION runtime/org.freedesktop.Sdk/$FLATPAK_ARCH/$FLATPAK_VERSION org.electronjs.Electron2.BaseApp/$FLATPAK_ARCH/$FLATPAK_VERSION

# Build setup
- name: Install dependencies
shell: bash
run: npm ci

- name: Update build info
shell: bash
run: npm run chore:update-build-info
- name: Run electron-forge

# Critical debugging configuration
- name: Run electron-forge build with enhanced logging
shell: bash
env:
DEBUG: "electron-osx-sign*,@electron/notarize*,electron-forge:*"
ELECTRON_NOTARIZE_DEBUG: 1
ELECTRON_ENABLE_LOGGING: 1
ELECTRON_DEBUG_NOTARIZATION: 1
# Pass through required environment variables for signing and notarization
APPLE_TEAM_ID: ${{ env.APPLE_TEAM_ID }}
APPLE_ID: ${{ env.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
run: |
# Map OS names to Electron Forge platform names
if [ "${{ inputs.os }}" = "macos" ]; then
PLATFORM="darwin"
elif [ "${{ inputs.os }}" = "windows" ]; then
PLATFORM="win32"
else
PLATFORM="${{ inputs.os }}"
fi

npm run electron-forge:make -- \
--arch=${{ inputs.arch }} \
--platform=$PLATFORM \
--verbose

# Add DMG signing step
- name: Sign DMG
if: inputs.os == 'macos'
shell: bash
run: |
echo "Signing DMG file..."
dmg_file=$(find out -name "*.dmg" -print -quit)
if [ -n "$dmg_file" ]; then
echo "Found DMG: $dmg_file"
# Get the first valid signing identity from the keychain
SIGNING_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$SIGNING_IDENTITY" ]; then
echo "Error: No valid Developer ID Application certificate found in keychain"
exit 1
fi
echo "Using signing identity: $SIGNING_IDENTITY"
# Sign the DMG
codesign --force --sign "$SIGNING_IDENTITY" --options runtime --timestamp "$dmg_file"
# Notarize the DMG
xcrun notarytool submit "$dmg_file" --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait
# Staple the notarization ticket
xcrun stapler staple "$dmg_file"
else
echo "No DMG found to sign"
fi

- name: Verify code signing
if: inputs.os == 'macos'
shell: bash
run: npm run electron-forge:make -- --arch=${{ inputs.arch }}
run: |
echo "Verifying code signing for all artifacts..."

# First check the .app bundle
echo "Looking for .app bundle..."
app_bundle=$(find out -name "*.app" -print -quit)
if [ -n "$app_bundle" ]; then
echo "Found app bundle: $app_bundle"
echo "Verifying app bundle signing..."
codesign --verify --deep --strict --verbose=2 "$app_bundle"
echo "Displaying app bundle signing info..."
codesign --display --verbose=2 "$app_bundle"

echo "Checking entitlements..."
codesign --display --entitlements :- "$app_bundle"

echo "Checking notarization status..."
xcrun stapler validate "$app_bundle" || echo "Warning: App bundle not notarized yet"
else
echo "No .app bundle found to verify"
fi

# Then check DMG if it exists
echo "Looking for DMG..."
dmg_file=$(find out -name "*.dmg" -print -quit)
if [ -n "$dmg_file" ]; then
echo "Found DMG: $dmg_file"
echo "Verifying DMG signing..."
codesign --verify --deep --strict --verbose=2 "$dmg_file"
echo "Displaying DMG signing info..."
codesign --display --verbose=2 "$dmg_file"

echo "Checking DMG notarization..."
xcrun stapler validate "$dmg_file" || echo "Warning: DMG not notarized yet"
else
echo "No DMG found to verify"
fi

# Finally check ZIP if it exists
echo "Looking for ZIP..."
zip_file=$(find out -name "*.zip" -print -quit)
if [ -n "$zip_file" ]; then
echo "Found ZIP: $zip_file"
echo "Note: ZIP files are not code signed, but their contents should be"
fi

- name: Prepare artifacts
shell: bash
run: |
mkdir -p upload;
for ext in ${{ inputs.extension }};
do
file=$(find out/make -name "*.$ext" -print -quit);
cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext";
done
mkdir -p upload

if [ "${{ inputs.os }}" = "macos" ]; then
# For macOS, we need to look in specific directories based on the maker
echo "Collecting macOS artifacts..."

# Look for DMG files recursively
echo "Looking for DMG files..."
dmg_file=$(find out -name "*.dmg" -print -quit)
if [ -n "$dmg_file" ]; then
echo "Found DMG: $dmg_file"
cp "$dmg_file" "upload/TriliumNextNotes-${{ github.ref_name }}-darwin-${{ inputs.arch }}.dmg"
else
echo "Warning: No DMG file found"
fi

# Look for ZIP files recursively
echo "Looking for ZIP files..."
zip_file=$(find out -name "*.zip" -print -quit)
if [ -n "$zip_file" ]; then
echo "Found ZIP: $zip_file"
cp "$zip_file" "upload/TriliumNextNotes-${{ github.ref_name }}-darwin-${{ inputs.arch }}.zip"
else
echo "Warning: No ZIP file found"
fi
else
# For other platforms, use the existing logic but with better error handling
echo "Collecting artifacts for ${{ inputs.os }}..."
for ext in ${{ inputs.extension }}; do
echo "Looking for .$ext files..."
file=$(find out -name "*.$ext" -print -quit)
if [ -n "$file" ]; then
echo "Found $file for extension $ext"
cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext"
else
echo "Warning: No file found with extension .$ext"
fi
done
fi

echo "Final contents of upload directory:"
ls -la upload/
41 changes: 41 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,36 @@ jobs:
runs-on: ${{ matrix.os.image }}
steps:
- uses: actions/checkout@v4

# Set up certificates and keychain for macOS
- name: Install Apple Certificates
if: matrix.os.name == 'macos'
env:
APP_CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_CERTIFICATE_BASE64 }}
APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_CERTIFICATE_PASSWORD }}
INSTALLER_CERTIFICATE_BASE64: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ github.run_id }}
run: |
# Create keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain

# Import application certificate
echo "$APP_CERTIFICATE_BASE64" | base64 --decode > application.p12
security import application.p12 -k build.keychain -P "$APP_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
rm application.p12

# Import installer certificate
echo "$INSTALLER_CERTIFICATE_BASE64" | base64 --decode > installer.p12
security import installer.p12 -k build.keychain -P "$INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
rm installer.p12

# Update keychain settings
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain

- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
Expand All @@ -43,6 +73,17 @@ jobs:
os: ${{ matrix.os.name }}
arch: ${{ matrix.arch }}
extension: ${{ matrix.os.extension }}
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}

# Clean up keychain after build
- name: Clean up keychain
if: matrix.os.name == 'macos' && always()
run: |
security delete-keychain build.keychain

- name: Publish artifacts
uses: actions/upload-artifact@v4
with:
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ jobs:
os: ${{ matrix.os.name }}
arch: ${{ matrix.arch }}
extension: ${{ join(matrix.os.extension, ' ') }}
env:
APPLE_APP_CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_CERTIFICATE_BASE64 }}
APPLE_APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_CERTIFICATE_PASSWORD }}
APPLE_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}

- name: Publish release
uses: softprops/action-gh-release@v2
with:
Expand Down
12 changes: 12 additions & 0 deletions entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?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>
44 changes: 24 additions & 20 deletions forge.config.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const path = require("path");
const fs = require("fs-extra");

const APP_NAME = "TriliumNext Notes";
const APP_NAME = "TriliumNextNotes";

const extraResourcesForPlatform = getExtraResourcesForPlatform();
const baseLinuxMakerConfigOptions = {
Expand All @@ -17,33 +17,37 @@ module.exports = {
overwrite: true,
asar: true,
icon: "./images/app-icons/icon",
osxSign: {},
osxNotarize: {
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
},
extraResource: [
// Moved to root
...extraResourcesForPlatform,
// All resources should stay in Resources directory for macOS
...(process.platform === "darwin" ? [] : extraResourcesForPlatform),

// Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
// These always go in Resources
"translations/",
"node_modules/@highlightjs/cdn-assets/styles"
],
afterComplete: [
(buildPath, _electronVersion, platform, _arch, callback) => {
for (const resource of extraResourcesForPlatform) {
const baseName = path.basename(resource);

// prettier-ignore
const sourcePath = (platform === "darwin")
? path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName)
: path.join(buildPath, "resources", baseName);

// prettier-ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add that comment back in above destPath please?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I put it back where you asked, now 👀

const destPath = (baseName !== "256x256.png")
? path.join(buildPath, baseName)
: path.join(buildPath, "icon.png");
// Only move resources on non-macOS platforms
if (platform !== "darwin") {
for (const resource of extraResourcesForPlatform) {
const baseName = path.basename(resource);
const sourcePath = path.join(buildPath, "resources", baseName);
const destPath = (baseName !== "256x256.png")
? path.join(buildPath, baseName)
: path.join(buildPath, "icon.png");

// Copy files from resources folder to root
fs.move(sourcePath, destPath)
.then(() => callback())
.catch((err) => callback(err));
fs.move(sourcePath, destPath)
.then(() => callback())
.catch((err) => callback(err));
}
} else {
callback();
}
}
]
Expand Down