ci: 增加Windows签名RC发布门禁
CI / backend-test (push) Canceled after 0s
CI / service-test (push) Canceled after 0s
CI / frontend-test (push) Canceled after 0s
CI / rust-core (push) Canceled after 0s
CI / docs-check (push) Canceled after 14m58s
CI / backend-test (push) Canceled after 0s
CI / service-test (push) Canceled after 0s
CI / frontend-test (push) Canceled after 0s
CI / rust-core (push) Canceled after 0s
CI / docs-check (push) Canceled after 14m58s
This commit is contained in:
@@ -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
|
||||||
@@ -1031,3 +1031,6 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写
|
|||||||
- 构建成功后默认删除可重建的 PyInstaller `work` 目录;只有显式 `--keep-work` 才保留。当前构建得到 1523 个文件、223.56 MiB,低于 300 MiB 基础包门槛。
|
- 构建成功后默认删除可重建的 PyInstaller `work` 目录;只有显式 `--keep-work` 才保留。当前构建得到 1523 个文件、223.56 MiB,低于 300 MiB 基础包门槛。
|
||||||
- 打包 Core 连续 20 次真实启动通过,每轮使用独立数据目录、不同会话代际、认证健康请求,并通过 Rust Workspace 写入及重读本地笔记;ready P95 为 2.676 秒,低于 10 秒。复制出的 Core 可执行文件修改一个字节后连续 20 次均在创建进程前返回 `CORE_INTEGRITY_FAILED`。
|
- 打包 Core 连续 20 次真实启动通过,每轮使用独立数据目录、不同会话代际、认证健康请求,并通过 Rust Workspace 写入及重读本地笔记;ready P95 为 2.676 秒,低于 10 秒。复制出的 Core 可执行文件修改一个字节后连续 20 次均在创建进程前返回 `CORE_INTEGRITY_FAILED`。
|
||||||
- 临时 Ed25519 PEM 夹具验证了发布签名输出格式;本机没有代码签名证书,Rust 工具链为 GNU 而非 MSVC,也不是干净标准用户离线 VM。因此这些结果是 A-01 本机预检,A-01 仍不计为通过。
|
- 临时 Ed25519 PEM 夹具验证了发布签名输出格式;本机没有代码签名证书,Rust 工具链为 GNU 而非 MSVC,也不是干净标准用户离线 VM。因此这些结果是 A-01 本机预检,A-01 仍不计为通过。
|
||||||
|
- 新增手动触发的 Gitea Actions `Windows RC` 作业。作业固定 `x86_64-pc-windows-msvc`,从仓库 Secrets 解码并导入 PFX 与 Core Ed25519 PEM,生成单一动态 Tauri 配置并构建 NSIS;缺任一签名材料立即失败。
|
||||||
|
- `verify-windows-rc.ps1` 要求恰好一个 NSIS 安装包、包体不超过 300 MiB、Host 与安装器 Authenticode 均为 `Valid` 且使用本次受控证书,同时检查 Core 签名/公钥格式并输出逐文件 SHA-256 清单。工作流只在全部门禁通过后上传 14 天保留的 RC,最后无条件移除证书、密钥目录和动态配置。
|
||||||
|
- 本机已解析工作流 YAML 和 PowerShell 脚本语法;由于没有 Windows Runner Secrets、MSVC/SDK 和证书,不能在当前会话声称该发布作业已成功产出签名 RC。
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user