mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-09 21:13:47 +00:00
Signed AI Studio for macOS and Windows (#1029)
This commit is contained in:
1 parent
f7451d0e38
commit
5308027003
6 files changed
+353
-33
No files matched your search
@@ -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"
|
||||
@@ -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".
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -32,7 +32,9 @@
|
||||
"resources/tokenizers/*"
|
||||
],
|
||||
"macOS": {
|
||||
"exceptionDomain": "localhost"
|
||||
"exceptionDomain": "localhost",
|
||||
"signingIdentity": "-",
|
||||
"entitlements": "./Entitlements.plist"
|
||||
},
|
||||
"linux": {
|
||||
"appimage": {
|
||||
|
||||
Reference in new issue
Block a user