diff --git a/.github/workflows/release-tauri.yml b/.github/workflows/release-tauri.yml index 392250137..0fe0c9e7c 100644 --- a/.github/workflows/release-tauri.yml +++ b/.github/workflows/release-tauri.yml @@ -429,21 +429,38 @@ jobs: shell: pwsh working-directory: 'openless-all/app' run: | - $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 - if ($LASTEXITCODE -ne 0) { - throw "NSIS installer smoke failed with exit $LASTEXITCODE" - } - if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') { - Write-Host "[ok] Skipping MSI smoke for beta release channel." - return - } - $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 - if ($LASTEXITCODE -ne 0) { - throw "MSI installer smoke failed with exit $LASTEXITCODE" + # The disposable runner must not be shut down by MSI Restart Manager + # 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. + $policy = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Installer' + $policyName = 'DisableAutomaticApplicationShutdown' + $previous = (Get-ItemProperty -LiteralPath $policy -Name $policyName -ErrorAction SilentlyContinue).$policyName + try { + New-Item -Path $policy -Force | Out-Null + 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 + if ($LASTEXITCODE -ne 0) { + throw "NSIS installer smoke failed with exit $LASTEXITCODE" + } + if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') { + Write-Host "[ok] Skipping MSI smoke for beta release channel." + return + } + $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 + if ($LASTEXITCODE -ne 0) { + throw "MSI installer smoke failed with exit $LASTEXITCODE" + } + } finally { + if ($null -ne $previous) { + New-ItemProperty -LiteralPath $policy -Name $policyName -PropertyType DWord -Value $previous -Force | Out-Null + } else { + Remove-ItemProperty -LiteralPath $policy -Name $policyName -ErrorAction SilentlyContinue + } } - name: Disambiguate macOS updater bundle filename diff --git a/docs/releases/2.0.1-windows-ci-isolation-20261016.md b/docs/releases/2.0.1-windows-ci-isolation-20261016.md new file mode 100644 index 000000000..74ded0d64 --- /dev/null +++ b/docs/releases/2.0.1-windows-ci-isolation-20261016.md @@ -0,0 +1,11 @@ +# 2.0.1 Windows 安装器 CI 隔离 + +Stable 标签 `v2.0.1-tauri` 固定在 `33d17a4e`,精确源码 CI `38876378343` 及 main CI `38878764285` 均七项通过。macOS 两架构、Android 四 ABI 已构建、下载并核验;不重建这些资产。 + +桌面 run `38878773775` 两次 Windows job(`116900142784`、`116916344722`)都已生成 NSIS 和数字 MSI 版本的安装包;NSIS 安装、COM/TSF/后端注册检查及卸载通过。两次均在随后的 `msiexec /i ... /qn /norestart` 开始约 1 秒后收到 runner shutdown signal,MSI 检查被取消,未上传 Windows 资产。重复位置排除了继续盲目重跑的价值,符合 MSI Restart Manager 关闭持有 TSF 组件的 runner 进程的现象;日志不把它记录为已通过安装验收。 + +Microsoft 官方定义 `/norestart` 不等于禁止 Restart Manager 关闭应用。CI 在真实安装器 smoke 之前,临时将 `HKLM\Software\Policies\Microsoft\Windows\Installer` 的 DWORD `DisableAutomaticApplicationShutdown` 设为 1,禁止 Windows Installer 与 Restart Manager 交互,并在 finally 恢复原值/移除本次新增属性。该设置仅存在于可丢弃的 Windows CI 主机,不写入 MSI/NSIS,不改变用户电脑策略,不跳过安装、卸载、DLL 路径或任何 COM/TSF 注册检查。 + +来源:[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。 diff --git a/openless-all/app/scripts/windows-msi-release-contract.test.mjs b/openless-all/app/scripts/windows-msi-release-contract.test.mjs index 7badaecdf..ac8b911b4 100644 --- a/openless-all/app/scripts/windows-msi-release-contract.test.mjs +++ b/openless-all/app/scripts/windows-msi-release-contract.test.mjs @@ -64,6 +64,17 @@ 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 smoke = steps.find((s) => s.name === 'Verify Windows installers register TSF IME'); +assert.equal(smoke.if, "matrix.platform == 'windows-latest'"); +assert.match(smoke.run, /DisableAutomaticApplicationShutdown/); +assert.match(smoke.run, /-PropertyType DWord -Value 1 -Force/); +assert.ok(smoke.run.indexOf('-Value 1 -Force') < smoke.run.indexOf('-InstallerKind nsis')); +assert.match(smoke.run, /finally[\s\S]*-Value \$previous[\s\S]*Remove-ItemProperty/); +for (const kind of ['nsis', 'msi']) { + assert.match(smoke.run, new RegExp('-InstallerKind ' + kind)); + assert.match(smoke.run, new RegExp(kind.toUpperCase() + ' installer smoke failed')); +} +assert.doesNotMatch(smoke.run, /continue-on-error|SkipUninstall|exit 0/); const schema = JSON.parse( readFileSync( new URL('../node_modules/@tauri-apps/cli/config.schema.json', import.meta.url),