ci工作区排障经历

CI 工作区解析、类型检查失败与 release-please 问题的排障记录

#status / growing #type / debug

[!info] related notes

CI 工作区解析与 Release Please 排障笔记

概要

这篇笔记记录了本次 CI 中反复出现的 typecheck / test 故障、最终修复方案,以及当前仓库里 release-please 为什么会不断生成 chore(main): release 0.0.1

这次问题的核心结论是:

  • typechecktest 不能靠临时 .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/api
  • apps/web
  • apps/desktop
  • packages/app-vue
  • packages/domain-shared
  • packages/governance
  • packages/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:test
  • desktop:test
  • web: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:testdesktop:test 最初用的是:

  • vitest run --project app-vue
  • vitest 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.json
  • apps/web/tsconfig.json
  • apps/desktop/tsconfig.json
  • packages/governance/tsconfig.json
  • packages/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.ts
  • packages/app-vue/project.json -> vitest run --config packages/app-vue/vitest.config.ts
  • apps/desktop/project.json -> vitest run --config apps/desktop/vitest.config.ts

这样就不再依赖 Vitest workspace project 自动选择。

D. 抽出共享的 Vite/Vitest workspace alias

为了让测试阶段的解析稳定,新增并统一使用:

  • vite.workspace-aliases.ts
  • vitest.shared.ts

它们负责集中生成测试阶段需要的 workspace 源码别名,包括:

  • @dailyuse/contracts/*
  • @dailyuse/ui-vue-shadcn
  • 以及若干测试实际用到的 workspace client/source 入口

E. 让 clean CI 的 typecheck 明确依赖上游 build

为了避免 clean CI 下缺少声明文件,补充了:

  • packages/app-vue/project.jsontypecheck.dependsOn: ["^build"]
  • apps/desktop/project.jsontypecheck.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.ts
  • packages/ipc-client/src/ipc-client.ts

  - 调整 preload bridge 类型,与桌面端实际使用保持一致

关键修改文件

本次和 CI / 配置直接相关的关键文件包括:

  • apps/api/project.json
  • apps/api/tsconfig.json
  • apps/api/tsconfig.typecheck.json
  • apps/web/project.json
  • apps/web/tsconfig.json
  • apps/web/vitest.config.ts
  • apps/desktop/project.json
  • apps/desktop/tsconfig.json
  • apps/desktop/vitest.config.ts
  • apps/desktop/vite.config.ts
  • packages/app-vue/project.json
  • packages/app-vue/tsconfig.json
  • packages/app-vue/vitest.config.ts
  • packages/domain-shared/project.json
  • packages/domain-shared/tsconfig.typecheck.json
  • packages/governance/tsconfig.json
  • packages/schedule/tsconfig.json
  • vitest.shared.ts
  • vite.workspace-aliases.ts

本次修掉的代码级问题主要集中在:

  • apps/desktop/src/main/ipc/__tests__/mocks/repositories.mock.ts
  • packages/editor/src/electron-entry/index.ts
  • packages/ipc-client/src/types.ts
  • packages/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

特别是:

  • typecheck
  • build
  • 任何读取 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

这说明最有可能发生了下面这件事:

  1. 第一轮 release PR 的合并 commit 已经进入 main
  2. 但真正的 tag / GitHub Release 没有成功创建出来
  3. 因此 release-please 没有拿到一个“完成的 release 基线”
  4. 于是它每次都还认为自己仍在准备第一次 release

也就是说,它反复生成 chore(main): release 0.0.1,不是因为它记住了某个老 commit 名,而是因为它没有看到真正完成的 v0.0.1

为什么会这样

常见原因通常是:

  • DAILYUSE_WORKFLOW token 权限不足
  • 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 看起来确实是这样。

下一步该检查什么

建议按这个顺序检查:

  1. 确认 GitHub 仓库里到底有没有真实的 v0.0.1 tag。
  2. chore: release 0.0.1 合并后的那次 release-please workflow 是否失败。
  3. 检查 DAILYUSE_WORKFLOW token 的权限是否足够。

至少要能做这些事:

  • 写 contents
  • 创建 / 更新 PR
  • 创建 release / tag

实操恢复思路

如果 014a43ee0 就是你预期的第一次 release 合并点,一个可行的恢复路径是:

  1. 先修好 token / workflow 权限问题
  2. 在正确 commit 上补出或恢复 v0.0.1
  3. 再重新跑 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 基线。

创建于 2026/3/13 更新于 2026/5/27