Signed AI Studio for macOS and Windows (#1029)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-09 15:56:19 +02:00
1 parent f7451d0e38
commit 5308027003
6 files changed
+353 -33

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
#
# Signs one Windows binary with our Certum code signing certificate. Tauri calls this script as its
# custom sign command for every file it signs: the main binary, the sidecar, PDFium, the NSIS
# plugins, the uninstaller, and the installer.
#
# Usage:
# sign-windows.ps1 <path of the file to sign>
#
# Environment:
# CERTUM_EMAIL e-mail address of the Certum SimplySign account
# CERTUM_OTP TOTP seed of that account, from which ssign computes each one-time code
#
# The script needs ssign (https://github.com/Le-Syl21/ssign) in the PATH. ssign adds the
# certificate chain and a timestamp, and keeps its cloud session for a while, so a build logs in
# only once.
#
# ssign does not sign in place here: it would replace the file by renaming its signed copy over it,
# and Windows refuses that while another process keeps the file open. Tauri does exactly that with
# the main binary while signing it. So ssign writes its copy elsewhere, and this script writes the
# copy back into the very same file, which an open handle with write sharing allows.
#
param(
[Parameter(Mandatory = $true)]
[string] $Path
)
$ErrorActionPreference = 'Stop'
# Tauri passes some paths relative to its working directory. .NET would resolve them against the
# working directory of the process instead, so every call below gets the full path:
$Path = (Resolve-Path -LiteralPath $Path).ProviderPath
# Tauri hands over some files without checking whether they are signed already, the NSIS plugins
# among them. ssign never replaces an existing signature, so a valid one stays as it is, and an
# invalid one stops the build with a clear message instead of three failed attempts:
$signature = Get-AuthenticodeSignature -FilePath $Path
if ($signature.Status -eq 'Valid') {
Write-Output "Already signed, skipping: $Path"
exit 0
}
if ($signature.Status -ne 'NotSigned') {
Write-Error "'$Path' carries a signature which is not valid ($($signature.Status)), and ssign cannot replace it."
}
# Tauri shows the output of its sign command only when signing succeeds. Everything ssign says
# therefore goes into a log, which the workflow prints when the build fails:
$logDirectory = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [System.IO.Path]::GetTempPath() }
$logFile = Join-Path $logDirectory 'sign-windows.log'
$outputDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "sign-windows-$([guid]::NewGuid())"
New-Item -ItemType Directory -Path $outputDirectory | Out-Null
try {
# Certum accepts every one-time code only once. When both Windows builds log in within the same
# 30 seconds, one of them is rejected and succeeds with the next code:
$maxAttempts = 3
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
$stdout = New-TemporaryFile
$stderr = New-TemporaryFile
$process = Start-Process -FilePath 'ssign' -ArgumentList @('--verbose', '--output-dir', "`"$outputDirectory`"", "`"$Path`"") -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdout.FullName -RedirectStandardError $stderr.FullName
$exitCode = $process.ExitCode
Add-Content -Path $logFile -Value "--- $(Get-Date -Format o) | attempt $attempt of $maxAttempts | exit code $exitCode | $Path"
Get-Content -Path $stdout.FullName, $stderr.FullName | Add-Content -Path $logFile
Get-Content -Path $stdout.FullName, $stderr.FullName
Remove-Item -Path $stdout.FullName, $stderr.FullName
if ($exitCode -eq 0) {
break
}
if ($attempt -eq $maxAttempts) {
Write-Error "ssign could not sign '$Path' in $maxAttempts attempts, see $logFile."
}
Write-Output "ssign failed in attempt $attempt of $maxAttempts, retrying in 35 seconds ..."
Start-Sleep -Seconds 35
}
$signedCopy = Join-Path $outputDirectory (Split-Path -Path $Path -Leaf)
$source = [System.IO.File]::OpenRead($signedCopy)
try {
$target = [System.IO.File]::Open($Path, [System.IO.FileMode]::Truncate, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
try {
$source.CopyTo($target)
}
finally {
$target.Dispose()
}
}
finally {
$source.Dispose()
}
}
finally {
Remove-Item -Path $outputDirectory -Recurse -Force -ErrorAction SilentlyContinue
}
$signature = Get-AuthenticodeSignature -FilePath $Path
if ($signature.Status -ne 'Valid') {
Write-Error "The signature of '$Path' is not valid: $($signature.Status), $($signature.StatusMessage)"
}
Write-Output "Signed by '$($signature.SignerCertificate.Subject)': $Path"
+218 -31
View File
@@ -26,7 +26,9 @@ env:
FLATPAK_UV_VERSION: 0.11.28
FLATPAK_UV_SHA256: e490a6464492183c5d4534a5527fb4440f7f2bb2f228162ad7e4afe076dc0224
FLATPAK_FREEDESKTOP_VERSION: "25.08"
SSIGN_VERSION: 0.1.7
SSIGN_SHA256: 4ee5389a74ddbeb67e18fe8c8b963734ca13de7b24beffbe430c3f427ecc5e40
jobs:
determine_run_mode:
name: Determine run mode
@@ -39,6 +41,7 @@ jobs:
is_labeled_pr: ${{ steps.determine.outputs.is_labeled_pr }}
is_pr_build: ${{ steps.determine.outputs.is_pr_build }}
is_internal_pr: ${{ steps.determine.outputs.is_internal_pr }}
signing_mode: ${{ steps.determine.outputs.signing_mode }}
build_enabled: ${{ steps.determine.outputs.build_enabled }}
artifact_retention_days: ${{ steps.determine.outputs.artifact_retention_days }}
skip_reason: ${{ steps.determine.outputs.skip_reason }}
@@ -60,6 +63,7 @@ jobs:
is_labeled_pr=false
is_pr_build=false
is_internal_pr=false
signing_mode=none
build_enabled=false
artifact_retention_days=0
skip_reason="Build disabled: event did not match main push, release tag, or labeled internal PR."
@@ -73,15 +77,17 @@ jobs:
has_run_pipeline_label=true
fi
# Pushes to main only check that every platform still builds. They neither sign
# anything nor upload their artifacts, so they need no retention period either:
if [[ "$REF" == refs/tags/v* ]]; then
is_release=true
signing_mode=release
build_enabled=true
artifact_retention_days=${{ env.RETENTION_INTERMEDIATE_ASSETS }}
skip_reason=""
elif [[ "$EVENT_NAME" == "push" && "$REF" == "refs/heads/main" ]]; then
is_main_push=true
build_enabled=true
artifact_retention_days=7
skip_reason=""
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" && "$ACTION_LABEL_NAME" == "run-pipeline" ]]; then
is_labeled_pr=true
@@ -101,11 +107,18 @@ jobs:
skip_reason="Build disabled: PR does not have the required 'run-pipeline' label."
fi
# Pull requests from forks never receive secrets, so only the labeled pull
# requests of this repository get signed builds:
if [[ "$is_pr_build" == "true" && "$is_internal_pr" == "true" ]]; then
signing_mode=pr
fi
echo "is_release=${is_release}" >> "$GITHUB_OUTPUT"
echo "is_main_push=${is_main_push}" >> "$GITHUB_OUTPUT"
echo "is_labeled_pr=${is_labeled_pr}" >> "$GITHUB_OUTPUT"
echo "is_pr_build=${is_pr_build}" >> "$GITHUB_OUTPUT"
echo "is_internal_pr=${is_internal_pr}" >> "$GITHUB_OUTPUT"
echo "signing_mode=${signing_mode}" >> "$GITHUB_OUTPUT"
echo "build_enabled=${build_enabled}" >> "$GITHUB_OUTPUT"
echo "artifact_retention_days=${artifact_retention_days}" >> "$GITHUB_OUTPUT"
echo "skip_reason=${skip_reason}" >> "$GITHUB_OUTPUT"
@@ -122,6 +135,7 @@ jobs:
IS_LABELED_PR: ${{ steps.determine.outputs.is_labeled_pr }}
IS_PR_BUILD: ${{ steps.determine.outputs.is_pr_build }}
IS_INTERNAL_PR: ${{ steps.determine.outputs.is_internal_pr }}
SIGNING_MODE: ${{ steps.determine.outputs.signing_mode }}
BUILD_ENABLED: ${{ steps.determine.outputs.build_enabled }}
ARTIFACT_RETENTION_DAYS: ${{ steps.determine.outputs.artifact_retention_days }}
SKIP_REASON: ${{ steps.determine.outputs.skip_reason }}
@@ -136,6 +150,7 @@ jobs:
echo "is_labeled_pr: ${IS_LABELED_PR}"
echo "is_pr_build: ${IS_PR_BUILD}"
echo "is_internal_pr: ${IS_INTERNAL_PR}"
echo "signing_mode: ${SIGNING_MODE}"
echo "build_enabled: ${BUILD_ENABLED}"
echo "artifact_retention_days: ${ARTIFACT_RETENTION_DAYS}"
echo "skip_reason: ${SKIP_REASON}"
@@ -155,6 +170,7 @@ jobs:
echo "| is_labeled_pr | ${IS_LABELED_PR} |"
echo "| is_pr_build | ${IS_PR_BUILD} |"
echo "| is_internal_pr | ${IS_INTERNAL_PR} |"
echo "| signing_mode | ${SIGNING_MODE} |"
echo "| build_enabled | ${BUILD_ENABLED} |"
echo "| artifact_retention_days | ${ARTIFACT_RETENTION_DAYS} |"
echo "| skip_reason | ${SKIP_REASON} |"
@@ -245,6 +261,7 @@ jobs:
runs-on: ubuntu-latest
needs: [determine_run_mode, read_metadata]
if: needs.determine_run_mode.outputs.is_release == 'true'
environment: Release-Signing
permissions:
contents: read
outputs:
@@ -500,6 +517,7 @@ jobs:
runs-on: ubuntu-latest
needs: [determine_run_mode, read_metadata, sync_flatpak_repo]
if: needs.determine_run_mode.outputs.is_release == 'true'
environment: Release-Signing
permissions:
contents: read
@@ -774,7 +792,7 @@ jobs:
key: verify-linux-x64-rust-${{ env.RUST_VERSION }}
- name: Setup Rust (stable)
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master (v1) of 2026-10-09
with:
toolchain: ${{ env.RUST_VERSION }}
components: clippy
@@ -813,6 +831,10 @@ jobs:
name: Build app (${{ matrix.dotnet_runtime }})
needs: [determine_run_mode, read_metadata, verify]
if: needs.determine_run_mode.outputs.build_enabled == 'true'
# Releases need their environment on every platform, because Linux signs its updater
# artifacts as well. Pull requests sign only for macOS and Windows; their Linux builds
# run without an environment and do not wait for an approval:
environment: ${{ (needs.determine_run_mode.outputs.signing_mode == 'release' && 'Release-Signing') || (needs.determine_run_mode.outputs.signing_mode == 'pr' && !startsWith(matrix.platform, 'ubuntu') && 'PR-Signing') || '' }}
permissions:
contents: read
@@ -825,42 +847,42 @@ jobs:
dotnet_runtime: 'osx-arm64'
dotnet_name_postfix: '-aarch64-apple-darwin'
tauri_bundle: 'dmg,app,updater'
tauri_bundle_pr: 'dmg'
tauri_bundle_without_updater: 'dmg'
- platform: 'macos-latest' # for Intel-based macOS
rust_target: 'x86_64-apple-darwin'
dotnet_runtime: 'osx-x64'
dotnet_name_postfix: '-x86_64-apple-darwin'
tauri_bundle: 'dmg,app,updater'
tauri_bundle_pr: 'dmg'
tauri_bundle_without_updater: 'dmg'
- platform: 'ubuntu-22.04' # for x86-based Linux
rust_target: 'x86_64-unknown-linux-gnu'
dotnet_runtime: 'linux-x64'
dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
tauri_bundle: 'appimage,updater'
tauri_bundle_pr: 'appimage'
tauri_bundle_without_updater: 'appimage'
- platform: 'ubuntu-22.04-arm' # for ARM-based Linux
rust_target: 'aarch64-unknown-linux-gnu'
dotnet_runtime: 'linux-arm64'
dotnet_name_postfix: '-aarch64-unknown-linux-gnu'
tauri_bundle: 'appimage,updater'
tauri_bundle_pr: 'appimage'
tauri_bundle_without_updater: 'appimage'
- platform: 'windows-latest' # for x86-based Windows
rust_target: 'x86_64-pc-windows-msvc'
dotnet_runtime: 'win-x64'
dotnet_name_postfix: '-x86_64-pc-windows-msvc.exe'
tauri_bundle: 'nsis,updater'
tauri_bundle_pr: 'nsis'
tauri_bundle_without_updater: 'nsis'
- platform: 'windows-latest' # for ARM-based Windows
rust_target: 'aarch64-pc-windows-msvc'
dotnet_runtime: 'win-arm64'
dotnet_name_postfix: '-aarch64-pc-windows-msvc.exe'
tauri_bundle: 'nsis,updater'
tauri_bundle_pr: 'nsis'
tauri_bundle_without_updater: 'nsis'
runs-on: ${{ matrix.platform }}
steps:
@@ -1084,7 +1106,39 @@ jobs:
echo "Cleaning up ..."
rm -fr "$TMP"
- name: Sign PDFium (macOS)
if: startsWith(matrix.platform, 'macos') && needs.determine_run_mode.outputs.signing_mode != 'none'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
set -euo pipefail
# Tauri signs the sidecar and the app, but none of the resources. Apple's notary
# service rejects every library inside the app which does not carry our Developer ID
# and a secure timestamp, so PDFium gets signed here, before Tauri bundles it. The
# keychain is set up the same way Tauri sets up its own one for the app:
keychain="${RUNNER_TEMP}/pdfium-signing.keychain-db"
keychain_password=$(openssl rand -hex 32)
certificate="${RUNNER_TEMP}/pdfium-signing.p12"
trap 'security delete-keychain "$keychain" 2>/dev/null || true; rm -f "$certificate"' EXIT
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$certificate"
current_keychains=$(security list-keychains -d user | tr -d '"')
security create-keychain -p "$keychain_password" "$keychain"
security unlock-keychain -p "$keychain_password" "$keychain"
security import "$certificate" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -k "$keychain"
security set-keychain-settings -t 3600 -u "$keychain"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain" > /dev/null
security list-keychains -d user -s $current_keychains "$keychain"
library="runtime/resources/libraries/libpdfium.dylib"
codesign --force --timestamp --options runtime --keychain "$keychain" --sign "$APPLE_SIGNING_IDENTITY" "$library"
codesign --verify --strict --verbose=2 "$library"
- name: Deploy PDFium (Windows)
if: matrix.platform == 'windows-latest'
env:
@@ -1174,7 +1228,7 @@ jobs:
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
- name: Setup Rust (stable)
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master (v1) of 2026-10-09
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.rust_target }}
@@ -1221,7 +1275,27 @@ jobs:
} else {
Write-Output "Tauri CLI v2 is already installed"
}
- name: Install ssign (Windows)
if: matrix.platform == 'windows-latest' && needs.determine_run_mode.outputs.signing_mode != 'none'
run: |
$ssignDir = Join-Path $env:RUNNER_TEMP "ssign"
New-Item -ItemType Directory -Force -Path $ssignDir | Out-Null
$archive = Join-Path $ssignDir "ssign.zip"
Invoke-WebRequest -Uri "https://github.com/Le-Syl21/ssign/releases/download/v$($env:SSIGN_VERSION)/ssign-windows-x86_64.zip" -OutFile $archive
# ssign gets the secrets of our code signing certificate, so only the exact build we
# checked may run. Both Windows builds run on x64, the ARM one cross-compiles:
$hash = (Get-FileHash -Path $archive -Algorithm SHA256).Hash.ToLowerInvariant()
if ($hash -ne $env:SSIGN_SHA256) {
Write-Error "The SHA-256 of ssign is '$hash', but '$($env:SSIGN_SHA256)' was expected."
exit 1
}
Expand-Archive -Path $archive -DestinationPath $ssignDir -Force
$ssignDir >> $env:GITHUB_PATH
- name: Delete previous artifact, which may exist due to caching (macOS)
if: startsWith(matrix.platform, 'macos')
run: |
@@ -1248,6 +1322,10 @@ jobs:
run: |
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe" -ErrorAction SilentlyContinue
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*" -ErrorAction SilentlyContinue
# A main binary from the cache may carry the signature of an earlier build, which turns
# invalid as soon as Tauri patches the binary. Cargo links a fresh one when it is missing:
rm -Force "runtime/target/${{ matrix.rust_target }}/release/MindWork AI Studio.exe" -ErrorAction SilentlyContinue
- name: Delete previous artifact, which may exist due to caching (Linux - AppImage)
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
@@ -1260,33 +1338,54 @@ jobs:
env:
PRIVATE_PUBLISH_KEY: ${{ secrets.PRIVATE_PUBLISH_KEY }}
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
SECRET_APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
SECRET_APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
SECRET_APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
SECRET_APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
SECRET_APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
SECRET_APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }}
run: |
bundles="${{ matrix.tauri_bundle }}"
tauri_config_args=()
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
echo "Running PR test build without updater bundle signing"
bundles="${{ matrix.tauri_bundle_pr }}"
if [ "${{ needs.determine_run_mode.outputs.is_release }}" != "true" ]; then
echo "Running a non-release build without updater artifacts"
bundles="${{ matrix.tauri_bundle_without_updater }}"
tauri_config_args=(--config '{"bundle":{"createUpdaterArtifacts":false}}')
else
export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
fi
# Only builds which may sign hand Tauri the Developer ID and the key for Apple's notary
# service. Every other build keeps the ad-hoc signature from tauri.conf.json, which an
# exported but empty APPLE_SIGNING_IDENTITY would override:
sign_for_macos=false
if [ "${{ needs.determine_run_mode.outputs.signing_mode }}" != "none" ] && [[ "${{ matrix.platform }}" == macos* ]]; then
sign_for_macos=true
export APPLE_CERTIFICATE="$SECRET_APPLE_CERTIFICATE"
export APPLE_CERTIFICATE_PASSWORD="$SECRET_APPLE_CERTIFICATE_PASSWORD"
export APPLE_SIGNING_IDENTITY="$SECRET_APPLE_SIGNING_IDENTITY"
export APPLE_API_ISSUER="$SECRET_APPLE_API_ISSUER"
export APPLE_API_KEY="$SECRET_APPLE_API_KEY"
export APPLE_API_KEY_PATH="${RUNNER_TEMP}/AuthKey_${APPLE_API_KEY}.p8"
(umask 077 && printf '%s\n' "$SECRET_APPLE_API_PRIVATE_KEY" > "$APPLE_API_KEY_PATH")
fi
cd runtime
cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" "${tauri_config_args[@]}"
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
if [ "${{ needs.determine_run_mode.outputs.is_release }}" != "true" ]; then
updater_artifact_count=$(find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \) | wc -l)
if [ "$updater_artifact_count" -ne 0 ]; then
echo "PR builds must not generate updater artifacts."
echo "Only release builds may generate updater artifacts."
find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \)
exit 1
fi
fi
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" != "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then
if [ "${{ needs.determine_run_mode.outputs.is_release }}" = "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then
app_update_archive_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz' | wc -l)
app_update_signature_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz.sig' | wc -l)
@@ -1295,40 +1394,127 @@ jobs:
exit 1
fi
fi
if [ "$sign_for_macos" = "true" ]; then
# Check what people download: the app inside the disk image. Builds of the disk image
# alone do not even keep the app next to it, because Tauri removes it afterwards:
dmg_path=$(find target/${{ matrix.rust_target }}/release/bundle/dmg -maxdepth 1 -name 'MindWork AI Studio_*.dmg' | head -n 1)
if [ -z "$dmg_path" ]; then
echo "The disk image to check was not found."
exit 1
fi
codesign --verify --verbose=2 "$dmg_path"
mount_point="${RUNNER_TEMP}/dmg-check"
hdiutil attach "$dmg_path" -readonly -nobrowse -mountpoint "$mount_point"
app_path=$(find "$mount_point" -maxdepth 1 -name '*.app' | head -n 1)
if [ -z "$app_path" ]; then
echo "The disk image contains no app."
exit 1
fi
codesign --verify --deep --strict --verbose=2 "$app_path"
xcrun stapler validate "$app_path"
# Gatekeeper has to accept the app as notarized, not just as signed:
assessment=$(spctl --assess --type execute --verbose=4 "$app_path" 2>&1) || true
echo "$assessment"
if [[ "$assessment" != *"source=Notarized Developer ID"* ]]; then
echo "Gatekeeper does not accept the app as notarized."
exit 1
fi
hdiutil detach "$mount_point"
fi
- name: Build Tauri project (Windows)
if: matrix.platform == 'windows-latest'
env:
PRIVATE_PUBLISH_KEY: ${{ secrets.PRIVATE_PUBLISH_KEY }}
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
SECRET_CERTUM_EMAIL: ${{ secrets.CERTUM_EMAIL }}
SECRET_CERTUM_OTP: ${{ secrets.CERTUM_OTP }}
run: |
$bundles = "${{ matrix.tauri_bundle }}"
$tauriConfigArgs = @()
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
Write-Output "Running PR test build without updater bundle signing"
$bundles = "${{ matrix.tauri_bundle_pr }}"
if ("${{ needs.determine_run_mode.outputs.is_release }}" -ne "true") {
Write-Output "Running a non-release build without updater artifacts"
$bundles = "${{ matrix.tauri_bundle_without_updater }}"
$tauriConfigArgs = @("--config", '{"bundle":{"createUpdaterArtifacts":false}}')
} else {
$env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
}
# Only builds which may sign hand Tauri a sign command, which in turn hands every file
# to our script for ssign. The forward slashes keep the path intact while Tauri passes
# the command on to NSIS for signing the uninstaller:
$signForWindows = "${{ needs.determine_run_mode.outputs.signing_mode }}" -ne "none"
if ($signForWindows) {
$env:CERTUM_EMAIL = $env:SECRET_CERTUM_EMAIL
$env:CERTUM_OTP = $env:SECRET_CERTUM_OTP
$signScript = (Join-Path $env:GITHUB_WORKSPACE ".github/scripts/sign-windows.ps1") -replace '\\', '/'
$signConfigPath = Join-Path $env:RUNNER_TEMP "tauri-windows-signing.json"
$signConfig = @{ bundle = @{ windows = @{ signCommand = @{ cmd = "pwsh"; args = @("-NoProfile", "-NonInteractive", "-File", $signScript, "%1") } } } }
$signConfig | ConvertTo-Json -Depth 6 | Set-Content -Path $signConfigPath -Encoding utf8
$tauriConfigArgs += @("--config", $signConfigPath)
}
cd runtime
cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles @tauriConfigArgs
if ($LASTEXITCODE -ne 0) {
# Tauri hides why a sign command failed, so show what our signing script logged:
$signLog = Join-Path $env:RUNNER_TEMP "sign-windows.log"
if (Test-Path $signLog) {
Write-Output "Log of the signing script:"
Get-Content -Path $signLog
}
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
exit $LASTEXITCODE
}
if ("${{ needs.determine_run_mode.outputs.is_release }}" -ne "true") {
$updaterArtifacts = Get-ChildItem -Path "target/${{ matrix.rust_target }}/release/bundle" -Recurse -File -Include "*.app.tar.gz*", "*.AppImage.tar.gz*", "*nsis.zip*" -ErrorAction SilentlyContinue
if ($updaterArtifacts.Count -ne 0) {
Write-Error "PR builds must not generate updater artifacts."
Write-Error "Only release builds may generate updater artifacts."
$updaterArtifacts | ForEach-Object { Write-Error $_.FullName }
exit 1
}
}
if ($signForWindows) {
$installer = Get-ChildItem -Path "target/${{ matrix.rust_target }}/release/bundle/nsis" -Filter "MindWork AI Studio_*.exe" | Select-Object -First 1
if (-not $installer) {
Write-Error "The installer to check was not found."
exit 1
}
# The main binary is missing here on purpose: after bundling, Tauri puts the unpatched,
# unsigned original back in place. The installer holds the signed one, and our signing
# script has already checked that signature right after signing:
$signedFiles = @(
$installer.FullName,
"../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer${{ matrix.dotnet_name_postfix }}",
"resources/libraries/pdfium.dll"
)
foreach ($file in $signedFiles) {
$signature = Get-AuthenticodeSignature -FilePath $file
Write-Output "$($signature.Status) | $($signature.SignerCertificate.Subject) | timestamp: $($null -ne $signature.TimeStamperCertificate) | $file"
if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notlike '*O=Open Source Developer*' -or $null -eq $signature.TimeStamperCertificate) {
Write-Error "'$file' is not signed with our certificate and a timestamp."
exit 1
}
}
}
- name: Upload artifact (macOS)
if: startsWith(matrix.platform, 'macos')
if: startsWith(matrix.platform, 'macos') && needs.determine_run_mode.outputs.is_main_push != 'true'
uses: actions/upload-artifact@v4
with:
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
@@ -1339,7 +1525,7 @@ jobs:
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
- name: Upload artifact (Windows - MSI)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi') && needs.determine_run_mode.outputs.is_main_push != 'true'
uses: actions/upload-artifact@v4
with:
name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }})
@@ -1350,7 +1536,7 @@ jobs:
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
- name: Upload artifact (Windows - NSIS)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis')
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis') && needs.determine_run_mode.outputs.is_main_push != 'true'
uses: actions/upload-artifact@v4
with:
name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }})
@@ -1361,7 +1547,7 @@ jobs:
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
- name: Upload artifact (Linux - AppImage)
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage') && needs.determine_run_mode.outputs.is_main_push != 'true'
uses: actions/upload-artifact@v4
with:
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
@@ -1585,7 +1771,8 @@ jobs:
runs-on: ubuntu-latest
needs: [read_metadata, create_release]
if: startsWith(github.ref, 'refs/tags/v')
environment: Release-Signing
permissions:
contents: write
@@ -1608,7 +1795,7 @@ jobs:
- name: Scan for threats
id: virus_total
uses: crazy-max/ghaction-virustotal@v4
uses: crazy-max/ghaction-virustotal@d34968c958ae283fe976efed637081b9f9dcf74f # v4.2.0
with:
vt_api_key: ${{ secrets.VIRUS_TOTAL_KEY }}
files: release/assets/*
@@ -1665,7 +1852,7 @@ jobs:
echo "EOOOF" >> $GITHUB_ENV
- name: Create release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
prerelease: true
draft: false
@@ -8,6 +8,8 @@
- Added a setting to share which features of AI Studio you use with the operators of self-hosted AI servers, usually your organization. When you switch it on, requests to self-hosted providers and LiteLLM name the feature that sent them, such as the chat, an assistant, or an agent, together with the version of AI Studio and your operating system. Cloud providers never receive these details, and AI Studio sends nothing to its developers. The setting is off by default, and you find it in the app settings. Thanks, Dominic Neuburg (`donework`), for this contribution.
- Added a way for IT departments to let requests to the self-hosted AI servers of your organization name the feature of AI Studio that sent them, either for everybody or as a default that users may change. This shows which features are used. The Enterprise IT documentation explains what is sent and what to clarify before you switch it on, for example, with your works council.
- Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it.
- Improved the installation on macOS: AI Studio is now signed and checked by Apple, so it opens like any other app. You no longer need the Terminal.
- Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better.
- Changed the banner on the start page to a fresh design that shows what AI Studio can do today.
- Fixed a warning about the confidence level that stayed in the dialog for adding a local data source, even after you chose an embedding provider that meets it. The dialog now checks again whenever you change either of them, so a warning also appears when you switch to a provider that falls short.
- Fixed two confidence levels sharing the same name in the German user interface. Moderate is called "Mäßig" again, and Medium stays "Mittel".
+9 -1
View File
@@ -65,6 +65,14 @@ You can now test your changes. To stop the application:
- Press ``Ctrl+C`` in the terminal where the app is running.
- Stop the process via your IDE’s run/debug controls.
## Test builds of pull requests
When a pull request carries the `run-pipeline` label, our GitHub workflow builds the app for every platform and attaches the installers to the workflow run. Whether these test builds are signed depends on where the branch of the pull request lives:
- **Branches in this repository:** The macOS and Windows builds wait until a maintainer approves them in the `PR-Signing` environment. Afterward, they are signed — and notarized for macOS — just like a release, so testers install them without any workaround.
- **Branches in forks:** GitHub never hands secrets to workflows of forks, so these builds stay unsigned. On macOS, testers have to allow the app once in the system settings under Privacy & Security; Windows shows its SmartScreen warning.
When a maintainer wants signed test builds for their own work, they push the branch to this repository instead of their fork. Pushes to `main` only check that every platform still builds; they are neither signed nor uploaded.
## Create a release
In order to create a release:
1. To create a new release, you need to be a maintainer of the repository—see step 8.
@@ -75,7 +83,7 @@ In order to create a release:
6. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes.
7. Your proposed changes will be reviewed and merged.
8. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`.
9. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest).
9. The GitHub Workflow will then build the release, sign it for macOS and Windows, and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). Every job which needs the secrets of the `Release-Signing` environment waits for a maintainer's approval: syncing the Flatpak repository, building the app, collecting the Flatpak artifacts, and publishing the release.
10. Building the release including virus scanning takes some time. Please be patient.
### Rebuild the current pre-release
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
+3 -1
View File
@@ -32,7 +32,9 @@
"resources/tokenizers/*"
],
"macOS": {
"exceptionDomain": "localhost"
"exceptionDomain": "localhost",
"signingIdentity": "-",
"entitlements": "./Entitlements.plist"
},
"linux": {
"appimage": {