mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
# Description of Changes This PR refactors Gradle caching across the GitHub Actions workflows to improve cache reuse, reduce dependency resolution overhead, and shorten CI execution times. ### What was changed - Replaced multiple `gradle/actions/setup-gradle` steps with a unified `actions/cache`-based Gradle User Home cache strategy. - Standardized cache paths across workflows to include: - `~/.gradle/caches` - `~/.gradle/wrapper` - Introduced consistent cache keys using: - Runner OS - Runner architecture - JDK version - Hashes of Gradle wrapper, version catalog, Gradle build files, and project build scripts. - Added restore keys to maximize cache hit rates across similar environments. - Added a new **`gradle-cache-prime`** job in the main build workflow that: - Restores or creates the shared Gradle cache. - Resolves backend dependencies before downstream jobs execute. - Makes the populated cache available to subsequent jobs. - Updated workflow dependencies so Gradle-based jobs wait for the cache priming job before execution. - Simplified and unified Gradle cache handling across numerous CI workflows, including backend builds, OpenAPI generation, database migration tests, Docker tests, Tauri builds, Swagger generation, enterprise builds, release workflows, and license generation. - Updated workflow comments to reflect the new caching strategy and shared cache behavior. ### Why the change was made The previous workflows used a mixture of Gradle setup actions and partial dependency caches, leading to duplicated dependency downloads, inconsistent cache behavior, and longer CI runtimes. Consolidating all workflows onto a shared Gradle User Home cache with a dedicated cache priming job improves cache reuse, reduces unnecessary dependency resolution, and makes CI execution more consistent. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details.
928 lines
44 KiB
YAML
928 lines
44 KiB
YAML
name: Multi-OS Tauri Releases
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
test_mode:
|
|
description: "Run in test mode (skip release step)"
|
|
required: false
|
|
default: "true"
|
|
type: choice
|
|
options:
|
|
- "true"
|
|
- "false"
|
|
platform:
|
|
description: "Platform to build (windows, windows-arm64, macos, linux, or all)"
|
|
required: true
|
|
default: "all"
|
|
type: choice
|
|
options:
|
|
- all
|
|
- windows
|
|
- windows-arm64
|
|
- macos
|
|
- linux
|
|
sign:
|
|
description: "Code sign the binaries (requires signing secrets)"
|
|
required: false
|
|
default: "true"
|
|
type: choice
|
|
options:
|
|
- "true"
|
|
- "false"
|
|
release:
|
|
types: [created]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
determine-matrix:
|
|
if: ${{ vars.CI_PROFILE != 'lite' }}
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
|
|
- name: Cache Gradle User Home
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
restore-keys: |
|
|
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
|
gradle-${{ runner.os }}-${{ runner.arch }}-
|
|
|
|
- name: Install Task
|
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
|
- name: Get version number
|
|
id: versionNumber
|
|
run: |
|
|
VERSION=$(./gradlew printVersion --quiet | tail -1)
|
|
echo "Extracted version: $VERSION"
|
|
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
|
env:
|
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
|
|
|
- name: Determine build matrix
|
|
id: set-matrix
|
|
run: |
|
|
# windows-arm64: NSIS only (WiX MSI has no arm64 support in Tauri) and no
|
|
# JPDFium natives yet - flip to windows-arm64 once JPDFium ships them.
|
|
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
|
WINDOWS_ARM64='{"platform":"windows-11-arm","args":"--target aarch64-pc-windows-msvc --bundles nsis","name":"windows-arm64","jpdfium_platforms":"none"}'
|
|
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"}'
|
|
ALL="$WINDOWS,$WINDOWS_ARM64,$MACOS,$LINUX"
|
|
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
case "${{ github.event.inputs.platform }}" in
|
|
"windows")
|
|
echo "matrix={\"include\":[$WINDOWS,$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT
|
|
;;
|
|
"windows-arm64")
|
|
echo "matrix={\"include\":[$WINDOWS_ARM64]}" >> $GITHUB_OUTPUT
|
|
;;
|
|
"macos")
|
|
echo "matrix={\"include\":[$MACOS]}" >> $GITHUB_OUTPUT
|
|
;;
|
|
"linux")
|
|
echo "matrix={\"include\":[$LINUX]}" >> $GITHUB_OUTPUT
|
|
;;
|
|
*)
|
|
echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT
|
|
;;
|
|
esac
|
|
else
|
|
# For push/release events, build all platforms
|
|
echo "matrix={\"include\":[$ALL]}" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
build-jars:
|
|
needs: determine-matrix
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
variant:
|
|
- name: "default"
|
|
disable_security: true
|
|
build_frontend: true
|
|
file_suffix: ""
|
|
- name: "with-login"
|
|
disable_security: false
|
|
build_frontend: true
|
|
file_suffix: "-with-login"
|
|
- name: "server-only"
|
|
disable_security: true
|
|
build_frontend: false
|
|
file_suffix: "-server"
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
|
|
- name: Cache Gradle User Home
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
restore-keys: |
|
|
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
|
gradle-${{ runner.os }}-${{ runner.arch }}-
|
|
|
|
- name: Setup Node.js
|
|
if: matrix.variant.build_frontend == true
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: 22
|
|
cache: "npm"
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Install Task
|
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
|
|
|
- name: Build JAR
|
|
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
env:
|
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
|
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.variant.disable_security }}
|
|
STIRLING_PDF_DESKTOP_UI: false
|
|
|
|
- name: Rename JAR
|
|
run: |
|
|
echo "Version from determine-matrix: ${{ needs.determine-matrix.outputs.version }}"
|
|
echo "Looking for: app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar"
|
|
ls -la app/core/build/libs/
|
|
mkdir -p ./jar-dist
|
|
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
|
|
|
|
- name: Upload JAR artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: jar${{ matrix.variant.file_suffix }}
|
|
path: ./jar-dist/*.jar
|
|
retention-days: 1
|
|
|
|
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 }}
|
|
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
|
with:
|
|
egress-policy: audit
|
|
allowed-endpoints: >
|
|
one.digicert.com:443
|
|
clientauth.one.digicert.com:443
|
|
|
|
- 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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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' || '' }}
|
|
|
|
# x86_64 JDK is set up first so the aarch64 step below can leave its
|
|
# JAVA_HOME as the active one. The macOS universal JRE build needs
|
|
# jmods from both arches; the x64 path is captured into the env
|
|
# before the second setup-java overwrites JAVA_HOME.
|
|
- 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"
|
|
|
|
# Temurin has no windows-aarch64 JDK 25 yet; Microsoft OpenJDK does.
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
|
|
|
- name: Cache Gradle User Home
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
restore-keys: |
|
|
gradle-${{ runner.os }}-${{ runner.arch }}-jdk-25-
|
|
gradle-${{ runner.os }}-${{ runner.arch }}-
|
|
|
|
- name: Install Task
|
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
|
|
|
# Build the universal JRE before desktop:prepare so the jlink:runtime
|
|
# task short-circuits on its `test -d runtime/jre` status check.
|
|
- 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
|
|
|
|
# DigiCert KeyLocker Setup (Cloud HSM)
|
|
- name: Setup DigiCert KeyLocker
|
|
id: digicert-setup
|
|
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
|
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: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
|
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: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY == '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
|
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' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
|
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' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
|
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."
|
|
|
|
# Pre-flight: verify smctl can talk to DigiCert and sync cert before we sign.
|
|
# Mirrors the setup from working public Tauri+KeyLocker repos (Labric, Meetily).
|
|
# Without this, signCommand failures are opaque (Tauri captures but drops
|
|
# smctl's stderr) - running these loudly surfaces auth/env/keypair issues.
|
|
- name: Preflight smctl
|
|
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
|
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" }
|
|
Write-Host "[SUCCESS] smctl preflight passed"
|
|
|
|
# Write platform-specific Tauri config that adds signCommand for Windows.
|
|
# Tauri auto-merges tauri.windows.conf.json with tauri.conf.json (RFC 7396).
|
|
# Tauri calls this command on every binary BEFORE bundling into the MSI,
|
|
# substituting %1 with the file path.
|
|
#
|
|
# Why OBJECT form (cmd + args) instead of string:
|
|
# Tauri's string-form parser does a naive split(' ') with no shell/quote handling.
|
|
# Args with spaces or quote characters get mangled. The object form passes each
|
|
# arg directly to Rust's Command::arg which handles Windows CreateProcess quoting.
|
|
#
|
|
# Why --keypair-alias instead of --fingerprint:
|
|
# --fingerprint requires smctl windows certsync to have synced the cert to the
|
|
# Windows cert store first. --keypair-alias goes direct through PKCS11 and works
|
|
# without certsync. All real-world working Tauri+smctl examples use this flag.
|
|
#
|
|
# smctl reads SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE, SM_CLIENT_CERT_PASSWORD
|
|
# from env (set by prior DigiCert setup step). No --config-file needed.
|
|
- name: Configure Windows code signing
|
|
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
|
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
|
|
echo "Generated tauri.windows.conf.json (alias masked):"
|
|
sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/editor/src-tauri/tauri.windows.conf.json
|
|
|
|
- name: Import release GPG signing key (Linux)
|
|
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
|
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 on a real release/dispatch+sign or V2-master, when secret is present.
|
|
SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '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' }}
|
|
# DigiCert KeyLocker env vars consumed by smctl during signCommand
|
|
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
|
CI: true
|
|
with:
|
|
projectPath: ./frontend/editor
|
|
tauriScript: npx tauri
|
|
args: ${{ matrix.args }}
|
|
updaterJsonKeepUniversal: true
|
|
|
|
# Bundled libwayland conflicts with the host's on some distros (Fedora
|
|
# Wayland: EGL_BAD_PARAMETER, blank window - #6878). Repack without it,
|
|
# then regenerate the updater .sig (repack invalidates the original) and
|
|
# GPG-sign again when release signing is on.
|
|
- name: Strip bundled Wayland libs from AppImage
|
|
if: matrix.platform == 'ubuntu-22.04'
|
|
continue-on-error: true
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
GPG_SIGN: ${{ (env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')) && '1' || '0' }}
|
|
SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }}
|
|
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }}
|
|
run: |
|
|
set -euo pipefail
|
|
AI=$(find "$PWD/frontend/editor/src-tauri/target" -name "*.AppImage" | head -1)
|
|
if [ -z "$AI" ]; then echo "No AppImage found - skipping"; exit 0; fi
|
|
chmod +x "$AI"
|
|
WORK=$(mktemp -d)
|
|
(cd "$WORK" && "$AI" --appimage-extract >/dev/null)
|
|
if ! ls "$WORK/squashfs-root/usr/lib/"libwayland-* >/dev/null 2>&1; then
|
|
echo "No bundled libwayland - nothing to strip"
|
|
rm -rf "$WORK"
|
|
exit 0
|
|
fi
|
|
rm -f "$WORK/squashfs-root/usr/lib/"libwayland-*
|
|
curl -fsSL -o "$WORK/appimagetool" \
|
|
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
|
# Pinned checksum: never execute an unverified downloaded binary. On
|
|
# mismatch (upstream rebuilt continuous) the step aborts and the
|
|
# original AppImage ships unchanged - update the pin deliberately.
|
|
echo "a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 $WORK/appimagetool" | sha256sum -c -
|
|
chmod +x "$WORK/appimagetool"
|
|
SIGN_ARGS=()
|
|
if [ "$GPG_SIGN" = "1" ] && [ -n "${SIGN_KEY:-}" ]; then
|
|
SIGN_ARGS=(--sign --sign-key "$SIGN_KEY")
|
|
fi
|
|
"$WORK/appimagetool" --appimage-extract-and-run "${SIGN_ARGS[@]}" "$WORK/squashfs-root" "$AI.new"
|
|
# Updater payload signature must match the repacked bytes. The CLI
|
|
# reads the key/password from env - never pass secrets as argv.
|
|
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
|
(cd frontend && npx tauri signer sign "$AI.new")
|
|
mv "$AI.new.sig" "$AI.sig"
|
|
fi
|
|
mv "$AI.new" "$AI"
|
|
rm -rf "$WORK"
|
|
echo "Stripped bundled libwayland from $(basename "$AI")"
|
|
|
|
- name: Clear release GPG key from runner keyring (Linux)
|
|
if: always() && matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
|
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
|
|
|
|
# Verify the MSI (outer wrapper users download) AND the inner exe extracted
|
|
# from it (what actually gets installed and what AV scans). We don't check
|
|
# target/.../release/stirling-pdf.exe - that's Tauri's intermediate build
|
|
# artifact. Tauri signs a COPY when bundling into the MSI and leaves the raw
|
|
# cargo output unsigned, so checking it produces false negatives.
|
|
- name: Verify Windows Code Signature
|
|
if: ${{ startsWith(matrix.platform, 'windows') && env.SM_API_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') }}
|
|
timeout-minutes: 15
|
|
shell: pwsh
|
|
run: |
|
|
# arm64 ships an NSIS installer, not an MSI. Tauri's signCommand signs the
|
|
# inner exe before packing and the setup exe after, so verifying the setup
|
|
# exe is the arm64 equivalent of the MSI + inner-exe check below.
|
|
if ("${{ matrix.platform }}" -eq "windows-11-arm") {
|
|
$setupExes = Get-ChildItem -Path "./frontend/editor/src-tauri/target" -Filter "*-setup.exe" -Recurse -File
|
|
if ($setupExes.Count -eq 0) {
|
|
Write-Host "[ERROR] No NSIS installer found under target/"
|
|
exit 1
|
|
}
|
|
foreach ($exe in $setupExes) {
|
|
$sig = Get-AuthenticodeSignature -FilePath $exe.FullName
|
|
Write-Host "NSIS installer: $($exe.Name) Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Host "[ERROR] NSIS installer is not signed"
|
|
exit 1
|
|
}
|
|
}
|
|
Write-Host "[SUCCESS] NSIS installer is properly signed"
|
|
exit 0
|
|
}
|
|
|
|
$allSigned = $true
|
|
|
|
# Check MSI installer (outer wrapper - what users download)
|
|
$msiFiles = Get-ChildItem -Path "./frontend/editor/src-tauri/target" -Filter "*.msi" -Recurse -File
|
|
if ($msiFiles.Count -eq 0) {
|
|
Write-Host "[ERROR] No MSI found under target/"
|
|
exit 1
|
|
}
|
|
foreach ($msi in $msiFiles) {
|
|
$sig = Get-AuthenticodeSignature -FilePath $msi.FullName
|
|
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 the inner exe (the file that actually gets installed).
|
|
# This is the critical check - AV flags the installed exe at runtime.
|
|
# Use lessmsi, not `msiexec /a`: msiexec serializes on the global
|
|
# _MSIExecute mutex and hangs forever on hosted runners when another
|
|
# installer is busy. lessmsi reads MSI tables directly - no mutex, no service.
|
|
$msi = $msiFiles[0].FullName
|
|
$extractDir = Join-Path $env:RUNNER_TEMP "msi-verify"
|
|
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
|
|
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
|
|
|
|
choco install lessmsi -y --no-progress --limit-output | Out-Null
|
|
|
|
# Bound the extraction and kill on hang (defence in depth over timeout-minutes).
|
|
$proc = Start-Process lessmsi -ArgumentList 'x', "`"$msi`"", "`"$extractDir\`"" -PassThru -NoNewWindow
|
|
if (-not $proc.WaitForExit(120000)) {
|
|
try { $proc.Kill() } catch {}
|
|
Write-Host "[ERROR] MSI extraction timed out after 120s"
|
|
$allSigned = $false
|
|
} elseif ($proc.ExitCode -ne 0) {
|
|
Write-Host "[ERROR] Failed to extract MSI for verification (exit code: $($proc.ExitCode))"
|
|
$allSigned = $false
|
|
} else {
|
|
$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 (from MSI): Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)"
|
|
if ($sig.Status -ne "Valid") {
|
|
Write-Host "[ERROR] Inner exe extracted from MSI 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
|
|
}
|
|
}
|
|
|
|
if (-not $allSigned) {
|
|
Write-Host "[ERROR] Signature verification failed"
|
|
exit 1
|
|
}
|
|
Write-Host "[SUCCESS] MSI and installed exe are properly signed"
|
|
|
|
# Dump smctl log files on failure. Tauri's signCommand captures smctl output
|
|
# but drops stderr when the command exits non-zero, making failures opaque.
|
|
# The real errors live in smctl's log files - surface them here for debugging.
|
|
- name: Dump smctl logs on failure
|
|
if: ${{ failure() && startsWith(matrix.platform, 'windows') && 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"
|
|
}
|
|
|
|
# Rename + Upload: use always() so artifacts are still collected when verify
|
|
# fails - we need them to manually inspect what actually came out of the build.
|
|
- name: Rename artifacts
|
|
if: always() && steps.digicert-setup.conclusion != 'failure'
|
|
shell: bash
|
|
run: |
|
|
# Absolute dist path so the cd below can't break the copy targets.
|
|
DIST="$GITHUB_WORKSPACE/dist"
|
|
mkdir -p "$DIST"
|
|
cd ./frontend/editor/src-tauri/target
|
|
|
|
echo "=== tauri bundle artifacts ==="
|
|
find . -path "*/bundle/*" \( -name "*.msi" -o -name "*.deb" \
|
|
-o -name "*.rpm" -o -name "*.AppImage" -o -name "*.dmg" \
|
|
-o -name "*.app.tar.gz" -o -name "*.sig" \) 2>/dev/null | sort || true
|
|
echo "=============================="
|
|
|
|
# createUpdaterArtifacts:true signs the native installers in place;
|
|
# each <bundle> ships with a sibling <bundle>.sig consumed by latest.json.
|
|
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
|
find . -name "*.msi" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi" \;
|
|
find . -name "*.msi.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.msi.sig" \;
|
|
elif [ "${{ matrix.platform }}" = "windows-11-arm" ]; then
|
|
# arm64 ships the NSIS installer (WiX MSI has no arm64 support in Tauri).
|
|
# The setup exe is also its own updater payload (-> sibling .sig).
|
|
find . -name "*-setup.exe" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe" \;
|
|
find . -name "*-setup.exe.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}-setup.exe.sig" \;
|
|
elif [ "${{ matrix.platform }}" = "macos-15" ]; then
|
|
# DMG = manual install; .app.tar.gz (+ .sig) = updater payload.
|
|
# Raw .app is intentionally not shipped (hundreds of MB of uncompressed input).
|
|
find . -name "*.dmg" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
|
find . -name "*.app.tar.gz" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.app.tar.gz" \;
|
|
find . -name "*.app.tar.gz.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.app.tar.gz.sig" \;
|
|
else
|
|
# The raw .AppImage IS its updater payload (signed -> .AppImage.sig),
|
|
# not a .tar.gz wrapper - that's only produced under v1Compatible.
|
|
find . -name "*.deb" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.deb" \;
|
|
find . -name "*.deb.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.deb.sig" \;
|
|
find . -name "*.rpm" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.rpm" \;
|
|
find . -name "*.rpm.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.rpm.sig" \;
|
|
find . -name "*.AppImage" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
|
find . -name "*.AppImage.sig" -exec cp {} "$DIST/Stirling-PDF-${{ matrix.name }}.AppImage.sig" \;
|
|
fi
|
|
|
|
- name: Upload build artifacts
|
|
if: always() && steps.digicert-setup.conclusion != 'failure'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: Stirling-PDF-${{ matrix.name }}
|
|
path: ./dist/*
|
|
retention-days: 1
|
|
|
|
collect-and-release:
|
|
needs: [determine-matrix, build, build-jars]
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
|
with:
|
|
egress-policy: audit
|
|
|
|
# Sparse-check out the verifier + pubkey before the artifact downloads
|
|
# so the checkout cannot clobber ./artifacts.
|
|
- name: Checkout updater verifier
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
sparse-checkout: |
|
|
.github/scripts/verify-updater-signatures.py
|
|
frontend/editor/src-tauri/tauri.conf.json
|
|
sparse-checkout-cone-mode: false
|
|
|
|
- name: Download all Tauri artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: Stirling-PDF-*
|
|
path: ./artifacts/tauri
|
|
|
|
- name: Download JAR artifact (default)
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: jar
|
|
path: ./artifacts/jars
|
|
|
|
- name: Download JAR artifact (with login)
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: jar-with-login
|
|
path: ./artifacts/jars
|
|
|
|
- name: Download JAR artifact (server only)
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: jar-server
|
|
path: ./artifacts/jars
|
|
|
|
- name: Display structure of downloaded files
|
|
run: ls -R ./artifacts
|
|
|
|
# tauri-action only emits latest.json when it also publishes the release
|
|
# (tagName/releaseId set). We publish separately via action-gh-release,
|
|
# so build latest.json here from the per-platform .sig files.
|
|
- name: Generate updater latest.json
|
|
env:
|
|
VERSION: ${{ needs.determine-matrix.outputs.version }}
|
|
TAG: v${{ needs.determine-matrix.outputs.version }}
|
|
REPO: ${{ github.repository }}
|
|
run: |
|
|
python3 - << 'PYEOF'
|
|
import json, os, sys
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
|
|
VERSION = os.environ['VERSION']
|
|
TAG = os.environ['TAG']
|
|
REPO = os.environ['REPO']
|
|
|
|
ART = Path('./artifacts/tauri')
|
|
|
|
# Tauri updater looks up {os}-{arch}-{installer} (e.g. linux-x86_64-deb)
|
|
# before bare {os}-{arch}, so per-format Linux keys let deb/rpm/appimage
|
|
# each self-update from their matching file. macOS universal serves both
|
|
# arches from the one .app.tar.gz.
|
|
PLATFORM_MAP = [
|
|
{
|
|
'bundles': ['Stirling-PDF-linux-x86_64.deb'],
|
|
'targets': ['linux-x86_64-deb'],
|
|
},
|
|
{
|
|
'bundles': ['Stirling-PDF-linux-x86_64.rpm'],
|
|
'targets': ['linux-x86_64-rpm'],
|
|
},
|
|
{
|
|
'bundles': ['Stirling-PDF-linux-x86_64.AppImage'],
|
|
'targets': ['linux-x86_64-appimage'],
|
|
},
|
|
{
|
|
'bundles': ['Stirling-PDF-windows-x86_64.msi'],
|
|
'targets': ['windows-x86_64-msi', 'windows-x86_64'],
|
|
},
|
|
{
|
|
'bundles': ['Stirling-PDF-windows-arm64-setup.exe'],
|
|
'targets': ['windows-aarch64-nsis', 'windows-aarch64'],
|
|
},
|
|
{
|
|
'bundles': ['Stirling-PDF-macos-universal.app.tar.gz'],
|
|
'targets': ['darwin-x86_64', 'darwin-aarch64'],
|
|
},
|
|
]
|
|
|
|
# rglob() because download-artifact varies layout: one artifact -> flat,
|
|
# many -> nested under <artifact-name>/.
|
|
def find_signed(name):
|
|
for bundle_path in sorted(ART.rglob(name)):
|
|
sig_path = bundle_path.with_name(bundle_path.name + '.sig')
|
|
if sig_path.exists():
|
|
return bundle_path, sig_path
|
|
return None
|
|
|
|
platforms = {}
|
|
skipped = []
|
|
for entry in PLATFORM_MAP:
|
|
picked = None
|
|
for name in entry['bundles']:
|
|
picked = find_signed(name)
|
|
if picked:
|
|
break
|
|
if not picked:
|
|
skipped.append(
|
|
f"{entry['targets']} (no signed bundle among "
|
|
f"{entry['bundles']} - TAURI_SIGNING_PRIVATE_KEY unset "
|
|
f"or createUpdaterArtifacts disabled?)"
|
|
)
|
|
continue
|
|
bundle_path, sig_path = picked
|
|
signature = sig_path.read_text(encoding='utf-8').strip()
|
|
url = f"https://github.com/{REPO}/releases/download/{TAG}/{bundle_path.name}"
|
|
for target in entry['targets']:
|
|
platforms[target] = {'signature': signature, 'url': url}
|
|
print(f"Added {entry['targets']} from {bundle_path.name}")
|
|
|
|
if skipped:
|
|
print("Skipped platforms:")
|
|
for s in skipped:
|
|
print(f" - {s}")
|
|
|
|
if not platforms:
|
|
print(
|
|
"WARN: no signed updater bundles found - "
|
|
"skipping latest.json generation"
|
|
)
|
|
sys.exit(0)
|
|
|
|
manifest = {
|
|
'version': VERSION,
|
|
'notes': f"See https://github.com/{REPO}/releases/tag/{TAG}",
|
|
'pub_date': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
'platforms': platforms,
|
|
}
|
|
|
|
out = Path('./artifacts/latest.json')
|
|
out.write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8')
|
|
print(f"Generated {out} with platforms: {sorted(platforms.keys())}")
|
|
PYEOF
|
|
|
|
- name: Upload merged artifacts for review
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: release-artifacts
|
|
path: ./artifacts/
|
|
retention-days: 7
|
|
|
|
# Gate publish on valid updater sigs. Runs after the review upload (so
|
|
# artifacts survive for debugging) and before action-gh-release.
|
|
- name: Verify updater signatures
|
|
run: |
|
|
python3 -m pip install --quiet 'cryptography==44.0.0'
|
|
python3 .github/scripts/verify-updater-signatures.py \
|
|
./artifacts/tauri frontend/editor/src-tauri/tauri.conf.json
|
|
|
|
# workflow_dispatch path requires platform=='all' so a single-platform
|
|
# dispatch can't overwrite an existing release's full latest.json with a
|
|
# partial one (action-gh-release defaults overwrite_files:true).
|
|
# release / V2-master always build the full matrix so no extra guard needed.
|
|
# fail_on_unmatched_files makes a missing latest.json or installer fail loudly
|
|
# instead of silently shipping a broken auto-update.
|
|
- name: Upload binaries to Release
|
|
if: (github.event_name == 'workflow_dispatch' && github.event.inputs.test_mode != 'true' && github.event.inputs.platform == 'all') || github.event_name == 'release' || github.ref == 'refs/heads/V2-master'
|
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
with:
|
|
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
|
# Don't regenerate/append notes on re-runs, and don't force this into the
|
|
# "Latest" slot - leave the release body and latest marker as they are.
|
|
generate_release_notes: false
|
|
append_body: false
|
|
make_latest: false
|
|
fail_on_unmatched_files: true
|
|
# Installers + updater payloads + manifest. .sig contents are embedded
|
|
# in latest.json so the .sig files themselves are not uploaded.
|
|
files: |
|
|
./artifacts/**/*.jar
|
|
./artifacts/**/*.msi
|
|
./artifacts/**/*-setup.exe
|
|
./artifacts/**/*.dmg
|
|
./artifacts/**/*.app.tar.gz
|
|
./artifacts/**/*.deb
|
|
./artifacts/**/*.rpm
|
|
./artifacts/**/*.AppImage
|
|
./artifacts/latest.json
|
|
draft: false
|
|
prerelease: false
|