From 7f9ebdcd2c00013e9d8b60e0fce7407a321ac2e5 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sat, 12 Sep 2026 02:59:53 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E5=A2=9E=E5=8A=A0Windows=E7=AD=BE?= =?UTF-8?q?=E5=90=8DRC=E5=8F=91=E5=B8=83=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/windows-rc.yml | 113 ++++++++++++++++++++++++++++++++ scripts/verify-windows-rc.ps1 | 42 ++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 .gitea/workflows/windows-rc.yml create mode 100644 scripts/verify-windows-rc.ps1 diff --git a/.gitea/workflows/windows-rc.yml b/.gitea/workflows/windows-rc.yml new file mode 100644 index 0000000..c5d808c --- /dev/null +++ b/.gitea/workflows/windows-rc.yml @@ -0,0 +1,113 @@ +name: Windows RC + +on: + workflow_dispatch: + +jobs: + signed-rc: + runs-on: windows-latest + permissions: + contents: read + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + components: rustfmt, clippy + + - name: 准备锁定依赖 + shell: pwsh + run: | + python -m pip install uv==0.9.24 + uv sync --frozen --group packaging --directory backend + corepack enable + corepack prepare pnpm@10.28.0 --activate + pnpm --dir frontend install --frozen-lockfile + + - name: 导入受控签名材料 + shell: pwsh + env: + WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + CORE_SIGNING_KEY_PEM_BASE64: ${{ secrets.CORE_SIGNING_KEY_PEM_BASE64 }} + run: | + if (-not $env:WINDOWS_CERTIFICATE_BASE64 -or -not $env:WINDOWS_CERTIFICATE_PASSWORD -or -not $env:CORE_SIGNING_KEY_PEM_BASE64) { + throw '缺少 Windows RC 签名秘密' + } + $secretRoot = Join-Path $env:RUNNER_TEMP 'opennexus-signing' + New-Item -ItemType Directory -Force -Path $secretRoot | Out-Null + $pfx = Join-Path $secretRoot 'codesign.pfx' + $coreKey = Join-Path $secretRoot 'core-ed25519.pem' + [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE_BASE64)) + [IO.File]::WriteAllBytes($coreKey, [Convert]::FromBase64String($env:CORE_SIGNING_KEY_PEM_BASE64)) + $password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force + $certificate = Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $password + if (-not $certificate.HasPrivateKey) { throw '代码签名证书没有私钥' } + "OPENNEXUS_CORE_SIGNING_KEY_FILE=$coreKey" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + "OPENNEXUS_WINDOWS_CERTIFICATE_THUMBPRINT=$($certificate.Thumbprint)" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + Remove-Item -LiteralPath $pfx -Force + + - name: 构建签名 Core + shell: pwsh + run: uv run --directory backend --group packaging python ../scripts/build-core.py --release + + - name: 生成签名打包配置 + shell: pwsh + run: | + $config = @{ + bundle = @{ + active = $true + targets = @('nsis') + resources = @{ + '../../.build/sidecar/dist/opennexus-core/' = 'core/' + } + windows = @{ + certificateThumbprint = $env:OPENNEXUS_WINDOWS_CERTIFICATE_THUMBPRINT + digestAlgorithm = 'sha256' + timestampUrl = 'http://timestamp.digicert.com' + } + } + } | ConvertTo-Json -Depth 5 + $path = Join-Path $env:GITHUB_WORKSPACE 'frontend\src-tauri\tauri.rc.conf.json' + [IO.File]::WriteAllText($path, $config, [Text.UTF8Encoding]::new($false)) + "OPENNEXUS_RC_CONFIG=$path" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + + - name: 构建 MSVC NSIS 安装包 + shell: pwsh + run: pnpm --dir frontend exec tauri build --target x86_64-pc-windows-msvc --features desktop --config src-tauri/tauri.rc.conf.json + + - name: 验证 RC 签名与大小 + shell: pwsh + run: ./scripts/verify-windows-rc.ps1 + + - uses: actions/upload-artifact@v4 + with: + name: OpenNexus-windows-x64-rc + if-no-files-found: error + retention-days: 14 + path: | + frontend/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe + .build/sidecar/manifest.json + .build/sidecar/manifest.sig + .build/sidecar/public-key.hex + .build/windows-rc-sha256.json + + - name: 清理签名材料 + if: always() + shell: pwsh + run: | + if ($env:OPENNEXUS_WINDOWS_CERTIFICATE_THUMBPRINT) { + Remove-Item -LiteralPath "Cert:\CurrentUser\My\$env:OPENNEXUS_WINDOWS_CERTIFICATE_THUMBPRINT" -Force -ErrorAction SilentlyContinue + } + Remove-Item -LiteralPath (Join-Path $env:RUNNER_TEMP 'opennexus-signing') -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath (Join-Path $env:GITHUB_WORKSPACE 'frontend\src-tauri\tauri.rc.conf.json') -Force -ErrorAction SilentlyContinue diff --git a/scripts/verify-windows-rc.ps1 b/scripts/verify-windows-rc.ps1 new file mode 100644 index 0000000..7e4a10f --- /dev/null +++ b/scripts/verify-windows-rc.ps1 @@ -0,0 +1,42 @@ +$ErrorActionPreference = 'Stop' +$root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +$bundle = Join-Path $root 'frontend\src-tauri\target\x86_64-pc-windows-msvc\release\bundle\nsis' +$installers = @(Get-ChildItem -LiteralPath $bundle -Filter '*.exe' -File) +if ($installers.Count -ne 1) { throw "应恰好生成一个 NSIS 安装包,实际为 $($installers.Count)" } +$installer = $installers[0] +if ($installer.Length -gt 300MB) { throw "基础安装包超过 300 MiB:$($installer.Length)" } + +$hostExecutable = Join-Path $root 'frontend\src-tauri\target\x86_64-pc-windows-msvc\release\notesagent-desktop.exe' +if (-not (Test-Path -LiteralPath $hostExecutable -PathType Leaf)) { throw '缺少 MSVC Host 可执行文件' } +foreach ($path in @($hostExecutable, $installer.FullName)) { + $signature = Get-AuthenticodeSignature -LiteralPath $path + if ($signature.Status -ne 'Valid') { throw "Authenticode 签名无效:$path ($($signature.Status))" } + if ($signature.SignerCertificate.Thumbprint -ne $env:OPENNEXUS_WINDOWS_CERTIFICATE_THUMBPRINT) { + throw "签名证书与受控证书不匹配:$path" + } +} + +$manifest = Join-Path $root '.build\sidecar\manifest.json' +$manifestSignature = Join-Path $root '.build\sidecar\manifest.sig' +$publicKey = Join-Path $root '.build\sidecar\public-key.hex' +foreach ($path in @($manifest, $manifestSignature, $publicKey)) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "缺少 Core 发布文件:$path" } +} +if ((Get-Item -LiteralPath $manifestSignature).Length -ne 64) { throw 'Core 清单签名长度必须为 64 字节' } +if ((Get-Content -LiteralPath $publicKey -Raw).Trim() -notmatch '^[0-9a-f]{64}$') { throw 'Core 发布公钥格式无效' } + +$result = [ordered]@{ + schema = 1 + product = 'OpenNexus' + target = 'x86_64-pc-windows-msvc' + installer_bytes = $installer.Length + certificate_thumbprint = $env:OPENNEXUS_WINDOWS_CERTIFICATE_THUMBPRINT + files = [ordered]@{} +} +foreach ($path in @($installer.FullName, $hostExecutable, $manifest, $manifestSignature, $publicKey)) { + $relative = [IO.Path]::GetRelativePath($root, $path).Replace('\', '/') + $result.files[$relative] = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() +} +$output = Join-Path $root '.build\windows-rc-sha256.json' +[IO.File]::WriteAllText($output, ($result | ConvertTo-Json -Depth 5), [Text.UTF8Encoding]::new($false)) +Write-Host "Windows RC 校验通过:$($installer.Name),$([math]::Round($installer.Length / 1MB, 2)) MiB"