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
19 changes: 17 additions & 2 deletions .github/workflows/release-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ jobs:
uses: swatinem/rust-cache@v2
with:
workspaces: 'openless-all/app/src-tauri -> target'
cache-on-failure: true

- name: Cache macOS release dependencies
if: startsWith(matrix.platform, 'macos')
Expand Down Expand Up @@ -424,6 +425,19 @@ jobs:
}
Write-Host "[ok] MSI rebuilt at $msiPath"

# Tests are tooling, not application payload. A targeted repair checks out
# immutable app source above, but uses the reviewed workflow commit's smoke
# script so fixes to test semantics do not require moving a release tag.
- name: Checkout installer smoke tooling
if: matrix.platform == 'windows-latest'
uses: actions/checkout@v4
with:
ref: ${{ github.workflow_sha }}
path: release-tooling
sparse-checkout: openless-all/app/scripts/windows-ime-install-smoke.ps1
sparse-checkout-cone-mode: false
persist-credentials: false

- name: Verify Windows installers register TSF IME
if: matrix.platform == 'windows-latest'
shell: pwsh
Expand All @@ -433,6 +447,7 @@ jobs:
# after the NSIS install/uninstall has loaded the TSF DLLs. /norestart
# suppresses an OS reboot, not Restart Manager application shutdown.
# Keep both real installer/registry checks; restore the machine policy.
$smokeScript = Join-Path $env:GITHUB_WORKSPACE 'release-tooling\openless-all\app\scripts\windows-ime-install-smoke.ps1'
$policy = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Installer'
$policyName = 'DisableAutomaticApplicationShutdown'
$previous = (Get-ItemProperty -LiteralPath $policy -Name $policyName -ErrorAction SilentlyContinue).$policyName
Expand All @@ -441,7 +456,7 @@ jobs:
New-ItemProperty -LiteralPath $policy -Name $policyName -PropertyType DWord -Value 1 -Force | Out-Null
$nsis = Get-ChildItem "src-tauri\target\release\bundle\nsis\*.exe" -ErrorAction Stop | Select-Object -First 1
if (-not $nsis) { throw "NSIS installer not found." }
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $nsis.FullName -InstallerKind nsis
powershell.exe -NoProfile -ExecutionPolicy Bypass -File $smokeScript -InstallerPath $nsis.FullName -InstallerKind nsis
if ($LASTEXITCODE -ne 0) {
throw "NSIS installer smoke failed with exit $LASTEXITCODE"
}
Expand All @@ -451,7 +466,7 @@ jobs:
}
$msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction Stop | Select-Object -First 1
if (-not $msi) { throw "MSI installer not found." }
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $msi.FullName -InstallerKind msi
powershell.exe -NoProfile -ExecutionPolicy Bypass -File $smokeScript -InstallerPath $msi.FullName -InstallerKind msi
if ($LASTEXITCODE -ne 0) {
throw "MSI installer smoke failed with exit $LASTEXITCODE"
}
Expand Down
8 changes: 8 additions & 0 deletions docs/releases/2.0.1-windows-ci-isolation-20261016.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ Microsoft 官方定义 `/norestart` 不等于禁止 Restart Manager 关闭应用
来源:[DisableAutomaticApplicationShutdown](https://learn.microsoft.com/en-us/windows/win32/msi/disableautomaticapplicationshutdown)、[MSIRESTARTMANAGERCONTROL](https://learn.microsoft.com/en-us/windows/win32/msi/msirestartmanagercontrol)。官方 Markdown 已从 MicrosoftDocs/win32 对应 desktop-src/Msi 文件核对。

工作流与契约经 PR/CI 后,仅使用已存在的管理员/草稿/精确 tag 源码/七项 CI 守卫的 Windows-only repair 入口,对 `v2.0.1-tauri` 构建并验证。应用源码、版本与标签不变;最终仍须 Windows smoke 成功、全部资产与签名核验后才公开 Stable。

## 隔离后的安装器返回值

PR #1213 精确 CI `38888000367` 七项通过并合入 beta。Windows-only run `38889597557` 不再关闭 runner,MSI 安装返回 **3010**。Microsoft 官方 [Windows Installer error codes](https://learn.microsoft.com/en-us/windows/win32/msi/error-codes) 定义它为安装成功、需要重启,旧 smoke 却将所有非零返回视为失败,尚未执行后续注册验证。

修正通用进程 helper 的可接受返回值参数:默认仍仅 0,只有 MSI 安装/卸载显式允许 0 与 3010;NSIS 保持 0-only,1603 等错误及会实际重启 CI 的 1641 仍拒绝。接受 3010 后仍完整执行文件、DLL 路径和所有 COM/TSF/后端注册检查,不将成功返回码本身当成安装验收。新增 Windows 上执行实际 helper 的六个 mock 返回值回归,不启动安装器或修改注册表。

为保留不可变应用源码,工作流单独 sparse checkout **`github.workflow_sha`** 固定的 smoke 工具脚本;应用 checkout 仍指向原始 release tag。复制的只有测试脚本,编译/打包 payload 不使用工具分支;正常 tag 构建和定向草稿修复采用同一工具来源规则。Windows 编译缓存允许失败时保存,避免 smoke 问题每次重编所有依赖。后续实际 smoke 成功前仍不公开 Stable。
12 changes: 8 additions & 4 deletions openless-all/app/scripts/windows-ime-install-smoke.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,19 @@ function Invoke-CheckedProcess {
[string]$FilePath,
[string[]]$ArgumentList = @(),
[Parameter(Mandatory = $true)]
[string]$Label
[string]$Label,
[int[]]$SuccessExitCodes = @(0)
)

$commandLine = Join-ProcessArguments $ArgumentList
Write-Host "[run] $Label`: $FilePath $commandLine"
$process = Start-Process -FilePath $FilePath -ArgumentList $commandLine -Wait -PassThru
if ($process.ExitCode -ne 0) {
if ($process.ExitCode -notin $SuccessExitCodes) {
throw "$Label failed with exit code $($process.ExitCode)"
}
if ($process.ExitCode -eq 3010) {
Write-Host "[ok] $Label succeeded; Windows requested a reboot (3010). Continuing payload/registry verification without rebooting."
}
}

function Open-LocalMachineSubKey {
Expand Down Expand Up @@ -178,7 +182,7 @@ function Uninstall-OpenLess {
}
Invoke-CheckedProcess -FilePath $uninstaller -ArgumentList @("/S") -Label "NSIS uninstall"
} else {
Invoke-CheckedProcess -FilePath "msiexec.exe" -ArgumentList @("/x", $InstallerPath, "/qn", "/norestart") -Label "MSI uninstall"
Invoke-CheckedProcess -FilePath "msiexec.exe" -ArgumentList @("/x", $InstallerPath, "/qn", "/norestart") -Label "MSI uninstall" -SuccessExitCodes @(0, 3010)
}
}

Expand All @@ -190,7 +194,7 @@ $InstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
if ($InstallerKind -eq "nsis") {
Invoke-CheckedProcess -FilePath $InstallerPath -ArgumentList @("/S", "/AllUsers") -Label "NSIS install"
} else {
Invoke-CheckedProcess -FilePath "msiexec.exe" -ArgumentList @("/i", $InstallerPath, "/qn", "/norestart") -Label "MSI install"
Invoke-CheckedProcess -FilePath "msiexec.exe" -ArgumentList @("/i", $InstallerPath, "/qn", "/norestart") -Label "MSI install" -SuccessExitCodes @(0, 3010)
}

$installRoot = Assert-OpenLessImeInstalled
Expand Down
47 changes: 47 additions & 0 deletions openless-all/app/scripts/windows-ime-install-smoke.test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
$ErrorActionPreference = 'Stop'

# Load only the actual helper definitions; never run installation or registry code.
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
(Join-Path $PSScriptRoot 'windows-ime-install-smoke.ps1'),
[ref]$tokens,
[ref]$parseErrors
)
if ($parseErrors) { throw ($parseErrors | Out-String) }
foreach ($name in @('Join-ProcessArguments', 'Invoke-CheckedProcess')) {
$function = $ast.Find({
param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name
}, $true)
if (-not $function) { throw "Missing helper: $name" }
. ([scriptblock]::Create($function.Extent.Text))
}

function Start-Process {
param([string]$FilePath, [string]$ArgumentList, [switch]$Wait, [switch]$PassThru)
if (-not $Wait -or -not $PassThru) { throw 'Helper must wait for the installer exit code' }
[pscustomobject]@{ ExitCode = $script:TestExitCode }
}

$cases = @(
@{ Code = 0; Allowed = @(0); ShouldThrow = $false },
@{ Code = 3010; Allowed = @(0); ShouldThrow = $true },
@{ Code = 0; Allowed = @(0, 3010); ShouldThrow = $false },
@{ Code = 3010; Allowed = @(0, 3010); ShouldThrow = $false },
@{ Code = 1603; Allowed = @(0, 3010); ShouldThrow = $true },
@{ Code = 1641; Allowed = @(0, 3010); ShouldThrow = $true }
)
foreach ($case in $cases) {
$script:TestExitCode = $case.Code
$threw = $false
try {
Invoke-CheckedProcess -FilePath 'test-only.exe' -Label 'mock installer' -SuccessExitCodes $case.Allowed
} catch {
$threw = $true
}
if ($threw -ne $case.ShouldThrow) {
throw "Wrong result for exit code $($case.Code), allowed $($case.Allowed -join ',')"
}
}
Write-Host 'windows-ime-install-smoke.test.ps1 passed: six real-helper exit-code cases, no installation'
19 changes: 19 additions & 0 deletions openless-all/app/scripts/windows-msi-release-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import vm from 'node:vm';
import { fileURLToPath } from 'node:url';
import yaml from 'js-yaml';

const workflow = yaml.load(
Expand Down Expand Up @@ -64,7 +65,13 @@ const publisher = steps.find((s) => s.uses?.startsWith('softprops/action-gh-rele
assert.equal(publisher.with.tag_name, '${{ env.OPENLESS_RELEASE_TAG }}');
assert.equal(publisher.with.draft, true);
assert.ok(steps.find((s) => s.name === 'Confirm repair target remains draft'));
const smokeTooling = steps.find((s) => s.name === 'Checkout installer smoke tooling');
assert.equal(smokeTooling.with.ref, '${{ github.workflow_sha }}');
assert.equal(smokeTooling.with['sparse-checkout'], 'openless-all/app/scripts/windows-ime-install-smoke.ps1');
assert.equal(smokeTooling.with['persist-credentials'], false);
const smoke = steps.find((s) => s.name === 'Verify Windows installers register TSF IME');
assert.match(smoke.run, /-File \$smokeScript/);
assert.ok(steps.indexOf(smokeTooling) < steps.indexOf(smoke));
assert.equal(smoke.if, "matrix.platform == 'windows-latest'");
assert.match(smoke.run, /DisableAutomaticApplicationShutdown/);
assert.match(smoke.run, /-PropertyType DWord -Value 1 -Force/);
Expand All @@ -75,6 +82,18 @@ for (const kind of ['nsis', 'msi']) {
assert.match(smoke.run, new RegExp(kind.toUpperCase() + ' installer smoke failed'));
}
assert.doesNotMatch(smoke.run, /continue-on-error|SkipUninstall|exit 0/);
const smokeSource = readFileSync(new URL('windows-ime-install-smoke.ps1', import.meta.url), 'utf8');
assert.match(smokeSource, /\[int\[\]\]\$SuccessExitCodes = @\(0\)/);
assert.match(smokeSource, /\$process.ExitCode -notin \$SuccessExitCodes/);
assert.match(smokeSource, /-Label "MSI install" -SuccessExitCodes @\(0, 3010\)/);
assert.match(smokeSource, /-Label "MSI uninstall" -SuccessExitCodes @\(0, 3010\)/);
assert.doesNotMatch(smokeSource, /-Label "NSIS (?:install|uninstall)" -SuccessExitCodes/);
assert.ok(smokeSource.indexOf('$installRoot = Assert-OpenLessImeInstalled') > smokeSource.indexOf('-Label "MSI install"'));
if (process.platform === 'win32') {
const runtime = spawnSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', fileURLToPath(new URL('windows-ime-install-smoke.test.ps1', import.meta.url))], { encoding: 'utf8' });
assert.equal(runtime.status, 0, runtime.stdout + runtime.stderr);
console.log(runtime.stdout.trim());
}
const schema = JSON.parse(
readFileSync(
new URL('../node_modules/@tauri-apps/cli/config.schema.json', import.meta.url),
Expand Down
Loading