Skip to content

Commit ca2663f

Browse files
sign
1 parent e3cb5d4 commit ca2663f

3 files changed

Lines changed: 259 additions & 13 deletions

File tree

.github/workflows/build-and-package.yml

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ on:
1010

1111
env:
1212
PYTHON_VERSION: "3.11"
13+
DISABLE_MACOS_DMG: "true" # Flag to disable DMG creation for macOS
14+
DISABLE_MACOS_NOTARIZATION: "true" # Flag to disable notarization for macOS
1315

1416
jobs:
1517
build:
@@ -25,6 +27,13 @@ jobs:
2527
executable-name: "Jumperless"
2628
icon-path: "assets/icons/icon.png"
2729

30+
- os: ubuntu-latest
31+
platform: linux
32+
arch: x86
33+
artifact-name: "Jumperless-Linux-x86"
34+
executable-name: "Jumperless"
35+
icon-path: "assets/icons/icon.png"
36+
2837
# macOS builds (Intel)
2938
- os: macos-13
3039
platform: macos
@@ -48,6 +57,13 @@ jobs:
4857
artifact-name: "Jumperless-Windows-x64"
4958
executable-name: "Jumperless"
5059
icon-path: "assets/icons/icon.ico"
60+
61+
- os: windows-latest
62+
platform: windows
63+
arch: x86
64+
artifact-name: "Jumperless-Windows-x86"
65+
executable-name: "Jumperless"
66+
icon-path: "assets/icons/icon.ico"
5167

5268
runs-on: ${{ matrix.os }}
5369

@@ -84,7 +100,10 @@ jobs:
84100
- name: Install system dependencies (macOS)
85101
if: matrix.platform == 'macos'
86102
run: |
87-
brew install create-dmg
103+
# Only install create-dmg if DMG creation is enabled
104+
if [ "${{ env.DISABLE_MACOS_DMG }}" != "true" ]; then
105+
brew install create-dmg
106+
fi
88107
89108
- name: Install Python dependencies
90109
run: |
@@ -128,6 +147,92 @@ jobs:
128147
# Use PyInstaller with direct command line arguments for consistent builds
129148
echo "Building with PyInstaller"
130149
python -m PyInstaller --clean --windowed --name "Jumperless" --distpath dist/macos --workpath build/macos --icon "assets/icons/icon.icns" JumperlessWokwiBridge.py
150+
151+
- name: Code Sign macOS App
152+
if: matrix.platform == 'macos'
153+
env:
154+
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
155+
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
156+
run: |
157+
# Only proceed if certificate is available
158+
if [ -z "$MACOS_CERTIFICATE" ]; then
159+
echo "No signing certificate provided, skipping code signing"
160+
exit 0
161+
fi
162+
163+
# Create temporary keychain
164+
security create-keychain -p temp_keychain_password temp_keychain
165+
security default-keychain -s temp_keychain
166+
security unlock-keychain -p temp_keychain_password temp_keychain
167+
168+
# Decode and import certificate
169+
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
170+
security import certificate.p12 -k temp_keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
171+
172+
# Enable codesign to access the keychain
173+
security set-key-partition-list -S apple-tool:,apple: -s -k temp_keychain_password temp_keychain
174+
175+
# Find the app bundle
176+
APP_PATH="dist/macos/Jumperless.app"
177+
if [ -d "$APP_PATH" ]; then
178+
echo "Signing $APP_PATH"
179+
# Sign the app bundle
180+
codesign --force --sign "Developer ID Application" --deep --options runtime "$APP_PATH"
181+
182+
# Verify the signature
183+
codesign --verify --verbose "$APP_PATH"
184+
echo "Code signing completed successfully"
185+
else
186+
echo "App bundle not found at $APP_PATH"
187+
exit 1
188+
fi
189+
190+
# Clean up
191+
rm certificate.p12
192+
security delete-keychain temp_keychain
193+
194+
- name: Notarize macOS App
195+
if: matrix.platform == 'macos'
196+
env:
197+
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
198+
APPLE_ID: ${{ secrets.APPLE_ID }}
199+
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
200+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
201+
DISABLE_MACOS_NOTARIZATION: ${{ env.DISABLE_MACOS_NOTARIZATION }}
202+
run: |
203+
# Check if notarization is disabled
204+
if [ "$DISABLE_MACOS_NOTARIZATION" = "true" ]; then
205+
echo "Notarization disabled by DISABLE_MACOS_NOTARIZATION flag"
206+
exit 0
207+
fi
208+
209+
# Only proceed if both signing certificate and Apple ID are available
210+
if [ -z "$MACOS_CERTIFICATE" ] || [ -z "$APPLE_ID" ]; then
211+
echo "No signing certificate or Apple ID provided, skipping notarization"
212+
exit 0
213+
fi
214+
215+
APP_PATH="dist/macos/Jumperless.app"
216+
if [ -d "$APP_PATH" ]; then
217+
echo "Creating archive for notarization"
218+
ditto -c -k --keepParent "$APP_PATH" "Jumperless.zip"
219+
220+
echo "Submitting for notarization"
221+
xcrun notarytool submit "Jumperless.zip" \
222+
--apple-id "$APPLE_ID" \
223+
--password "$APPLE_ID_PASSWORD" \
224+
--team-id "$APPLE_TEAM_ID" \
225+
--wait
226+
227+
echo "Stapling notarization"
228+
xcrun stapler staple "$APP_PATH"
229+
230+
echo "Notarization completed successfully"
231+
rm "Jumperless.zip"
232+
else
233+
echo "App bundle not found for notarization"
234+
exit 1
235+
fi
131236
132237
- name: Build with PyInstaller (Windows)
133238
if: matrix.platform == 'windows'
@@ -147,11 +252,15 @@ jobs:
147252
if: matrix.platform == 'linux'
148253
run: |
149254
python Scripts/package_app.py --platform linux --arch ${{ matrix.arch }}
255+
env:
256+
PREFER_TARGZ: "true" # Signal to prefer tar.gz over zip for Linux
150257

151258
- name: Create platform-specific package (macOS)
152259
if: matrix.platform == 'macos'
153260
run: |
154261
python Scripts/package_app.py --platform macos --arch ${{ matrix.arch }}
262+
env:
263+
DISABLE_MACOS_DMG: ${{ env.DISABLE_MACOS_DMG }}
155264

156265
- name: Create platform-specific package (Windows)
157266
if: matrix.platform == 'windows'
@@ -220,10 +329,22 @@ jobs:
220329
Multi-platform release of the Jumperless Wokwi Bridge application.
221330
222331
### Downloads
223-
- **Linux x64**: Jumperless-Linux-x64 (AppImage + Python fallback)
224-
- **macOS Intel**: Jumperless-macOS-Intel (DMG + Python fallback)
225-
- **macOS Apple Silicon**: Jumperless-macOS-Apple-Silicon (DMG + Python fallback)
332+
- **Linux x64**: Jumperless-Linux-x64 (tar.gz + Python fallback)
333+
- **Linux x86**: Jumperless-Linux-x86 (tar.gz + Python fallback)
334+
- **macOS Intel**: Jumperless-macOS-Intel (App bundle + Python fallback)
335+
- **macOS Apple Silicon**: Jumperless-macOS-Apple-Silicon (App bundle + Python fallback)
226336
- **Windows x64**: Jumperless-Windows-x64 (EXE + Python fallback)
337+
- **Windows x86**: Jumperless-Windows-x86 (EXE + Python fallback)
338+
339+
### macOS Users - Important Note
340+
If the app is code-signed with a Developer ID, you should be able to run it directly.
341+
342+
**If you get a security warning, try these steps:**
343+
1. Right-click the app and select "Open" to bypass Gatekeeper
344+
2. Or remove the quarantine attribute:
345+
```bash
346+
xattr -d com.apple.quarantine Jumperless.app
347+
```
227348
228349
### Installation Methods
229350
Each package includes multiple ways to run Jumperless:
@@ -239,10 +360,12 @@ jobs:
239360
See the README.md in each package for detailed instructions.
240361
241362
### What's New
242-
- Multi-platform CI/CD pipeline
363+
- Multi-platform CI/CD pipeline with x86 support
364+
- macOS code signing support (notarization disabled by default for faster builds)
243365
- Improved packaging with modern tools
244366
- Better error handling and logging
245367
- Enhanced platform-specific optimizations
368+
- Linux packages now use tar.gz format
246369
247370
draft: false
248371
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# macOS Code Signing Setup Guide
2+
3+
This guide explains how to set up code signing for the Jumperless macOS app using GitHub secrets.
4+
5+
## Prerequisites
6+
7+
- Apple Developer Program membership
8+
- macOS with Xcode installed
9+
- Developer ID Application certificate in your Keychain
10+
11+
## Step 1: Export Your Certificate
12+
13+
1. Open **Keychain Access** on your Mac
14+
2. Select **login** keychain and **My Certificates** category
15+
3. Find your "Developer ID Application" certificate
16+
4. Right-click and select **Export**
17+
5. Choose **Personal Information Exchange (.p12)** format
18+
6. Save as `Certificates.p12` and set a strong password
19+
7. Remember this password - you'll need it for GitHub secrets
20+
21+
## Step 2: Prepare Certificate for GitHub
22+
23+
```bash
24+
# Convert the .p12 file to base64 for GitHub secrets
25+
base64 -i Certificates.p12 -o Certificates.p12.base64.txt
26+
27+
# The output file contains the base64-encoded certificate
28+
cat Certificates.p12.base64.txt
29+
```
30+
31+
## Step 3: Set Up GitHub Secrets
32+
33+
Go to your GitHub repository → Settings → Secrets and variables → Actions
34+
35+
Add these secrets:
36+
37+
### Required for Code Signing:
38+
- `MACOS_CERTIFICATE`: Contents of `Certificates.p12.base64.txt`
39+
- `MACOS_CERTIFICATE_PASSWORD`: Password you used when exporting the .p12 file
40+
- `APPLE_TEAM_ID`: Your Apple Developer Team ID (found in Apple Developer Console)
41+
42+
### Optional for Notarization (disabled by default):
43+
- `APPLE_ID`: Your Apple ID email
44+
- `APPLE_ID_PASSWORD`: App-specific password (create in Apple ID settings)
45+
46+
**Note**: Notarization is disabled by default via the `DISABLE_MACOS_NOTARIZATION` environment variable. This is sufficient for direct distribution of developer tools.
47+
48+
To enable notarization, set `DISABLE_MACOS_NOTARIZATION: "false"` in the workflow environment variables.
49+
50+
## Step 4: Get Your Team ID
51+
52+
1. Go to [Apple Developer Console](https://developer.apple.com/account/)
53+
2. Sign in with your Apple ID
54+
3. Go to **Membership** tab
55+
4. Copy your **Team ID** (10-character string)
56+
57+
## Step 5: Create App-Specific Password (Optional)
58+
59+
1. Go to [Apple ID Account Page](https://appleid.apple.com/)
60+
2. Sign in and go to **Security** section
61+
3. Under **App-Specific Passwords**, click **Generate Password**
62+
4. Label it "GitHub Actions Notarization"
63+
5. Save the generated password as `APPLE_ID_PASSWORD` secret
64+
65+
## Security Notes
66+
67+
- Never commit certificates or passwords to your repository
68+
- Use strong, unique passwords for certificate export
69+
- App-specific passwords are safer than your main Apple ID password
70+
- Consider rotating secrets periodically
71+
72+
## Testing
73+
74+
After setting up secrets, the GitHub Actions workflow will automatically:
75+
1. Import your certificate into the build environment
76+
2. Sign the app bundle with your Developer ID
77+
3. Optionally notarize the app (disabled by default, enable by setting `DISABLE_MACOS_NOTARIZATION=false`)
78+
79+
With code signing alone, most users will no longer need to use `xattr` commands to run your app!
80+
81+
## Troubleshooting
82+
83+
### "No signing identity found" error
84+
- Verify `MACOS_CERTIFICATE` and `MACOS_CERTIFICATE_PASSWORD` are correct
85+
- Ensure you exported the certificate with its private key
86+
87+
### Notarization fails
88+
- Check `APPLE_ID` and `APPLE_ID_PASSWORD` are correct
89+
- Verify the app is properly signed before notarization
90+
- Check Apple Developer Console for any account issues
91+
92+
### Certificate expires
93+
- Renew your Developer ID certificate in Apple Developer Console
94+
- Export and re-encode the new certificate
95+
- Update the `MACOS_CERTIFICATE` secret

Scripts/package_app.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,12 @@ def create_python_readme(python_dir, platform):
306306
./run_jumperless.sh
307307
```
308308
309+
### macOS Security Warning
310+
If you get a security warning about unidentified developer:
311+
```bash
312+
xattr -d com.apple.quarantine run_jumperless.sh
313+
```
314+
309315
### Missing Dependencies
310316
```bash
311317
pip install -r requirements.txt
@@ -369,7 +375,12 @@ def package_platform(platform, arch, output_dir):
369375
print(f"{platform.title()} package created in {platform_dir}")
370376

371377
def try_create_macos_dmg(macos_dir, arch):
372-
"""Try to create macOS DMG if create-dmg is available"""
378+
"""Try to create macOS DMG if create-dmg is available and not disabled"""
379+
# Check if DMG creation is disabled by environment variable
380+
if os.environ.get("DISABLE_MACOS_DMG", "").lower() == "true":
381+
print("DMG creation disabled by DISABLE_MACOS_DMG environment variable")
382+
return
383+
373384
try:
374385
subprocess.run(["create-dmg", "--version"], check=True, capture_output=True)
375386
create_macos_dmg(macos_dir, arch)
@@ -484,6 +495,13 @@ def create_platform_readme(platform_dir, platform):
484495
```
485496
486497
The app will automatically launch in a new Terminal window for the best CLI experience.
498+
499+
**Important for macOS users:** If you get a security warning about the app being from an unidentified developer, you may need to:
500+
1. Right-click the app and select "Open" to bypass Gatekeeper, OR
501+
2. Remove the quarantine attribute:
502+
```bash
503+
xattr -d com.apple.quarantine Jumperless.app
504+
```
487505
'''
488506
elif platform == "windows":
489507
readme_content += '''
@@ -541,20 +559,30 @@ def create_archives(platform, output_dir):
541559
print(f"WARNING: No {platform} directory found")
542560
return
543561

544-
# Create ZIP archive (universal)
562+
# Check if we should prefer tar.gz over zip
563+
prefer_targz = os.environ.get("PREFER_TARGZ", "").lower() == "true"
564+
565+
# Create tar.gz for Linux/macOS (especially when preferred)
566+
if platform in ['linux', 'macos']:
567+
tar_path = output_dir / f"Jumperless-{platform.title()}.tar.gz"
568+
with tarfile.open(tar_path, 'w:gz') as tar:
569+
tar.add(platform_dir, arcname=f"Jumperless-{platform.title()}")
570+
print(f"Created tar.gz archive: {tar_path}")
571+
572+
# Skip ZIP creation for Linux if tar.gz is preferred
573+
if platform == 'linux' and prefer_targz:
574+
print("Skipping ZIP creation for Linux (tar.gz preferred)")
575+
return
576+
577+
# Create ZIP archive (universal fallback or for Windows)
545578
zip_path = output_dir / f"Jumperless-{platform.title()}.zip"
546579
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
547580
for root, dirs, files in os.walk(platform_dir):
548581
for file in files:
549582
file_path = Path(root) / file
550583
arcname = file_path.relative_to(platform_dir)
551584
zipf.write(file_path, arcname)
552-
553-
# Create tar.gz for Linux/macOS
554-
if platform in ['linux', 'macos']:
555-
tar_path = output_dir / f"Jumperless-{platform.title()}.tar.gz"
556-
with tarfile.open(tar_path, 'w:gz') as tar:
557-
tar.add(platform_dir, arcname=f"Jumperless-{platform.title()}")
585+
print(f"Created ZIP archive: {zip_path}")
558586

559587
print(f"Archives created for {platform}")
560588

0 commit comments

Comments
 (0)