mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
## Summary This pull request restructures Gradle dependency caching across the GitHub Actions workflows. The central `gradle-cache-prime` job is responsible for preparing the shared backend Gradle cache. Reusable workflows restore that shared cache without writing to the same key, while independently triggered workflows use isolated cache namespaces. ## What changed ### Shared Gradle cache - Added a stable `gradle-v1-` cache namespace for the shared backend cache. - The cache key includes the runner OS, runner architecture, JDK version, and the relevant Gradle configuration files. - The cache key is calculated before Gradle runs and reused for the later save step. - The prime job performs a lookup first and resolves backend dependencies only when the exact cache is missing. - This prevents Gradle or Spotless changes during the prime step from producing a different save key from the key used by downstream jobs. ### Reusable workflows - Backend, OpenAPI, license, Docker, E2E, and migration workflows restore the shared cache instead of writing to the shared key. - The backend build matrix includes `matrix.jdk-version` in its cache key. - Enterprise, Tauri, and generated-model workflows support the `use_shared_cache` boolean input. - When `use_shared_cache` is enabled, those workflows restore the shared cache. - When it is disabled, they use workflow-specific cache namespaces. ### Independent workflows Independent workflows now use separate cache prefixes, including: - `gradle-license-report-v1-` - `gradle-swagger-v1-` - `gradle-push-docker-v1-` - `gradle-tauri-releases-v1-` - `gradle-deploy-pr-v1-` - `gradle-playwright-e2e-v1-` - `gradle-generated-models-v1-` This prevents them from creating or affecting the shared backend cache before the prime job. ### Build and E2E flow - Removed the `-PnoSpotless` option from the central Gradle dependency-resolution command. - Removed the separate Gradle dependency prime/retry logic from the live E2E workflow. - Connected the Tauri build and generated-models check to the central cache-prime job. ## Motivation Previously, multiple workflows could use and save the same Gradle cache key independently. The first workflow to save the cache could therefore determine its contents, even if it had resolved a different or incomplete set of dependencies. The cache key was also evaluated after some Gradle tasks had run. If Gradle or Spotless modified a file covered by `hashFiles(...)`, the save key could differ from the restore key used by downstream jobs. This change gives the shared cache a single owner, isolates workflow-specific caches, and makes cache usage deterministic across the CI pipeline. ## Expected result - `gradle-cache-prime` is the single writer for the shared backend Gradle cache. - Downstream jobs restore the same cache without competing cache writes. - Independently triggered workflows remain isolated through their own cache namespaces. - Changes to the monitored Gradle configuration files produce a new cache key. - The normal Gradle/Spotless path is included when the shared cache is populated. ## Validation - Compared the cache key expressions and `hashFiles(...)` inputs across the affected workflows. - Verified that the central restore and save steps use the same precomputed key. - CI should confirm that the prime job populates the shared cache and downstream workflows only restore it. ## Checklist - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have performed a self-review of my changes - [ ] I have run the relevant CI checks - [ ] I have tested the workflow changes
902 lines
43 KiB
YAML
902 lines
43 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:
|
|
environment: ci-unsigned
|
|
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@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Cache Gradle
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
|
|
- 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 "${INPUT_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
|
|
|
|
env:
|
|
INPUT_PLATFORM: ${{ github.event.inputs.platform }}
|
|
build-jars:
|
|
environment: ci-unsigned
|
|
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@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Cache Gradle
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
|
|
- name: Set up JDK 25
|
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: "temurin"
|
|
|
|
- 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:
|
|
environment: release-signing
|
|
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 }}
|
|
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
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' || '' }}
|
|
|
|
- name: Cache Gradle
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: gradle-tauri-releases-v1-${{ runner.os }}-${{ runner.arch }}-jdk-25-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/libs.versions.toml', 'buildSrc/**', 'settings.gradle', 'build.gradle', 'app/**/build.gradle', 'gradle/**/*.gradle') }}
|
|
|
|
# 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@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.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@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
|
with:
|
|
java-version: "25"
|
|
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
|
|
|
- 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/release') }}
|
|
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/release') }}
|
|
shell: pwsh
|
|
run: |
|
|
Write-Host "Setting up DigiCert KeyLocker environment..."
|
|
|
|
# Decode client certificate
|
|
$certBytes = [Convert]::FromBase64String("$env: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=$env:SM_HOST" >> $env:GITHUB_ENV
|
|
echo "SM_API_KEY=$env:SM_API_KEY" >> $env:GITHUB_ENV
|
|
echo "SM_CLIENT_CERT_PASSWORD=$env:SM_CLIENT_CERT_PASSWORD" >> $env:GITHUB_ENV
|
|
echo "SM_KEYPAIR_ALIAS=$env: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"
|
|
}
|
|
}
|
|
|
|
env:
|
|
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
|
|
SM_HOST: ${{ secrets.SM_HOST }}
|
|
SM_API_KEY: ${{ secrets.SM_API_KEY }}
|
|
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
|
|
SM_KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }}
|
|
- 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/release')
|
|
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/release')
|
|
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/release') }}
|
|
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/release') }}
|
|
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/release')
|
|
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@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
|
|
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 the release branch, 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/release')) && '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 }}
|
|
|
|
# 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/release')) && '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/release')
|
|
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/release') }}
|
|
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@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
|
|
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
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: Install uv
|
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: |
|
|
engine/pyproject.toml
|
|
engine/uv.lock
|
|
- name: Verify updater signatures
|
|
run: |
|
|
uv run --project engine --locked --only-group updater-signatures python .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 event / release branch 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/release'
|
|
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
|
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
|