mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
# Description of Changes Add lite mode for CI which just runs the most important jobs for deployment. This won't be used in this repo, but allows other repos containing Stirling to easily disable jobs like desktop builds etc. if they're unnecessary, without needing to deal with conflicts in the files. They'll just need to set the repo variable `CI_PROFILE` to `lite`. We have an upstream repo that we'd like these changes for.
647 lines
29 KiB
YAML
647 lines
29 KiB
YAML
name: Build Tauri Applications
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
platform:
|
|
description: "Platform to build (windows, macos, linux, or all)"
|
|
required: true
|
|
default: "all"
|
|
type: choice
|
|
options:
|
|
- all
|
|
- windows
|
|
- macos
|
|
- linux
|
|
pull_request:
|
|
branches: [main, V2, V2-tauri-windows]
|
|
paths:
|
|
- 'frontend/src-tauri/**'
|
|
- 'frontend/src/desktop/**'
|
|
- 'frontend/tsconfig.desktop.json'
|
|
- '.github/workflows/tauri-build.yml'
|
|
push:
|
|
branches: [main, V2]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
determine-matrix:
|
|
if: ${{ vars.CI_PROFILE != 'lite' }}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
steps:
|
|
- name: Determine build matrix
|
|
id: set-matrix
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
case "${{ github.event.inputs.platform }}" in
|
|
"windows")
|
|
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
|
;;
|
|
"macos")
|
|
echo 'matrix={"include":[{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
|
;;
|
|
"linux")
|
|
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
|
;;
|
|
*)
|
|
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
|
;;
|
|
esac
|
|
else
|
|
# For PR/push events, build all platforms
|
|
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
build:
|
|
needs: determine-matrix
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
|
|
runs-on: ${{ matrix.platform }}
|
|
env:
|
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
|
|
- name: Install dependencies (ubuntu only)
|
|
if: matrix.platform == 'ubuntu-22.04'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
cache: 'npm'
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Setup Rust
|
|
uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
toolchain: stable
|
|
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
|
with:
|
|
java-version: "21"
|
|
distribution: "temurin"
|
|
|
|
- name: Build Java backend with JLink
|
|
working-directory: ./
|
|
shell: bash
|
|
run: |
|
|
chmod +x ./gradlew
|
|
echo "🔧 Building Stirling-PDF JAR..."
|
|
# STIRLING_PDF_DESKTOP_UI=false ./gradlew clean bootJar --no-daemon
|
|
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
|
|
# Find the built JAR
|
|
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
|
echo "✅ Built JAR: $STIRLING_JAR"
|
|
|
|
# Create Tauri directories
|
|
mkdir -p ./frontend/src-tauri/libs
|
|
mkdir -p ./frontend/src-tauri/runtime
|
|
|
|
# Copy JAR to Tauri libs
|
|
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
|
|
echo "✅ JAR copied to Tauri libs"
|
|
|
|
# Analyze JAR dependencies for jlink modules
|
|
echo "🔍 Analyzing JAR dependencies..."
|
|
if command -v jdeps &> /dev/null; then
|
|
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
|
|
if [ -n "$DETECTED_MODULES" ]; then
|
|
echo "📋 jdeps detected modules: $DETECTED_MODULES"
|
|
MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
|
else
|
|
echo "⚠️ jdeps analysis failed, using predefined modules"
|
|
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
|
fi
|
|
else
|
|
echo "⚠️ jdeps not available, using predefined modules"
|
|
MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
|
fi
|
|
|
|
# Create custom JRE with jlink (always rebuild)
|
|
echo "🔧 Creating custom JRE with jlink..."
|
|
echo "📋 Using modules: $MODULES"
|
|
|
|
# Remove any existing JRE
|
|
rm -rf ./frontend/src-tauri/runtime/jre
|
|
|
|
# Create the custom JRE
|
|
jlink \
|
|
--add-modules "$MODULES" \
|
|
--strip-debug \
|
|
--compress=2 \
|
|
--no-header-files \
|
|
--no-man-pages \
|
|
--output ./frontend/src-tauri/runtime/jre
|
|
|
|
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
|
|
echo "❌ Failed to create JLink runtime"
|
|
exit 1
|
|
fi
|
|
|
|
# Test the bundled runtime
|
|
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
|
|
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
|
|
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
|
|
else
|
|
echo "❌ Custom JRE executable not found"
|
|
exit 1
|
|
fi
|
|
|
|
# Calculate runtime size
|
|
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
|
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
|
env:
|
|
DISABLE_ADDITIONAL_FEATURES: true
|
|
|
|
- name: Install frontend dependencies
|
|
working-directory: ./frontend
|
|
run: npm install
|
|
|
|
# DigiCert KeyLocker Setup (Cloud HSM)
|
|
- name: Setup DigiCert KeyLocker
|
|
id: digicert-setup
|
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
|
uses: digicert/ssm-code-signing@v1.1.0
|
|
env:
|
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
|
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
|
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
|
|
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
|
SM_HOST: ${{ secrets.SM_HOST }}
|
|
|
|
- name: Setup DigiCert KeyLocker Certificate
|
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
|
shell: pwsh
|
|
run: |
|
|
Write-Host "Setting up DigiCert KeyLocker environment..."
|
|
|
|
# Decode client certificate
|
|
$certBytes = [Convert]::FromBase64String("${{ secrets.SM_CLIENT_CERT_FILE_B64 }}")
|
|
$certPath = "D:\Certificate_pkcs12.p12"
|
|
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
|
|
|
# Set environment variables
|
|
echo "SM_CLIENT_CERT_FILE=D:\Certificate_pkcs12.p12" >> $env:GITHUB_ENV
|
|
echo "SM_HOST=${{ secrets.SM_HOST }}" >> $env:GITHUB_ENV
|
|
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> $env:GITHUB_ENV
|
|
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> $env:GITHUB_ENV
|
|
echo "SM_KEYPAIR_ALIAS=${{ secrets.SM_KEYPAIR_ALIAS }}" >> $env:GITHUB_ENV
|
|
|
|
# Get PKCS11 config path from DigiCert action
|
|
$pkcs11Config = $env:PKCS11_CONFIG
|
|
if ($pkcs11Config) {
|
|
Write-Host "Found PKCS11_CONFIG: $pkcs11Config"
|
|
echo "PKCS11_CONFIG=$pkcs11Config" >> $env:GITHUB_ENV
|
|
} else {
|
|
Write-Host "PKCS11_CONFIG not set by DigiCert action, using default path"
|
|
$defaultPath = "C:\Users\RUNNER~1\AppData\Local\Temp\smtools-windows-x64\pkcs11properties.cfg"
|
|
if (Test-Path $defaultPath) {
|
|
Write-Host "Found config at default path: $defaultPath"
|
|
echo "PKCS11_CONFIG=$defaultPath" >> $env:GITHUB_ENV
|
|
} else {
|
|
Write-Host "Warning: Could not find PKCS11 config file"
|
|
}
|
|
}
|
|
|
|
# Traditional PFX Certificate Import (fallback if KeyLocker not configured)
|
|
- name: Import Windows Code Signing Certificate
|
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY == '' && github.ref == 'refs/heads/main' }}
|
|
env:
|
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
|
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
|
shell: powershell
|
|
run: |
|
|
if ($env:WINDOWS_CERTIFICATE) {
|
|
Write-Host "Importing Windows Code Signing Certificate..."
|
|
|
|
# Decode base64 certificate and save to file
|
|
$certBytes = [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)
|
|
$certPath = Join-Path $env:RUNNER_TEMP "certificate.pfx"
|
|
[IO.File]::WriteAllBytes($certPath, $certBytes)
|
|
|
|
# Import certificate to CurrentUser\My store
|
|
$cert = Import-PfxCertificate -FilePath $certPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force)
|
|
|
|
# Extract and set thumbprint as environment variable
|
|
$thumbprint = $cert.Thumbprint
|
|
Write-Host "Certificate imported with thumbprint: $thumbprint"
|
|
echo "WINDOWS_CERTIFICATE_THUMBPRINT=$thumbprint" >> $env:GITHUB_ENV
|
|
|
|
# Clean up certificate file
|
|
Remove-Item $certPath
|
|
|
|
Write-Host "Windows certificate import completed."
|
|
} else {
|
|
Write-Host "⚠️ WINDOWS_CERTIFICATE secret not set - building unsigned binary"
|
|
}
|
|
|
|
- name: Import Apple Developer Certificate
|
|
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
run: |
|
|
echo "Importing Apple Developer Certificate..."
|
|
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
|
# Create temporary keychain
|
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
# Import certificate
|
|
security import certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
|
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
|
# Clean up
|
|
rm certificate.p12
|
|
|
|
- name: Verify Certificate
|
|
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
|
run: |
|
|
echo "Verifying Apple Developer Certificate..."
|
|
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
|
CERT_INFO=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application")
|
|
echo "Certificate Info: $CERT_INFO"
|
|
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
|
echo "Certificate ID: $CERT_ID"
|
|
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
|
echo "Certificate imported successfully."
|
|
|
|
- name: Check DMG creation dependencies (macOS only)
|
|
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
|
run: |
|
|
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
|
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
|
echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
|
echo "Available disk space: $(df -h /tmp | tail -1)"
|
|
echo "macOS version: $(sw_vers -productVersion)"
|
|
echo "Available tools:"
|
|
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
|
|
|
- name: Build Tauri app
|
|
uses: tauri-apps/tauri-action@v0
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY }}
|
|
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL }}
|
|
# Only enable Windows signing in Tauri when on main
|
|
SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }}
|
|
CI: true
|
|
with:
|
|
projectPath: ./frontend
|
|
tauriScript: npx tauri
|
|
args: ${{ matrix.args }}
|
|
|
|
# Sign with DigiCert KeyLocker (post-build)
|
|
- name: Sign Windows binaries with DigiCert KeyLocker
|
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
|
shell: pwsh
|
|
run: |
|
|
Write-Host "=== DigiCert KeyLocker Signing ==="
|
|
|
|
# Test smctl connectivity first
|
|
Write-Host "Testing smctl connection..."
|
|
$healthCheck = & smctl healthcheck 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Host "[SUCCESS] Connected to DigiCert KeyLocker"
|
|
} else {
|
|
Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker"
|
|
Write-Host $healthCheck
|
|
exit 1
|
|
}
|
|
Write-Host ""
|
|
|
|
# Sync certificates to Windows certificate store
|
|
Write-Host "Syncing certificates to Windows certificate store..."
|
|
$syncOutput = & smctl windows certsync 2>&1
|
|
Write-Host "Cert sync result: $syncOutput"
|
|
Write-Host ""
|
|
|
|
# List available certificates and check if they have certificates attached
|
|
Write-Host "Checking for available certificates..."
|
|
$certList = & smctl keypair ls 2>&1
|
|
Write-Host "Keypair list output:"
|
|
Write-Host $certList
|
|
Write-Host ""
|
|
|
|
# Parse the output to check certificate status
|
|
$lines = $certList -split "`n"
|
|
$foundKeypair = $false
|
|
$hasCertificate = $false
|
|
|
|
foreach ($line in $lines) {
|
|
if ($line -match "${{ secrets.SM_KEYPAIR_ALIAS }}") {
|
|
$foundKeypair = $true
|
|
Write-Host "[SUCCESS] Found keypair in list"
|
|
|
|
# Check if this line has certificate info (not just empty spaces after alias)
|
|
$parts = $line -split "\s+"
|
|
if ($parts.Count -gt 2 -and $parts[1] -ne "" -and $parts[1] -ne "CERTIFICATE") {
|
|
$hasCertificate = $true
|
|
Write-Host "[SUCCESS] Certificate is associated with keypair"
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $foundKeypair) {
|
|
Write-Host "[ERROR] Keypair not found: ${{ secrets.SM_KEYPAIR_ALIAS }}"
|
|
Write-Host "Available keypairs are listed above"
|
|
Write-Host ""
|
|
Write-Host "Please verify:"
|
|
Write-Host " 1. Keypair alias is correct in GitHub secret"
|
|
Write-Host " 2. API key has access to this keypair"
|
|
exit 1
|
|
}
|
|
|
|
if (-not $hasCertificate) {
|
|
Write-Host "[ERROR] No certificate associated with keypair"
|
|
Write-Host "This usually means:"
|
|
Write-Host " 1. Certificate not yet synced to KeyLocker (run sync manually)"
|
|
Write-Host " 2. Certificate is pending approval"
|
|
Write-Host " 3. Certificate needs to be attached to the keypair"
|
|
Write-Host ""
|
|
Write-Host "Try running in DigiCert ONE portal:"
|
|
Write-Host " smctl keypair sync"
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "[SUCCESS] Certificate check passed"
|
|
Write-Host ""
|
|
|
|
# Find only the files we need to sign (not build scripts)
|
|
$filesToSign = @()
|
|
|
|
# Main application executable
|
|
$mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue
|
|
if ($mainExe) { $filesToSign += $mainExe }
|
|
|
|
# MSI installer
|
|
$msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File
|
|
$filesToSign += $msiFiles
|
|
|
|
if ($filesToSign.Count -eq 0) {
|
|
Write-Host "[ERROR] No files found to sign"
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "Found $($filesToSign.Count) files to sign:"
|
|
foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" }
|
|
Write-Host ""
|
|
|
|
$signedCount = 0
|
|
foreach ($file in $filesToSign) {
|
|
Write-Host "Signing: $($file.Name)"
|
|
|
|
# Get PKCS11 config file path (set by DigiCert action)
|
|
$pkcs11Config = $env:PKCS11_CONFIG
|
|
if (-not $pkcs11Config) {
|
|
Write-Host "[ERROR] PKCS11_CONFIG environment variable not set"
|
|
Write-Host "DigiCert KeyLocker action may not have run correctly"
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "Using PKCS11 config: $pkcs11Config"
|
|
|
|
# Try signing with certificate fingerprint first (if available)
|
|
$fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
|
|
if ($fingerprint -and $fingerprint -ne "") {
|
|
Write-Host "Attempting to sign with certificate fingerprint..."
|
|
$output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
|
$exitCode = $LASTEXITCODE
|
|
} else {
|
|
Write-Host "No fingerprint provided, using keypair alias..."
|
|
# Use smctl to sign with keypair alias
|
|
$output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1
|
|
$exitCode = $LASTEXITCODE
|
|
}
|
|
|
|
Write-Host "Exit code: $exitCode"
|
|
Write-Host "Output: $output"
|
|
|
|
# Check if output contains "FAILED" even with exit code 0
|
|
if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") {
|
|
Write-Host ""
|
|
Write-Host "[ERROR] Signing failed for $($file.Name)"
|
|
Write-Host "[ERROR] smctl returned success but output indicates failure"
|
|
Write-Host ""
|
|
Write-Host "Possible issues:"
|
|
Write-Host " 1. Certificate not fully synced to KeyLocker (wait a few minutes)"
|
|
Write-Host " 2. Incorrect keypair alias"
|
|
Write-Host " 3. API key lacks signing permissions"
|
|
Write-Host ""
|
|
Write-Host "Please verify in DigiCert ONE portal:"
|
|
Write-Host " - Certificate status is 'Issued' (not Pending)"
|
|
Write-Host " - Keypair status is 'Online'"
|
|
Write-Host " - 'Can sign' is set to 'Yes'"
|
|
exit 1
|
|
}
|
|
|
|
if ($exitCode -ne 0) {
|
|
Write-Host "[ERROR] Failed to sign $($file.Name)"
|
|
Write-Host "Full error output:"
|
|
Write-Host $output
|
|
exit 1
|
|
}
|
|
|
|
$signedCount++
|
|
Write-Host "[SUCCESS] Signed: $($file.Name)"
|
|
Write-Host ""
|
|
}
|
|
|
|
Write-Host "=== Summary ==="
|
|
Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully"
|
|
|
|
- name: Verify notarization (macOS only)
|
|
if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel'
|
|
run: |
|
|
echo "🔍 Verifying notarization status..."
|
|
cd ./frontend/src-tauri/target
|
|
DMG_FILE=$(find . -name "*.dmg" | head -1)
|
|
if [ -n "$DMG_FILE" ]; then
|
|
echo "Found DMG: $DMG_FILE"
|
|
echo "Checking notarization ticket..."
|
|
spctl -a -vvv -t install "$DMG_FILE" || echo "⚠️ Notarization check failed or not yet complete"
|
|
stapler validate "$DMG_FILE" || echo "⚠️ No notarization ticket attached"
|
|
else
|
|
echo "⚠️ No DMG file found to verify"
|
|
fi
|
|
|
|
- name: Rename artifacts
|
|
shell: bash
|
|
run: |
|
|
mkdir -p ./dist
|
|
cd ./frontend/src-tauri/target
|
|
|
|
# Find and rename artifacts based on platform
|
|
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
|
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
|
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
|
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
|
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
|
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
|
else
|
|
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
|
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
|
fi
|
|
|
|
- name: Verify Windows Code Signature
|
|
if: matrix.platform == 'windows-latest' && github.ref == 'refs/heads/main'
|
|
shell: pwsh
|
|
run: |
|
|
Write-Host "Verifying Windows code signatures..."
|
|
|
|
$exePath = "./dist/Stirling-PDF-${{ matrix.name }}.exe"
|
|
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
|
|
|
|
$allSigned = $true
|
|
$usingKeyLocker = "${{ env.SM_API_KEY }}" -ne ""
|
|
$usingPfx = "${{ env.WINDOWS_CERTIFICATE }}" -ne ""
|
|
|
|
# Check EXE signature
|
|
if (Test-Path $exePath) {
|
|
$exeSig = Get-AuthenticodeSignature -FilePath $exePath
|
|
Write-Host "EXE Signature Status: $($exeSig.Status)"
|
|
Write-Host "EXE Signer: $($exeSig.SignerCertificate.Subject)"
|
|
Write-Host "EXE Timestamp: $($exeSig.TimeStamperCertificate.NotAfter)"
|
|
|
|
if ($exeSig.Status -ne "Valid") {
|
|
Write-Host "[WARNING] EXE is not properly signed (Status: $($exeSig.Status))"
|
|
if ($usingKeyLocker -or $usingPfx) {
|
|
Write-Host "[ERROR] Certificate was provided but signing failed"
|
|
$allSigned = $false
|
|
} else {
|
|
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
|
|
}
|
|
} else {
|
|
Write-Host "[SUCCESS] EXE is properly signed"
|
|
}
|
|
}
|
|
|
|
# Check MSI signature
|
|
if (Test-Path $msiPath) {
|
|
$msiSig = Get-AuthenticodeSignature -FilePath $msiPath
|
|
Write-Host "MSI Signature Status: $($msiSig.Status)"
|
|
Write-Host "MSI Signer: $($msiSig.SignerCertificate.Subject)"
|
|
Write-Host "MSI Timestamp: $($msiSig.TimeStamperCertificate.NotAfter)"
|
|
|
|
if ($msiSig.Status -ne "Valid") {
|
|
Write-Host "[WARNING] MSI is not properly signed (Status: $($msiSig.Status))"
|
|
if ($usingKeyLocker -or $usingPfx) {
|
|
Write-Host "[ERROR] Certificate was provided but signing failed"
|
|
$allSigned = $false
|
|
} else {
|
|
Write-Host "[INFO] Building unsigned binary (no certificate provided)"
|
|
}
|
|
} else {
|
|
Write-Host "[SUCCESS] MSI is properly signed"
|
|
}
|
|
}
|
|
|
|
if (($usingKeyLocker -or $usingPfx) -and -not $allSigned) {
|
|
Write-Host "[ERROR] Code signing verification failed"
|
|
exit 1
|
|
} else {
|
|
Write-Host "[SUCCESS] Code signature verification completed"
|
|
}
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
with:
|
|
name: Stirling-PDF-${{ matrix.name }}
|
|
path: ./dist/*
|
|
retention-days: 7
|
|
|
|
- name: Verify build artifacts
|
|
shell: bash
|
|
run: |
|
|
cd ./frontend/src-tauri/target
|
|
|
|
# Check for expected artifacts based on platform
|
|
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
|
echo "Checking for Windows artifacts..."
|
|
find . -name "*.exe" -o -name "*.msi" | head -5
|
|
if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then
|
|
echo "❌ No Windows executable found"
|
|
exit 1
|
|
fi
|
|
elif [ "${{ matrix.platform }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then
|
|
echo "Checking for macOS artifacts..."
|
|
find . -name "*.dmg" -o -name "*.app" | head -5
|
|
if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
|
|
echo "❌ No macOS artifacts found"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "Checking for Linux artifacts..."
|
|
find . -name "*.deb" -o -name "*.AppImage" | head -5
|
|
if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
|
|
echo "❌ No Linux artifacts found"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo "✅ Build artifacts found for ${{ matrix.name }}"
|
|
|
|
- name: Test artifact sizes
|
|
shell: bash
|
|
run: |
|
|
cd ./frontend/src-tauri/target
|
|
echo "Artifact sizes for ${{ matrix.name }}:"
|
|
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.msi" | while read file; do
|
|
if [ -f "$file" ]; then
|
|
size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown")
|
|
echo "$file: $size bytes"
|
|
# Check if file is suspiciously small (less than 1MB)
|
|
if [ "$size" != "unknown" ] && [ "$size" -lt 1048576 ]; then
|
|
echo "⚠️ Warning: $file is smaller than 1MB"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
report:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: always()
|
|
steps:
|
|
- name: Report build results
|
|
run: |
|
|
if [ "${{ needs.build.result }}" = "success" ]; then
|
|
echo "✅ All Tauri builds completed successfully!"
|
|
echo "Artifacts are ready for distribution."
|
|
elif [ "${{ needs.build.result }}" = "skipped" ]; then
|
|
echo "⏭️ Tauri builds skipped (CI lite mode enabled)"
|
|
else
|
|
echo "❌ Some Tauri builds failed."
|
|
echo "Please check the logs and fix any issues."
|
|
exit 1
|
|
fi
|