2 Commits

Author SHA1 Message Date
Cheng Zhou 795e0a75ca build(release): simplify v0.1.0 visible assets
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
2026-07-17 12:40:16 +08:00
Cheng Zhou e3489f3b06 build(release): stage local macOS v0.1.0 distribution
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
2026-07-17 11:31:11 +08:00
21 changed files with 260 additions and 13 deletions
@@ -40,6 +40,8 @@ jobs:
web: web:
name: Shared client and Web name: Shared client and Web
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
defaults: defaults:
run: run:
working-directory: frontend working-directory: frontend
@@ -104,6 +106,8 @@ jobs:
desktop: desktop:
name: macOS Desktop name: macOS Desktop
runs-on: macos-latest runs-on: macos-latest
env:
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
defaults: defaults:
run: run:
working-directory: frontend working-directory: frontend
@@ -55,6 +55,7 @@ jobs:
env: env:
VITE_BUILD_CHANNEL: release VITE_BUILD_CHANNEL: release
VITE_BUILD_COMMIT: ${{ github.sha }} VITE_BUILD_COMMIT: ${{ github.sha }}
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
RELEASE_BUILD: "true" RELEASE_BUILD: "true"
DESKTOP_RELEASE_PLATFORM: macos DESKTOP_RELEASE_PLATFORM: macos
DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }} DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }}
@@ -194,6 +195,7 @@ jobs:
env: env:
VITE_BUILD_CHANNEL: release VITE_BUILD_CHANNEL: release
VITE_BUILD_COMMIT: ${{ github.sha }} VITE_BUILD_COMMIT: ${{ github.sha }}
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
RELEASE_BUILD: "true" RELEASE_BUILD: "true"
DESKTOP_RELEASE_PLATFORM: windows DESKTOP_RELEASE_PLATFORM: windows
DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }} DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }}
@@ -14,6 +14,8 @@ jobs:
windows-internal: windows-internal:
name: Windows NSIS internal validation name: Windows NSIS internal validation
runs-on: windows-latest runs-on: windows-latest
env:
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
defaults: defaults:
run: run:
working-directory: frontend working-directory: frontend
+3 -1
View File
@@ -14,7 +14,9 @@
- 未完成后端 token 校验和 `/me` 身份确认前,不得展示业务缓存;登出、切换服务器、切换用户、401/403、权限上下文变化或缓存 schema 变化时必须清理或失效相关缓存。 - 未完成后端 token 校验和 `/me` 身份确认前,不得展示业务缓存;登出、切换服务器、切换用户、401/403、权限上下文变化或缓存 schema 变化时必须清理或失效相关缓存。
- 新增或调整 Tauri command、capability、CSP、updater、凭据、文件、通知、本地缓存持久化或底层存储能力时,必须同步评估 `frontend/scripts/verify-desktop-release.mjs``npm run runtime:check` 和桌面发布检查清单是否需要更新。 - 新增或调整 Tauri command、capability、CSP、updater、凭据、文件、通知、本地缓存持久化或底层存储能力时,必须同步评估 `frontend/scripts/verify-desktop-release.mjs``npm run runtime:check` 和桌面发布检查清单是否需要更新。
- token、附件下载凭据和敏感业务信息不得写入 URL、日志、系统通知正文或明文浏览器存储。 - token、附件下载凭据和敏感业务信息不得写入 URL、日志、系统通知正文或明文浏览器存储。
- 正式桌面客户端的默认 CTMS 服务端入口必须由构建环境变量 `VITE_DESKTOP_SERVER_URL` 注入,不得在运行时代码中写死生产域名;用户仍可在桌面服务器设置中手动覆盖,手动值优先并沿用既有的切换服务器登出与缓存失效边界。该变量只表示业务服务端 origin,不得与 updater 制品前缀 `DESKTOP_UPDATE_BASE_URL` 混用。
- Windows x64 NSIS 已获准作为正式桌面发布目标;正式制品必须由 `.github/workflows/desktop-release-candidate.yml` 从与 macOS/Web 相同的 `vX.Y.Z` tag 和 SHA 构建。平台签名默认要求组织 Windows 代码签名证书、RFC 3161 时间戳和 Authenticode 校验;只有 `frontend/desktop-release-policy.json` 中按精确版本记录、经发布负责人批准的例外可跳过平台签名。无论是否采用平台签名例外,都必须使用 updater 私钥、校验 updater feed,并明确标记平台未签名风险。`.github/workflows/desktop-windows-internal.yml` 仍只用于分支上的无签名兼容性验证,不得生成生产 `latest.json`、updater feed 或正式发布制品。 - Windows x64 NSIS 已获准作为正式桌面发布目标;正式制品必须由 `.github/workflows/desktop-release-candidate.yml` 从与 macOS/Web 相同的 `vX.Y.Z` tag 和 SHA 构建。平台签名默认要求组织 Windows 代码签名证书、RFC 3161 时间戳和 Authenticode 校验;只有 `frontend/desktop-release-policy.json` 中按精确版本记录、经发布负责人批准的例外可跳过平台签名。无论是否采用平台签名例外,都必须使用 updater 私钥、校验 updater feed,并明确标记平台未签名风险。`.github/workflows/desktop-windows-internal.yml` 仍只用于分支上的无签名兼容性验证,不得生成生产 `latest.json`、updater feed 或正式发布制品。
- v0.1.0 另获准在 GitHub Actions 额度不可用时,从最终 `release` 上不可移动的 `v0.1.0` tag/SHA 在受控 macOS 主机本地构建并先行上传 macOS ad-hoc 制品;Windows 只能在额度恢复后从同一 tag/SHA 后补。面向安装用户的 GitHub Release 只保留 DMG 与只校验该安装包的 `SHA256SUMS.txt`,平台未签名风险和 Windows pending 状态写入 Release Notes。macOS updater 包及 `.sig`、完整 checksum、provenance、`UNSIGNED-PLATFORM` 与 Windows pending 证据必须保存在被 Git 忽略的私有发布目录,待 Windows `NotSigned` 制品和联合 feed 均验证通过后再发布到可匿名读取的独立 HTTPS updater 源;此前不得发布或替换生产 `latest.json`。该本地/分阶段例外不适用于后续版本。
## 分支与发布治理 ## 分支与发布治理
@@ -57,4 +59,4 @@ npm run desktop:build:app
本地缓存相关变更至少执行 `runtime:check``desktop:release:check``ui:contract``type-check``test:unit``build`;若新增 Tauri command、capability、CSP 或底层持久化存储,还需执行 `desktop:build:app` 并在真实桌面 App 中验证缓存清理和诊断入口。 本地缓存相关变更至少执行 `runtime:check``desktop:release:check``ui:contract``type-check``test:unit``build`;若新增 Tauri command、capability、CSP 或底层持久化存储,还需执行 `desktop:build:app` 并在真实桌面 App 中验证缓存清理和诊断入口。
正式桌面发布构建必须使用组织批准的 updater 签名私钥,updater 签名不可因平台签名例外而关闭。平台签名默认要求 macOS 完成 Apple 签名/公证、Windows 完成组织代码签名、RFC 3161 时间戳和 Authenticode 校验。当前仅批准 v0.1.0 采用受控分发例外:macOS 使用 ad-hoc 签名且不公证,Windows 应用和安装器保持 Authenticode 未签名;制品名、发布说明、校验清单和 provenance 必须清楚标注 `UNSIGNED-PLATFORM`,并提示 Gatekeeper/SmartScreen 警告。该例外不自动适用于后续版本。 正式桌面发布构建必须使用组织批准的 updater 签名私钥,updater 签名不可因平台签名例外而关闭。平台签名默认要求 macOS 完成 Apple 签名/公证、Windows 完成组织代码签名、RFC 3161 时间戳和 Authenticode 校验。当前仅批准 v0.1.0 采用受控分发例外:macOS 使用 ad-hoc 签名且不公证,Windows 应用和安装器保持 Authenticode 未签名;制品名、Release Notes、私有发布证据、完整 updater 校验清单和 provenance 必须清楚标注 `UNSIGNED-PLATFORM`,并提示 Gatekeeper/SmartScreen 警告。面向安装用户的 GitHub Release 可按精确版本策略精简为安装包与对应 checksum,但不得因此删除私有 updater 签名制品或验证证据。该例外不自动适用于后续版本。
@@ -88,7 +88,8 @@ npm run desktop:build:app
| 登录与项目恢复 | 必测 | 必测 | 登录成功后恢复可访问项目;401 后重新登录 | | 登录与项目恢复 | 必测 | 必测 | 登录成功后恢复可访问项目;401 后重新登录 |
| 记住密码 | 必测 | 必测 | Web 使用浏览器凭据管理/自动填充;Desktop 使用系统凭据库;未勾选时不继续写入保存密码 | | 记住密码 | 必测 | 必测 | Web 使用浏览器凭据管理/自动填充;Desktop 使用系统凭据库;未勾选时不继续写入保存密码 |
| 30 天免登录 | 不适用 | 必测 | 关闭并重启 App 后复用系统凭据库中的后端在线会话;超过 30 天或 `/me` 校验失败后重新登录 | | 30 天免登录 | 不适用 | 必测 | 关闭并重启 App 后复用系统凭据库中的后端在线会话;超过 30 天或 `/me` 校验失败后重新登录 |
| 服务器地址未配置 | 不适用 | 必测 | 自动进入服务器设置,不进入业务页 | | 构建默认服务器地址已注入 | 不适用 | 必测 | 首次启动使用 `VITE_DESKTOP_SERVER_URL`,不要求重复输入;运行时代码不含生产域名 |
| 构建默认地址和本地配置均缺失 | 不适用 | 必测 | 自动进入服务器设置,不进入业务页 |
| 服务器地址切换 | 不适用 | 必测 | 清除当前会话和项目上下文,要求重新登录 | | 服务器地址切换 | 不适用 | 必测 | 清除当前会话和项目上下文,要求重新登录 |
| 服务端不可达 | 必测 | 必测 | 显示可恢复错误,不进入离线模式 | | 服务端不可达 | 必测 | 必测 | 显示可恢复错误,不进入离线模式 |
| 本地缓存命中 | 必测 | 必测 | `/me` 校验通过后可先展示缓存再后台刷新 | | 本地缓存命中 | 必测 | 必测 | `/me` 校验通过后可先展示缓存再后台刷新 |
@@ -288,7 +289,10 @@ npm run desktop:build:app
- macOS 使用 Tauri `signingIdentity: "-"` 生成 ad-hoc 签名 Universal app/DMG,必须通过 `codesign --verify` 并确认 `Signature=adhoc`,不执行或声称 Apple 公证。 - macOS 使用 Tauri `signingIdentity: "-"` 生成 ad-hoc 签名 Universal app/DMG,必须通过 `codesign --verify` 并确认 `Signature=adhoc`,不执行或声称 Apple 公证。
- Windows 应用和 NSIS 安装器不做 Authenticode 签名,必须通过 `Get-AuthenticodeSignature` 确认 `NotSigned`,不执行或声称 RFC 3161 时间戳。 - Windows 应用和 NSIS 安装器不做 Authenticode 签名,必须通过 `Get-AuthenticodeSignature` 确认 `NotSigned`,不执行或声称 RFC 3161 时间戳。
- 两端仍必须从同一 `v0.1.0` tag 和 SHA 构建,使用同一 updater 私钥生成 `.sig`,并通过联合 feed、checksum 和 provenance 校验。 - 两端仍必须从同一 `v0.1.0` tag 和 SHA 构建,使用同一 updater 私钥生成 `.sig`,并通过联合 feed、checksum 和 provenance 校验。
- 制品与 Actions artifact 名称必须含 `_UNSIGNED``UNSIGNED-PLATFORM`,发布目录必须携带 `UNSIGNED-PLATFORM-RELEASE.txt``DESKTOP-RELEASE-PROVENANCE.json` - GitHub Actions 额度不可用期间,发布负责人额外批准从最终 `v0.1.0` tag/SHA 在受控 macOS 主机本地构建并先行上传 macOS 制品;Windows 必须在恢复后从同一 tag/SHA 后补
- macOS 先行的用户可见 GitHub Release 只包含 DMG、仅覆盖该 DMG 的 checksum 和 GitHub 自动源码归档;Release Notes 必须明确 macOS ad-hoc/未公证风险和 Windows pending,且不得包含或替换生产 `latest.json`。updater 包及 `.sig`、完整 checksum、provenance、`UNSIGNED-PLATFORM` 和 Windows pending 证据必须先复制到权限受限且被 Git 忽略的私有发布目录。
- Windows `NotSigned` 实物和联合 feed 全部验证通过后,才允许激活生产 updater feedtag 不得为补充 Windows 制品而移动。
- 制品与 Actions artifact 名称必须含 `_UNSIGNED``UNSIGNED-PLATFORM`,私有证据/完整 updater 发布目录必须携带 `UNSIGNED-PLATFORM-RELEASE.txt``DESKTOP-RELEASE-PROVENANCE.json`installer-only GitHub Release 通过 `_UNSIGNED` 安装包名和 Release Notes 呈现风险,不要求公开展示内部证据文件。
- 受控分发说明必须明确提示 macOS Gatekeeper 与 Windows SmartScreen 警告;平台未签名制品不得描述为 Apple/Microsoft 信任或已公证/已 Authenticode 签名。 - 受控分发说明必须明确提示 macOS Gatekeeper 与 Windows SmartScreen 警告;平台未签名制品不得描述为 Apple/Microsoft 信任或已公证/已 Authenticode 签名。
本地实物验证已完成:使用当前 updater 私钥成功构建 Universal `x86_64 arm64` macOS app、DMG、`.app.tar.gz``.sig``codesign --verify --deep --strict` 通过,签名详情为 `Signature=adhoc``TeamIdentifier=not set`Gatekeeper 拒绝符合预期。Windows `NotSigned`、NSIS 和 updater `.sig` 仍须由 `windows-latest` 原生 job 在正式 tag 上验证。 本地实物验证已完成:使用当前 updater 私钥成功构建 Universal `x86_64 arm64` macOS app、DMG、`.app.tar.gz``.sig``codesign --verify --deep --strict` 通过,签名详情为 `Signature=adhoc``TeamIdentifier=not set`Gatekeeper 拒绝符合预期。Windows `NotSigned`、NSIS 和 updater `.sig` 仍须由 `windows-latest` 原生 job 在正式 tag 上验证。
+15
View File
@@ -317,6 +317,21 @@ Platform signing remains the default for versions without an exact approved
exception. An exception never permits disabling Tauri updater signatures or exception. An exception never permits disabling Tauri updater signatures or
building Web, macOS, and Windows from different tags or commits. building Web, macOS, and Windows from different tags or commits.
For v0.1.0 only, the release owner also approved a staged contingency while
hosted Actions capacity is unavailable: macOS may be built on a controlled
local macOS host from the immutable final `v0.1.0` tag and published first.
Windows remains pending and must later be built from that same tag and SHA.
The user-facing macOS-only GitHub Release may contain only the DMG and a
checksum manifest that covers that installer; platform-trust and
Windows-pending status must remain explicit in the Release Notes. The updater
package and signature, full checksum manifest, provenance, warning, and
Windows-pending evidence must be retained in a Git-ignored private release
directory. After Windows is built from the same tag and SHA, those updater
artifacts may be published only to a separately configured anonymous HTTPS
update origin. The staged release must not publish or replace production
`latest.json`; updater feed activation waits for combined macOS and Windows
verification.
Reference: Reference:
- [docs/guides/release-checklist.md](guides/release-checklist.md) - [docs/guides/release-checklist.md](guides/release-checklist.md)
+6 -2
View File
@@ -108,6 +108,8 @@ npm run desktop:build:app
正式桌面发布构建必须从同一正式 tag 分别在 macOS 和 Windows 原生 CI job 执行。两端都必须设置 updater 签名私钥,updater 签名不可关闭。平台签名默认路径仍要求 macOS 设置 Apple 签名/公证变量并以 `REQUIRE_DESKTOP_SIGNING=true` 执行,Windows 设置 PFX、密码和 RFC 3161 时间戳变量并以 `REQUIRE_WINDOWS_SIGNING=true` 执行。只有 `frontend/desktop-release-policy.json` 对当前精确版本存在已批准例外时,workflow 才可改为 macOS ad-hoc、Windows Authenticode 未签名路径;例外制品必须标注 `UNSIGNED-PLATFORM`、生成 provenance 和风险说明。两个平台与联合 feed 校验通过后才能汇总正式 updater feed。 正式桌面发布构建必须从同一正式 tag 分别在 macOS 和 Windows 原生 CI job 执行。两端都必须设置 updater 签名私钥,updater 签名不可关闭。平台签名默认路径仍要求 macOS 设置 Apple 签名/公证变量并以 `REQUIRE_DESKTOP_SIGNING=true` 执行,Windows 设置 PFX、密码和 RFC 3161 时间戳变量并以 `REQUIRE_WINDOWS_SIGNING=true` 执行。只有 `frontend/desktop-release-policy.json` 对当前精确版本存在已批准例外时,workflow 才可改为 macOS ad-hoc、Windows Authenticode 未签名路径;例外制品必须标注 `UNSIGNED-PLATFORM`、生成 provenance 和风险说明。两个平台与联合 feed 校验通过后才能汇总正式 updater feed。
v0.1.0 额外批准 GitHub Actions 额度不可用时的分阶段应急路径:先在受控 macOS 主机从最终、不可移动的 `v0.1.0` tag/SHA 本地构建 macOS ad-hoc 制品,面向安装用户的 GitHub Release 只保留 DMG 与仅覆盖该安装包的 checksum,平台未签名警告和 Windows pending 状态写入 Release Notes。macOS updater 包及 `.sig`、完整 checksum、provenance、风险说明和 Windows pending 证据必须保存在被 Git 忽略的私有发布目录;Windows 恢复后必须从同一 tag/SHA 后补,并在联合验证通过后把 updater 制品发布到可匿名读取的独立 HTTPS 更新源。macOS 先行阶段不得发布或替换生产 `latest.json`,联合 feed 必须等待 Windows `NotSigned` 实物验证完成。
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。 后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
### 4. 创建提交 ### 4. 创建提交
@@ -295,7 +297,9 @@ git tag -a v1.2.0 -m "CTMS v1.2.0"
git push origin v1.2.0 git push origin v1.2.0
``` ```
网页端、macOS 和 Windows 桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。两个平台的 updater 签名制品必须始终验证通过;macOS 签名/公证和 Windows 代码签名/RFC 3161 时间戳必须验证通过,除非 `frontend/desktop-release-policy.json` 对该精确版本记录了已批准例外。采用例外时必须验证 macOS 确为 ad-hoc、Windows 确为 `NotSigned`,并随安装包分发 `UNSIGNED-PLATFORM` 风险说明、provenance 和 checksum,之后才能最后原子替换生产 `latest.json` 网页端、macOS 和 Windows 桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。两个平台的 updater 签名制品必须始终验证通过;macOS 签名/公证和 Windows 代码签名/RFC 3161 时间戳必须验证通过,除非 `frontend/desktop-release-policy.json` 对该精确版本记录了已批准例外。采用例外时必须验证 macOS 确为 ad-hoc、Windows 确为 `NotSigned`,并在 Release Notes、私有发布证据、完整 updater checksum 和 provenance 中记录 `UNSIGNED-PLATFORM` 风险;面向安装用户的 Release 可按精确版本策略只显示安装包与安装包 checksum,但 updater 制品及验证证据必须保留,之后才能最后原子替换生产 `latest.json`
若执行 v0.1.0 的已批准分阶段应急路径,首次 macOS Release 可先于 Windows 发布,但 tag/SHA 不得移动;GitHub Release 只保留 DMG、仅覆盖该 DMG 的 checksum 和 GitHub 自动提供的源码归档,并在 Release Notes 中明确标记未签名风险与 Windows pending。updater 包、`.sig`、完整 checksum、provenance 和说明文件必须在私有发布目录留存。Windows 后补且联合 feed 验证通过后,才允许把这些 updater 制品发布到独立匿名 HTTPS 更新源并按上述顺序激活生产 updater feed。
发布记录至少包含: 发布记录至少包含:
@@ -450,7 +454,7 @@ git log --oneline --decorate --graph --all -30
- [ ] `npm run build` 通过 - [ ] `npm run build` 通过
- [ ] `npm run desktop:build:app` 通过 - [ ] `npm run desktop:build:app` 通过
- [ ] 正式桌面发布构建已使用 updater 签名私钥执行 - [ ] 正式桌面发布构建已使用 updater 签名私钥执行
- [ ] 平台签名已验证,或当前精确版本已在 `frontend/desktop-release-policy.json` 获批例外且 `UNSIGNED-PLATFORM` 风险说明、provenance 和 checksum 齐备 - [ ] 平台签名已验证,或当前精确版本已在 `frontend/desktop-release-policy.json` 获批例外且 Release Notes、私有 `UNSIGNED-PLATFORM` 证据、provenance 和完整 updater checksum 齐备;若 GitHub Release 采用 installer-only profile,公开 checksum 只覆盖公开安装包
- [ ] 数据库迁移与回滚方案确认 - [ ] 数据库迁移与回滚方案确认
- [ ] 发布说明完成 - [ ] 发布说明完成
- [ ] `main -> release` 合并完成 - [ ] `main -> release` 合并完成
+44 -5
View File
@@ -163,11 +163,39 @@ Platform signing defaults to `signed`. An exception is allowed only when
`frontend/desktop-release-policy.json` names the exact product version and `frontend/desktop-release-policy.json` names the exact product version and
records release-owner approval. The current v0.1.0 exception uses macOS ad-hoc records release-owner approval. The current v0.1.0 exception uses macOS ad-hoc
signing and unsigned Windows application/installer binaries. Its artifact names signing and unsigned Windows application/installer binaries. Its artifact names
and release directory contain `UNSIGNED-PLATFORM`; the directory must include and verified updater release directory contain `UNSIGNED-PLATFORM`; the private
`UNSIGNED-PLATFORM-RELEASE.txt`, `DESKTOP-RELEASE-PROVENANCE.json`, and evidence/update directory must include `UNSIGNED-PLATFORM-RELEASE.txt`,
`SHA256SUMS.txt`. This does not establish Apple or Microsoft publisher trust, `DESKTOP-RELEASE-PROVENANCE.json`, and the full `SHA256SUMS.txt`. The
and Gatekeeper or SmartScreen warnings are expected. Later versions return to user-facing GitHub Release may use the exact-version
the signed default unless separately approved. `installer-and-checksum-only` profile, with platform warnings in Release Notes
and a separate checksum manifest covering only the displayed installer. This
does not establish Apple or Microsoft publisher trust, and Gatekeeper or
SmartScreen warnings are expected. Later versions return to the signed default
unless separately approved.
### v0.1.0 staged macOS contingency
The release owner approved one additional v0.1.0 contingency for unavailable
hosted Actions capacity. A controlled local macOS host may build the macOS
ad-hoc artifacts from the immutable final `v0.1.0` tag and publish them first.
That initial user-facing GitHub Release contains only the DMG and a
`SHA256SUMS.txt` that covers the DMG, in addition to GitHub's automatic source
archives. The Release Notes must state that macOS is ad-hoc/not notarized and
that Windows remains pending. The macOS updater package and `.sig`, full
checksum manifest, provenance, unsigned-platform warning, and Windows-pending
notice must be copied to a permission-restricted, Git-ignored private release
directory before any visible asset is removed. Windows must later be built from
the same tag and SHA.
The staged macOS release is an installer distribution, not an activated
cross-platform updater release. Do not upload or replace production
`latest.json` until the Windows `NotSigned` installer/updater has been built and
the combined macOS/Windows feed passes full verification. The combined updater
artifacts, signatures, provenance, full checksum manifest, and `latest.json`
must use a separately configured HTTPS update origin that anonymous production
clients can read; a private GitHub Release is not a valid production updater
origin. This fallback is limited to v0.1.0 and does not authorize local formal
builds for later versions.
## Windows Release and Internal Validation ## Windows Release and Internal Validation
@@ -216,9 +244,19 @@ The formal workflow always requires these organization settings:
- secrets: `TAURI_SIGNING_PRIVATE_KEY` and - secrets: `TAURI_SIGNING_PRIVATE_KEY` and
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`;
- client default CTMS origin: repository variable `VITE_DESKTOP_SERVER_URL`;
- shared versioned HTTPS artifact prefix: `DESKTOP_UPDATE_BASE_URL` or the - shared versioned HTTPS artifact prefix: `DESKTOP_UPDATE_BASE_URL` or the
manual workflow input. manual workflow input.
`VITE_DESKTOP_SERVER_URL` must be an HTTPS origin without credentials, path,
query, or fragment. Vite embeds it as the Desktop client's first-run default;
the runtime source must not contain the production hostname. A user can still
override this default in Desktop server settings, where the persisted manual
value takes precedence and switching origins clears the existing session and
server-scoped client state. Do not reuse this value as
`DESKTOP_UPDATE_BASE_URL`: the latter points to immutable updater artifacts,
not the CTMS business API.
The default signed path additionally requires `WINDOWS_CERTIFICATE`, The default signed path additionally requires `WINDOWS_CERTIFICATE`,
`WINDOWS_CERTIFICATE_PASSWORD`, and `WINDOWS_TIMESTAMP_URL`, plus the Apple `WINDOWS_CERTIFICATE_PASSWORD`, and `WINDOWS_TIMESTAMP_URL`, plus the Apple
credentials documented by the release owner. The v0.1.0 exception does not credentials documented by the release owner. The v0.1.0 exception does not
@@ -239,6 +277,7 @@ npm ci
npm run version:check npm run version:check
export VITE_BUILD_CHANNEL=release export VITE_BUILD_CHANNEL=release
export VITE_BUILD_COMMIT="$(git rev-parse HEAD)" export VITE_BUILD_COMMIT="$(git rev-parse HEAD)"
export VITE_DESKTOP_SERVER_URL="https://ctms.example.com"
npm run release:env:check npm run release:env:check
npm run runtime:check npm run runtime:check
npm run desktop:release:check npm run desktop:release:check
+2
View File
@@ -2,6 +2,8 @@
VITE_RUNTIME_ENV=production VITE_RUNTIME_ENV=production
VITE_ALLOW_INSECURE_DEV_LOGIN=false VITE_ALLOW_INSECURE_DEV_LOGIN=false
VITE_DEV_API_PROXY_TARGET=http://backend:8000 VITE_DEV_API_PROXY_TARGET=http://backend:8000
# Default CTMS origin embedded in Desktop builds; users can override it in Desktop settings.
VITE_DESKTOP_SERVER_URL=https://ctms.example.com
# Set to 8888 when Vite HMR is accessed through the Docker nginx dev entry. # Set to 8888 when Vite HMR is accessed through the Docker nginx dev entry.
VITE_HMR_CLIENT_PORT= VITE_HMR_CLIENT_PORT=
VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3 VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3
+7 -1
View File
@@ -9,8 +9,14 @@
"macos": "ad-hoc", "macos": "ad-hoc",
"windows": "unsigned", "windows": "unsigned",
"distribution": "controlled", "distribution": "controlled",
"macosLocalExactTagFallback": true,
"windowsDelivery": "deferred-same-tag",
"updaterFeedActivation": "after-combined-platform-verification",
"githubReleaseAssetProfile": "installer-and-checksum-only",
"stagedEvidenceRetention": "private-until-updater-publish",
"updaterArtifactPublishTarget": "anonymous-https-origin",
"approvedOn": "2026-07-17", "approvedOn": "2026-07-17",
"reason": "The v0.1.0 release owner cannot currently provide Apple Developer signing/notarization credentials or an organization Windows code-signing certificate." "reason": "The v0.1.0 release owner cannot currently provide Apple Developer signing/notarization credentials or an organization Windows code-signing certificate, and hosted Windows build capacity is temporarily unavailable."
} }
] ]
} }
@@ -6,6 +6,7 @@ This release is distributed under the version-scoped v0.1.0 platform-signing exc
- The Windows application and installer have no Authenticode signature or RFC 3161 timestamp. - The Windows application and installer have no Authenticode signature or RFC 3161 timestamp.
- macOS Gatekeeper and Windows SmartScreen warnings are expected. - macOS Gatekeeper and Windows SmartScreen warnings are expected.
- Tauri updater artifacts remain cryptographically signed and must pass updater feed verification. - Tauri updater artifacts remain cryptographically signed and must pass updater feed verification.
- Platform availability may be staged; production latest.json remains withheld until macOS and Windows artifacts from the same tag pass combined verification.
- Install only after verifying the release tag, source commit, SHA256SUMS.txt, and DESKTOP-RELEASE-PROVENANCE.json through a trusted channel. - Install only after verifying the release tag, source commit, SHA256SUMS.txt, and DESKTOP-RELEASE-PROVENANCE.json through a trusted channel.
This exception does not apply to later versions unless they are separately approved in desktop-release-policy.json. This exception does not apply to later versions unless they are separately approved in desktop-release-policy.json.
@@ -0,0 +1,9 @@
CTMS v0.1.0 WINDOWS RELEASE PENDING
This GitHub Release is the approved first stage of the v0.1.0 Desktop distribution.
- macOS Universal artifacts are available and use an ad-hoc platform signature.
- Windows x64 NSIS artifacts are not yet available.
- Windows must later be built from the same immutable v0.1.0 tag and Git commit.
- Production latest.json is intentionally withheld until Windows NotSigned artifacts and the combined updater feed pass verification.
- Do not interpret this staged release as a complete cross-platform automatic-update release.
@@ -17,6 +17,7 @@ const tag = value("--tag") || process.env.GITHUB_REF_NAME;
const commit = value("--commit") || process.env.GITHUB_SHA; const commit = value("--commit") || process.env.GITHUB_SHA;
const macosSigning = value("--macos-signing"); const macosSigning = value("--macos-signing");
const windowsSigning = value("--windows-signing"); const windowsSigning = value("--windows-signing");
const releaseStage = value("--stage") || "complete";
const outputPath = resolve( const outputPath = resolve(
frontendDir, frontendDir,
value("--output") || "src-tauri/target/desktop-release-feed/DESKTOP-RELEASE-PROVENANCE.json", value("--output") || "src-tauri/target/desktop-release-feed/DESKTOP-RELEASE-PROVENANCE.json",
@@ -31,6 +32,13 @@ assert(tag === `v${packageInfo.version}`, `Release provenance tag must be v${pac
assert(/^[0-9a-f]{40}$/i.test(commit || ""), "Release provenance commit must be a full 40-character SHA."); assert(/^[0-9a-f]{40}$/i.test(commit || ""), "Release provenance commit must be a full 40-character SHA.");
assert(macosSigning === resolution.macosSigning, `macOS signing mode must be ${resolution.macosSigning}.`); assert(macosSigning === resolution.macosSigning, `macOS signing mode must be ${resolution.macosSigning}.`);
assert(windowsSigning === resolution.windowsSigning, `Windows signing mode must be ${resolution.windowsSigning}.`); assert(windowsSigning === resolution.windowsSigning, `Windows signing mode must be ${resolution.windowsSigning}.`);
assert(["complete", "macos-first"].includes(releaseStage), "Release provenance stage must be complete or macos-first.");
if (releaseStage === "macos-first") {
assert(
resolution.macosLocalExactTagFallback === true && resolution.windowsDelivery === "deferred-same-tag",
`v${packageInfo.version} does not approve a staged local macOS-first release.`,
);
}
if (failures.length > 0) { if (failures.length > 0) {
throw new Error(`Desktop release provenance creation failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`); throw new Error(`Desktop release provenance creation failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
@@ -50,6 +58,13 @@ const provenance = {
updaterSigning: "required-and-verified", updaterSigning: "required-and-verified",
artifactLabel: resolution.artifactLabel, artifactLabel: resolution.artifactLabel,
distribution: resolution.distribution, distribution: resolution.distribution,
stage: releaseStage,
platformAvailability:
releaseStage === "macos-first"
? { macos: "available", windows: "pending-same-tag" }
: { macos: "available", windows: "available" },
updaterFeedActivation:
releaseStage === "macos-first" ? "withheld-pending-combined-verification" : "combined-platform-verification-required",
warnings: resolution.warningRequired warnings: resolution.warningRequired
? [ ? [
"macOS is ad-hoc signed and not Apple-notarized; Gatekeeper warnings are expected.", "macOS is ad-hoc signed and not Apple-notarized; Gatekeeper warnings are expected.",
@@ -34,6 +34,26 @@ const validatePolicy = (policy) => {
assert(exception?.macos === "ad-hoc", `${prefix} must constrain macOS to ad-hoc signing.`); assert(exception?.macos === "ad-hoc", `${prefix} must constrain macOS to ad-hoc signing.`);
assert(exception?.windows === "unsigned", `${prefix} must constrain Windows to unsigned.`); assert(exception?.windows === "unsigned", `${prefix} must constrain Windows to unsigned.`);
assert(exception?.distribution === "controlled", `${prefix} must constrain distribution to controlled.`); assert(exception?.distribution === "controlled", `${prefix} must constrain distribution to controlled.`);
if (exception?.version === "0.1.0") {
assert(exception?.macosLocalExactTagFallback === true, `${prefix} must explicitly approve the local exact-tag macOS fallback.`);
assert(exception?.windowsDelivery === "deferred-same-tag", `${prefix} must defer Windows only from the same tag.`);
assert(
exception?.updaterFeedActivation === "after-combined-platform-verification",
`${prefix} must withhold the production updater feed until both platforms are verified.`,
);
assert(
exception?.githubReleaseAssetProfile === "installer-and-checksum-only",
`${prefix} must keep the user-facing GitHub Release limited to installers and their checksum manifest.`,
);
assert(
exception?.stagedEvidenceRetention === "private-until-updater-publish",
`${prefix} must retain updater artifacts and release evidence privately until updater publication.`,
);
assert(
exception?.updaterArtifactPublishTarget === "anonymous-https-origin",
`${prefix} must publish updater artifacts to an anonymous HTTPS origin.`,
);
}
assert(/^\d{4}-\d{2}-\d{2}$/.test(exception?.approvedOn || ""), `${prefix} must record an approval date.`); assert(/^\d{4}-\d{2}-\d{2}$/.test(exception?.approvedOn || ""), `${prefix} must record an approval date.`);
assert( assert(
typeof exception?.reason === "string" && exception.reason.trim().length >= 30, typeof exception?.reason === "string" && exception.reason.trim().length >= 30,
@@ -81,6 +101,12 @@ export const resolveDesktopReleasePolicy = (version, policy) => {
artifactLabel: "UNSIGNED-PLATFORM", artifactLabel: "UNSIGNED-PLATFORM",
distribution: exception.distribution, distribution: exception.distribution,
warningRequired: true, warningRequired: true,
macosLocalExactTagFallback: exception.macosLocalExactTagFallback === true,
windowsDelivery: exception.windowsDelivery,
updaterFeedActivation: exception.updaterFeedActivation,
githubReleaseAssetProfile: exception.githubReleaseAssetProfile,
stagedEvidenceRetention: exception.stagedEvidenceRetention,
updaterArtifactPublishTarget: exception.updaterArtifactPublishTarget,
exception, exception,
}; };
}; };
@@ -70,6 +70,25 @@ const validateBaseUrl = () => {
); );
}; };
const validateClientServerUrl = () => {
const raw = env.VITE_DESKTOP_SERVER_URL;
requireEnv("VITE_DESKTOP_SERVER_URL");
if (!raw) return;
let url;
try {
url = new URL(raw);
} catch (error) {
fail(`VITE_DESKTOP_SERVER_URL is invalid: ${error.message}`);
return;
}
assert(url.protocol === "https:", "VITE_DESKTOP_SERVER_URL must use HTTPS.");
assert(url.username === "" && url.password === "", "VITE_DESKTOP_SERVER_URL must not include credentials.");
assert(url.pathname === "/", "VITE_DESKTOP_SERVER_URL must be an origin without a path.");
assert(url.search === "" && url.hash === "", "VITE_DESKTOP_SERVER_URL must not include query parameters or fragments.");
};
const validateWindowsTimestampUrl = () => { const validateWindowsTimestampUrl = () => {
const raw = env.WINDOWS_TIMESTAMP_URL; const raw = env.WINDOWS_TIMESTAMP_URL;
requireEnv("WINDOWS_TIMESTAMP_URL"); requireEnv("WINDOWS_TIMESTAMP_URL");
@@ -173,6 +192,7 @@ if (requiresWindowsSigning) {
} }
validateBaseUrl(); validateBaseUrl();
validateClientServerUrl();
if (failures.length > 0) { if (failures.length > 0) {
console.error(`Desktop release readiness check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`); console.error(`Desktop release readiness check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
@@ -387,6 +387,19 @@ const verifyUpdaterBoundary = async () => {
assert(source.includes("server origin must not include credentials"), "Desktop updater must reject server origins that include credentials."); assert(source.includes("server origin must not include credentials"), "Desktop updater must reject server origins that include credentials.");
}; };
const verifyDesktopServerConfigurationBoundary = async () => {
const source = await readFile(resolve(sourceDir, "runtime/desktopServerConfig.ts"), "utf8");
assert(
source.includes("import.meta.env.VITE_DESKTOP_SERVER_URL"),
"Desktop server defaults must come from VITE_DESKTOP_SERVER_URL.",
);
assert(!source.includes("ctms.huapont.cn"), "The production CTMS origin must not be hard-coded in Desktop runtime source.");
assert(
source.includes("getStoredDesktopServerUrl() || getDefaultDesktopServerUrl()"),
"A manually stored Desktop server URL must override the build-time default.",
);
};
const verifyWorkflowGates = async () => { const verifyWorkflowGates = async () => {
const packageInfo = await readJson(resolve(frontendDir, "package.json")); const packageInfo = await readJson(resolve(frontendDir, "package.json"));
assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0."); assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0.");
@@ -406,6 +419,18 @@ const verifyWorkflowGates = async () => {
assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`); assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`);
} }
const provenanceSource = await readFile(resolve(frontendDir, "scripts/create-desktop-release-provenance.mjs"), "utf8");
assert(provenanceSource.includes('"macos-first"'), "Release provenance must support the approved v0.1.0 macOS-first stage.");
assert(
provenanceSource.includes("withheld-pending-combined-verification"),
"Staged macOS provenance must record that updater feed activation is withheld.",
);
const windowsPendingNotice = await readFile(resolve(frontendDir, "desktop-release-windows-pending.txt"), "utf8");
assert(
windowsPendingNotice.includes("same immutable v0.1.0 tag") && windowsPendingNotice.includes("latest.json is intentionally withheld"),
"The staged v0.1.0 private release evidence must include an explicit Windows-pending and updater-feed notice.",
);
const releasePolicy = await readJson(resolve(frontendDir, "desktop-release-policy.json")); const releasePolicy = await readJson(resolve(frontendDir, "desktop-release-policy.json"));
assert(releasePolicy.schemaVersion === 1, "Desktop release policy schemaVersion must be 1."); assert(releasePolicy.schemaVersion === 1, "Desktop release policy schemaVersion must be 1.");
assert( assert(
@@ -418,6 +443,15 @@ const verifyWorkflowGates = async () => {
); );
if (packageInfo.version === "0.1.0") { if (packageInfo.version === "0.1.0") {
assert(Boolean(currentUnsignedException), "Desktop release policy must record the approved v0.1.0 exception."); assert(Boolean(currentUnsignedException), "Desktop release policy must record the approved v0.1.0 exception.");
assert(
currentUnsignedException?.macosLocalExactTagFallback === true &&
currentUnsignedException?.windowsDelivery === "deferred-same-tag" &&
currentUnsignedException?.updaterFeedActivation === "after-combined-platform-verification" &&
currentUnsignedException?.githubReleaseAssetProfile === "installer-and-checksum-only" &&
currentUnsignedException?.stagedEvidenceRetention === "private-until-updater-publish" &&
currentUnsignedException?.updaterArtifactPublishTarget === "anonymous-https-origin",
"The v0.1.0 exception must constrain local macOS staging, deferred Windows delivery, visible GitHub assets, private evidence retention, and updater publication.",
);
} }
if (currentUnsignedException) { if (currentUnsignedException) {
assert( assert(
@@ -444,6 +478,7 @@ const verifyWorkflowGates = async () => {
} }
assert(workflow.includes("VITE_BUILD_CHANNEL"), "Client quality gates workflow must inject VITE_BUILD_CHANNEL."); assert(workflow.includes("VITE_BUILD_CHANNEL"), "Client quality gates workflow must inject VITE_BUILD_CHANNEL.");
assert(workflow.includes("VITE_BUILD_COMMIT"), "Client quality gates workflow must inject VITE_BUILD_COMMIT."); assert(workflow.includes("VITE_BUILD_COMMIT"), "Client quality gates workflow must inject VITE_BUILD_COMMIT.");
assert(workflow.includes("VITE_DESKTOP_SERVER_URL"), "Client quality gates workflow must inject VITE_DESKTOP_SERVER_URL.");
assert(workflow.match(/node-version: "22\.13"/g)?.length === 2, "Client quality gates must use Node.js 22.13 for Web and Desktop jobs."); assert(workflow.match(/node-version: "22\.13"/g)?.length === 2, "Client quality gates must use Node.js 22.13 for Web and Desktop jobs.");
const releaseWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-release-candidate.yml"), "utf8"); const releaseWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-release-candidate.yml"), "utf8");
@@ -463,6 +498,7 @@ const verifyWorkflowGates = async () => {
"DESKTOP-RELEASE-PROVENANCE.json", "DESKTOP-RELEASE-PROVENANCE.json",
"--require-provenance", "--require-provenance",
"TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY",
"VITE_DESKTOP_SERVER_URL",
"APPLE_ID", "APPLE_ID",
"APPLE_PASSWORD", "APPLE_PASSWORD",
"APPLE_TEAM_ID", "APPLE_TEAM_ID",
@@ -496,6 +532,7 @@ const verifyWorkflowGates = async () => {
"runs-on: windows-latest", "runs-on: windows-latest",
"VITE_BUILD_CHANNEL", "VITE_BUILD_CHANNEL",
"VITE_BUILD_COMMIT", "VITE_BUILD_COMMIT",
"VITE_DESKTOP_SERVER_URL",
"npm run release:env:check", "npm run release:env:check",
"npm run version:check", "npm run version:check",
"npm run runtime:check", "npm run runtime:check",
@@ -554,6 +591,7 @@ await verifySourceSafety();
await verifyNotificationBoundary(); await verifyNotificationBoundary();
await verifySessionBoundary(); await verifySessionBoundary();
await verifyUpdaterBoundary(); await verifyUpdaterBoundary();
await verifyDesktopServerConfigurationBoundary();
await verifyOnlyOfficeBoundary(); await verifyOnlyOfficeBoundary();
await verifyWorkflowGates(); await verifyWorkflowGates();
@@ -35,6 +35,25 @@ const isNativeDesktopRelease = Boolean(
requiresUpdaterSigning, requiresUpdaterSigning,
); );
const validateDesktopServerUrl = () => {
const raw = env.VITE_DESKTOP_SERVER_URL;
requireEnv("VITE_DESKTOP_SERVER_URL");
if (!raw) return;
let url;
try {
url = new URL(raw);
} catch (error) {
fail(`VITE_DESKTOP_SERVER_URL is invalid: ${error.message}`);
return;
}
assert(url.protocol === "https:", "VITE_DESKTOP_SERVER_URL must use HTTPS.");
assert(url.username === "" && url.password === "", "VITE_DESKTOP_SERVER_URL must not include credentials.");
assert(url.pathname === "/", "VITE_DESKTOP_SERVER_URL must be an origin without a path.");
assert(url.search === "" && url.hash === "", "VITE_DESKTOP_SERVER_URL must not include query parameters or fragments.");
};
const fail = (message) => failures.push(message); const fail = (message) => failures.push(message);
const assert = (condition, message) => { const assert = (condition, message) => {
if (!condition) fail(message); if (!condition) fail(message);
@@ -61,6 +80,7 @@ assert(allowedChannels.has(channel), `VITE_BUILD_CHANNEL must be one of ${[...al
if (isCi || isReleaseBuild) { if (isCi || isReleaseBuild) {
assert(channel !== "local", "CI and release builds must inject VITE_BUILD_CHANNEL."); assert(channel !== "local", "CI and release builds must inject VITE_BUILD_CHANNEL.");
assert(fullShaPattern.test(commit), "CI and release builds must inject a full 40-character VITE_BUILD_COMMIT."); assert(fullShaPattern.test(commit), "CI and release builds must inject a full 40-character VITE_BUILD_COMMIT.");
validateDesktopServerUrl();
} }
if (env.GITHUB_SHA) { if (env.GITHUB_SHA) {
+1
View File
@@ -3,6 +3,7 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_BUILD_CHANNEL?: "dev" | "main" | "release" | "local"; readonly VITE_BUILD_CHANNEL?: "dev" | "main" | "release" | "local";
readonly VITE_BUILD_COMMIT?: string; readonly VITE_BUILD_COMMIT?: string;
readonly VITE_DESKTOP_SERVER_URL?: string;
} }
interface ImportMeta { interface ImportMeta {
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { import {
DESKTOP_SERVER_URL_CHANGED_EVENT, DESKTOP_SERVER_URL_CHANGED_EVENT,
DESKTOP_SERVER_URL_KEY, DESKTOP_SERVER_URL_KEY,
getDefaultDesktopServerUrl,
getDesktopServerUrl, getDesktopServerUrl,
normalizeDesktopServerUrl, normalizeDesktopServerUrl,
setDesktopServerUrl, setDesktopServerUrl,
@@ -31,6 +32,7 @@ const createMemoryStorage = (): Storage => {
}; };
beforeEach(() => { beforeEach(() => {
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "");
Object.defineProperty(window, "localStorage", { Object.defineProperty(window, "localStorage", {
value: createMemoryStorage(), value: createMemoryStorage(),
configurable: true, configurable: true,
@@ -40,6 +42,7 @@ beforeEach(() => {
afterEach(() => { afterEach(() => {
window.localStorage.clear(); window.localStorage.clear();
setTauriRuntime(false); setTauriRuntime(false);
vi.unstubAllEnvs();
}); });
describe("desktop server config", () => { describe("desktop server config", () => {
@@ -70,6 +73,23 @@ describe("desktop server config", () => {
expect(listener).toHaveBeenCalledOnce(); expect(listener).toHaveBeenCalledOnce();
}); });
it("uses the build-time default while allowing a persisted manual override", () => {
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "https://default.ctms.example.com");
expect(getDefaultDesktopServerUrl()).toBe("https://default.ctms.example.com/");
expect(getDesktopServerUrl()).toBe("https://default.ctms.example.com/");
setDesktopServerUrl("https://manual.ctms.example.com");
expect(getDesktopServerUrl()).toBe("https://manual.ctms.example.com/");
});
it("ignores an invalid build-time default", () => {
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "http://public.ctms.example.com");
expect(getDefaultDesktopServerUrl()).toBeNull();
expect(getDesktopServerUrl()).toBeNull();
});
it("requires a server URL only inside the Tauri runtime", () => { it("requires a server URL only inside the Tauri runtime", () => {
expect(shouldRequireDesktopServerUrl()).toBe(false); expect(shouldRequireDesktopServerUrl()).toBe(false);
setTauriRuntime(true); setTauriRuntime(true);
@@ -77,4 +97,11 @@ describe("desktop server config", () => {
setDesktopServerUrl("https://ctms.example.com"); setDesktopServerUrl("https://ctms.example.com");
expect(shouldRequireDesktopServerUrl()).toBe(false); expect(shouldRequireDesktopServerUrl()).toBe(false);
}); });
it("does not require first-run configuration when a valid build-time default exists", () => {
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "https://default.ctms.example.com");
setTauriRuntime(true);
expect(shouldRequireDesktopServerUrl()).toBe(false);
});
}); });
+11 -2
View File
@@ -51,13 +51,22 @@ export const normalizeDesktopServerUrl = (value: string): DesktopServerUrlValida
return { ok: true, url: `${parsed.origin}/` }; return { ok: true, url: `${parsed.origin}/` };
}; };
export const getDesktopServerUrl = (): string | null => { export const getDefaultDesktopServerUrl = (): string | null => {
const configured = import.meta.env.VITE_DESKTOP_SERVER_URL?.trim();
if (!configured) return null;
const result = normalizeDesktopServerUrl(configured);
return result.ok ? result.url : null;
};
const getStoredDesktopServerUrl = (): string | null => {
const stored = getStorage()?.getItem(DESKTOP_SERVER_URL_KEY); const stored = getStorage()?.getItem(DESKTOP_SERVER_URL_KEY);
if (!stored) return null; if (!stored) return null;
const result = normalizeDesktopServerUrl(stored); const result = normalizeDesktopServerUrl(stored);
return result.ok ? result.url : null; return result.ok ? result.url : null;
}; };
export const getDesktopServerUrl = (): string | null => getStoredDesktopServerUrl() || getDefaultDesktopServerUrl();
export const hasDesktopServerUrl = (): boolean => Boolean(getDesktopServerUrl()); export const hasDesktopServerUrl = (): boolean => Boolean(getDesktopServerUrl());
export const setDesktopServerUrl = (value: string): DesktopServerUrlValidationResult => { export const setDesktopServerUrl = (value: string): DesktopServerUrlValidationResult => {
@@ -74,7 +83,7 @@ export const setDesktopServerUrl = (value: string): DesktopServerUrlValidationRe
export const clearDesktopServerUrl = (): void => { export const clearDesktopServerUrl = (): void => {
const previous = getDesktopServerUrl(); const previous = getDesktopServerUrl();
getStorage()?.removeItem(DESKTOP_SERVER_URL_KEY); getStorage()?.removeItem(DESKTOP_SERVER_URL_KEY);
emitServerUrlChanged(previous, null); emitServerUrlChanged(previous, getDesktopServerUrl());
}; };
export const shouldRequireDesktopServerUrl = (): boolean => isTauriRuntime() && !hasDesktopServerUrl(); export const shouldRequireDesktopServerUrl = (): boolean => isTauriRuntime() && !hasDesktopServerUrl();
+1
View File
@@ -2,6 +2,7 @@ export { clientRuntime, type ClientRuntime, type RuntimeCapabilities } from "./c
export { export {
clearDesktopServerUrl, clearDesktopServerUrl,
DESKTOP_SERVER_URL_CHANGED_EVENT, DESKTOP_SERVER_URL_CHANGED_EVENT,
getDefaultDesktopServerUrl,
getDesktopServerUrl, getDesktopServerUrl,
hasDesktopServerUrl, hasDesktopServerUrl,
normalizeDesktopServerUrl, normalizeDesktopServerUrl,