1
0

fix: 发布产物自包含数据库迁移资源,修复 macOS DMG 安装后无法启动

使用 go:embed 嵌入迁移 SQL 到二进制,移除 runtime.Caller 源码路径依赖,
server 和 desktop 发布产物均可在无源码目录环境下完成数据库初始化和迁移。
This commit is contained in:
2026-05-05 23:47:58 +08:00
parent 407d008e19
commit 8600a39b6c
12 changed files with 368 additions and 44 deletions

View File

@@ -104,14 +104,14 @@
### Requirement: 应用启动时迁移
应用 SHALL 在启动时执行迁移。
应用 SHALL 在启动时执行迁移,并 SHALL 使用随应用构建产物可用的打包迁移资源
#### Scenario: 自动迁移
- **WHEN** 应用启动
- **THEN** SHALL 根据 `database.driver` 配置选择对应的迁移目录和 goose dialect
- **THEN** SHALL 在 `driver=sqlite` 时使用 `migrations/sqlite/` 目录goose dialect 为 `sqlite3`
- **THEN** SHALL 在 `driver=mysql` 时使用 `migrations/mysql/` 目录goose dialect 为 `mysql`
- **THEN** SHALL 根据 `database.driver` 配置选择对应的迁移资源和 goose dialect
- **THEN** SHALL 在 `driver=sqlite` 时使用 SQLite 方言迁移资源goose dialect 为 `sqlite3`
- **THEN** SHALL 在 `driver=mysql` 时使用 MySQL 方言迁移资源goose dialect 为 `mysql`
- **THEN** SHALL 自动执行待执行的迁移
- **THEN** SHALL 在迁移失败时拒绝启动
- **THEN** SHALL 记录迁移日志
@@ -122,6 +122,15 @@
- **THEN** SHALL 检查数据库迁移版本
- **THEN** SHALL 在版本不匹配时执行迁移
#### Scenario: 发布产物无源码目录时自动迁移
- **WHEN** 应用以发布产物形式运行,且安装环境不存在仓库源码目录
- **THEN** 应用启动 SHALL 能找到迁移资源
- **THEN** SHALL 自动执行待执行迁移
- **THEN** SHALL NOT 依赖源码工作区中的 `backend/migrations/...` 文件系统路径
- **THEN** SHALL NOT 通过 `runtime.Caller` 推导构建机或源码目录作为运行时迁移目录
- **THEN** 日志中的迁移资源位置 SHALL NOT 指向构建机路径,如 `/Users/runner/work/...`
### Requirement: 连接池配置
系统 SHALL 配置数据库连接池。
@@ -157,7 +166,7 @@
### Requirement: 迁移文件管理
迁移文件 SHALL 版本化管理。
迁移文件 SHALL 版本化管理,并 SHALL 在构建发布产物时作为运行时迁移资源打包
#### Scenario: 迁移文件命名
@@ -171,3 +180,10 @@
- **WHEN** 创建迁移文件
- **THEN** SHALL 按 SQL 方言存储在对应子目录(`migrations/sqlite/``migrations/mysql/`
- **THEN** SHALL 提交到版本控制系统
#### Scenario: 迁移文件打包
- **WHEN** 构建 server 或 desktop 二进制
- **THEN** SQLite 和 MySQL 迁移文件 SHALL 被作为运行时迁移资源打包进二进制或等效发布资源
- **THEN** 应用启动迁移 SHALL 使用该打包资源
- **THEN** backend Makefile 的 goose CLI 迁移命令 MAY 继续使用文件系统中的 `migrations/<dialect>/` 目录

View File

@@ -316,3 +316,28 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
#### Scenario: 多行文本处理
- **WHEN** 对话框消息包含换行符 `\n`
- **THEN** AppleScript 正确显示多行文本
### Requirement: 桌面应用打包迁移资源
桌面应用 SHALL 在打包安装后仍能访问数据库迁移资源,并 SHALL 在首次启动时完成数据库初始化和迁移。
#### Scenario: 打包安装后首次启动执行迁移
- **WHEN** 用户从 macOS DMG 安装并首次启动 `Nex.app`
- **THEN** 系统 SHALL 初始化默认配置和数据库
- **THEN** 系统 SHALL 使用打包在应用内的迁移资源执行 SQLite 迁移
- **THEN** 系统 SHALL NOT 尝试访问构建机源码路径或仓库源码路径
- **THEN** 系统 SHALL 成功启动后端服务、托盘和管理界面
#### Scenario: .app 包含运行时必需迁移资源
- **WHEN** 执行 macOS 桌面打包脚本
- **THEN** `Nex.app` SHALL 包含启动后端服务所需的数据库迁移资源
- **THEN** 迁移资源 SHALL 随应用移动到任意安装位置后仍可用
- **THEN** `.app` SHALL NOT 依赖构建目录、源码目录或 GitHub Actions runner 路径
#### Scenario: DMG 安装后运行时资源完整
- **WHEN** 用户从 DMG 将 `Nex.app` 拖入 `/Applications` 并启动
- **THEN** 应用 SHALL 能访问数据库迁移资源
- **THEN** 应用 SHALL NOT 因 `migrations/sqlite``migrations/mysql` 文件系统目录不存在而启动失败

View File

@@ -54,13 +54,13 @@
### Requirement: 数据库初始化公共包
系统 SHALL 提供 `internal/database` 公共包,封装数据库初始化、迁移执行和连接关闭逻辑,供 `cmd/server``cmd/desktop` 共同调用。
系统 SHALL 提供 `internal/database` 公共包,封装数据库初始化、迁移执行和连接关闭逻辑,供 `cmd/server``cmd/desktop` 共同调用,并 SHALL 使用随应用构建产物打包的迁移资源执行运行时迁移
#### Scenario: 公共包 Init 函数
- **WHEN** 调用 `database.Init(cfg, logger)`
- **THEN** SHALL 根据 `cfg.Driver` 选择对应的 GORM 驱动打开连接
- **THEN** SHALL 执行对应方言的 goose 迁移
- **THEN** SHALL 使用随应用构建产物打包的迁移资源执行对应方言的 goose 迁移
- **THEN** SHALL 配置连接池参数
- **THEN** SHALL 在 `driver=sqlite` 时执行 `PRAGMA journal_mode=WAL`
- **THEN** SHALL 在 `driver=mysql` 时跳过 SQLite 专有 PRAGMA
@@ -71,11 +71,20 @@
- **WHEN** 调用 `database.Close(db)`
- **THEN** SHALL 获取底层 `sql.DB` 并关闭连接
#### Scenario: 迁移目录选择
#### Scenario: 迁移方言资源选择
- **WHEN** 执行迁移
- **THEN** SHALL 在 `driver=sqlite`使用 `migrations/sqlite/` 目录goose dialect 为 `sqlite3`
- **THEN** SHALL 在 `driver=mysql`使用 `migrations/mysql/` 目录goose dialect 为 `mysql`
- **WHEN** 执行运行时迁移
- **THEN** SHALL 在 `driver=sqlite`选择 SQLite 方言迁移资源goose dialect 为 `sqlite3`
- **THEN** SHALL 在 `driver=mysql`选择 MySQL 方言迁移资源goose dialect 为 `mysql`
- **THEN** 运行时迁移资源 SHALL 来源于打包资源而非源码目录
- **THEN** SHALL 在方言子资源解析失败时返回明确错误并拒绝启动
#### Scenario: 公共包迁移资源来源
- **WHEN** 调用 `database.Init(cfg, logger)` 且当前工作目录不是仓库根目录或 `backend/` 目录
- **THEN** SHALL 仍能解析并执行对应方言的迁移资源
- **THEN** SHALL NOT 要求当前进程工作目录位于仓库根目录或 `backend/` 目录
- **THEN** SHALL NOT 依赖 `runtime.Caller` 推导源码路径
### Requirement: MySQL 方言迁移文件

View File

@@ -220,3 +220,32 @@
- **WHEN** 任一构建 job 尝试上传 release artifact 但匹配不到目标文件
- **THEN** 该 job SHALL 失败
- **AND** Draft Release 组装 SHALL NOT 继续发布不完整资产集合
### Requirement: 发布产物运行时资源完整性
发布流水线 SHALL 确保 server 和 desktop 发布产物包含运行时启动所需的数据库迁移资源,且 SHALL NOT 依赖 CI runner 的源码路径。
#### Scenario: desktop 发布产物包含迁移资源
- **WHEN** 发布流水线构建 desktop 发布资产
- **THEN** 生成的 desktop 二进制或应用包 SHALL 包含 SQLite 和 MySQL 迁移资源
- **THEN** macOS `.app``.zip``.dmg` 安装后 SHALL 不需要仓库源码目录即可执行启动迁移
#### Scenario: server 发布产物包含迁移资源
- **WHEN** 发布流水线构建 server 发布资产
- **THEN** 生成的 server 二进制 SHALL 包含 SQLite 和 MySQL 迁移资源
- **THEN** server 发布资产 SHALL 不需要仓库源码目录即可执行启动迁移
#### Scenario: 发布产物不泄漏构建机迁移路径
- **WHEN** 发布流水线完成 server 或 desktop 构建
- **THEN** 构建产物 SHALL NOT 在运行时使用 `/Users/runner/work/.../backend/migrations/...` 作为迁移目录
- **THEN** 若检测到运行时迁移路径依赖 CI runner 源码路径,发布构建 SHALL 失败
#### Scenario: 发布构建迁移资源验证
- **WHEN** 发布流水线执行 release 构建验证
- **THEN** 验证 SHALL 覆盖迁移资源可用性
- **THEN** 验证 SHALL 覆盖安装包内应用在无源码目录环境下可解析迁移资源
- **THEN** 验证 MAY 通过 Go 测试或轻量资源自检完成,不要求启动图形托盘界面

View File

@@ -279,3 +279,27 @@
- **WHEN** mockgen 生成的 mock 就绪
- **THEN** handler 测试中的手写 mock SHALL 被替换为生成的 mock
- **THEN** 所有测试 SHALL 继续通过,行为不变
### Requirement: 运行时迁移资源测试覆盖
系统 SHALL 覆盖打包迁移资源解析和启动迁移回归场景,确保发布产物不依赖源码迁移目录。
#### Scenario: 运行时迁移资源解析测试
- **WHEN** 运行 database 包单元测试
- **THEN** SHALL 验证 `database.Init` 在当前工作目录不是仓库根目录或 `backend/` 目录时仍能执行迁移
- **THEN** SHALL 验证迁移资源不依赖 `runtime.Caller` 推导的源码路径
- **THEN** SHALL 覆盖 SQLite 方言迁移资源解析
#### Scenario: 双方言迁移资源选择测试
- **WHEN** 运行迁移资源选择相关测试
- **THEN** SHALL 验证 SQLite 方言资源可被解析
- **THEN** SHALL 验证 MySQL 方言资源可被解析
- **THEN** SHALL 验证未知或非法 driver 不会被静默映射到错误方言资源
#### Scenario: desktop 打包迁移资源测试
- **WHEN** 运行 desktop 专属测试
- **THEN** SHALL 验证 desktop 模式启动数据库迁移时使用打包迁移资源
- **THEN** SHALL 验证应用在发布产物环境中可执行迁移而不依赖源码迁移目录