ci工作区排障经历
CI 工作区解析、类型检查失败与 release-please 问题的排障记录
[!info] related notes
CI 工作区解析与 Release Please 排障笔记
概要
这篇笔记记录了本次 CI 中反复出现的 typecheck / test 故障、最终修复方案,以及当前仓库里 release-please 为什么会不断生成 chore(main): release 0.0.1。
这次问题的核心结论是:
typecheck和test不能靠临时.d.ts声明文件糊过去。- workspace 包解析必须显式、稳定、可重复。
- 干净的 CI 环境不能假设本地已经存在
dist/*.d.ts。 - 当前
release-please一直重复第一轮 release,根因大概率是第一次 release 没有真正落出 tag。
主要现象
Typecheck 报错
CI 中频繁出现的报错包括:
Cannot find module '@dailyuse/http-client'Could not find a declaration file for module '@dailyuse/contracts/*'Cannot find module '@dailyuse/*/domain-client'Cannot find module '@dailyuse/*/application-client'Cannot find module '@dailyuse/*/electron-entry'
这些错误主要出现在:
apps/apiapps/webapps/desktoppackages/app-vuepackages/domain-sharedpackages/governancepackages/schedule
Test 报错
CI 中测试阶段的典型错误包括:
Failed to resolve entry for package "@dailyuse/ui-vue-shadcn"Failed to resolve import "@dailyuse/contracts/*"Failed to resolve import "@dailyuse/*/infrastructure-client"Failed to resolve import "@dailyuse/contracts/authentication"
这些错误主要出现在:
app-vue:testdesktop:testweb:test
根因分析
1. 项目级 paths 覆盖了根级别名
多个项目自己的 tsconfig.json 里重写了 compilerOptions.paths。一旦项目自己定义了 paths,根级 tsconfig.base.json 里的那套别名就会被覆盖。
这会导致一些关键映射丢失,比如:
@dailyuse/contracts/result@dailyuse/contracts/shared@dailyuse/contracts/*@dailyuse/http-client
一旦这些映射缺失,TypeScript 就会退回到包导出和 dist/*.d.ts。在本地这可能暂时可用,但在干净的 CI 环境里非常脆弱。
2. packages/app-vue/tsconfig.json 的路径基准实际是错的
packages/app-vue/tsconfig.json 原本没有显式设置 baseUrl: ".",于是它继承了根级 baseUrl。但它内部写的 paths 又是按“相对当前项目目录”来写的。
这会造成类似下面的问题:
- 预期路径:
../../packages/contracts/src/modules/task/index.ts - 实际解析基准:workspace 根目录
- 错误候选路径:
D:/home/packages/contracts/…
当这条路径找不到时,TypeScript 会静默退回去找 packages/contracts/dist/…。
这就是为什么本地看起来“偶尔能过”,但 CI 的干净环境直接炸掉。
3. 干净 CI 没有足够的上游 dist/*.d.ts
有些 target 仍然依赖 workspace 包导出到 dist 的声明文件,这本身没问题,但前提是这些 target 必须依赖上游 build。
修复前:
app-vue:typecheck没有dependsOn: ["^build"]desktop:typecheck没有dependsOn: ["^build"]
所以 clean CI 下,TypeScript 会在声明文件尚未生成之前就开始解析这些包,最终报出大量“找不到模块”。
4. vitest run --project … 在 CI 下过于隐式
app-vue:test 和 desktop:test 最初用的是:
vitest run --project app-vuevitest run --project desktop
这在本地通常没问题,但在 CI 里依赖了 workspace 扫描和项目配置自动选择,不够稳定。
结果就是:
- 测试真正吃到的并不一定是项目自己的 alias 配置
- 有些情况下会退回到包入口解析
- 最终出现
@dailyuse/contracts/*、@dailyuse/ui-vue-shadcn、@dailyuse/*/infrastructure-client无法解析
5. API 源码直连后撞上了 rootDir
在 apps/api 中把 @dailyuse/contracts/* 映射回源码后,TypeScript 会把 apps/api/src 之外的文件也纳入 program。
但原来的 apps/api/tsconfig.json 里有:
"rootDir": "./src"
于是 api:typecheck 开始报 TS6059,因为 TypeScript 认为导入的 workspace 源码文件超出了 rootDir。
最终修复策略
A. 在真正需要源码联调的项目里显式补回 source alias
对于需要 source-level 类型联调的项目,不能继续放任它们退回到 dist,而应该把 workspace 源码别名显式补齐。
关键修改涉及:
apps/api/tsconfig.jsonapps/web/tsconfig.jsonapps/desktop/tsconfig.jsonpackages/governance/tsconfig.jsonpackages/domain-shared/tsconfig.typecheck.json
核心模式是:
"@dailyuse/contracts/*": [
"../../packages/contracts/src/modules/*/index.ts",
"../../packages/contracts/src/*/index.ts"
]
或者对应的包级相对路径版本。
这样做的目的就是:
- 让
result/shared这类顶层子入口回到源码 - 让
task/goal/setting/…这类模块子入口也回到源码 - 避免伪造
declare module '…'
像下面这种兜底方式不是真修复:
declare module '@dailyuse/contracts/schedule';
它只会掩盖解析问题,不会解决 CI 根因。
B. 当 rootDir 太窄时,拆分专用 typecheck 配置
对于 apps/api,最终新增了:
apps/api/tsconfig.typecheck.json
并把 apps/api/project.json 里的 typecheck 改成:
tsc --noEmit -p tsconfig.typecheck.json
这样:
- build 仍然保持原来的编译边界
- typecheck 可以合法地把 workspace 源码纳入检查
packages/domain-shared 也是同类问题,因此保留了:
packages/domain-shared/tsconfig.typecheck.json
C. 测试统一改成显式 --config
CI 下的测试入口统一改成明确指定配置文件:
apps/web/project.json->vitest run --config apps/web/vitest.config.tspackages/app-vue/project.json->vitest run --config packages/app-vue/vitest.config.tsapps/desktop/project.json->vitest run --config apps/desktop/vitest.config.ts
这样就不再依赖 Vitest workspace project 自动选择。
D. 抽出共享的 Vite/Vitest workspace alias
为了让测试阶段的解析稳定,新增并统一使用:
vite.workspace-aliases.tsvitest.shared.ts
它们负责集中生成测试阶段需要的 workspace 源码别名,包括:
@dailyuse/contracts/*@dailyuse/ui-vue-shadcn- 以及若干测试实际用到的 workspace client/source 入口
E. 让 clean CI 的 typecheck 明确依赖上游 build
为了避免 clean CI 下缺少声明文件,补充了:
packages/app-vue/project.json的typecheck.dependsOn: ["^build"]apps/desktop/project.json的typecheck.dependsOn: ["^build"]
这一步很关键,因为这两个 target 仍然会消费部分 workspace 包导出的 dist/*.d.ts。
F. 顺手修了几处被 CI 揪出来的真实代码回归
这些不是 alias 问题,而是被 CI 加严后暴露出来的真实问题:
apps/desktop/src/main/ipc/__tests__/mocks/repositories.mock.ts
- 更新了过期的 Goal mock 恢复逻辑
packages/editor/src/electron-entry/index.ts
- 收窄导出,避免 Electron 入口把 Prisma-heavy server 链路一起带进来
packages/ipc-client/src/types.tspackages/ipc-client/src/ipc-client.ts
- 调整 preload bridge 类型,与桌面端实际使用保持一致
关键修改文件
本次和 CI / 配置直接相关的关键文件包括:
apps/api/project.jsonapps/api/tsconfig.jsonapps/api/tsconfig.typecheck.jsonapps/web/project.jsonapps/web/tsconfig.jsonapps/web/vitest.config.tsapps/desktop/project.jsonapps/desktop/tsconfig.jsonapps/desktop/vitest.config.tsapps/desktop/vite.config.tspackages/app-vue/project.jsonpackages/app-vue/tsconfig.jsonpackages/app-vue/vitest.config.tspackages/domain-shared/project.jsonpackages/domain-shared/tsconfig.typecheck.jsonpackages/governance/tsconfig.jsonpackages/schedule/tsconfig.jsonvitest.shared.tsvite.workspace-aliases.ts
本次修掉的代码级问题主要集中在:
apps/desktop/src/main/ipc/__tests__/mocks/repositories.mock.tspackages/editor/src/electron-entry/index.tspackages/ipc-client/src/types.tspackages/ipc-client/src/ipc-client.ts
验证命令
本次主要使用了以下命令验证修复结果。
验证核心 typecheck
pnpm exec nx run-many -t typecheck -p api web desktop governance domain-shared --outputStyle=static
验证核心 test
pnpm exec nx run-many -t test -p app-vue web desktop --outputStyle=static -- --passWithNoTests
用接近 clean CI 的方式验证
pnpm exec nx run-many -t typecheck -p app-vue desktop --outputStyle=static --skipNxCache
这次排障沉淀出的规则
1. 不要用假的模块声明掩盖 workspace 解析问题
如果 CI 报的是:
Could not find a declaration file for module '@dailyuse/contracts/task'
正确做法通常是:
- 补回正确的 workspace alias
- 或者给当前 target 增加对上游
build的依赖
错误做法是:
- 加一个
declare module '…'
这种方式只会把错误藏起来,不会真正修复。
2. 只要项目自己定义了 paths,就必须完整审计
不要只补一个单点 alias。项目一旦定义了自己的 compilerOptions.paths,就要默认它已经覆盖了根级别名集合,必须整体审视。
3. 只要 target 依赖 package exports,就要警惕 ^build
特别是:
typecheckbuild- 任何读取 workspace 包声明输出的 target
如果会走 dist/*.d.ts,就要认真检查是否需要 dependsOn: ["^build"]。
4. 多项目 monorepo 里的 CI 测试,优先显式 --config
对 Vitest 来说,显式 --config 比 --project 更稳定,尤其是在 monorepo + 多项目 + workspace 扫描的场景里。
Release Please:为什么一直生成 chore(main): release 0.0.1
当前现象
仓库里的 release-please workflow 定义在:
它的触发条件是:
on:
push:
branches:
- main
所以只要往 main 提交,每次都会触发这个 workflow。
这件事本身是正常的。
chore(main): release 0.0.1 这个标题到底怎么来的
这个标题不是取你的最新 commit message,也不是取“开始统计时的第一个 commit 名字”。
它是 release-please 自动生成的 release PR 标题,含义是:
chore:release PR 的固定类型main:目标分支名release 0.0.1:当前它判断应该准备的版本号
也就是说:
- 它不是按你最新提交名来命名
- 也不是按最老提交名来命名
- 它根本不是从你的 commit title 里直接取标题
这个标题是 release-please 自己合成的。
类似地,分支名 release-please--branches--main 也是自动生成的,表达的是“给 main 分支准备 release PR”。
为什么它一直停在 0.0.1
当前仓库里可以看到几个关键信号:
{
".": "0.0.1"
}
- 本地
git tag --list为空 main历史里已经有:
014a43ee0 chore: release 0.0.1
这说明最有可能发生了下面这件事:
- 第一轮 release PR 的合并 commit 已经进入
main - 但真正的 tag / GitHub Release 没有成功创建出来
- 因此 release-please 没有拿到一个“完成的 release 基线”
- 于是它每次都还认为自己仍在准备第一次 release
也就是说,它反复生成 chore(main): release 0.0.1,不是因为它记住了某个老 commit 名,而是因为它没有看到真正完成的 v0.0.1。
为什么会这样
常见原因通常是:
DAILYUSE_WORKFLOWtoken 权限不足- release-please 在合并后调用 GitHub API 创建 tag/release 时失败
- workflow 只完成了“更新/创建 release PR”,但没有完成“真正创建 release/tag”
这个仓库之前还出现过一次明显的异常信号:
- release-please 返回了 GitHub 的 HTML 错误页,而不是正常 API 响应
这进一步说明问题更像是 action 侧权限/API 异常,而不是仓库代码配置语义本身出了大问题。
正常情况下应该怎样演进
如果仓库里已经真实存在:
v0.0.1
那之后再合入普通 fix: 提交,下一轮 release PR 理论上应该是:
chore(main): release 0.0.2
如果合入 feat: 提交,则通常应该变成:
chore(main): release 0.1.0
前提是当前仓库采用的是标准 Node release-please 语义,而当前配置 release-please-config.json 看起来确实是这样。
下一步该检查什么
建议按这个顺序检查:
- 确认 GitHub 仓库里到底有没有真实的
v0.0.1tag。 - 看
chore: release 0.0.1合并后的那次 release-please workflow 是否失败。 - 检查
DAILYUSE_WORKFLOWtoken 的权限是否足够。
至少要能做这些事:
- 写 contents
- 创建 / 更新 PR
- 创建 release / tag
实操恢复思路
如果 014a43ee0 就是你预期的第一次 release 合并点,一个可行的恢复路径是:
- 先修好 token / workflow 权限问题
- 在正确 commit 上补出或恢复
v0.0.1 - 再重新跑 release-please
一旦 release-please 能看到真实的 v0.0.1 基线,它通常就不会再反复维护第一轮 0.0.1 release PR。
最后总结
这次 CI 的反复报错,本质上大多数都不是“缺类型定义”,而是:
- workspace 包解析不稳定
- 项目级
paths覆盖了根配置 - clean CI 缺少必要的上游 build 产物
- test / typecheck 入口过于隐式
而 release-please 持续生成 chore(main): release 0.0.1,也不是因为它在取某个旧提交标题,而是因为当前仓库还没有建立起一个真正完成的首个 release 基线。