Skip to content

Package Multiplatform Installers #23

Package Multiplatform Installers

Package Multiplatform Installers #23

Workflow file for this run

name: Package Multiplatform Installers
on:
release:
types: [ published ]
workflow_dispatch:
inputs:
test_mode:
description: 'Is this a test run? (Saves files to Action run instead of updating a live Release)'
type: boolean
required: true
default: true
version_tag:
description: 'Release Tag to attach binaries to (Only used if Test Mode is false)'
type: string
required: false
default: 'v2.2.0'
jobs:
build-and-package:
name: Package for ${{ matrix.friendly-name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
friendly-name: Windows (EXE & Portable Zip)
platform-profile: jar-with-dependencies
artifact-path: |
target/CertWizard_*.exe
target/CertWizard-windows.zip
artifact-name: CertWizard-Windows
- os: macos-latest
friendly-name: macOS Apple Silicon (DMG/PKG/Zip)
platform-profile: jar-with-dependencies
artifact-path: |
target/CertWizard-mac.zip
artifact-name: CertWizard-Mac-ARM64-APP
- os: macos-26-intel
friendly-name: macOS Intel (DMG/PKG/Zip)
platform-profile: jar-with-dependencies
artifact-path: |
target/CertWizard-mac.zip
artifact-name: CertWizard-Mac-x64-APP
- os: ubuntu-latest
friendly-name: Linux (JAR)
platform-profile: jar-with-dependencies
artifact-path: |
target/*-jar-with-dependencies.jar
artifact-name: CertWizard-Linux-JAR
steps:
# 1. Checkout repository
- name: Checkout Code
uses: actions/checkout@v7
# 2. Set up Java Environment
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'zulu'
cache: 'maven'
# 3. Build and Package
- name: Build and Native Package with Maven
run: mvn clean package -P ${{ matrix.platform-profile }}
# ---------- macOS-only: signing, notarization, launch test ----------
# 4. Import certificates into a temporary keychain
- name: Import Code-Signing Certificates
if: runner.os == 'macOS'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE_P12 }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
MACOS_INSTALLER_CERTIFICATE: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_P12 }}
MACOS_INSTALLER_CERTIFICATE_PWD: ${{ secrets.MACOS_INSTALLER_CERTIFICATE_PWD }}
KEYCHAIN_PWD: ${{ secrets.KEYCHAIN_PWD }}
run: |
set -euo pipefail
security create-keychain -p "$KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PWD" build.keychain
security set-keychain-settings -lut 3600 build.keychain
# Developer ID Application cert — signs the .app and .dmg
echo "$MACOS_CERTIFICATE" | base64 --decode -o app_cert.p12
security import app_cert.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
rm -f app_cert.p12
# Developer ID Installer cert — signs the .pkg (different cert type; only if provided)
if [ -n "${MACOS_INSTALLER_CERTIFICATE:-}" ]; then
echo "$MACOS_INSTALLER_CERTIFICATE" | base64 --decode -o installer_cert.p12
security import installer_cert.p12 -k build.keychain -P "$MACOS_INSTALLER_CERTIFICATE_PWD" -T /usr/bin/productsign
rm -f installer_cert.p12
fi
security set-key-partition-list -S apple-tool:,apple:,codesign:,productsign: -s -k "$KEYCHAIN_PWD" build.keychain
# 5. Sign the .app bundle
- name: Code Sign App Bundle
if: runner.os == 'macOS'
run: |
set -euo pipefail
APP_ZIP="target/CertWizard-mac.zip"
WORKDIR="$(mktemp -d)"
APP_IDENTITY=$(security find-identity -v -p codesigning build.keychain \
| grep "Developer ID Application" | head -1 | sed -E 's/.*"(.*)"/\1/')
echo "Using app signing identity: $APP_IDENTITY"
ditto -x -k "$APP_ZIP" "$WORKDIR"
APP_PATH=$(find "$WORKDIR" -maxdepth 1 -name "*.app")
echo "Signing $APP_PATH"
cat > entitlements.plist <<'EOF'
<?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/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
</dict>
</plist>
EOF
find "$APP_PATH" -type f \( -name "*.dylib" -o -name "*.jnilib" -o -perm -111 \) \
! -path "$APP_PATH/Contents/MacOS/*" \
-exec codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" {} \;
for jre in "$APP_PATH"/Contents/PlugIns/*.jre; do
[ -d "$jre" ] && codesign --force --deep --timestamp --options runtime --sign "$APP_IDENTITY" "$jre"
done
codesign --force --timestamp --options runtime \
--entitlements entitlements.plist \
--sign "$APP_IDENTITY" "$APP_PATH"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
echo "APP_PATH=$APP_PATH" >> "$GITHUB_ENV"
echo "APP_IDENTITY=$APP_IDENTITY" >> "$GITHUB_ENV"
# 6. Notarize + staple the .app
- name: Notarize App Bundle
if: runner.os == 'macOS'
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_B64: ${{ secrets.APPLE_API_KEY_P8 }}
run: |
set -euo pipefail
mkdir -p ~/private_keys
echo "$APPLE_API_KEY_B64" | base64 --decode -o ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8
NOTARIZE_ZIP="$(mktemp -d)/notarize.zip"
ditto -c -k --keepParent "$APP_PATH" "$NOTARIZE_ZIP"
xcrun notarytool submit "$NOTARIZE_ZIP" \
--key ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER" \
--wait
xcrun stapler staple "$APP_PATH"
xcrun stapler validate "$APP_PATH"
# 7. Smoke test: does it actually launch?
- name: Launch Smoke Test
if: runner.os == 'macOS'
run: |
set -euo pipefail
# 1. Pre-flight check: Gatekeeper & LaunchServices assessment
echo "Assessing Gatekeeper policy for $APP_PATH..."
if ! spctl --assess --type execute --verbose "$APP_PATH"; then
echo "::error::Gatekeeper rejected $APP_PATH! App will fail to open."
exit 1
fi
# 2. Launch the application and capture open errors
echo "Launching $APP_PATH..."
LAUNCH_LOG=$(open -n "$APP_PATH" 2>&1 || true)
if echo "$LAUNCH_LOG" | grep -qE "LSOpenURLsWithRole|can't be opened|Error"; then
echo "::error::LaunchServices failed to open application:"
echo "$LAUNCH_LOG"
exit 1
fi
# 3. Poll for the specific executable process (longer window for JVM startup)
APP_PID=""
for i in $(seq 1 20); do
# Target the inner binary path to avoid matching the launcher stub alone
APP_PID=$(pgrep -n -f "CertWizard.app/Contents/MacOS" || pgrep -n -f "CertWizard" || true)
if [ -n "$APP_PID" ]; then
echo "Detected process, PID: $APP_PID"
break
fi
sleep 1
done
if [ -z "$APP_PID" ]; then
echo "::error::App process was not created within 20s."
log show --predicate 'process == "CertWizard" || process == "universalJavaApplicationStub"' --last 2m || true
exit 1
fi
# 4. Give JVM time to settle and verify process stability
sleep 10
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "::error::App launched but crashed or exited within 10s!"
log show --predicate 'process == "CertWizard"' --last 2m || true
exit 1
fi
echo "App is running normally — smoke test passed."
pkill -f "CertWizard" || true
# 8. Re-zip the signed & notarized app for the release artifact
- name: Re-zip Signed & Notarized App
if: runner.os == 'macOS'
run: |
set -euo pipefail
rm -f target/CertWizard-mac.zip
ditto -c -k --keepParent "$APP_PATH" target/CertWizard-mac.zip
# 9. Clean up the temporary keychain
- name: Clean up Keychain
if: always() && runner.os == 'macOS'
run: security delete-keychain build.keychain || true
# 10. Upload artifacts
- name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact-name }}
path: ${{ matrix.artifact-path }}
if-no-files-found: error