Skip to content

Commit 4240af6

Browse files
authored
Merge pull request #1267 from perfectra1n/develop
Enable MacOS code signing and notarization in GitHub Actions
2 parents d85c670 + 9d6caa8 commit 4240af6

File tree

5 files changed

+263
-28
lines changed

5 files changed

+263
-28
lines changed
+179-10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
name: "Build Electron App"
2+
description: "Builds and packages the Electron app for different platforms"
3+
14
inputs:
25
os:
36
description: "One of the supported platforms: macos, linux, windows"
@@ -8,13 +11,45 @@ inputs:
811
extension:
912
description: "Platform specific extensions to copy in the output: dmg, deb, rpm, exe, zip"
1013
required: true
14+
1115
runs:
1216
using: composite
1317
steps:
14-
- name: Set up Python for appdmg to be installed
18+
# Certificate setup
19+
- name: Import Apple certificates
20+
if: inputs.os == 'macos'
21+
uses: apple-actions/import-codesign-certs@v2
22+
with:
23+
p12-file-base64: ${{ env.APPLE_APP_CERTIFICATE_BASE64 }}
24+
p12-password: ${{ env.APPLE_APP_CERTIFICATE_PASSWORD }}
25+
keychain: build
26+
keychain-password: ${{ github.run_id }}
27+
28+
- name: Install Installer certificate
29+
if: inputs.os == 'macos'
30+
uses: apple-actions/import-codesign-certs@v2
31+
with:
32+
p12-file-base64: ${{ env.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
33+
p12-password: ${{ env.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
34+
keychain: build
35+
keychain-password: ${{ github.run_id }}
36+
# We don't need to create a keychain here because we're using the build keychain that was created in the previous step
37+
create-keychain: false
38+
39+
- name: Verify certificates
40+
if: inputs.os == 'macos'
41+
shell: bash
42+
run: |
43+
echo "Available signing identities:"
44+
security find-identity -v -p codesigning build.keychain
45+
46+
- name: Set up Python and other macOS dependencies
1547
if: ${{ inputs.os == 'macos' }}
1648
shell: bash
17-
run: brew install python-setuptools
49+
run: |
50+
brew install python-setuptools
51+
brew install create-dmg
52+
1853
- name: Install dependencies for RPM and Flatpak package building
1954
if: ${{ inputs.os == 'linux' }}
2055
shell: bash
@@ -24,21 +59,155 @@ runs:
2459
FLATPAK_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi)
2560
FLATPAK_VERSION='24.08'
2661
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
62+
63+
# Build setup
2764
- name: Install dependencies
2865
shell: bash
2966
run: npm ci
67+
3068
- name: Update build info
3169
shell: bash
3270
run: npm run chore:update-build-info
33-
- name: Run electron-forge
71+
72+
# Critical debugging configuration
73+
- name: Run electron-forge build with enhanced logging
74+
shell: bash
75+
env:
76+
# Pass through required environment variables for signing and notarization
77+
APPLE_TEAM_ID: ${{ env.APPLE_TEAM_ID }}
78+
APPLE_ID: ${{ env.APPLE_ID }}
79+
APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
80+
run: |
81+
# Map OS names to Electron Forge platform names
82+
if [ "${{ inputs.os }}" = "macos" ]; then
83+
PLATFORM="darwin"
84+
elif [ "${{ inputs.os }}" = "windows" ]; then
85+
PLATFORM="win32"
86+
else
87+
PLATFORM="${{ inputs.os }}"
88+
fi
89+
90+
npm run electron-forge:make -- \
91+
--arch=${{ inputs.arch }} \
92+
--platform=$PLATFORM
93+
94+
# Add DMG signing step
95+
- name: Sign DMG
96+
if: inputs.os == 'macos'
97+
shell: bash
98+
run: |
99+
echo "Signing DMG file..."
100+
dmg_file=$(find out -name "*.dmg" -print -quit)
101+
if [ -n "$dmg_file" ]; then
102+
echo "Found DMG: $dmg_file"
103+
# Get the first valid signing identity from the keychain
104+
SIGNING_IDENTITY=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
105+
if [ -z "$SIGNING_IDENTITY" ]; then
106+
echo "Error: No valid Developer ID Application certificate found in keychain"
107+
exit 1
108+
fi
109+
echo "Using signing identity: $SIGNING_IDENTITY"
110+
# Sign the DMG
111+
codesign --force --sign "$SIGNING_IDENTITY" --options runtime --timestamp "$dmg_file"
112+
# Notarize the DMG
113+
xcrun notarytool submit "$dmg_file" --apple-id "$APPLE_ID" --password "$APPLE_ID_PASSWORD" --team-id "$APPLE_TEAM_ID" --wait
114+
# Staple the notarization ticket
115+
xcrun stapler staple "$dmg_file"
116+
else
117+
echo "No DMG found to sign"
118+
fi
119+
120+
- name: Verify code signing
121+
if: inputs.os == 'macos'
34122
shell: bash
35-
run: npm run electron-forge:make -- --arch=${{ inputs.arch }}
123+
run: |
124+
echo "Verifying code signing for all artifacts..."
125+
126+
# First check the .app bundle
127+
echo "Looking for .app bundle..."
128+
app_bundle=$(find out -name "*.app" -print -quit)
129+
if [ -n "$app_bundle" ]; then
130+
echo "Found app bundle: $app_bundle"
131+
echo "Verifying app bundle signing..."
132+
codesign --verify --deep --strict --verbose=2 "$app_bundle"
133+
echo "Displaying app bundle signing info..."
134+
codesign --display --verbose=2 "$app_bundle"
135+
136+
echo "Checking entitlements..."
137+
codesign --display --entitlements :- "$app_bundle"
138+
139+
echo "Checking notarization status..."
140+
xcrun stapler validate "$app_bundle" || echo "Warning: App bundle not notarized yet"
141+
else
142+
echo "No .app bundle found to verify"
143+
fi
144+
145+
# Then check DMG if it exists
146+
echo "Looking for DMG..."
147+
dmg_file=$(find out -name "*.dmg" -print -quit)
148+
if [ -n "$dmg_file" ]; then
149+
echo "Found DMG: $dmg_file"
150+
echo "Verifying DMG signing..."
151+
codesign --verify --deep --strict --verbose=2 "$dmg_file"
152+
echo "Displaying DMG signing info..."
153+
codesign --display --verbose=2 "$dmg_file"
154+
155+
echo "Checking DMG notarization..."
156+
xcrun stapler validate "$dmg_file" || echo "Warning: DMG not notarized yet"
157+
else
158+
echo "No DMG found to verify"
159+
fi
160+
161+
# Finally check ZIP if it exists
162+
echo "Looking for ZIP..."
163+
zip_file=$(find out -name "*.zip" -print -quit)
164+
if [ -n "$zip_file" ]; then
165+
echo "Found ZIP: $zip_file"
166+
echo "Note: ZIP files are not code signed, but their contents should be"
167+
fi
168+
36169
- name: Prepare artifacts
37170
shell: bash
38171
run: |
39-
mkdir -p upload;
40-
for ext in ${{ inputs.extension }};
41-
do
42-
file=$(find out/make -name "*.$ext" -print -quit);
43-
cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext";
44-
done
172+
mkdir -p upload
173+
174+
if [ "${{ inputs.os }}" = "macos" ]; then
175+
# For macOS, we need to look in specific directories based on the maker
176+
echo "Collecting macOS artifacts..."
177+
178+
# Look for DMG files recursively
179+
echo "Looking for DMG files..."
180+
dmg_file=$(find out -name "*.dmg" -print -quit)
181+
if [ -n "$dmg_file" ]; then
182+
echo "Found DMG: $dmg_file"
183+
cp "$dmg_file" "upload/TriliumNextNotes-${{ github.ref_name }}-darwin-${{ inputs.arch }}.dmg"
184+
else
185+
echo "Warning: No DMG file found"
186+
fi
187+
188+
# Look for ZIP files recursively
189+
echo "Looking for ZIP files..."
190+
zip_file=$(find out -name "*.zip" -print -quit)
191+
if [ -n "$zip_file" ]; then
192+
echo "Found ZIP: $zip_file"
193+
cp "$zip_file" "upload/TriliumNextNotes-${{ github.ref_name }}-darwin-${{ inputs.arch }}.zip"
194+
else
195+
echo "Warning: No ZIP file found"
196+
fi
197+
else
198+
# For other platforms, use the existing logic but with better error handling
199+
echo "Collecting artifacts for ${{ inputs.os }}..."
200+
for ext in ${{ inputs.extension }}; do
201+
echo "Looking for .$ext files..."
202+
file=$(find out -name "*.$ext" -print -quit)
203+
if [ -n "$file" ]; then
204+
echo "Found $file for extension $ext"
205+
cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext"
206+
else
207+
echo "Warning: No file found with extension .$ext"
208+
fi
209+
done
210+
fi
211+
212+
echo "Final contents of upload directory:"
213+
ls -la upload/

.github/workflows/main.yml

+41
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,36 @@ jobs:
3333
runs-on: ${{ matrix.os.image }}
3434
steps:
3535
- uses: actions/checkout@v4
36+
37+
# Set up certificates and keychain for macOS
38+
- name: Install Apple Certificates
39+
if: matrix.os.name == 'macos'
40+
env:
41+
APP_CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_CERTIFICATE_BASE64 }}
42+
APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_CERTIFICATE_PASSWORD }}
43+
INSTALLER_CERTIFICATE_BASE64: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
44+
INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
45+
KEYCHAIN_PASSWORD: ${{ github.run_id }}
46+
run: |
47+
# Create keychain
48+
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
49+
security default-keychain -s build.keychain
50+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
51+
security set-keychain-settings -t 3600 -u build.keychain
52+
53+
# Import application certificate
54+
echo "$APP_CERTIFICATE_BASE64" | base64 --decode > application.p12
55+
security import application.p12 -k build.keychain -P "$APP_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
56+
rm application.p12
57+
58+
# Import installer certificate
59+
echo "$INSTALLER_CERTIFICATE_BASE64" | base64 --decode > installer.p12
60+
security import installer.p12 -k build.keychain -P "$INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
61+
rm installer.p12
62+
63+
# Update keychain settings
64+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
65+
3666
- name: Set up node & dependencies
3767
uses: actions/setup-node@v4
3868
with:
@@ -43,6 +73,17 @@ jobs:
4373
os: ${{ matrix.os.name }}
4474
arch: ${{ matrix.arch }}
4575
extension: ${{ matrix.os.extension }}
76+
env:
77+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
78+
APPLE_ID: ${{ secrets.APPLE_ID }}
79+
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
80+
81+
# Clean up keychain after build
82+
- name: Clean up keychain
83+
if: matrix.os.name == 'macos' && always()
84+
run: |
85+
security delete-keychain build.keychain
86+
4687
- name: Publish artifacts
4788
uses: actions/upload-artifact@v4
4889
with:

.github/workflows/release.yml

+9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ jobs:
4040
os: ${{ matrix.os.name }}
4141
arch: ${{ matrix.arch }}
4242
extension: ${{ join(matrix.os.extension, ' ') }}
43+
env:
44+
APPLE_APP_CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_CERTIFICATE_BASE64 }}
45+
APPLE_APP_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_CERTIFICATE_PASSWORD }}
46+
APPLE_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_BASE64 }}
47+
APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
48+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
49+
APPLE_ID: ${{ secrets.APPLE_ID }}
50+
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
51+
4352
- name: Publish release
4453
uses: softprops/action-gh-release@v2
4554
with:

entitlements.plist

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.cs.allow-jit</key>
6+
<true/>
7+
<key>com.apple.security.files.user-selected.read-write</key>
8+
<true/>
9+
</dict>
10+
</plist>

forge.config.cjs

+24-18
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,39 @@ module.exports = {
1717
overwrite: true,
1818
asar: true,
1919
icon: "./images/app-icons/icon",
20+
osxSign: {},
21+
osxNotarize: {
22+
appleId: process.env.APPLE_ID,
23+
appleIdPassword: process.env.APPLE_ID_PASSWORD,
24+
teamId: process.env.APPLE_TEAM_ID
25+
},
2026
extraResource: [
21-
// Moved to root
22-
...extraResourcesForPlatform,
27+
// All resources should stay in Resources directory for macOS
28+
...(process.platform === "darwin" ? [] : extraResourcesForPlatform),
2329

24-
// Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
30+
// These always go in Resources
2531
"translations/",
2632
"node_modules/@highlightjs/cdn-assets/styles"
2733
],
2834
afterComplete: [
2935
(buildPath, _electronVersion, platform, _arch, callback) => {
30-
for (const resource of extraResourcesForPlatform) {
31-
const baseName = path.basename(resource);
32-
33-
// prettier-ignore
34-
const sourcePath = (platform === "darwin")
35-
? path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName)
36-
: path.join(buildPath, "resources", baseName);
36+
// Only move resources on non-macOS platforms
37+
if (platform !== "darwin") {
38+
for (const resource of extraResourcesForPlatform) {
39+
const baseName = path.basename(resource);
40+
const sourcePath = path.join(buildPath, "resources", baseName);
3741

38-
// prettier-ignore
39-
const destPath = (baseName !== "256x256.png")
40-
? path.join(buildPath, baseName)
41-
: path.join(buildPath, "icon.png");
42+
// prettier-ignore
43+
const destPath = (baseName !== "256x256.png")
44+
? path.join(buildPath, baseName)
45+
: path.join(buildPath, "icon.png");
4246

43-
// Copy files from resources folder to root
44-
fs.move(sourcePath, destPath)
45-
.then(() => callback())
46-
.catch((err) => callback(err));
47+
fs.move(sourcePath, destPath)
48+
.then(() => callback())
49+
.catch((err) => callback(err));
50+
}
51+
} else {
52+
callback();
4753
}
4854
}
4955
]

0 commit comments

Comments
 (0)