refactor(client): unify web and desktop release workflow
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
name: Client Quality Gates
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
- ".github/workflows/client-quality-gates.yml"
|
||||||
|
- "docs/branch-governance.md"
|
||||||
|
- "docs/guides/client-release.md"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- main
|
||||||
|
- release
|
||||||
|
paths:
|
||||||
|
- "frontend/**"
|
||||||
|
- ".github/workflows/client-quality-gates.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
web:
|
||||||
|
name: Shared client and Web
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
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: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Check synchronized client version
|
||||||
|
run: npm run version:check
|
||||||
|
|
||||||
|
- name: Check runtime boundary
|
||||||
|
run: npm run runtime:check
|
||||||
|
|
||||||
|
- 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: ${{ github.ref_name }}
|
||||||
|
VITE_BUILD_COMMIT: ${{ github.sha }}
|
||||||
|
|
||||||
|
desktop:
|
||||||
|
name: macOS Desktop
|
||||||
|
runs-on: macos-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
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: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build macOS application
|
||||||
|
run: npm run desktop:build -- --bundles app
|
||||||
|
env:
|
||||||
|
VITE_BUILD_CHANNEL: ${{ github.ref_name }}
|
||||||
|
VITE_BUILD_COMMIT: ${{ github.sha }}
|
||||||
@@ -3,3 +3,24 @@
|
|||||||
处理 CTMS 桌面端任务前,必须先阅读 `docs/desktop-project-plan.md`。桌面端任务包括但不限于 Tauri、macOS、Windows、桌面打包、桌面存储、文件集成、系统通知和桌面端安全边界。
|
处理 CTMS 桌面端任务前,必须先阅读 `docs/desktop-project-plan.md`。桌面端任务包括但不限于 Tauri、macOS、Windows、桌面打包、桌面存储、文件集成、系统通知和桌面端安全边界。
|
||||||
|
|
||||||
桌面端仅限该计划书中的第一阶段和第二阶段。除非先明确修改计划书,否则不要实现离线功能、本地业务数据存储、内嵌后端服务或离线同步。
|
桌面端仅限该计划书中的第一阶段和第二阶段。除非先明确修改计划书,否则不要实现离线功能、本地业务数据存储、内嵌后端服务或离线同步。
|
||||||
|
|
||||||
|
## 分支与发布治理
|
||||||
|
|
||||||
|
处理代码提交、分支同步、版本晋级、正式发布或生产热修复前,必须先阅读:
|
||||||
|
|
||||||
|
- `docs/guides/branch-maintenance-sop-zh.md`
|
||||||
|
- `docs/branch-governance.md`
|
||||||
|
- 涉及网页端或桌面端客户端发布时,还需阅读 `docs/guides/client-release.md`
|
||||||
|
|
||||||
|
必须遵守以下规则:
|
||||||
|
|
||||||
|
- CTMS 网页端和桌面端属于同一个产品,共用 `dev`、`main`、`release` 分支,不创建 `web-dev`、`desktop-dev`、`web-release`、`desktop-release` 等长期平行分支。
|
||||||
|
- 默认晋级路径为短期任务分支进入 `dev`,再由 `dev` 晋级到 `main`,最后由 `main` 发布到 `release`。
|
||||||
|
- Agent 创建分支时默认使用 `codex/<任务名称>`;分支必须从最新 `dev` 创建,并在合并到 `dev` 后删除。
|
||||||
|
- `codex/ctms-desktop` 仅是当前桌面端临时集成分支。Tauri 基线合并到 `dev` 后,不得继续将其作为长期桌面主线。
|
||||||
|
- 生产热修复从 `release` 创建,合并到 `release` 后必须依次回合并到 `main` 和 `dev`。
|
||||||
|
- 网页端和桌面端必须使用同一个语义化版本号、正式标签和源代码提交。修改客户端版本时使用 `frontend/package.json` 中的 `version:set` 和 `version:check` 命令。
|
||||||
|
- 平台差异必须收敛在 `frontend/src/runtime/` 之后,不能通过长期分支或复制业务代码维护桌面差异。
|
||||||
|
- 未经用户明确要求,不执行提交、推送、合并、变基、打标签、删除分支或强制更新远程分支。
|
||||||
|
- 执行用户明确要求的 Git 操作前,先检查工作区和目标分支,只暂存本次任务相关文件,不覆盖或撤销用户已有改动。
|
||||||
|
- 分支治理规则发生变化时,必须同步更新上述治理文档,不能只修改 `AGENTS.md`。
|
||||||
|
|||||||
@@ -46,9 +46,12 @@
|
|||||||
- 生产构建:进入 `frontend/` 后执行 `npm run desktop:build`;DMG 构建执行 `npm run desktop:bundle:dmg`。
|
- 生产构建:进入 `frontend/` 后执行 `npm run desktop:build`;DMG 构建执行 `npm run desktop:bundle:dmg`。
|
||||||
- 首次启动桌面端会要求配置 CTMS 服务端地址,并在保存前检查 `${serverUrl}/health`。
|
- 首次启动桌面端会要求配置 CTMS 服务端地址,并在保存前检查 `${serverUrl}/health`。
|
||||||
- 生产或非本地服务地址必须使用 HTTPS;本地开发允许 `http://localhost` 或 `http://127.0.0.1`。
|
- 生产或非本地服务地址必须使用 HTTPS;本地开发允许 `http://localhost` 或 `http://127.0.0.1`。
|
||||||
|
- Web 与桌面端共用产品版本;执行 `npm run version:set -- <version>` 统一升级,执行 `npm run version:check` 检查漂移。
|
||||||
|
- 桌面端与 Web 端从同一发布标签和 Git 提交构建,具体流程见 `docs/guides/client-release.md`。
|
||||||
|
|
||||||
## 仓库治理文档
|
## 仓库治理文档
|
||||||
- 分支治理规范:`docs/branch-governance.md`
|
- 分支治理规范:`docs/branch-governance.md`
|
||||||
|
- 分支维护中文 SOP:`docs/guides/branch-maintenance-sop-zh.md`
|
||||||
- 分支环境安装配置:`docs/guides/branch-environment-installation.md`
|
- 分支环境安装配置:`docs/guides/branch-environment-installation.md`
|
||||||
- 发布检查清单:`docs/guides/release-checklist.md`
|
- 发布检查清单:`docs/guides/release-checklist.md`
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ CTMS 文档入口只展示当前仍会影响开发、发布和运维决策的内
|
|||||||
- [`desktop-phase-1-design.md`](desktop-phase-1-design.md): 桌面端第一阶段 macOS 在线客户端详细方案
|
- [`desktop-phase-1-design.md`](desktop-phase-1-design.md): 桌面端第一阶段 macOS 在线客户端详细方案
|
||||||
- [`branch-governance.md`](branch-governance.md): 长期分支治理规则
|
- [`branch-governance.md`](branch-governance.md): 长期分支治理规则
|
||||||
- [`guides/release-checklist.md`](guides/release-checklist.md): 发布前检查项与回归门禁
|
- [`guides/release-checklist.md`](guides/release-checklist.md): 发布前检查项与回归门禁
|
||||||
|
- [`guides/client-release.md`](guides/client-release.md): Web/桌面端统一版本、构建与发布流程
|
||||||
|
- [`guides/branch-maintenance-sop-zh.md`](guides/branch-maintenance-sop-zh.md): 分支维护、版本晋级、发布和热修复中文标准操作规程
|
||||||
- [`audits/storage-persistence-governance.md`](audits/storage-persistence-governance.md): 重要数据落库治理基线
|
- [`audits/storage-persistence-governance.md`](audits/storage-persistence-governance.md): 重要数据落库治理基线
|
||||||
- [`audits/module-level-permissions-transition.md`](audits/module-level-permissions-transition.md): 模块级权限迁移状态与约束
|
- [`audits/module-level-permissions-transition.md`](audits/module-level-permissions-transition.md): 模块级权限迁移状态与约束
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,27 @@ Meaning:
|
|||||||
|
|
||||||
Direct promotion that skips stages is discouraged and must be justified in writing.
|
Direct promotion that skips stages is discouraged and must be justified in writing.
|
||||||
|
|
||||||
|
## 3.1 Unified Web and Desktop Mainline
|
||||||
|
|
||||||
|
CTMS Web and Desktop are two delivery targets of the same product version. They
|
||||||
|
share the Vue application, API contract, and product branches.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Do not create long-lived `web-dev`, `desktop-dev`, `web-release`, or
|
||||||
|
`desktop-release` branches.
|
||||||
|
- Web and Desktop changes both follow `feature/*` -> `dev` -> `main` ->
|
||||||
|
`release`.
|
||||||
|
- A platform-specific feature branch is allowed while work is in progress, for
|
||||||
|
example `feature/desktop-file-picker`, but it must merge back into `dev`.
|
||||||
|
- `codex/ctms-desktop` is a temporary desktop integration branch. After the
|
||||||
|
Tauri baseline is accepted into `dev`, new desktop work must use short-lived
|
||||||
|
feature branches from the current `dev`.
|
||||||
|
- Platform differences belong behind `frontend/src/runtime/`. Shared business
|
||||||
|
modules must not import Tauri APIs directly.
|
||||||
|
- A release tag identifies one product source state. Web and Desktop artifacts
|
||||||
|
for that release must be built from the same tag and Git commit.
|
||||||
|
|
||||||
## 4. Branch Entry Rules
|
## 4. Branch Entry Rules
|
||||||
|
|
||||||
### Changes allowed into `dev`
|
### Changes allowed into `dev`
|
||||||
@@ -214,6 +235,13 @@ Rules:
|
|||||||
- tags are created on `release`, not on `dev`
|
- tags are created on `release`, not on `dev`
|
||||||
- a tag must point to the exact production release commit
|
- a tag must point to the exact production release commit
|
||||||
- patch hotfixes on `release` should increment the patch version
|
- patch hotfixes on `release` should increment the patch version
|
||||||
|
- Web and Desktop use the same semantic version. Do not add a separate Desktop
|
||||||
|
product version.
|
||||||
|
- Desktop packaging-only rebuilds may add build metadata to the artifact name,
|
||||||
|
but must retain the product version and record the source commit.
|
||||||
|
- Before creating a tag, run `cd frontend && npm run version:check`.
|
||||||
|
- Change the shared client version with
|
||||||
|
`cd frontend && npm run version:set -- <version>`.
|
||||||
|
|
||||||
## 7. Hotfix Back-Merge Rules
|
## 7. Hotfix Back-Merge Rules
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,500 @@
|
|||||||
|
# CTMS 分支维护与版本更新标准操作规程
|
||||||
|
|
||||||
|
## 一、目的
|
||||||
|
|
||||||
|
本规程用于统一 CTMS 网页端和桌面端的代码提交、分支维护、版本晋级、正式发布及生产热修复流程。
|
||||||
|
|
||||||
|
CTMS 网页端和桌面端属于同一个产品,必须共用:
|
||||||
|
|
||||||
|
- 同一个代码仓库
|
||||||
|
- 同一套业务核心代码
|
||||||
|
- 同一条版本晋级链路
|
||||||
|
- 同一个语义化版本号
|
||||||
|
- 同一个正式发布标签
|
||||||
|
- 同一个源代码提交
|
||||||
|
|
||||||
|
不得为网页端和桌面端分别建立长期开发、测试或发布分支。
|
||||||
|
|
||||||
|
## 二、长期分支职责
|
||||||
|
|
||||||
|
| 分支 | 职责 | 允许进入的内容 | 稳定性要求 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `dev` | 日常开发与集成 | 已评审的功能、修复和重构 | 可持续集成 |
|
||||||
|
| `main` | 下一正式版本候选 | 从 `dev` 晋级的完整版本范围、候选版本修复 | 原则上可部署 |
|
||||||
|
| `release` | 当前生产稳定版本 | 从 `main` 验收通过的正式版本、生产热修复 | 最高 |
|
||||||
|
|
||||||
|
默认晋级方向:
|
||||||
|
|
||||||
|
```text
|
||||||
|
功能分支 -> dev -> main -> release -> 正式版本标签
|
||||||
|
```
|
||||||
|
|
||||||
|
禁止以下长期分支:
|
||||||
|
|
||||||
|
```text
|
||||||
|
web-dev
|
||||||
|
desktop-dev
|
||||||
|
web-release
|
||||||
|
desktop-release
|
||||||
|
macos-main
|
||||||
|
windows-main
|
||||||
|
```
|
||||||
|
|
||||||
|
桌面端差异必须放在 `frontend/src/runtime/` 适配层之后,不通过长期分支保存平台差异。
|
||||||
|
|
||||||
|
## 三、临时分支命名
|
||||||
|
|
||||||
|
| 类型 | 命名格式 | 示例 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 新功能 | `feature/<功能名称>` | `feature/desktop-file-picker` |
|
||||||
|
| 缺陷修复 | `fix/<问题名称>` | `fix/session-timeout` |
|
||||||
|
| 生产热修复 | `hotfix/<问题名称>` | `hotfix/login-loop` |
|
||||||
|
| 文档调整 | `docs/<文档名称>` | `docs/release-sop` |
|
||||||
|
| 发布准备 | `release-prep/<版本号>` | `release-prep/v1.2.0` |
|
||||||
|
| 临时集成 | `codex/<任务名称>` | `codex/ctms-desktop` |
|
||||||
|
|
||||||
|
分支名称使用小写英文和连字符,不使用个人姓名、日期或模糊名称。
|
||||||
|
|
||||||
|
## 四、日常功能开发流程
|
||||||
|
|
||||||
|
### 1. 从最新 `dev` 创建分支
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git switch dev
|
||||||
|
git pull --ff-only origin dev
|
||||||
|
git switch -c feature/<功能名称>
|
||||||
|
```
|
||||||
|
|
||||||
|
不得从旧功能分支、`main` 或 `release` 创建普通功能分支。
|
||||||
|
|
||||||
|
### 2. 开发过程中同步 `dev`
|
||||||
|
|
||||||
|
短期分支优先使用变基保持提交清晰:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git rebase origin/dev
|
||||||
|
```
|
||||||
|
|
||||||
|
已经由多人共同使用的分支,不得擅自强制推送。此时可使用合并:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git merge origin/dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 提交前检查
|
||||||
|
|
||||||
|
前端或桌面端改动至少执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run version:check
|
||||||
|
npm run runtime:check
|
||||||
|
npm run type-check
|
||||||
|
npm run test:unit
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
涉及 Tauri、macOS 打包或桌面适配层时,还必须在 macOS 执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run desktop:build -- --bundles app
|
||||||
|
```
|
||||||
|
|
||||||
|
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
|
||||||
|
|
||||||
|
### 4. 创建提交
|
||||||
|
|
||||||
|
只暂存本次任务相关文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
git add <本次任务相关文件>
|
||||||
|
git diff --cached
|
||||||
|
git diff --cached --check
|
||||||
|
git commit -m "<类型>(<范围>): <变更说明>"
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐提交类型:
|
||||||
|
|
||||||
|
| 类型 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `feat` | 新功能 |
|
||||||
|
| `fix` | 缺陷修复 |
|
||||||
|
| `refactor` | 不改变业务行为的重构 |
|
||||||
|
| `test` | 测试调整 |
|
||||||
|
| `docs` | 文档调整 |
|
||||||
|
| `build` | 构建和依赖调整 |
|
||||||
|
| `ci` | 持续集成调整 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```text
|
||||||
|
feat(desktop): 增加原生文件选择适配器
|
||||||
|
fix(auth): 修复会话超时后的重复跳转
|
||||||
|
refactor(client): 统一网页端和桌面端运行时入口
|
||||||
|
```
|
||||||
|
|
||||||
|
一次提交只处理一个明确目的。不得将无关格式化、个人配置或临时产物混入提交。
|
||||||
|
|
||||||
|
### 5. 推送并创建合并请求
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push -u origin feature/<功能名称>
|
||||||
|
```
|
||||||
|
|
||||||
|
创建:
|
||||||
|
|
||||||
|
```text
|
||||||
|
feature/<功能名称> -> dev
|
||||||
|
```
|
||||||
|
|
||||||
|
合并要求:
|
||||||
|
|
||||||
|
- 代码评审通过
|
||||||
|
- 必要测试通过
|
||||||
|
- 客户端质量门禁通过
|
||||||
|
- 没有误提交密钥、环境文件或构建产物
|
||||||
|
- 桌面能力符合第一阶段或第二阶段边界
|
||||||
|
|
||||||
|
功能分支进入 `dev` 可使用合并请求合并或变基合并。提交过于零散时应先整理。
|
||||||
|
|
||||||
|
### 6. 合并后清理
|
||||||
|
|
||||||
|
确认改动已经进入远程 `dev` 后删除临时分支:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git switch dev
|
||||||
|
git pull --ff-only origin dev
|
||||||
|
git branch -d feature/<功能名称>
|
||||||
|
git push origin --delete feature/<功能名称>
|
||||||
|
```
|
||||||
|
|
||||||
|
工作树正在使用的分支不能直接删除,应先切换分支或移除对应工作树。
|
||||||
|
|
||||||
|
## 五、当前桌面集成分支处理流程
|
||||||
|
|
||||||
|
`codex/ctms-desktop` 是临时桌面集成分支,不作为长期桌面主线。
|
||||||
|
|
||||||
|
处理步骤:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
git add <本次桌面端和治理优化相关文件>
|
||||||
|
git diff --cached --check
|
||||||
|
git commit -m "refactor(client): 统一网页端和桌面端发布流程"
|
||||||
|
git fetch origin
|
||||||
|
git rebase origin/dev
|
||||||
|
git push origin codex/ctms-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
创建:
|
||||||
|
|
||||||
|
```text
|
||||||
|
codex/ctms-desktop -> dev
|
||||||
|
```
|
||||||
|
|
||||||
|
合并并验证 `dev` 后:
|
||||||
|
|
||||||
|
- 删除远程 `codex/ctms-desktop`
|
||||||
|
- 不再从该分支继续开发
|
||||||
|
- 后续桌面功能从最新 `dev` 创建短期 `feature/*` 分支
|
||||||
|
|
||||||
|
## 六、从 `dev` 晋级到 `main`
|
||||||
|
|
||||||
|
当一个版本范围在 `dev` 完成集成后,创建:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dev -> main
|
||||||
|
```
|
||||||
|
|
||||||
|
进入 `main` 前必须确认:
|
||||||
|
|
||||||
|
- 本版本范围已经冻结
|
||||||
|
- 未完成功能已经排除或关闭入口
|
||||||
|
- 前后端测试通过
|
||||||
|
- 网页端构建通过
|
||||||
|
- macOS 桌面端构建通过
|
||||||
|
- 数据库迁移经过验证
|
||||||
|
- 已知风险和回滚方式已记录
|
||||||
|
|
||||||
|
正式创建晋级合并请求前,应按照第七节完成统一版本号更新,并确保版本提交已经进入 `dev`。
|
||||||
|
|
||||||
|
按照当前仓库治理规则,`dev` 进入 `main` 使用压缩合并,并使用版本候选级提交说明:
|
||||||
|
|
||||||
|
```text
|
||||||
|
release(main): 准备 v1.2.0 候选版本
|
||||||
|
```
|
||||||
|
|
||||||
|
不得从功能分支直接跳过 `dev` 合并到 `main`。
|
||||||
|
|
||||||
|
## 七、统一更新客户端版本
|
||||||
|
|
||||||
|
网页端和桌面端只能使用同一个产品版本号。
|
||||||
|
|
||||||
|
从最新 `dev` 创建发布准备分支:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git switch dev
|
||||||
|
git pull --ff-only origin dev
|
||||||
|
git switch -c release-prep/v1.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
统一更新版本并提交:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run version:set -- 1.2.0
|
||||||
|
npm run version:check
|
||||||
|
cd ..
|
||||||
|
git add frontend/package.json frontend/package-lock.json frontend/src-tauri/tauri.conf.json
|
||||||
|
git add frontend/src-tauri/Cargo.toml frontend/src-tauri/Cargo.lock
|
||||||
|
git commit -m "build(release): 更新客户端版本至 v1.2.0"
|
||||||
|
git push -u origin release-prep/v1.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
创建 `release-prep/v1.2.0 -> dev` 合并请求。合并后再执行 `dev -> main` 的版本晋级。
|
||||||
|
|
||||||
|
该命令同步更新:
|
||||||
|
|
||||||
|
- `frontend/package.json`
|
||||||
|
- `frontend/package-lock.json`
|
||||||
|
- `frontend/src-tauri/tauri.conf.json`
|
||||||
|
- `frontend/src-tauri/Cargo.toml`
|
||||||
|
- `frontend/src-tauri/Cargo.lock`
|
||||||
|
|
||||||
|
版本号遵循:
|
||||||
|
|
||||||
|
| 类型 | 示例 | 使用场景 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 主版本 | `2.0.0` | 不兼容变更或重大架构调整 |
|
||||||
|
| 次版本 | `1.3.0` | 向后兼容的新功能 |
|
||||||
|
| 修订版本 | `1.2.1` | 向后兼容的缺陷修复 |
|
||||||
|
|
||||||
|
禁止单独设置桌面端版本号。
|
||||||
|
|
||||||
|
## 八、从 `main` 发布到 `release`
|
||||||
|
|
||||||
|
候选版本验收通过后,创建:
|
||||||
|
|
||||||
|
```text
|
||||||
|
main -> release
|
||||||
|
```
|
||||||
|
|
||||||
|
按照当前仓库治理规则,使用普通合并提交,保留候选版本与生产版本之间的关系。
|
||||||
|
|
||||||
|
合并前必须确认:
|
||||||
|
|
||||||
|
- 回归测试通过
|
||||||
|
- 数据库迁移和回滚方案确认
|
||||||
|
- 网页端生产构建通过
|
||||||
|
- 桌面端生产构建通过
|
||||||
|
- 发布说明完成
|
||||||
|
- 生产配置和密钥不在仓库中
|
||||||
|
- 正式版本号已经统一
|
||||||
|
|
||||||
|
合并后立即在 `release` 的准确提交上创建标签:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git switch release
|
||||||
|
git pull --ff-only origin release
|
||||||
|
git tag -a v1.2.0 -m "CTMS v1.2.0"
|
||||||
|
git push origin v1.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
网页端和桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。
|
||||||
|
|
||||||
|
发布记录至少包含:
|
||||||
|
|
||||||
|
- 产品版本号
|
||||||
|
- Git 标签
|
||||||
|
- 完整提交编号
|
||||||
|
- 网页端制品编号
|
||||||
|
- 桌面端制品编号
|
||||||
|
- 数据库迁移版本
|
||||||
|
- 发布日期和负责人
|
||||||
|
|
||||||
|
## 九、生产热修复流程
|
||||||
|
|
||||||
|
### 1. 从 `release` 创建热修复分支
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git switch release
|
||||||
|
git pull --ff-only origin release
|
||||||
|
git switch -c hotfix/<问题名称>
|
||||||
|
```
|
||||||
|
|
||||||
|
热修复只能包含解决生产问题所需的最小改动,不得顺带加入新功能或大规模重构。
|
||||||
|
|
||||||
|
### 2. 更新修订版本
|
||||||
|
|
||||||
|
例如从 `1.2.0` 更新到 `1.2.1`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run version:set -- 1.2.1
|
||||||
|
npm run version:check
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证并提交
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add <热修复相关文件和版本文件>
|
||||||
|
git diff --cached --check
|
||||||
|
git commit -m "fix(<范围>): <生产问题说明>"
|
||||||
|
git push -u origin hotfix/<问题名称>
|
||||||
|
```
|
||||||
|
|
||||||
|
创建:
|
||||||
|
|
||||||
|
```text
|
||||||
|
hotfix/<问题名称> -> release
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 合并并创建标签
|
||||||
|
|
||||||
|
热修复合并到 `release` 并验证后创建 `v1.2.1` 标签。
|
||||||
|
|
||||||
|
### 5. 强制回合并
|
||||||
|
|
||||||
|
生产热修复必须立即回合并:
|
||||||
|
|
||||||
|
```text
|
||||||
|
release -> main -> dev
|
||||||
|
```
|
||||||
|
|
||||||
|
不得假设 `main` 或 `dev` 已经包含相同修复。发生冲突时必须立即解决并在合并请求中记录原因。
|
||||||
|
|
||||||
|
## 十、冲突处理规则
|
||||||
|
|
||||||
|
发生冲突时:
|
||||||
|
|
||||||
|
1. 先确认冲突两侧的业务意图。
|
||||||
|
2. 不使用整文件覆盖方式跳过判断。
|
||||||
|
3. 保留双方仍然有效的修改。
|
||||||
|
4. 重新执行受影响测试。
|
||||||
|
5. 在合并请求中记录冲突文件和处理结果。
|
||||||
|
|
||||||
|
禁止使用以下方式处理普通同步冲突:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git reset --hard
|
||||||
|
git checkout -- <文件>
|
||||||
|
```
|
||||||
|
|
||||||
|
除非已经明确确认可以丢弃本地修改,否则不得执行破坏性命令。
|
||||||
|
|
||||||
|
## 十一、分支保护建议
|
||||||
|
|
||||||
|
### `release`
|
||||||
|
|
||||||
|
- 禁止直接推送
|
||||||
|
- 必须通过合并请求
|
||||||
|
- 至少一名评审人批准
|
||||||
|
- 必须通过状态检查
|
||||||
|
- 正式标签只由发布负责人创建
|
||||||
|
|
||||||
|
### `main`
|
||||||
|
|
||||||
|
- 禁止直接推送
|
||||||
|
- 必须通过合并请求
|
||||||
|
- 必须完成回归和构建检查
|
||||||
|
- 只接收版本候选内容
|
||||||
|
|
||||||
|
### `dev`
|
||||||
|
|
||||||
|
- 优先通过合并请求
|
||||||
|
- 必须通过相关测试
|
||||||
|
- 禁止提交密钥和本地配置
|
||||||
|
- 禁止合入明确不可构建的代码
|
||||||
|
|
||||||
|
## 十二、每周分支维护
|
||||||
|
|
||||||
|
每周至少执行一次:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch --prune origin
|
||||||
|
git branch -vv
|
||||||
|
git log --oneline --decorate --graph --all -30
|
||||||
|
```
|
||||||
|
|
||||||
|
检查事项:
|
||||||
|
|
||||||
|
- 已合并临时分支是否删除
|
||||||
|
- 是否出现未经批准的长期平台分支
|
||||||
|
- `dev`、`main`、`release` 是否符合各自职责
|
||||||
|
- 生产热修复是否已回合并到 `main` 和 `dev`
|
||||||
|
- 版本文件是否一致
|
||||||
|
- 正式标签是否准确指向 `release`
|
||||||
|
- 持续集成门禁是否持续通过
|
||||||
|
|
||||||
|
## 十三、禁止事项
|
||||||
|
|
||||||
|
- 禁止长期维护网页端和桌面端平行分支
|
||||||
|
- 禁止通过复制代码维护桌面端业务页面
|
||||||
|
- 禁止在业务模块中直接使用 Tauri API
|
||||||
|
- 禁止在不同提交上构建同一版本的网页端和桌面端
|
||||||
|
- 禁止未经 `dev` 和 `main` 直接向 `release` 发布普通功能
|
||||||
|
- 禁止生产热修复只进入 `release` 而不回合并
|
||||||
|
- 禁止在正式构建中包含未提交文件
|
||||||
|
- 禁止提交 `.env`、密钥、证书、令牌和个人配置
|
||||||
|
- 禁止提交 `node_modules`、`dist`、Tauri `target` 等构建产物
|
||||||
|
|
||||||
|
## 十四、发布前最终检查清单
|
||||||
|
|
||||||
|
- [ ] 本次发布范围已经冻结
|
||||||
|
- [ ] `dev` 集成测试通过
|
||||||
|
- [ ] `main` 候选版本验收通过
|
||||||
|
- [ ] 网页端与桌面端版本一致
|
||||||
|
- [ ] `npm run version:check` 通过
|
||||||
|
- [ ] `npm run runtime:check` 通过
|
||||||
|
- [ ] `npm run type-check` 通过
|
||||||
|
- [ ] `npm run test:unit` 通过
|
||||||
|
- [ ] `npm run build` 通过
|
||||||
|
- [ ] macOS 桌面端构建通过
|
||||||
|
- [ ] 数据库迁移与回滚方案确认
|
||||||
|
- [ ] 发布说明完成
|
||||||
|
- [ ] `main -> release` 合并完成
|
||||||
|
- [ ] 正式标签创建在准确的 `release` 提交上
|
||||||
|
- [ ] 网页端和桌面端从同一标签构建
|
||||||
|
- [ ] 发布记录包含版本、标签和完整提交编号
|
||||||
|
|
||||||
|
## 十五、流程速查
|
||||||
|
|
||||||
|
普通功能:
|
||||||
|
|
||||||
|
```text
|
||||||
|
最新 dev
|
||||||
|
-> feature/*
|
||||||
|
-> 开发、测试、评审
|
||||||
|
-> dev
|
||||||
|
-> 删除临时分支
|
||||||
|
```
|
||||||
|
|
||||||
|
正式发布:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dev
|
||||||
|
-> main
|
||||||
|
-> 统一版本号
|
||||||
|
-> 回归与验收
|
||||||
|
-> release
|
||||||
|
-> 创建 vX.Y.Z 标签
|
||||||
|
-> 同一标签构建网页端和桌面端
|
||||||
|
```
|
||||||
|
|
||||||
|
生产热修复:
|
||||||
|
|
||||||
|
```text
|
||||||
|
release
|
||||||
|
-> hotfix/*
|
||||||
|
-> release
|
||||||
|
-> 创建修订版本标签
|
||||||
|
-> main
|
||||||
|
-> dev
|
||||||
|
```
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# CTMS Web and Desktop Release Guide
|
||||||
|
|
||||||
|
## Release Unit
|
||||||
|
|
||||||
|
CTMS uses one repository, one promotion path, and one product version. Web and
|
||||||
|
Desktop are build targets from the same source commit, not separately versioned
|
||||||
|
products.
|
||||||
|
|
||||||
|
The release identity consists of:
|
||||||
|
|
||||||
|
- semantic version, for example `1.8.0`
|
||||||
|
- Git tag, for example `v1.8.0`
|
||||||
|
- full Git commit SHA
|
||||||
|
- build channel: `dev`, `main`, or `release`
|
||||||
|
- client type: `web` or `desktop`
|
||||||
|
|
||||||
|
## Runtime Boundary
|
||||||
|
|
||||||
|
Shared Vue business code lives under `frontend/src/`. Platform decisions are
|
||||||
|
exposed through `frontend/src/runtime/index.ts`.
|
||||||
|
|
||||||
|
The runtime contract currently provides:
|
||||||
|
|
||||||
|
- API base URL resolution
|
||||||
|
- Web, macOS, Windows, and Linux runtime identification
|
||||||
|
- app version, source commit, build channel, and client type metadata
|
||||||
|
- explicit capability flags
|
||||||
|
- Desktop server address configuration
|
||||||
|
|
||||||
|
Business modules must use this public runtime entry point. They must not inspect
|
||||||
|
Tauri globals or import Tauri packages directly. Native files, notifications,
|
||||||
|
secure session storage, and automatic updates remain disabled until their
|
||||||
|
second-phase adapters are implemented and reviewed.
|
||||||
|
|
||||||
|
## Version Change
|
||||||
|
|
||||||
|
Update every client manifest with one command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run version:set -- 1.8.0
|
||||||
|
npm run version:check
|
||||||
|
```
|
||||||
|
|
||||||
|
This synchronizes:
|
||||||
|
|
||||||
|
- `frontend/package.json`
|
||||||
|
- `frontend/package-lock.json`
|
||||||
|
- `frontend/src-tauri/tauri.conf.json`
|
||||||
|
- `frontend/src-tauri/Cargo.toml`
|
||||||
|
- `frontend/src-tauri/Cargo.lock`
|
||||||
|
|
||||||
|
Manual edits that leave these files inconsistent fail CI.
|
||||||
|
|
||||||
|
## Promotion
|
||||||
|
|
||||||
|
1. Merge feature branches into `dev`.
|
||||||
|
2. Require the shared client/Web and macOS Desktop CI jobs to pass.
|
||||||
|
3. Promote the accepted scope from `dev` to `main`.
|
||||||
|
4. Set the release version and complete regression testing on `main`.
|
||||||
|
5. Promote `main` to `release`.
|
||||||
|
6. Create the matching `vX.Y.Z` tag on the accepted `release` commit.
|
||||||
|
7. Build both Web and Desktop artifacts from that exact tag.
|
||||||
|
|
||||||
|
Build metadata is injected by CI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_BUILD_CHANNEL=release
|
||||||
|
VITE_BUILD_COMMIT="$(git rev-parse HEAD)"
|
||||||
|
```
|
||||||
|
|
||||||
|
These values support diagnostics but do not replace the semantic version.
|
||||||
|
|
||||||
|
## Required Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
npm run version:check
|
||||||
|
npm run runtime:check
|
||||||
|
npm run type-check
|
||||||
|
npm run test:unit
|
||||||
|
npm run build
|
||||||
|
npm run desktop:build -- --bundles app
|
||||||
|
```
|
||||||
|
|
||||||
|
The Desktop build must run on macOS for the current first-phase target. A signed
|
||||||
|
or notarized public release additionally requires the Apple credentials defined
|
||||||
|
by the release owner; an unsigned internal build is not a formal distribution.
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"desktop:dev": "tauri dev",
|
"desktop:dev": "tauri dev",
|
||||||
"desktop:build": "tauri build",
|
"desktop:build": "tauri build",
|
||||||
"desktop:bundle:dmg": "tauri build -- --bundles dmg",
|
"desktop:bundle:dmg": "tauri build --bundles dmg",
|
||||||
|
"version:check": "node scripts/client-version.mjs --check",
|
||||||
|
"version:set": "node scripts/client-version.mjs --set",
|
||||||
|
"runtime:check": "node scripts/verify-runtime-boundary.mjs",
|
||||||
"test:unit": "vitest run --environment jsdom",
|
"test:unit": "vitest run --environment jsdom",
|
||||||
"type-check": "vue-tsc --noEmit",
|
"type-check": "vue-tsc --noEmit",
|
||||||
"ui:contract": "node scripts/verify-ui-contract.mjs"
|
"ui:contract": "node scripts/verify-ui-contract.mjs"
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
|
const paths = {
|
||||||
|
packageJson: new URL("../package.json", import.meta.url),
|
||||||
|
packageLock: new URL("../package-lock.json", import.meta.url),
|
||||||
|
tauriConfig: new URL("../src-tauri/tauri.conf.json", import.meta.url),
|
||||||
|
cargoToml: new URL("../src-tauri/Cargo.toml", import.meta.url),
|
||||||
|
cargoLock: new URL("../src-tauri/Cargo.lock", import.meta.url),
|
||||||
|
};
|
||||||
|
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||||
|
|
||||||
|
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
|
||||||
|
const writeJson = async (path, value) => writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||||
|
|
||||||
|
const readCargoPackageVersion = async () => {
|
||||||
|
const cargo = await readFile(paths.cargoToml, "utf8");
|
||||||
|
const packageSection = cargo.match(/\[package\]([\s\S]*?)(?=\n\[|$)/)?.[1];
|
||||||
|
const version = packageSection?.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
|
||||||
|
if (!version) throw new Error("Cannot find [package].version in src-tauri/Cargo.toml");
|
||||||
|
return version;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readCargoLockPackageVersion = async () => {
|
||||||
|
const cargoLock = await readFile(paths.cargoLock, "utf8");
|
||||||
|
const packageSection = cargoLock
|
||||||
|
.match(/\[\[package\]\]([\s\S]*?)(?=\n\[\[package\]\]|$)/g)
|
||||||
|
?.find((section) => /^name\s*=\s*"ctms-desktop"$/m.test(section));
|
||||||
|
const version = packageSection?.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
|
||||||
|
if (!version) throw new Error("Cannot find ctms-desktop version in src-tauri/Cargo.lock");
|
||||||
|
return version;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readVersions = async () => {
|
||||||
|
const [packageJson, packageLock, tauriConfig, cargoVersion, cargoLockVersion] = await Promise.all([
|
||||||
|
readJson(paths.packageJson),
|
||||||
|
readJson(paths.packageLock),
|
||||||
|
readJson(paths.tauriConfig),
|
||||||
|
readCargoPackageVersion(),
|
||||||
|
readCargoLockPackageVersion(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
"package.json": packageJson.version,
|
||||||
|
"package-lock.json": packageLock.version,
|
||||||
|
"package-lock.json root": packageLock.packages?.[""]?.version,
|
||||||
|
"tauri.conf.json": tauriConfig.version,
|
||||||
|
"Cargo.toml": cargoVersion,
|
||||||
|
"Cargo.lock": cargoLockVersion,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertVersionsMatch = async () => {
|
||||||
|
const versions = await readVersions();
|
||||||
|
const uniqueVersions = new Set(Object.values(versions));
|
||||||
|
if (uniqueVersions.size !== 1 || uniqueVersions.has(undefined)) {
|
||||||
|
const details = Object.entries(versions)
|
||||||
|
.map(([file, version]) => ` ${file}: ${version ?? "<missing>"}`)
|
||||||
|
.join("\n");
|
||||||
|
throw new Error(`Client versions are not synchronized:\n${details}`);
|
||||||
|
}
|
||||||
|
const [version] = uniqueVersions;
|
||||||
|
console.log(`Client version ${version} is synchronized.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setVersion = async (version) => {
|
||||||
|
if (!SEMVER_PATTERN.test(version)) {
|
||||||
|
throw new Error(`Invalid semantic version: ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [packageJson, packageLock, tauriConfig, tauriConfigSource, cargo, cargoLock] = await Promise.all([
|
||||||
|
readJson(paths.packageJson),
|
||||||
|
readJson(paths.packageLock),
|
||||||
|
readJson(paths.tauriConfig),
|
||||||
|
readFile(paths.tauriConfig, "utf8"),
|
||||||
|
readFile(paths.cargoToml, "utf8"),
|
||||||
|
readFile(paths.cargoLock, "utf8"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
packageJson.version = version;
|
||||||
|
packageLock.version = version;
|
||||||
|
packageLock.packages[""].version = version;
|
||||||
|
|
||||||
|
const tauriVersionPattern = /("version"\s*:\s*")[^"]+(")/;
|
||||||
|
const packageSectionPattern = /(\[package\][\s\S]*?^version\s*=\s*")[^"]+(")/m;
|
||||||
|
const lockPackagePattern =
|
||||||
|
/(\[\[package\]\]\nname\s*=\s*"ctms-desktop"\nversion\s*=\s*")[^"]+(")/m;
|
||||||
|
if (typeof tauriConfig.version !== "string" || !tauriVersionPattern.test(tauriConfigSource)) {
|
||||||
|
throw new Error("Cannot update version in src-tauri/tauri.conf.json");
|
||||||
|
}
|
||||||
|
if (!packageSectionPattern.test(cargo)) {
|
||||||
|
throw new Error("Cannot update [package].version in src-tauri/Cargo.toml");
|
||||||
|
}
|
||||||
|
if (!lockPackagePattern.test(cargoLock)) {
|
||||||
|
throw new Error("Cannot update ctms-desktop version in src-tauri/Cargo.lock");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
writeJson(paths.packageJson, packageJson),
|
||||||
|
writeJson(paths.packageLock, packageLock),
|
||||||
|
writeFile(paths.tauriConfig, tauriConfigSource.replace(tauriVersionPattern, `$1${version}$2`)),
|
||||||
|
writeFile(paths.cargoToml, cargo.replace(packageSectionPattern, `$1${version}$2`)),
|
||||||
|
writeFile(paths.cargoLock, cargoLock.replace(lockPackagePattern, `$1${version}$2`)),
|
||||||
|
]);
|
||||||
|
console.log(`Updated CTMS Web and Desktop client version to ${version} in ${frontendDir}`);
|
||||||
|
await assertVersionsMatch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [, , command = "--check", value] = process.argv;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (command === "--check") {
|
||||||
|
await assertVersionsMatch();
|
||||||
|
} else if (command === "--set" && value) {
|
||||||
|
await setVersion(value);
|
||||||
|
} else {
|
||||||
|
throw new Error("Usage: node scripts/client-version.mjs [--check | --set <semver>]");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { readdir, readFile } from "node:fs/promises";
|
||||||
|
import { extname, relative, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
|
const sourceDir = resolve(frontendDir, "src");
|
||||||
|
const runtimeDir = resolve(sourceDir, "runtime");
|
||||||
|
const sourceExtensions = new Set([".ts", ".tsx", ".vue", ".js", ".jsx"]);
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
const walk = async (directory) => {
|
||||||
|
const entries = await readdir(directory, { withFileTypes: true });
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
entries.map(async (entry) => {
|
||||||
|
const path = resolve(directory, entry.name);
|
||||||
|
return entry.isDirectory() ? walk(path) : path;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).flat();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const path of await walk(sourceDir)) {
|
||||||
|
if (!sourceExtensions.has(extname(path)) || path.startsWith(`${runtimeDir}/`)) continue;
|
||||||
|
|
||||||
|
const source = await readFile(path, "utf8");
|
||||||
|
const file = relative(frontendDir, path);
|
||||||
|
if (source.includes("@tauri-apps/") || source.includes("__TAURI")) {
|
||||||
|
violations.push(`${file}: direct Tauri access is only allowed inside src/runtime`);
|
||||||
|
}
|
||||||
|
if (/from\s+["'][^"']*\/runtime\/[^"']+["']/.test(source)) {
|
||||||
|
violations.push(`${file}: import platform behavior through src/runtime/index.ts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
console.error(`Runtime boundary violations:\n${violations.map((item) => ` ${item}`).join("\n")}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
console.log("Runtime boundary is respected.");
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { resolveApiBaseUrl } from "../runtime/apiBaseUrl";
|
import { clientRuntime, DESKTOP_SERVER_URL_CHANGED_EVENT } from "../runtime";
|
||||||
import { DESKTOP_SERVER_URL_CHANGED_EVENT } from "../runtime/desktopServerConfig";
|
|
||||||
|
|
||||||
const authClient = axios.create({
|
const authClient = axios.create({
|
||||||
baseURL: resolveApiBaseUrl(),
|
baseURL: clientRuntime.apiBaseUrl(),
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const refreshAuthClientBaseUrl = (): void => {
|
export const refreshAuthClientBaseUrl = (): void => {
|
||||||
authClient.defaults.baseURL = resolveApiBaseUrl();
|
authClient.defaults.baseURL = clientRuntime.apiBaseUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ import { ElMessage } from "element-plus";
|
|||||||
import { getToken } from "../utils/auth";
|
import { getToken } from "../utils/auth";
|
||||||
import type { ApiError } from "../types/api";
|
import type { ApiError } from "../types/api";
|
||||||
import { TEXT } from "../locales";
|
import { TEXT } from "../locales";
|
||||||
import { resolveApiBaseUrl } from "../runtime/apiBaseUrl";
|
import { clientRuntime, DESKTOP_SERVER_URL_CHANGED_EVENT } from "../runtime";
|
||||||
import { DESKTOP_SERVER_URL_CHANGED_EVENT } from "../runtime/desktopServerConfig";
|
|
||||||
|
|
||||||
const instance: AxiosInstance = axios.create({
|
const instance: AxiosInstance = axios.create({
|
||||||
baseURL: resolveApiBaseUrl(),
|
baseURL: clientRuntime.apiBaseUrl(),
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const refreshApiBaseUrl = (): void => {
|
export const refreshApiBaseUrl = (): void => {
|
||||||
instance.defaults.baseURL = resolveApiBaseUrl();
|
instance.defaults.baseURL = clientRuntime.apiBaseUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
|
|||||||
Vendored
+9
@@ -1,5 +1,14 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_BUILD_CHANNEL?: "dev" | "main" | "release" | "local";
|
||||||
|
readonly VITE_BUILD_COMMIT?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
declare module "*.vue" {
|
declare module "*.vue" {
|
||||||
import type { DefineComponent } from "vue";
|
import type { DefineComponent } from "vue";
|
||||||
const component: DefineComponent<{}, {}, any>;
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import App from "./App.vue";
|
|||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { getToken } from "./utils/auth";
|
import { getToken } from "./utils/auth";
|
||||||
import { useStudyStore } from "./store/study";
|
import { useStudyStore } from "./store/study";
|
||||||
import { shouldRequireDesktopServerUrl } from "./runtime/desktopServerConfig";
|
import { shouldRequireDesktopServerUrl } from "./runtime";
|
||||||
|
|
||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from "vue-router";
|
||||||
import { hasDesktopServerUrl, shouldRequireDesktopServerUrl } from "../runtime/desktopServerConfig";
|
import { hasDesktopServerUrl, isTauriRuntime, shouldRequireDesktopServerUrl } from "../runtime";
|
||||||
import { isTauriRuntime } from "../runtime/platform";
|
|
||||||
|
|
||||||
const DESKTOP_SETTINGS_PATH = "/desktop/server-settings";
|
const DESKTOP_SETTINGS_PATH = "/desktop/server-settings";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import packageInfo from "../../package.json";
|
||||||
|
import { clientRuntime } from "./clientRuntime";
|
||||||
|
import { getAppMetadata } from "./appMetadata";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Reflect.deleteProperty(window, "__TAURI_INTERNALS__");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("client runtime", () => {
|
||||||
|
it("reports a web runtime with desktop capabilities disabled", () => {
|
||||||
|
expect(getAppMetadata()).toMatchObject({
|
||||||
|
version: packageInfo.version,
|
||||||
|
commit: "local",
|
||||||
|
channel: "local",
|
||||||
|
clientType: "web",
|
||||||
|
platform: "web",
|
||||||
|
});
|
||||||
|
expect(clientRuntime.capabilities()).toEqual({
|
||||||
|
serverConfiguration: false,
|
||||||
|
nativeFiles: false,
|
||||||
|
systemNotifications: false,
|
||||||
|
secureSessionStorage: false,
|
||||||
|
automaticUpdates: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes only the implemented desktop capability", () => {
|
||||||
|
Object.defineProperty(window, "__TAURI_INTERNALS__", { value: {}, configurable: true });
|
||||||
|
|
||||||
|
expect(getAppMetadata().clientType).toBe("desktop");
|
||||||
|
expect(clientRuntime.capabilities().serverConfiguration).toBe(true);
|
||||||
|
expect(clientRuntime.capabilities().secureSessionStorage).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import packageInfo from "../../package.json";
|
||||||
|
import { getRuntimePlatform, isTauriRuntime, type RuntimePlatform } from "./platform";
|
||||||
|
|
||||||
|
export type ClientType = "web" | "desktop";
|
||||||
|
export type BuildChannel = "dev" | "main" | "release" | "local";
|
||||||
|
|
||||||
|
export interface AppMetadata {
|
||||||
|
version: string;
|
||||||
|
commit: string;
|
||||||
|
channel: BuildChannel;
|
||||||
|
clientType: ClientType;
|
||||||
|
platform: RuntimePlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILD_CHANNELS = new Set<BuildChannel>(["dev", "main", "release", "local"]);
|
||||||
|
|
||||||
|
const resolveBuildChannel = (value: string | undefined): BuildChannel =>
|
||||||
|
value && BUILD_CHANNELS.has(value as BuildChannel) ? (value as BuildChannel) : "local";
|
||||||
|
|
||||||
|
export const getAppMetadata = (): AppMetadata => ({
|
||||||
|
version: packageInfo.version,
|
||||||
|
commit: import.meta.env.VITE_BUILD_COMMIT || "local",
|
||||||
|
channel: resolveBuildChannel(import.meta.env.VITE_BUILD_CHANNEL),
|
||||||
|
clientType: isTauriRuntime() ? "desktop" : "web",
|
||||||
|
platform: getRuntimePlatform(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { resolveApiBaseUrl } from "./apiBaseUrl";
|
||||||
|
import { getAppMetadata } from "./appMetadata";
|
||||||
|
import { isTauriRuntime } from "./platform";
|
||||||
|
|
||||||
|
export interface RuntimeCapabilities {
|
||||||
|
serverConfiguration: boolean;
|
||||||
|
nativeFiles: boolean;
|
||||||
|
systemNotifications: boolean;
|
||||||
|
secureSessionStorage: boolean;
|
||||||
|
automaticUpdates: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientRuntime {
|
||||||
|
apiBaseUrl(): string;
|
||||||
|
metadata: typeof getAppMetadata;
|
||||||
|
capabilities(): RuntimeCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clientRuntime: ClientRuntime = {
|
||||||
|
apiBaseUrl: resolveApiBaseUrl,
|
||||||
|
metadata: getAppMetadata,
|
||||||
|
capabilities: () => ({
|
||||||
|
serverConfiguration: isTauriRuntime(),
|
||||||
|
nativeFiles: false,
|
||||||
|
systemNotifications: false,
|
||||||
|
secureSessionStorage: false,
|
||||||
|
automaticUpdates: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export { clientRuntime, type ClientRuntime, type RuntimeCapabilities } from "./clientRuntime";
|
||||||
|
export {
|
||||||
|
clearDesktopServerUrl,
|
||||||
|
DESKTOP_SERVER_URL_CHANGED_EVENT,
|
||||||
|
getDesktopServerUrl,
|
||||||
|
hasDesktopServerUrl,
|
||||||
|
normalizeDesktopServerUrl,
|
||||||
|
setDesktopServerUrl,
|
||||||
|
shouldRequireDesktopServerUrl,
|
||||||
|
} from "./desktopServerConfig";
|
||||||
|
export { getAppMetadata, type AppMetadata, type BuildChannel, type ClientType } from "./appMetadata";
|
||||||
|
export { getRuntimePlatform, isTauriRuntime, type RuntimePlatform } from "./platform";
|
||||||
@@ -37,7 +37,7 @@ import { useRouter } from "vue-router";
|
|||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { useAuthStore } from "../store/auth";
|
import { useAuthStore } from "../store/auth";
|
||||||
import { useStudyStore } from "../store/study";
|
import { useStudyStore } from "../store/study";
|
||||||
import { getDesktopServerUrl, normalizeDesktopServerUrl, setDesktopServerUrl } from "../runtime/desktopServerConfig";
|
import { getDesktopServerUrl, normalizeDesktopServerUrl, setDesktopServerUrl } from "../runtime";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ import { useAuthStore } from "../store/auth";
|
|||||||
import { useStudyStore } from "../store/study";
|
import { useStudyStore } from "../store/study";
|
||||||
import { fetchEmailDomains } from "../api/auth";
|
import { fetchEmailDomains } from "../api/auth";
|
||||||
import { TEXT, requiredMessage } from "../locales";
|
import { TEXT, requiredMessage } from "../locales";
|
||||||
import { isTauriRuntime } from "../runtime/platform";
|
import { isTauriRuntime } from "../runtime";
|
||||||
import {
|
import {
|
||||||
consumeLogoutReason,
|
consumeLogoutReason,
|
||||||
LOGOUT_REASON_AUTH_EXPIRED,
|
LOGOUT_REASON_AUTH_EXPIRED,
|
||||||
|
|||||||
Reference in New Issue
Block a user