mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
# Description of Changes #6402 introduced a Rust test `refresh_token_fallback.rs`, but it wasn't moved properly after the restructure of the `frontend/` folder in #6404. This PR moves the file to the right place, and also hooks up Task and CI rules for `cargo test` since nothing was actually running the test in the first place.
673 lines
30 KiB
YAML
673 lines
30 KiB
YAML
name: Build Tauri Applications
|
|
|
|
# Multi-OS Tauri desktop bundle build matrix (Windows / macOS universal /
|
|
# Linux). Called from build.yml on PRs that touch desktop sources (gated
|
|
# via the `tauri` filter in .github/config/.files.yaml). Also runnable
|
|
# on demand via workflow_dispatch with a per-platform selector.
|
|
#
|
|
# Note: editing this file is itself enough to make the `tauri` path filter
|
|
# match, which is how non-desktop PRs (e.g. backend-only fixes) opt into a
|
|
# desktop smoke build.
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
platform:
|
|
description: "Platform to build (windows, macos, linux, or all)."
|
|
required: false
|
|
type: string
|
|
default: "all"
|
|
workflow_dispatch:
|
|
inputs:
|
|
platform:
|
|
description: "Platform to build (windows, macos, linux, or all)"
|
|
required: true
|
|
default: "all"
|
|
type: choice
|
|
options:
|
|
- all
|
|
- windows
|
|
- macos
|
|
- linux
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
|
|
jobs:
|
|
determine-matrix:
|
|
if: ${{ vars.CI_PROFILE != 'lite' }}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
steps:
|
|
- name: Harden the runner (Audit all outbound calls)
|
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Determine build matrix
|
|
id: set-matrix
|
|
env:
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
PLATFORM: ${{ inputs.platform }}
|
|
run: |
|
|
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
|
MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}'
|
|
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}'
|
|
|
|
case "$PLATFORM" in
|
|
windows) ENTRIES=("$WINDOWS") ;;
|
|
macos) ENTRIES=("$MACOS") ;;
|
|
linux) ENTRIES=("$LINUX") ;;
|
|
*) ENTRIES=("$WINDOWS" "$MACOS" "$LINUX") ;;
|
|
esac
|
|
|
|
# Drop macOS entries when Apple certificate secret is unavailable
|
|
if [ -z "$APPLE_CERTIFICATE" ]; then
|
|
echo "⚠️ APPLE_CERTIFICATE secret not available - skipping macOS builds"
|
|
FILTERED=()
|
|
for entry in "${ENTRIES[@]}"; do
|
|
[[ "$entry" != *'"macos'* ]] && FILTERED+=("$entry")
|
|
done
|
|
ENTRIES=("${FILTERED[@]}")
|
|
fi
|
|
|
|
JOINED=$(IFS=','; echo "${ENTRIES[*]}")
|
|
echo "matrix={\"include\":[$JOINED]}" >> $GITHUB_OUTPUT
|
|
|
|
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 }}
|
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
|
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
|
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: 22
|
|
cache: "npm"
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Setup Rust
|
|
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
|
|
with:
|
|
toolchain: stable
|
|
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
|
|
|
- name: Set up x86_64 JDK 25 (macOS universal JRE)
|
|
if: matrix.platform == 'macos-15'
|
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
architecture: "x64"
|
|
|
|
- name: Capture x86_64 JAVA_HOME
|
|
if: matrix.platform == 'macos-15'
|
|
run: echo "X64_JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
|
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
|
|
- name: Setup Gradle
|
|
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
|
|
with:
|
|
gradle-version: 9.3.1
|
|
|
|
- name: Setup Task
|
|
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
|
|
|
- name: Build universal macOS JRE
|
|
if: matrix.platform == 'macos-15'
|
|
env:
|
|
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
|
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
|
run: task desktop:jlink:universal-mac
|
|
|
|
- name: Prepare desktop build
|
|
env:
|
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
|
DISABLE_ADDITIONAL_FEATURES: true
|
|
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
|
run: task desktop:prepare
|
|
|
|
- name: Run Tauri/Cargo tests
|
|
run: task desktop:test
|
|
|
|
# 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@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1
|
|
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' && env.APPLE_CERTIFICATE != ''
|
|
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' && env.APPLE_CERTIFICATE != ''
|
|
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: Sign JPDFium dylibs inside bootJar (macOS only)
|
|
if: matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
|
run: bash frontend/scripts/sign-jpdfium-dylibs-in-bootjar.sh
|
|
|
|
- name: Check DMG creation dependencies (macOS only)
|
|
if: matrix.platform == 'macos-15'
|
|
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: Preflight smctl
|
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
|
shell: pwsh
|
|
env:
|
|
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
|
run: |
|
|
& smctl healthcheck
|
|
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 }
|
|
& smctl keypair ls
|
|
if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 }
|
|
& smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS"
|
|
if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" }
|
|
|
|
- name: Configure Windows code signing
|
|
if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }}
|
|
shell: bash
|
|
env:
|
|
KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
|
run: |
|
|
cat > ./frontend/editor/src-tauri/tauri.windows.conf.json <<EOF
|
|
{
|
|
"bundle": {
|
|
"windows": {
|
|
"signCommand": {
|
|
"cmd": "smctl",
|
|
"args": ["sign", "--keypair-alias", "${KEYPAIR_ALIAS}", "--input", "%1", "--verbose"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
- name: Import release GPG signing key (Linux)
|
|
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
|
|
run: |
|
|
echo "$RELEASE_GPG_PRIVATE_KEY" | gpg --batch --import
|
|
gpg --list-secret-keys --keyid-format=long
|
|
|
|
- name: Make libjvm discoverable for linuxdeploy (Linux AppImage)
|
|
if: matrix.platform == 'ubuntu-22.04'
|
|
run: |
|
|
JAVA_LIBJVM="$JAVA_HOME/lib/server/libjvm.so"
|
|
if [ -f "$JAVA_LIBJVM" ]; then
|
|
sudo ln -sf "$JAVA_LIBJVM" /usr/lib/libjvm.so
|
|
echo "Linked libjvm from $JAVA_LIBJVM -> /usr/lib/libjvm.so"
|
|
else
|
|
echo "libjvm not found at $JAVA_LIBJVM"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Build Tauri app
|
|
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
|
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 }}
|
|
# AppImage signing — three env vars work together:
|
|
# SIGN=1 tells linuxdeploy-plugin-appimage to forward --sign to appimagetool
|
|
# APPIMAGETOOL_SIGN_PASSPHRASE appimagetool uses this to unlock the GPG key non-interactively
|
|
# SIGN_KEY appimagetool picks the key matching this fingerprint
|
|
# Without SIGN=1, the other two are ignored and the AppImage is built unsigned even if a key is present.
|
|
# Mirror the Windows/macOS gate: only sign when secret is present AND ref is main (skips PRs from forks/Dependabot).
|
|
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }}
|
|
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
|
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
|
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 || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }}
|
|
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
|
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
|
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
|
CI: true
|
|
with:
|
|
projectPath: ./frontend/editor
|
|
tauriScript: npx tauri
|
|
# Linux: build deb+rpm only here. AppImage runs in its own
|
|
# continue-on-error step below so its persistent linuxdeploy
|
|
# failure (#6127 onwards) does not tank deb/rpm uploads.
|
|
args: ${{ matrix.platform == 'ubuntu-22.04' && '--bundles deb,rpm' || matrix.args }}
|
|
|
|
# AppImage is decoupled so its linuxdeploy run gets a fresh process
|
|
# (rpm scratch state torn down) and its failure can't tank deb/rpm.
|
|
- name: Build Tauri app (Linux AppImage)
|
|
if: matrix.platform == 'ubuntu-22.04'
|
|
continue-on-error: true
|
|
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main') && '1' || '0' }}
|
|
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
|
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
|
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 || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }} # gitleaks:allow
|
|
VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }}
|
|
VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }}
|
|
CI: true
|
|
with:
|
|
projectPath: ./frontend/editor
|
|
tauriScript: npx tauri
|
|
args: --bundles appimage
|
|
|
|
- name: Clear release GPG key from runner keyring (Linux)
|
|
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && github.ref == 'refs/heads/main'
|
|
env:
|
|
RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
|
run: |
|
|
if [ -n "$RELEASE_GPG_FINGERPRINT" ]; then
|
|
gpg --batch --yes --delete-secret-keys "$RELEASE_GPG_FINGERPRINT" || true
|
|
gpg --batch --yes --delete-keys "$RELEASE_GPG_FINGERPRINT" || true
|
|
fi
|
|
|
|
- name: Verify notarization (macOS only)
|
|
if: matrix.platform == 'macos-15'
|
|
run: |
|
|
echo "🔍 Verifying notarization status..."
|
|
cd ./frontend/editor/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/editor/src-tauri/target
|
|
|
|
# Find and rename artifacts based on platform
|
|
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
|
# Only ship the MSI installer. The loose exe and WiX toolset exes
|
|
# are not the user-facing installer - the MSI contains the signed inner exe.
|
|
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
|
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
|
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
|
else
|
|
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
|
find . -name "*.rpm" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.rpm" \;
|
|
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
|
fi
|
|
|
|
# Verify the MSI AND the inner exe extracted from it are signed.
|
|
# The inner exe is what gets installed on users' machines and what AV scans.
|
|
- name: Verify Windows Code Signature
|
|
if: matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main'
|
|
shell: pwsh
|
|
run: |
|
|
$allSigned = $true
|
|
$msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi"
|
|
|
|
# Check MSI (outer wrapper)
|
|
if (Test-Path $msiPath) {
|
|
$sig = Get-AuthenticodeSignature -FilePath $msiPath
|
|
Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Host "[ERROR] MSI is not signed"
|
|
$allSigned = $false
|
|
}
|
|
|
|
# Extract MSI and verify inner exe
|
|
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
|
|
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
|
|
$proc = Start-Process msiexec.exe -ArgumentList '/a', $msiPath, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow
|
|
if ($proc.ExitCode -eq 0) {
|
|
$innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1
|
|
if ($innerExe) {
|
|
$sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName
|
|
Write-Host "Inner EXE: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Host "[ERROR] Inner exe is NOT signed - AV will flag this at runtime"
|
|
$allSigned = $false
|
|
}
|
|
} else {
|
|
Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI"
|
|
$allSigned = $false
|
|
}
|
|
} else {
|
|
Write-Host "[ERROR] MSI extraction failed (exit code: $($proc.ExitCode))"
|
|
$allSigned = $false
|
|
}
|
|
} else {
|
|
Write-Host "[ERROR] MSI not found at $msiPath"
|
|
$allSigned = $false
|
|
}
|
|
|
|
if (-not $allSigned) {
|
|
Write-Host "[ERROR] Signature verification failed"
|
|
exit 1
|
|
}
|
|
Write-Host "[SUCCESS] MSI and inner exe are properly signed"
|
|
|
|
- name: Dump smctl logs on failure
|
|
if: ${{ failure() && matrix.platform == 'windows-latest' && env.SM_API_KEY != '' }}
|
|
shell: pwsh
|
|
run: |
|
|
$logDir = "$env:USERPROFILE\.signingmanager\logs"
|
|
if (Test-Path $logDir) {
|
|
Get-ChildItem $logDir | ForEach-Object {
|
|
Write-Host "=== $($_.FullName) ==="
|
|
Get-Content $_.FullName -Tail 200
|
|
Write-Host ""
|
|
}
|
|
} else {
|
|
Write-Host "smctl log directory not found at $logDir"
|
|
}
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: Stirling-PDF-${{ matrix.name }}
|
|
path: ./dist/*
|
|
retention-days: 7
|
|
|
|
- name: Verify build artifacts
|
|
shell: bash
|
|
run: |
|
|
cd ./frontend/editor/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" ]; then
|
|
echo "Checking for macOS artifacts..."
|
|
find . -name "*.dmg" | head -5
|
|
if [ $(find . -name "*.dmg" | wc -l) -eq 0 ]; then
|
|
echo "❌ No macOS artifacts found"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "Checking for Linux artifacts..."
|
|
find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | head -5
|
|
if [ $(find . -name "*.deb" -o -name "*.rpm" -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/editor/src-tauri/target
|
|
echo "Artifact sizes for ${{ matrix.name }}:"
|
|
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" -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
|
|
|
|
pr-comment:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: github.event_name == 'pull_request' && needs.build.result == 'success'
|
|
permissions:
|
|
pull-requests: write
|
|
steps:
|
|
- name: Harden the runner
|
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Post/Update PR Comment with Download Links
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const prNumber = context.issue.number;
|
|
const runId = context.runId;
|
|
|
|
// Fetch artifacts for this workflow run
|
|
const { data: artifactsList } = await github.rest.actions.listWorkflowRunArtifacts({
|
|
owner,
|
|
repo,
|
|
run_id: runId
|
|
});
|
|
|
|
// Map of expected artifact names to display info
|
|
const artifactMap = {
|
|
'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' },
|
|
'Stirling-PDF-macos-universal': { icon: '🍎', platform: 'macOS Universal', files: '.dmg' },
|
|
'Stirling-PDF-linux-x86_64': { icon: '🐧', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' }
|
|
};
|
|
|
|
let commentBody = `## 📦 Tauri Desktop Builds Ready!\n\n`;
|
|
commentBody += `The desktop applications have been built and are ready for testing.\n\n`;
|
|
commentBody += `### Download Artifacts:\n\n`;
|
|
|
|
// Add links for each found artifact
|
|
let foundArtifacts = 0;
|
|
for (const artifact of artifactsList.artifacts) {
|
|
const info = artifactMap[artifact.name];
|
|
if (info) {
|
|
foundArtifacts++;
|
|
// GitHub doesn't provide direct download URLs via API, but we can link to the artifact on the Actions page
|
|
const artifactUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id}`;
|
|
commentBody += `${info.icon} **${info.platform}**: [Download ${artifact.name}](${artifactUrl}) `;
|
|
commentBody += `(${info.files}) - ${(artifact.size_in_bytes / 1024 / 1024).toFixed(1)} MB\n`;
|
|
}
|
|
}
|
|
|
|
if (foundArtifacts === 0) {
|
|
commentBody += `⚠️ **Warning**: No artifacts found in workflow run.\n`;
|
|
commentBody += `[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})\n`;
|
|
}
|
|
|
|
commentBody += `\n---\n`;
|
|
commentBody += `_Built from commit ${context.sha.substring(0, 7)}_\n`;
|
|
commentBody += `_Artifacts expire in 7 days_`;
|
|
|
|
// Find existing comment
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
owner,
|
|
repo,
|
|
issue_number: prNumber
|
|
});
|
|
|
|
const botComment = comments.find(comment =>
|
|
comment.user.type === 'Bot' &&
|
|
comment.body.includes('📦 Tauri Desktop Builds Ready!')
|
|
);
|
|
|
|
if (botComment) {
|
|
// Update existing comment
|
|
await github.rest.issues.updateComment({
|
|
owner,
|
|
repo,
|
|
comment_id: botComment.id,
|
|
body: commentBody
|
|
});
|
|
console.log('Updated existing comment');
|
|
} else {
|
|
// Create new comment
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: prNumber,
|
|
body: commentBody
|
|
});
|
|
console.log('Created new comment');
|
|
}
|
|
|
|
report:
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: always()
|
|
steps:
|
|
- name: Harden the runner (Audit all outbound calls)
|
|
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- 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
|