feat(x): sign Windows builds with Azure Trusted Signing in CI

Wire windowsSign (packager + Squirrel maker) through Azure Trusted
Signing's signtool dlib. Signing activates only when the CI env vars
are present; local and mac/linux builds are unaffected. The Windows
job stages the dlib, metadata.json, and a modern SDK signtool under
C:\azsign (space-free paths — @electron/windows-sign splits
signWithParams on spaces).

Requires repo secrets: AZURE_TENANT_ID, AZURE_CLIENT_ID,
AZURE_CLIENT_SECRET, AZURE_ENDPOINT, AZURE_CODE_SIGNING_NAME,
AZURE_CERT_PROFILE_NAME.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-23 10:33:35 +05:30
parent 873187805f
commit 71c5034e10
2 changed files with 65 additions and 0 deletions

View file

@ -243,8 +243,45 @@ jobs:
run: pnpm install --frozen-lockfile
working-directory: apps/x
- name: Setup Azure Trusted Signing
shell: pwsh
run: |
# Stage everything under a path WITHOUT spaces: @electron/windows-sign
# splits signWithParams on spaces, so the /dlib and /dmdf paths must be
# space-free.
$dir = "C:\azsign"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
# The signtool plugin that delegates signing to Azure Trusted Signing.
nuget install Microsoft.Trusted.Signing.Client -x -OutputDirectory "$dir\nuget"
$dlib = "$dir\nuget\Microsoft.Trusted.Signing.Client\bin\x64\Azure.CodeSigning.Dlib.dll"
if (-not (Test-Path $dlib)) { throw "Azure.CodeSigning.Dlib.dll not found at $dlib" }
# Tells the dlib which account/profile to sign with.
@{
Endpoint = "${{ secrets.AZURE_ENDPOINT }}"
CodeSigningAccountName = "${{ secrets.AZURE_CODE_SIGNING_NAME }}"
CertificateProfileName = "${{ secrets.AZURE_CERT_PROFILE_NAME }}"
} | ConvertTo-Json | Out-File "$dir\metadata.json" -Encoding utf8
# Newest Windows SDK signtool (the one vendored by @electron/windows-sign
# is too old to load the dlib). Copied into the space-free dir.
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe" |
Where-Object { $_.Directory.Parent.Name -match '^\d+(\.\d+)+$' } |
Sort-Object { [version]$_.Directory.Parent.Name } | Select-Object -Last 1
if (-not $signtool) { throw "signtool.exe not found in Windows SDK" }
Copy-Item $signtool.FullName "$dir\signtool.exe"
& "$dir\signtool.exe" | Select-Object -First 3
echo "AZURE_CODE_SIGNING_DLIB=$dlib" >> $env:GITHUB_ENV
echo "AZURE_METADATA_JSON=$dir\metadata.json" >> $env:GITHUB_ENV
echo "SIGNTOOL_PATH=$dir\signtool.exe" >> $env:GITHUB_ENV
- name: Build electron app
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
VITE_PUBLIC_POSTHOG_KEY: ${{ secrets.VITE_PUBLIC_POSTHOG_KEY }}
VITE_PUBLIC_POSTHOG_HOST: ${{ secrets.VITE_PUBLIC_POSTHOG_HOST }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -12,6 +12,28 @@ const pkg = require('./package.json');
const SKIP_PACMAN = process.env.ROWBOAT_SKIP_PACMAN === '1';
const SKIP_CODE_SIGNING = process.env.ROWBOAT_SKIP_CODE_SIGNING === '1';
// Windows code signing via Azure Trusted Signing — CI-only. The GitHub workflow
// downloads the Azure dlib, writes metadata.json, and exports these env vars;
// when they're absent (local builds, mac/linux jobs) Windows signing is skipped.
// signtool loads Azure.CodeSigning.Dlib.dll (/dlib), which reads metadata.json
// (/dmdf) for the endpoint/account/profile and authenticates via
// AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET.
// NOTE: @electron/windows-sign splits signWithParams on spaces, so the dlib and
// metadata paths must not contain spaces (the workflow stages them in C:\azsign).
const WINDOWS_SIGN =
!SKIP_CODE_SIGNING &&
process.env.AZURE_CODE_SIGNING_DLIB &&
process.env.AZURE_METADATA_JSON
? {
// The signtool vendored by @electron/windows-sign is too old for the
// Azure dlib; the workflow points this at the Windows SDK's signtool.
...(process.env.SIGNTOOL_PATH ? { signToolPath: process.env.SIGNTOOL_PATH } : {}),
signWithParams: `/v /debug /dlib ${process.env.AZURE_CODE_SIGNING_DLIB} /dmdf ${process.env.AZURE_METADATA_JSON}`,
timestampServer: 'http://timestamp.acs.microsoft.com',
hashes: ['sha256'],
}
: undefined;
// Stage the ACP coding-adapters (@agentclientprotocol/*-acp) and their full
// production dependency closure into the packaged app.
//
@ -202,6 +224,9 @@ module.exports = {
NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)',
NSCameraUsageDescription: 'Rowboat uses your camera in video chat mode so the assistant can see you and give feedback (e.g. pitch practice).',
},
// Signs the packaged app's executables (rowboat.exe etc.); the Squirrel
// maker below separately signs the installer it produces.
...(WINDOWS_SIGN ? { windowsSign: WINDOWS_SIGN } : {}),
...(SKIP_CODE_SIGNING ? {} : {
osxSign: {
batchCodesignCalls: true,
@ -260,6 +285,9 @@ module.exports = {
// GitHub release page next to setup.exe and users grab the
// wrong one (it neither launches the app nor auto-updates).
noMsi: true,
// Sign the Squirrel installer (setup.exe) and the binaries it
// repackages. No-op unless the CI signing env vars are set.
...(WINDOWS_SIGN ? { windowsSign: WINDOWS_SIGN } : {}),
})
},
{