Skip to content
Closed
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
17 changes: 17 additions & 0 deletions .github/workflows/release-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,15 @@ jobs:
LITHE_VERSION: ${{ steps.version.outputs.version }}
LITHE_WINDOWS_TIMESTAMP_SERVER: ${{ secrets.WINDOWS_TIMESTAMP_SERVER }}
WINDOWS_RELEASE_SIGNED: ${{ steps.signing.outputs.signed }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
LITHE_UPDATER_PUBLIC_KEY: ${{ vars.TAURI_UPDATER_PUBLIC_KEY }}
run: |
$packageArgs = @{
Configuration = "Release"
Version = $env:LITHE_VERSION
UpdaterEndpoint = "https://github.com/$env:GITHUB_REPOSITORY/releases/latest/download/latest.json"
RequireUpdaterArtifacts = $true
}
if ($env:WINDOWS_RELEASE_SIGNED -eq "true") {
$packageArgs.RequireAuthenticodeSignature = $true
Expand All @@ -99,6 +104,15 @@ jobs:
}
./scripts/package-windows.ps1 @packageArgs

- name: Create updater manifest
shell: pwsh
run: |
./scripts/create-windows-updater-manifest.ps1 `
-Version "${{ steps.version.outputs.version }}" `
-Repository "$env:GITHUB_REPOSITORY" `
-ReleaseTag "${{ steps.version.outputs.tag }}" `
-ReleaseNotesPath "docs/releases/v${{ steps.version.outputs.version }}.md"

- name: Verify installer checksum
shell: pwsh
run: |
Expand Down Expand Up @@ -134,4 +148,7 @@ jobs:
gh release upload $env:RELEASE_TAG `
"dist/Lithe-$env:LITHE_VERSION-windows-x64.exe" `
"dist/Lithe-$env:LITHE_VERSION-windows-x64.exe.sha256" `
"dist/Lithe-$env:LITHE_VERSION-windows-x64.nsis.zip" `
"dist/Lithe-$env:LITHE_VERSION-windows-x64.nsis.zip.sig" `
"dist/latest.json" `
--repo $env:GITHUB_REPOSITORY --clobber
63 changes: 63 additions & 0 deletions docs/releases/windows-updater.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Windows in-app updates

The Windows application uses the Tauri v2 updater with the existing NSIS
bundle. Stable releases publish a signed updater archive and `latest.json`
alongside the normal Windows installer. The application checks the manifest at
the `latest` stable GitHub Release for the repository that built it.

## One-time repository configuration

An owner of the release repository must generate the updater signing keypair:

```powershell
cd windows/tauri
bunx tauri signer generate
```

Store the generated values in the release repository settings:

| Kind | Name | Value |
| --- | --- | --- |
| Actions secret | `TAURI_SIGNING_PRIVATE_KEY` | Complete generated private key |
| Actions secret | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Private-key password; omit this secret when the key has no password |
| Actions variable | `TAURI_UPDATER_PUBLIC_KEY` | Complete generated public key |

Never commit the private key or its password. The public key is injected into
the packaged application by the release workflow and is safe to store as a
repository variable.

The existing `WINDOWS_SIGNING_CERTIFICATE_BASE64`,
`WINDOWS_SIGNING_CERTIFICATE_PASSWORD`, and `WINDOWS_TIMESTAMP_SERVER` settings
continue to control Windows Authenticode signing. Authenticode and Tauri updater
signatures serve different purposes and should both be configured for a public
release.

## Stable release artifacts

The `Release Windows` workflow publishes:

- `Lithe-<version>-windows-x64.exe`
- `Lithe-<version>-windows-x64.exe.sha256`
- `Lithe-<version>-windows-x64.nsis.zip`
- `Lithe-<version>-windows-x64.nsis.zip.sig`
- `latest.json`

`latest.json` points at the versioned updater archive in the same GitHub
Release. The workflow fails instead of publishing an unsigned updater when the
updater keypair is not configured.

## Release verification

Before announcing a stable release, install the preceding Windows version and
verify this sequence against the new release:

1. **Help > Check for Updates** reports the new version and release notes.
2. Download progress reaches completion.
3. Unsaved buffers are handled before the application exits.
4. The updater installs the signed NSIS bundle and relaunches Lithe.
5. The relaunched application reports the new version.
6. A second manual check reports that the application is current.

Also verify that a manifest with a modified signature is rejected. Use a
temporary keypair and a fork Release for development tests; never reuse a test
private key for official releases.
63 changes: 63 additions & 0 deletions scripts/create-windows-updater-manifest.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidatePattern('^[0-9]+\.[0-9]+\.[0-9]+$')]
[string]$Version,
[Parameter(Mandatory = $true)]
[ValidatePattern('^[^/\s]+/[^/\s]+$')]
[string]$Repository,
[string]$ReleaseTag = "v$Version",
[string]$OutputDirectory = "dist",
[string]$ReleaseNotesPath
)

$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$output = Join-Path $root $OutputDirectory
$assetName = "Lithe-$Version-windows-x64.nsis.zip"
$asset = Join-Path $output $assetName
$signaturePath = "$asset.sig"

if (-not (Test-Path -LiteralPath $asset -PathType Leaf)) {
throw "Windows updater bundle does not exist: $asset"
}
if (-not (Test-Path -LiteralPath $signaturePath -PathType Leaf)) {
throw "Windows updater signature does not exist: $signaturePath"
}

$signature = (Get-Content -LiteralPath $signaturePath -Raw).Trim()
if ([string]::IsNullOrWhiteSpace($signature)) {
throw "Windows updater signature is empty: $signaturePath"
}

$notes = "Lithe $Version for Windows."
if (-not [string]::IsNullOrWhiteSpace($ReleaseNotesPath)) {
$resolvedNotesPath = if ([System.IO.Path]::IsPathRooted($ReleaseNotesPath)) {
$ReleaseNotesPath
} else {
Join-Path $root $ReleaseNotesPath
}
if (Test-Path -LiteralPath $resolvedNotesPath -PathType Leaf) {
$notes = (Get-Content -LiteralPath $resolvedNotesPath -Raw).Trim()
}
}

$encodedTag = [System.Uri]::EscapeDataString($ReleaseTag)
$encodedAssetName = [System.Uri]::EscapeDataString($assetName)
$downloadURL = "https://github.com/$Repository/releases/download/$encodedTag/$encodedAssetName"
$manifest = [ordered]@{
version = $Version
notes = $notes
pub_date = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
platforms = [ordered]@{
"windows-x86_64" = [ordered]@{
signature = $signature
url = $downloadURL
}
}
}

New-Item -ItemType Directory -Force -Path $output | Out-Null
$manifestPath = Join-Path $output "latest.json"
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding utf8
Write-Output "Windows updater manifest created: $manifestPath"
90 changes: 70 additions & 20 deletions scripts/package-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,61 @@ param(
[string]$OutputDirectory = "dist",
[string]$CertificateThumbprint = $env:LITHE_WINDOWS_CERTIFICATE_THUMBPRINT,
[string]$TimestampServer = $env:LITHE_WINDOWS_TIMESTAMP_SERVER,
[switch]$RequireAuthenticodeSignature
[switch]$RequireAuthenticodeSignature,
[string]$UpdaterPublicKey = $env:LITHE_UPDATER_PUBLIC_KEY,
[string]$UpdaterEndpoint = $env:LITHE_UPDATER_ENDPOINT,
[switch]$RequireUpdaterArtifacts
)

$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$windowsApp = Join-Path $root "windows/tauri"
$output = Join-Path $root $OutputDirectory
$versionConfig = Join-Path $env:RUNNER_TEMP "lithe-tauri-version.json"
$taskTempRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
[System.IO.Path]::GetTempPath()
} else {
$env:RUNNER_TEMP
}
$versionConfig = Join-Path $taskTempRoot "lithe-tauri-version.json"

$versionOverrides = @{
version = $Version
bundle = @{}
}
if (-not [string]::IsNullOrWhiteSpace($CertificateThumbprint)) {
$windowsSigning = @{
certificateThumbprint = $CertificateThumbprint
digestAlgorithm = "sha256"
}
if (-not [string]::IsNullOrWhiteSpace($TimestampServer)) {
$windowsSigning.timestampUrl = $TimestampServer
}
$versionOverrides.bundle.windows = $windowsSigning
} elseif ($RequireAuthenticodeSignature) {
throw "Authenticode signing is required but no certificate thumbprint was configured."
}

if ($RequireUpdaterArtifacts) {
if ([string]::IsNullOrWhiteSpace($env:TAURI_SIGNING_PRIVATE_KEY)) {
throw "Tauri updater signing is required but TAURI_SIGNING_PRIVATE_KEY is not configured."
}
if ([string]::IsNullOrWhiteSpace($UpdaterPublicKey)) {
throw "Tauri updater signing is required but LITHE_UPDATER_PUBLIC_KEY is not configured."
}
if ([string]::IsNullOrWhiteSpace($UpdaterEndpoint)) {
throw "Tauri updater signing is required but LITHE_UPDATER_ENDPOINT is not configured."
}

@{ version = $Version } | ConvertTo-Json | Set-Content -Encoding utf8 $versionConfig
$versionOverrides.bundle.createUpdaterArtifacts = true
$versionOverrides.plugins = @{
updater = @{
pubkey = $UpdaterPublicKey
endpoints = @($UpdaterEndpoint)
}
}
}

$versionOverrides | ConvertTo-Json -Depth 5 | Set-Content -Encoding utf8 $versionConfig
Set-Location $windowsApp
& bun install --frozen-lockfile
if ($LASTEXITCODE -ne 0) { throw "Windows frontend dependency installation failed" }
Expand Down Expand Up @@ -43,27 +88,32 @@ $installer = Join-Path $output "Lithe-$Version-windows-x64.exe"
Copy-Item -LiteralPath $bundle.FullName -Destination $installer -Force

if (-not [string]::IsNullOrWhiteSpace($CertificateThumbprint)) {
$certificate = Get-ChildItem -LiteralPath "Cert:\CurrentUser\My\$CertificateThumbprint" `
-ErrorAction SilentlyContinue
if ($null -eq $certificate) {
throw "The requested Authenticode certificate is not installed: $CertificateThumbprint"
}
$signatureArgs = @{
FilePath = $installer
Certificate = $certificate
HashAlgorithm = "SHA256"
}
if (-not [string]::IsNullOrWhiteSpace($TimestampServer)) {
$signatureArgs.TimestampServer = $TimestampServer
}
$signature = Set-AuthenticodeSignature @signatureArgs
$signature = Get-AuthenticodeSignature -LiteralPath $installer
if ($signature.Status -ne "Valid") {
throw "Authenticode signing failed: $($signature.Status)"
throw "Tauri Authenticode signing failed: $($signature.Status)"
}
} elseif ($RequireAuthenticodeSignature) {
throw "Authenticode signing is required but no certificate thumbprint was configured."
}

$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $installer).Hash.ToLowerInvariant()
"$hash $(Split-Path -Leaf $installer)" | Set-Content -Encoding ascii "$installer.sha256"
Write-Output "Windows installer created: $installer"

if ($RequireUpdaterArtifacts) {
$updaterBundle = Get-ChildItem -LiteralPath $bundleDirectory -Filter "*.nsis.zip" -File |
Select-Object -First 1
if ($null -eq $updaterBundle) {
throw "Tauri updater bundle was not found in $bundleDirectory"
}

$updaterSignature = Get-Item -LiteralPath "$($updaterBundle.FullName).sig" `
-ErrorAction SilentlyContinue
if ($null -eq $updaterSignature) {
throw "Tauri updater signature was not found for $($updaterBundle.Name)"
}

$publishedUpdaterBundle = Join-Path $output "Lithe-$Version-windows-x64.nsis.zip"
Copy-Item -LiteralPath $updaterBundle.FullName -Destination $publishedUpdaterBundle -Force
Copy-Item -LiteralPath $updaterSignature.FullName `
-Destination "$publishedUpdaterBundle.sig" -Force
Write-Output "Windows updater bundle created: $publishedUpdaterBundle"
}
31 changes: 31 additions & 0 deletions scripts/test-windows-updater-manifest.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$testOutput = Join-Path $root "dist/updater-manifest-test"
$version = "9.8.7"
$bundle = Join-Path $testOutput "Lithe-$version-windows-x64.nsis.zip"

try {
New-Item -ItemType Directory -Force -Path $testOutput | Out-Null
Set-Content -LiteralPath $bundle -Value "test updater bundle" -Encoding ascii
Set-Content -LiteralPath "$bundle.sig" -Value "test-signature" -Encoding ascii

& "$PSScriptRoot/create-windows-updater-manifest.ps1" `
-Version $version `
-Repository "example/Lithe-IDEA" `
-ReleaseTag "v$version" `
-OutputDirectory "dist/updater-manifest-test"

$manifest = Get-Content -LiteralPath (Join-Path $testOutput "latest.json") -Raw |
ConvertFrom-Json
if ($manifest.version -ne $version) { throw "Manifest version is incorrect" }
$platform = $manifest.platforms.'windows-x86_64'
if ($platform.signature -ne "test-signature") { throw "Manifest signature is incorrect" }
$expectedURL = "https://github.com/example/Lithe-IDEA/releases/download/v$version/Lithe-$version-windows-x64.nsis.zip"
if ($platform.url -ne $expectedURL) { throw "Manifest download URL is incorrect" }

Write-Output "Windows updater manifest test passed."
} finally {
if (Test-Path -LiteralPath $testOutput) {
Remove-Item -LiteralPath $testOutput -Recurse -Force
}
}
Loading