ci: sign only what ships, stop signing previews, log signing output
Secret Scan / scan (push) Successful in 5s
Build App (Preview) / compute-version (pull_request) Successful in 4s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / test (pull_request) Successful in 5m25s
Build App (Preview) / build-macos (pull_request) Successful in 4m15s
Build App (Preview) / build-windows (pull_request) Successful in 6m11s
Build App (Preview) / build-linux (pull_request) Successful in 14m11s
Build App (Preview) / prune-previews (pull_request) Successful in 5s
Secret Scan / scan (push) Successful in 5s
Build App (Preview) / compute-version (pull_request) Successful in 4s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / test (pull_request) Successful in 5m25s
Build App (Preview) / build-macos (pull_request) Successful in 4m15s
Build App (Preview) / build-windows (pull_request) Successful in 6m11s
Build App (Preview) / build-linux (pull_request) Successful in 14m11s
Build App (Preview) / prune-previews (pull_request) Successful in 5s
Artifact Signing is metered at about 1000 signatures a month, and there were 67 Windows builds last month. - windows-sign.ps1 signs only what reaches users: the app binary, the MSI, the NSIS installer and the uninstaller. It skips the WiX extension DLLs and NSIS plugins that Tauri also offers, and any file that is already validly signed. That is 4 signatures per release. - Previews are no longer signed, so the preview workflow no longer references the signing secrets. A PR's workflow runs the PR's own code. The inline TAURI_CONFIG there, which the v2 CLI never read, becomes a real `--config`. - Tauri reports a failed sign command only as "failed to run powershell". The script now keeps a transcript, signtool /debug included, and the release job prints it on failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ name: Build App (Preview)
|
|||||||
# This is also the **PR build check**: it compiles Linux, macOS and Windows, so
|
# This is also the **PR build check**: it compiles Linux, macOS and Windows, so
|
||||||
# a push that breaks any of them fails here. Its `test` job runs vitest and
|
# a push that breaks any of them fails here. Its `test` job runs vitest and
|
||||||
# `cargo test` too, so a push that breaks either suite fails here as well.
|
# `cargo test` too, so a push that breaks either suite fails here as well.
|
||||||
|
# Previews are not code-signed (releases are, in build-app.yml): see the
|
||||||
|
# comment on the Windows job's "Build Tauri app" step.
|
||||||
# build-app.yml used to do the build-check job in parallel and publish nothing,
|
# build-app.yml used to do the build-check job in parallel and publish nothing,
|
||||||
# which meant six OS builds per push and one unreachable set of bundles; it is
|
# which meant six OS builds per push and one unreachable set of bundles; it is
|
||||||
# now releases-only.
|
# now releases-only.
|
||||||
@@ -75,7 +77,6 @@ on:
|
|||||||
- "app/**"
|
- "app/**"
|
||||||
- "VERSION"
|
- "VERSION"
|
||||||
- ".gitea/workflows/build-app-preview.yml"
|
- ".gitea/workflows/build-app-preview.yml"
|
||||||
- "scripts/windows-*.ps1"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -671,9 +672,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
rem Pinned to the @tauri-apps/cli version in app/package-lock.json, which
|
rem Pinned to the @tauri-apps/cli version in app/package-lock.json, which
|
||||||
rem the Linux and macOS jobs run: the Windows code-signing path (sign
|
rem the Linux and macOS jobs run, and kept identical to build-app.yml so
|
||||||
rem command, NSIS uninstaller signing) was verified against it, and "^2"
|
rem a preview is built by the same bundler as the release it previews.
|
||||||
rem would change it underneath the pipeline on any Tauri release.
|
|
||||||
cargo install tauri-cli --version "=2.11.0" --locked
|
cargo install tauri-cli --version "=2.11.0" --locked
|
||||||
|
|
||||||
- name: Fix npm platform detection
|
- name: Fix npm platform detection
|
||||||
@@ -695,45 +695,23 @@ jobs:
|
|||||||
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Previews are signed exactly like releases (build-app.yml): a preview is
|
|
||||||
# what gets installed for testing, and SmartScreen treats an unsigned one
|
|
||||||
# no differently from malware. The setup fetches the Artifact Signing
|
|
||||||
# client and a job-local .NET runtime, and writes the Tauri config holding
|
|
||||||
# the sign command, which "Build Tauri app" passes with --config.
|
|
||||||
- name: Prepare code signing
|
|
||||||
env:
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
|
||||||
ARTIFACT_SIGNING_ENDPOINT: ${{ secrets.ARTIFACT_SIGNING_ENDPOINT }}
|
|
||||||
ARTIFACT_SIGNING_ACCOUNT_NAME: ${{ secrets.ARTIFACT_SIGNING_ACCOUNT_NAME }}
|
|
||||||
ARTIFACT_SIGNING_PROFILE_NAME: ${{ secrets.ARTIFACT_SIGNING_PROFILE_NAME }}
|
|
||||||
run: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File scripts\windows-signing-setup.ps1
|
|
||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
# The sign command comes in through --config, from the file "Prepare
|
# Previews are not code-signed: signing is metered, previews are built
|
||||||
# code signing" wrote. Not TAURI_CONFIG: the v2 CLI never reads that
|
# on every PR push, and a PR's workflow runs the PR's own code - so the
|
||||||
# variable (the inline one this step used to set was a no-op), and
|
# signing secrets stay out of this workflow entirely. Releases are
|
||||||
# "Verify signatures" is what caught it.
|
# signed in build-app.yml.
|
||||||
|
#
|
||||||
|
# beforeBuildCommand is blanked through --config because the frontend
|
||||||
|
# was built in the step above. Not TAURI_CONFIG: the v2 CLI never
|
||||||
|
# reads that variable, and the inline one this step used to set was a
|
||||||
|
# no-op.
|
||||||
env:
|
env:
|
||||||
# See the matching comment on the Linux job's "Build Tauri app" step.
|
# See the matching comment on the Linux job's "Build Tauri app" step.
|
||||||
TRIPLE_C_BUILD_SUFFIX: ${{ needs.compute-version.outputs.suffix }}
|
TRIPLE_C_BUILD_SUFFIX: ${{ needs.compute-version.outputs.suffix }}
|
||||||
# Read by the signing dlib itself, never passed on a command line.
|
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
|
|
||||||
run: |
|
run: |
|
||||||
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
cargo tauri build --config "%TRIPLE_C_TAURI_SIGN_CONFIG%"
|
cargo tauri build --config "{\"build\":{\"beforeBuildCommand\":\"\"}}"
|
||||||
|
|
||||||
- name: Verify signatures
|
|
||||||
run: >-
|
|
||||||
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass
|
|
||||||
-File scripts\windows-verify-signatures.ps1
|
|
||||||
app\src-tauri\target\release\triple-c.exe
|
|
||||||
app\src-tauri\target\release\bundle\msi\*.msi
|
|
||||||
app\src-tauri\target\release\bundle\nsis\*.exe
|
|
||||||
|
|
||||||
- name: Collect artifacts
|
- name: Collect artifacts
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -686,6 +686,12 @@ jobs:
|
|||||||
app\src-tauri\target\release\bundle\msi\*.msi
|
app\src-tauri\target\release\bundle\msi\*.msi
|
||||||
app\src-tauri\target\release\bundle\nsis\*.exe
|
app\src-tauri\target\release\bundle\nsis\*.exe
|
||||||
|
|
||||||
|
# Tauri reports a failed sign command as just "failed to run powershell";
|
||||||
|
# windows-sign.ps1 keeps its own transcript, signtool /debug included.
|
||||||
|
- name: Show signing output
|
||||||
|
if: failure()
|
||||||
|
run: if exist .code-signing\sign-output.log type .code-signing\sign-output.log
|
||||||
|
|
||||||
- name: Collect artifacts
|
- name: Collect artifacts
|
||||||
run: |
|
run: |
|
||||||
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
|||||||
+78
-37
@@ -1,7 +1,18 @@
|
|||||||
# windows-sign.ps1 <file> - sign one file with Azure Artifact Signing.
|
# windows-sign.ps1 <file> - sign one file with Azure Artifact Signing.
|
||||||
#
|
#
|
||||||
# Tauri's bundle.windows.signCommand, set up by windows-signing-setup.ps1.
|
# Tauri's bundle.windows.signCommand, set up by windows-signing-setup.ps1.
|
||||||
# Tauri calls it once per file it signs and fails the build on a non-zero exit.
|
# Tauri calls it once per file it wants signed and fails the build on a
|
||||||
|
# non-zero exit - but shows none of this script's output when it does, so
|
||||||
|
# everything is also appended to $TRIPLE_C_SIGN_OUTPUT, which the workflow
|
||||||
|
# prints if the job fails.
|
||||||
|
#
|
||||||
|
# Only what ships is signed. Tauri also offers build-time tools - the WiX
|
||||||
|
# extension DLLs candle/light load, the NSIS plugins makensis embeds - and
|
||||||
|
# each signature is metered (about 1000 a month), so those are skipped. The
|
||||||
|
# allowlist below is the whole of what reaches users: the app binary, the MSI,
|
||||||
|
# the NSIS installer, and the uninstaller makensis writes to the job's temp
|
||||||
|
# directory. A file that already carries a valid signature is skipped too:
|
||||||
|
# Tauri presents the app binary once per bundle type.
|
||||||
#
|
#
|
||||||
# This may run as 32-bit PowerShell: the NSIS uninstaller is signed from inside
|
# This may run as 32-bit PowerShell: the NSIS uninstaller is signed from inside
|
||||||
# makensis, which is 32-bit and resolves `powershell` to the SysWOW64 copy. So
|
# makensis, which is 32-bit and resolves `powershell` to the SysWOW64 copy. So
|
||||||
@@ -15,47 +26,77 @@
|
|||||||
param([Parameter(Mandatory = $true)][string]$Path)
|
param([Parameter(Mandatory = $true)][string]$Path)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||||
|
|
||||||
foreach ($name in 'TRIPLE_C_SIGNTOOL', 'TRIPLE_C_SIGN_DLIB', 'TRIPLE_C_SIGN_METADATA', 'TRIPLE_C_SIGN_TIMESTAMP',
|
function Write-Log([string]$Text) {
|
||||||
'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET') {
|
Write-Host $Text
|
||||||
if (-not [Environment]::GetEnvironmentVariable($name)) {
|
if ($env:TRIPLE_C_SIGN_OUTPUT) {
|
||||||
throw "$name is not set - run windows-signing-setup.ps1 first and pass the AZURE_* secrets to this step"
|
try { [IO.File]::AppendAllText($env:TRIPLE_C_SIGN_OUTPUT, "$Text`n", $utf8) } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (-not (Test-Path -LiteralPath $Path)) { throw "No such file to sign: $Path" }
|
|
||||||
|
|
||||||
# /d names the product in the UAC prompt, which for an MSI would otherwise show
|
try {
|
||||||
# a temporary file name. The timestamp is what keeps the signature valid after
|
foreach ($name in 'TRIPLE_C_SIGNTOOL', 'TRIPLE_C_SIGN_DLIB', 'TRIPLE_C_SIGN_METADATA', 'TRIPLE_C_SIGN_TIMESTAMP',
|
||||||
# the short-lived Artifact Signing certificate expires, so it is not optional.
|
'TRIPLE_C_SIGN_TMP', 'TRIPLE_C_SIGN_LOG', 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET') {
|
||||||
$arguments = @(
|
if (-not [Environment]::GetEnvironmentVariable($name)) {
|
||||||
'sign', '/v',
|
throw "$name is not set - run windows-signing-setup.ps1 first and pass the AZURE_* secrets to this step"
|
||||||
'/fd', 'SHA256',
|
|
||||||
'/tr', $env:TRIPLE_C_SIGN_TIMESTAMP, '/td', 'SHA256',
|
|
||||||
'/d', 'Triple-C',
|
|
||||||
'/dlib', $env:TRIPLE_C_SIGN_DLIB,
|
|
||||||
'/dmdf', $env:TRIPLE_C_SIGN_METADATA,
|
|
||||||
$Path
|
|
||||||
)
|
|
||||||
|
|
||||||
# Timestamp servers and the signing endpoint both fail transiently now and
|
|
||||||
# then; a retry is cheaper than a failed three-platform release.
|
|
||||||
#
|
|
||||||
# Stop is relaxed around the call: Tauri captures this script's output, and
|
|
||||||
# PowerShell 5.1 turns a native command's stderr into error records when its
|
|
||||||
# own streams are redirected - under Stop, signtool's first warning would kill
|
|
||||||
# the script before its exit code is read.
|
|
||||||
$ErrorActionPreference = 'Continue'
|
|
||||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
|
||||||
& $env:TRIPLE_C_SIGNTOOL @arguments 2>&1 | ForEach-Object { "$_" }
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
# The evidence "Verify signatures" needs for files it cannot see
|
|
||||||
# afterwards - the NSIS uninstaller is embedded in the installer.
|
|
||||||
if ($env:TRIPLE_C_SIGN_LOG) {
|
|
||||||
[IO.File]::AppendAllText($env:TRIPLE_C_SIGN_LOG, "$Path`n", (New-Object System.Text.UTF8Encoding $false))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) { throw "No such file to sign: $Path" }
|
||||||
|
$full = (Resolve-Path -LiteralPath $Path).ProviderPath
|
||||||
|
Write-Log "== $full"
|
||||||
|
|
||||||
|
$ships = ($full -match '\\target\\release\\[^\\]+\.exe$') -or
|
||||||
|
($full -match '\\target\\release\\bundle\\(msi|nsis)\\[^\\]+\.(msi|exe)$') -or
|
||||||
|
$full.StartsWith($env:TRIPLE_C_SIGN_TMP.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)
|
||||||
|
if (-not $ships) {
|
||||||
|
Write-Log 'skipped: build-time file, not shipped'
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
Write-Host "signtool exited $LASTEXITCODE signing $Path (attempt $attempt of 3)"
|
|
||||||
if ($attempt -lt 3) { Start-Sleep -Seconds (10 * $attempt) }
|
$existing = Get-AuthenticodeSignature -LiteralPath $full
|
||||||
|
if ($existing.Status -eq 'Valid' -and $existing.TimeStamperCertificate) {
|
||||||
|
Write-Log "skipped: already signed by $($existing.SignerCertificate.Subject)"
|
||||||
|
[IO.File]::AppendAllText($env:TRIPLE_C_SIGN_LOG, "$full`n", $utf8)
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# /d names the product in the UAC prompt, which for an MSI would otherwise
|
||||||
|
# show a temporary file name. The timestamp is what keeps the signature
|
||||||
|
# valid after the short-lived Artifact Signing certificate expires, so it
|
||||||
|
# is not optional. /debug makes the dlib say why it failed, into the log.
|
||||||
|
$arguments = @(
|
||||||
|
'sign', '/v', '/debug',
|
||||||
|
'/fd', 'SHA256',
|
||||||
|
'/tr', $env:TRIPLE_C_SIGN_TIMESTAMP, '/td', 'SHA256',
|
||||||
|
'/d', 'Triple-C',
|
||||||
|
'/dlib', $env:TRIPLE_C_SIGN_DLIB,
|
||||||
|
'/dmdf', $env:TRIPLE_C_SIGN_METADATA,
|
||||||
|
$full
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stop is relaxed around the call: Tauri captures this script's output,
|
||||||
|
# and PowerShell 5.1 turns a native command's stderr into error records
|
||||||
|
# when its own streams are redirected - under Stop, signtool's first
|
||||||
|
# warning would kill the script before its exit code is read. Timestamp
|
||||||
|
# servers and the signing endpoint fail transiently now and then, hence
|
||||||
|
# the retries.
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||||
|
& $env:TRIPLE_C_SIGNTOOL @arguments 2>&1 | ForEach-Object { Write-Log "$_" }
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
if ($code -eq 0) {
|
||||||
|
# The evidence "Verify signatures" needs for the file it cannot see
|
||||||
|
# afterwards - the NSIS uninstaller is embedded in the installer.
|
||||||
|
[IO.File]::AppendAllText($env:TRIPLE_C_SIGN_LOG, "$full`n", $utf8)
|
||||||
|
Write-Log 'signed'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
Write-Log "signtool exited $code (attempt $attempt of 3)"
|
||||||
|
if ($attempt -lt 3) { Start-Sleep -Seconds (10 * $attempt) }
|
||||||
|
}
|
||||||
|
exit 1
|
||||||
|
} catch {
|
||||||
|
Write-Log "windows-sign.ps1 failed: $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
}
|
}
|
||||||
exit 1
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ $tauriConfigPath = Join-Path $root 'tauri.signing.conf.json'
|
|||||||
$tmpDir = Join-Path $root 'tmp'
|
$tmpDir = Join-Path $root 'tmp'
|
||||||
New-Item -ItemType Directory -Path $tmpDir | Out-Null
|
New-Item -ItemType Directory -Path $tmpDir | Out-Null
|
||||||
$signLog = Join-Path $root 'signed.log'
|
$signLog = Join-Path $root 'signed.log'
|
||||||
|
$signOutput = Join-Path $root 'sign-output.log'
|
||||||
|
|
||||||
# $GITHUB_ENV is KEY=VALUE lines. Written without a BOM: PowerShell 5.1's
|
# $GITHUB_ENV is KEY=VALUE lines. Written without a BOM: PowerShell 5.1's
|
||||||
# utf8 encoding adds one, which would corrupt the first key.
|
# utf8 encoding adds one, which would corrupt the first key.
|
||||||
@@ -157,6 +158,7 @@ $lines = @(
|
|||||||
"DOTNET_ROOT_X64=$dotnetDir"
|
"DOTNET_ROOT_X64=$dotnetDir"
|
||||||
"TRIPLE_C_TAURI_SIGN_CONFIG=$tauriConfigPath"
|
"TRIPLE_C_TAURI_SIGN_CONFIG=$tauriConfigPath"
|
||||||
"TRIPLE_C_SIGN_LOG=$signLog"
|
"TRIPLE_C_SIGN_LOG=$signLog"
|
||||||
|
"TRIPLE_C_SIGN_OUTPUT=$signOutput"
|
||||||
"TRIPLE_C_SIGN_TMP=$tmpDir"
|
"TRIPLE_C_SIGN_TMP=$tmpDir"
|
||||||
"TEMP=$tmpDir"
|
"TEMP=$tmpDir"
|
||||||
"TMP=$tmpDir"
|
"TMP=$tmpDir"
|
||||||
|
|||||||
Reference in New Issue
Block a user