From b03e30d0dd8a32926320dd10e2d14ee0bd403cf2 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Thu, 9 Jul 2026 09:30:44 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E6=96=B0=E5=A2=9E=20Windows=20=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E7=AB=AF=E5=86=85=E6=B5=8B=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/client-quality-gates.yml | 4 + .../workflows/desktop-windows-internal.yml | 114 ++++++++++++++++++ docs/desktop-project-plan.md | 14 +++ docs/guides/client-release.md | 10 ++ frontend/scripts/verify-desktop-release.mjs | 39 ++++++ 5 files changed, 181 insertions(+) create mode 100644 .github/workflows/desktop-windows-internal.yml diff --git a/.github/workflows/client-quality-gates.yml b/.github/workflows/client-quality-gates.yml index 539a335f..804b0dd0 100644 --- a/.github/workflows/client-quality-gates.yml +++ b/.github/workflows/client-quality-gates.yml @@ -6,6 +6,8 @@ on: - "AGENTS.md" - "frontend/**" - ".github/workflows/client-quality-gates.yml" + - ".github/workflows/desktop-release-candidate.yml" + - ".github/workflows/desktop-windows-internal.yml" - "docs/branch-governance.md" - "docs/desktop-project-plan.md" - "docs/desktop-phase-1-design.md" @@ -24,6 +26,8 @@ on: - "AGENTS.md" - "frontend/**" - ".github/workflows/client-quality-gates.yml" + - ".github/workflows/desktop-release-candidate.yml" + - ".github/workflows/desktop-windows-internal.yml" - "docs/branch-governance.md" - "docs/desktop-project-plan.md" - "docs/desktop-phase-1-design.md" diff --git a/.github/workflows/desktop-windows-internal.yml b/.github/workflows/desktop-windows-internal.yml new file mode 100644 index 00000000..656acf08 --- /dev/null +++ b/.github/workflows/desktop-windows-internal.yml @@ -0,0 +1,114 @@ +name: Desktop Windows Internal Build + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: desktop-windows-internal-${{ github.ref }} + cancel-in-progress: false + +jobs: + windows-internal: + name: Windows NSIS internal validation + runs-on: windows-latest + defaults: + run: + working-directory: frontend + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Resolve internal build metadata + id: build-metadata + shell: pwsh + run: | + if ($env:GITHUB_REF_TYPE -eq "tag") { + throw "Windows internal validation builds must run from a branch, not a release tag." + } + + $channel = "dev" + if ($env:GITHUB_REF_NAME -in @("dev", "main", "release")) { + $channel = $env:GITHUB_REF_NAME + } + + "channel=$channel" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "commit=$env:GITHUB_SHA" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + - name: Install dependencies + run: npm ci + + - name: Check release build metadata + run: npm run release:env:check + env: + VITE_BUILD_CHANNEL: ${{ steps.build-metadata.outputs.channel }} + VITE_BUILD_COMMIT: ${{ steps.build-metadata.outputs.commit }} + + - name: Check synchronized client version + run: npm run version:check + + - name: Check runtime boundary + run: npm run runtime:check + + - name: Check desktop release and security gate + run: npm run desktop:release:check + + - name: Check UI contract + run: npm run ui:contract + + - name: Type check + run: npm run type-check + + - name: Unit tests + run: npm run test:unit + + - name: Build Web artifact + run: npm run build + env: + VITE_BUILD_CHANNEL: ${{ steps.build-metadata.outputs.channel }} + VITE_BUILD_COMMIT: ${{ steps.build-metadata.outputs.commit }} + + - name: Build unsigned Windows NSIS internal installer + run: npm run desktop:build -- --config '{"bundle":{"createUpdaterArtifacts":false}}' --bundles nsis + env: + VITE_BUILD_CHANNEL: ${{ steps.build-metadata.outputs.channel }} + VITE_BUILD_COMMIT: ${{ steps.build-metadata.outputs.commit }} + + - name: Create Windows installer checksum manifest + shell: pwsh + run: | + $bundleDir = "src-tauri/target/release/bundle/nsis" + $installers = Get-ChildItem -Path $bundleDir -Filter "*.exe" -File + if ($installers.Count -eq 0) { + throw "No Windows NSIS EXE installer was produced." + } + + $checksums = foreach ($installer in $installers) { + $hash = Get-FileHash -Path $installer.FullName -Algorithm SHA256 + "$($hash.Hash.ToLowerInvariant()) $($installer.Name)" + } + + $manifest = Join-Path $bundleDir "SHA256SUMS.txt" + $checksums | Set-Content -Path $manifest -Encoding utf8 + Get-Content $manifest + + - name: Upload Windows internal installer + uses: actions/upload-artifact@v4 + with: + name: ctms-windows-internal-${{ steps.build-metadata.outputs.channel }}-${{ steps.build-metadata.outputs.commit }} + path: | + frontend/src-tauri/target/release/bundle/nsis/*.exe + frontend/src-tauri/target/release/bundle/nsis/SHA256SUMS.txt + if-no-files-found: error diff --git a/docs/desktop-project-plan.md b/docs/desktop-project-plan.md index b6f6bf23..64daec56 100644 --- a/docs/desktop-project-plan.md +++ b/docs/desktop-project-plan.md @@ -198,6 +198,20 @@ 如果后续任务试图新增离线登录、离线写入、本地业务队列、离线同步、内嵌后端、本地 API 镜像或绕过后端权限审计,应先修改并评审本计划书,不能直接实现。 +## 2026-07-09 Windows 内测构建流水线记录 + +本次补充 Windows 第二阶段兼容性验证的手动构建入口,仍不改变“未获明确批准前不发布正式 Windows 安装包”的边界。 + +已补齐的内部验证自动化: + +- 新增 `.github/workflows/desktop-windows-internal.yml`,通过 `workflow_dispatch` 手动触发,在 GitHub `windows-latest` 环境构建 Windows NSIS 安装器。 +- 工作流从用户在 GitHub Actions 页面选择的分支上下文注入 `VITE_BUILD_CHANNEL` 和 `VITE_BUILD_COMMIT`,并拒绝从 release tag 触发,避免和正式发布链路混用。 +- 工作流执行 `release:env:check`、`version:check`、`runtime:check`、`desktop:release:check`、`ui:contract`、`type-check`、`test:unit` 和 `build` 后,再构建 Windows NSIS 内测安装器。 +- Windows 内测构建显式关闭 updater artifacts,只上传 `.exe` 和 `SHA256SUMS.txt` GitHub artifact;不生成生产 `latest.json`,不创建 update feed,不替代 Windows 代码签名或正式发布审批。 +- `npm run desktop:release:check` 已纳入 Windows 内测 workflow 约束,防止该入口误加入 updater feed、签名 release 或生产发布逻辑。 + +该 workflow 只用于内部测试和 Windows 兼容性验证。正式 Windows 发布仍需先完成 Windows 代码签名、安装器验收、更新链路和发布治理审批。 + ## 当前质量门禁 前端或桌面端代码变更应按影响范围执行相关检查。发布、桌面端适配层、Tauri 配置或安全边界相关变更至少考虑: diff --git a/docs/guides/client-release.md b/docs/guides/client-release.md index bb7de085..0af11e45 100644 --- a/docs/guides/client-release.md +++ b/docs/guides/client-release.md @@ -154,6 +154,16 @@ Windows validation must cover: - notification and updater compilation; - future code-signing requirements. +The manual internal Windows validation workflow lives at +`.github/workflows/desktop-windows-internal.yml`. It is `workflow_dispatch` +only, runs on `windows-latest`, injects `VITE_BUILD_CHANNEL` and +`VITE_BUILD_COMMIT` from the selected branch context, executes the shared client +and Desktop safety gates, builds an unsigned NSIS installer with updater +artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub +Actions artifact. This workflow is for internal compatibility verification +only; it must not generate `latest.json`, update feeds, signed release +directories, or formal Windows release artifacts. + ## Required Checks ```bash diff --git a/frontend/scripts/verify-desktop-release.mjs b/frontend/scripts/verify-desktop-release.mjs index cb6d6f04..6e88cc47 100644 --- a/frontend/scripts/verify-desktop-release.mjs +++ b/frontend/scripts/verify-desktop-release.mjs @@ -308,6 +308,45 @@ const verifyWorkflowGates = async () => { for (const token of requiredReleaseWorkflowTokens) { assert(releaseWorkflow.includes(token), `Desktop release candidate workflow must include ${token}.`); } + + const windowsInternalWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-windows-internal.yml"), "utf8"); + const requiredWindowsInternalWorkflowTokens = [ + "workflow_dispatch", + "runs-on: windows-latest", + "VITE_BUILD_CHANNEL", + "VITE_BUILD_COMMIT", + "npm run release:env:check", + "npm run version:check", + "npm run runtime:check", + "npm run desktop:release:check", + "npm run ui:contract", + "npm run type-check", + "npm run test:unit", + "npm run build", + "npm run desktop:build", + "createUpdaterArtifacts", + "false", + "--bundles nsis", + "SHA256SUMS.txt", + "actions/upload-artifact", + ]; + + for (const token of requiredWindowsInternalWorkflowTokens) { + assert(windowsInternalWorkflow.includes(token), `Desktop Windows internal workflow must include ${token}.`); + } + + const forbiddenWindowsInternalWorkflowTokens = [ + "desktop:update-feed:create", + "desktop:update-feed:check", + "desktop:release-readiness:check", + "TAURI_SIGNING_PRIVATE_KEY", + "REQUIRE_DESKTOP_SIGNING", + "latest.json", + ]; + + for (const token of forbiddenWindowsInternalWorkflowTokens) { + assert(!windowsInternalWorkflow.includes(token), `Desktop Windows internal workflow must not include ${token}.`); + } }; await verifyTauriConfig();