feat(compression): add vite-plugin-compression for gzip and Brotli support #2069
Workflow file for this run
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: 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] | |
| types: [opened, reopened, synchronize, ready_for_review] | |
| paths: | |
| - "frontend/src-tauri/**" | |
| - "frontend/src/desktop/**" | |
| - "frontend/tsconfig.desktop.json" | |
| - "frontend/package.json" | |
| - "frontend/package-lock.json" | |
| - "frontend/vite.config.ts" | |
| - ".github/workflows/tauri-build.yml" | |
| 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@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 | |
| with: | |
| egress-policy: audit | |
| - name: Determine build matrix | |
| id: set-matrix | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| run: | | |
| WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}' | |
| MACOS_ARM='{"platform":"macos-15","args":"--target aarch64-apple-darwin","name":"macos-aarch64"}' | |
| MACOS_INTEL='{"platform":"macos-15-intel","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}' | |
| LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}' | |
| # Resolve requested platform (non-dispatch events always build all) | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| PLATFORM="${{ github.event.inputs.platform }}" | |
| else | |
| PLATFORM="all" | |
| fi | |
| # Build candidate list | |
| case "$PLATFORM" in | |
| windows) ENTRIES=("$WINDOWS") ;; | |
| macos) ENTRIES=("$MACOS_ARM" "$MACOS_INTEL") ;; | |
| linux) ENTRIES=("$LINUX") ;; | |
| *) ENTRIES=("$WINDOWS" "$MACOS_ARM" "$MACOS_INTEL" "$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 }} | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 | |
| 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.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' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} | |
| - 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@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1 | |
| with: | |
| gradle-version: 9.3.1 | |
| - name: Setup Task | |
| uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 | |
| - name: Prepare desktop build | |
| run: task desktop:prepare | |
| env: | |
| MAVEN_USER: ${{ secrets.MAVEN_USER }} | |
| MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} | |
| MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} | |
| DISABLE_ADDITIONAL_FEATURES: true | |
| # 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' || matrix.platform == 'macos-15-intel') && 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' || matrix.platform == 'macos-15-intel') && 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: 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: 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/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@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25 | |
| 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 | |
| tauriScript: npx tauri | |
| args: ${{ matrix.args }} | |
| - 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' || 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 | |
| # 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" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| 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" | 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/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@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 | |
| with: | |
| egress-policy: audit | |
| - name: Post/Update PR Comment with Download Links | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.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-aarch64': { icon: '🍎', platform: 'macOS ARM64', files: '.dmg' }, | |
| 'Stirling-PDF-macos-x86_64': { icon: '🍎', platform: 'macOS Intel', 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@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 | |
| 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 |