Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 70 additions & 10 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,16 +281,35 @@ jobs:
env:
WINDOWS_CERTIFICATE: '${{ secrets.WINDOWS_CERTIFICATE }}'
WINDOWS_CERTIFICATE_PASSWORD: '${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}'
LEGACY_WIN_CSC_LINK: '${{ secrets.WIN_CSC_LINK }}'
LEGACY_WIN_CSC_KEY_PASSWORD: '${{ secrets.WIN_CSC_KEY_PASSWORD }}'
run: |
if (-not $env:WINDOWS_CERTIFICATE -or -not $env:WINDOWS_CERTIFICATE_PASSWORD) {
throw 'WINDOWS_CERTIFICATE and WINDOWS_CERTIFICATE_PASSWORD are required for published Windows releases.'
# Prefer the Tauri-style WINDOWS_CERTIFICATE pair; fall back to the
# legacy electron-builder WIN_CSC_LINK pair if only that exists.
$pfx = $null
$pfxPassword = $null
$primaryIncomplete = ([bool]$env:WINDOWS_CERTIFICATE) -ne ([bool]$env:WINDOWS_CERTIFICATE_PASSWORD)
$legacyIncomplete = ([bool]$env:LEGACY_WIN_CSC_LINK) -ne ([bool]$env:LEGACY_WIN_CSC_KEY_PASSWORD)
if ($primaryIncomplete -or $legacyIncomplete) {
throw 'Incomplete Windows signing configuration: provide a complete WINDOWS_CERTIFICATE/WINDOWS_CERTIFICATE_PASSWORD pair or WIN_CSC_LINK/WIN_CSC_KEY_PASSWORD pair.'
}
if ($env:WINDOWS_CERTIFICATE -and $env:WINDOWS_CERTIFICATE_PASSWORD) {
Comment thread
yiliang114 marked this conversation as resolved.
$pfx = $env:WINDOWS_CERTIFICATE
$pfxPassword = $env:WINDOWS_CERTIFICATE_PASSWORD
} elseif ($env:LEGACY_WIN_CSC_LINK -and $env:LEGACY_WIN_CSC_KEY_PASSWORD) {
$pfx = $env:LEGACY_WIN_CSC_LINK
$pfxPassword = $env:LEGACY_WIN_CSC_KEY_PASSWORD
}
if ($pfx) {
$path = Join-Path $env:RUNNER_TEMP 'qwen-code-desktop.pfx'
[IO.File]::WriteAllBytes($path, [Convert]::FromBase64String($pfx))
$password = ConvertTo-SecureString $pfxPassword -AsPlainText -Force
$certificate = Import-PfxCertificate -FilePath $path -CertStoreLocation Cert:\CurrentUser\My -Password $password
$windowsConfig = @{ bundle = @{ windows = @{ certificateThumbprint = $certificate.Thumbprint } } } | ConvertTo-Json -Compress -Depth 3
"WINDOWS_CONFIG=$windowsConfig" | Out-File -FilePath $env:GITHUB_ENV -Append
} else {
Write-Output "::warning::Windows signing certificate is not configured. Windows artifacts will be unsigned and may trigger SmartScreen warnings."
}
Comment thread
yiliang114 marked this conversation as resolved.
$path = Join-Path $env:RUNNER_TEMP 'qwen-code-desktop.pfx'
[IO.File]::WriteAllBytes($path, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE))
$password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force
$certificate = Import-PfxCertificate -FilePath $path -CertStoreLocation Cert:\CurrentUser\My -Password $password
$windowsConfig = @{ bundle = @{ windows = @{ certificateThumbprint = $certificate.Thumbprint } } } | ConvertTo-Json -Compress -Depth 3
"WINDOWS_CONFIG=$windowsConfig" | Out-File -FilePath $env:GITHUB_ENV -Append

- name: 'Prepare bundled runtime'
working-directory: 'packages/desktop-shell'
Expand All @@ -308,6 +327,39 @@ jobs:
working-directory: 'packages/desktop-shell'
run: 'npm run test:release'

- name: 'Sign bundled vendor binaries (macOS)'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] Signed macOS binaries invalidate the bundled checksums.json

prepare-runtime.js writes checksums.json at the end of Prepare bundled runtime (line 314) and smoke:runtime verifies it immediately after. This step then calls codesign --force on node/bin/node and the rg binaries, changing their SHA-256. The published macOS app bundle therefore ships an integrity manifest whose listed hashes no longer match the actual signed binaries.

Suggested fix: regenerate checksums.json after the codesign calls (extract the checksum-writing logic into a reusable script), or move the signing calls before writeChecksums() inside prepare-runtime.js when APPLE_SIGNING_IDENTITY is available.

if: "runner.os == 'macOS' && inputs.dry_run == false"
Comment thread
yiliang114 marked this conversation as resolved.
working-directory: 'packages/desktop-shell'
shell: 'bash'
env:
APPLE_SIGNING_IDENTITY: '${{ env.APPLE_SIGNING_IDENTITY }}'
run: |
set -euo pipefail
# Sign all native macOS executables in the bundled runtime so
# notarization does not reject them. Tauri only signs the main
# app binary; resources like ripgrep and the Node.js runtime are
# embedded verbatim and must be signed beforehand.
runtime_dir="runtime/qwen-code"
# ripgrep vendor binaries
rg_dir="$runtime_dir/lib/vendor/ripgrep"
if [ -d "$rg_dir" ]; then
Comment thread
yiliang114 marked this conversation as resolved.
find "$rg_dir" -type f -name 'rg' -path '*-darwin/*' -exec \
codesign --force --sign "$APPLE_SIGNING_IDENTITY" \
--options runtime --timestamp \
--entitlements src-tauri/Entitlements.plist {} +
else
echo "::warning::Ripgrep vendor directory not found at $rg_dir; no ripgrep binaries signed."
fi
# Node.js runtime binary
node_bin="$runtime_dir/node/bin/node"
if [ -f "$node_bin" ]; then
codesign --force --sign "$APPLE_SIGNING_IDENTITY" \
Comment thread
yiliang114 marked this conversation as resolved.
--options runtime --timestamp \
--entitlements src-tauri/Entitlements.plist "$node_bin"
else
echo "::warning::Node.js runtime binary not found at $node_bin; no Node.js binary signed."
fi

- name: 'Build desktop installers'
working-directory: 'packages/desktop-shell'
shell: 'bash'
Expand All @@ -321,7 +373,7 @@ jobs:
args=( ${{ matrix.tauri_args }} )
if [ "$DRY_RUN" = 'true' ]; then
args+=(--no-sign)
elif [ "$RUNNER_OS" = 'Windows' ]; then
elif [ "$RUNNER_OS" = 'Windows' ] && [ -n "$WINDOWS_CONFIG" ]; then
args+=(--config "$WINDOWS_CONFIG")
fi
npm run tauri -- build "${args[@]}"
Expand All @@ -338,10 +390,18 @@ jobs:
- name: 'Verify Windows signature'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] Windows signature verification only covers NSIS .exe

The step checks nsis/*.exe | Select-Object -First 1. If Tauri is later configured to produce .msi installers or multiple per-target artifacts, this step will silently ignore them.

Consider iterating over all Windows installer artifacts produced by the build, or documenting that NSIS is the only attested format.

if: "runner.os == 'Windows' && inputs.dry_run == false"
shell: 'pwsh'
env:
WINDOWS_CONFIG: '${{ env.WINDOWS_CONFIG }}'
run: |
$installer = Get-ChildItem packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/nsis/*.exe | Select-Object -First 1
$signature = Get-AuthenticodeSignature $installer.FullName
if ($signature.Status -ne 'Valid') { throw "Invalid Authenticode signature: $($signature.Status)" }
if ($signature.Status -eq 'Valid') {
Write-Output "Windows installer is Authenticode-signed."
} elseif ($signature.Status -eq 'NotSigned' -and -not $env:WINDOWS_CONFIG) {
Write-Output "::warning::Windows installer is unsigned (no code signing certificate configured). SmartScreen will warn users on first install."
} else {
throw "Invalid Authenticode signature: $($signature.Status)"
}

- name: 'Create Electron bridge archive'
if: "runner.os == 'macOS' && inputs.electron_bridge"
Expand Down
60 changes: 60 additions & 0 deletions packages/desktop-shell/scripts/test-release.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ try {
testBootstrapBridgeConfiguration();
testLegacyApplicationIdentity();
testElectronBridgeWorkflow();
testDesktopReleaseSigningWorkflow();
testResolveLogRoot();
testSliceNewLog();
testUpdateManifest(path.join(root, 'manifest'));
Expand Down Expand Up @@ -79,6 +80,65 @@ function testElectronBridgeWorkflow() {
}
}

function testDesktopReleaseSigningWorkflow() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] String-fragile workflow-contract tests

testDesktopReleaseSigningWorkflow asserts exact YAML string fragments and uses indexOf on step names. A formatting-only refactor (e.g., renaming a step, rewrapping a command) would break these tests without changing workflow behavior.

Consider parsing the workflow YAML once and asserting against the parsed AST, or at least isolating the literal fragments into a single source of truth.

const workflow = fs.readFileSync(
path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'),
'utf8',
);
const primaryIncomplete =
'$primaryIncomplete = ([bool]$env:WINDOWS_CERTIFICATE) -ne ' +
'([bool]$env:WINDOWS_CERTIFICATE_PASSWORD)';
const legacyIncomplete =
'$legacyIncomplete = ([bool]$env:LEGACY_WIN_CSC_LINK) -ne ' +
'([bool]$env:LEGACY_WIN_CSC_KEY_PASSWORD)';
assert.ok(
workflow.includes(primaryIncomplete),
'Windows signing must fail closed when the primary certificate pair is incomplete',
);
assert.ok(
workflow.includes(legacyIncomplete),
'Windows signing must fail closed when the legacy certificate pair is incomplete',
);
assert.ok(
workflow.includes(
'elif [ "$RUNNER_OS" = \'Windows\' ] && [ -n "$WINDOWS_CONFIG" ]; then',
),
'Windows builds must only pass a Tauri config when signing config exists',
);
assert.ok(
workflow.includes(
"$signature.Status -eq 'NotSigned' -and -not $env:WINDOWS_CONFIG",
),
'Unsigned Windows installers are only allowed when no signing config exists',
);
assert.ok(
workflow.includes(
"--entitlements src-tauri/Entitlements.plist {} +",
),
'ripgrep codesign failures must fail the signing step',
);
assert.match(
workflow,
/Ripgrep vendor directory not found at \$rg_dir/,
'missing ripgrep binaries must be visible in release logs',
);
assert.match(
workflow,
/Node\.js runtime binary not found at \$node_bin/,
'missing Node.js runtime binary must be visible in release logs',
);
assert.ok(
workflow.indexOf("name: 'Prepare bundled runtime'") <
workflow.indexOf("name: 'Sign bundled vendor binaries (macOS)'"),
'vendor binaries must be signed after the runtime is prepared',
);
assert.ok(
workflow.indexOf("name: 'Sign bundled vendor binaries (macOS)'") <
workflow.indexOf("name: 'Build desktop installers'"),
'vendor binaries must be signed before Tauri builds installers',
);
}

function testBootstrapBridgeConfiguration() {
assert.equal(
tauriConfig.app?.withGlobalTauri,
Expand Down
Loading