Package Multiplatform Installers #23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |