Oh My CodeX CI 加速实战:用 dist 产物复用与聚焦门禁消除重复构建工作
【免费下载链接】oh-my-codexOmX - Oh My codeX: Your codex is not alone. Add hooks, agent teams, HUDs, and so much more.项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-codex
本文是一份围绕 Oh My CodeX 仓库 CI 工作流精简改造的技术指南。项目当前的持续集成(见 .github/workflows/ci.yml)曾长期在多个并行 lane 中重复执行依赖安装、TypeScript 类型检查、构建与仅报告性质的覆盖率统计,本指南完整解析这套"保留发布安全信号、削减重复劳动"的清理方案:单一 Node 20 构建产物如何供给编译后测试与覆盖率门禁、Node 22 如何以聚焦 smoke lane 保留跨运行时信心、昂贵的全量覆盖率报告为何从必需路径中移除,以及 Rust 覆盖率摘要 lane 与crates/omx-sparkshell清单路径的落地方式。读完本文,你将掌握如何在不牺牲发布安全性的前提下,设计出可审查、可维护、fail-closed 的增量式 CI 架构,并理解与之配套的本地验证命令与 PR 模板校验清单的对应关系。
一、问题背景:CI 中重复劳动从何而来
Oh My CodeX 是一个同时包含 TypeScript 源码(CLI、配置、Agent 编排、MCP 服务器、Hooks、团队模式、验证逻辑)与多个 Rust crate(omx-api、omx-explore、omx-mux、omx-runtime、omx-runtime-core、omx-sparkshell)的多语言仓库。早期 CI 的痛点在于:
- 重复依赖安装:多个测试与覆盖率 lane 各自执行
npm ci,同一份依赖树被反复安装; - 重复类型检查:TypeScript-only 检查在多个运行时矩阵上重复执行;
- 重复构建:测试 lane 与覆盖率 lane 各自执行
npm run build,dist被反复编译; - 报告型覆盖率冗余:全量 TypeScript 与全量 Rust 的覆盖率 artifact 报告(report-only)每次 CI 运行都会生成,却只用于产出报告、不充当发布门禁。
清理的目标非常明确:保留 release-safety 信号,减少重复工作。改造后的 CI 将测试、类型检查、覆盖率三种信号分别交给最合适的 lane,避免任何一条路径做两遍相同的事。该 PR 的清理理由与验证命令已记录于 docs/prs/dev-ci-slowness-cleanup.md,本节及后续内容即围绕其展开。
二、方案总览:四个关键设计决策
清理方案的核心骨架可以概括为四条原则,它们共同决定了新的 CI 拓扑:
- 一个 Node 20 构建产物喂养所有编译后 lane:
build-distjob 只构建一次dist,以ci-dist-node20artifact 形式上传,后续test、coverage-team-critical、ralph-persistence-gate、build、native-cache-integrity等 lane 直接下载复用,不再各自重建; - TypeScript-only 检查收敛到单一运行时:
typechecklane 不再使用运行时矩阵,只在 Node 20 上跑npx tsc --noEmit与npm run check:no-unused,同时保留check:no-unused门禁; - Node 22 保留为聚焦 smoke lane:跨运行时信心由
test矩阵中的node-version: 22 / lane: smoke分支承担,避免清理掉全部跨运行时信号; - 覆盖率从"报告 artifact"收敛为"必需门禁":全量 TypeScript/Rust 覆盖率 artifact 报告不再进入必需 CI 状态路径;保留两类必需信号——Rust 测试/覆盖率摘要(workspace +
crates/omx-sparkshell/Cargo.toml),以及强制的 team/state 覆盖率门禁。
这一拓扑在每个 job 上都体现为明确的if:条件与needs:依赖,且全部 job 都定义了timeout-minutes,避免卡死的 Actions 运行无限挂起(该约定被 src/verification/tests/ci-rust-gates.test.ts 逐一断言)。
三、核心机制一:单一 dist artifact 的构建与复用
3.1 构建一次:build-distjob
build-dist是编译后消费方的前置依赖,其要点见 .github/workflows/ci.yml:
- 使用 Node 20 +
cache: npm的setup-node; - 执行
npm ci --ignore-scripts(跳过生命周期脚本,保证构建环境干净); - 执行
npm run build——对应 package.json 中定义的构建脚本:先清空dist,再跑tsc,最后为dist/cli/omx.js设置可执行位; - 通过
actions/upload-artifact@v4以ci-dist-node20名称上传dist/,并设置if-no-files-found: error强制产物必须存在。
3.2 复用多次:消费方统一下载
test、coverage-team-critical、ralph-persistence-gate、build、native-cache-integrity五个 job 均以needs: [changes, build-dist]声明依赖,并通过actions/download-artifact@v8将ci-dist-node20解压到dist目录。以testjob 为例(.github/workflows/ci.yml):在npm ci之后、执行测试之前先下载预构建产物,再调用node dist/scripts/run-test-files.js <test-roots>运行分组测试。测试断言明确要求消费方 lane 中不得出现npm run build(见 src/verification/tests/ci-rust-gates.test.ts),从而把"重复编译"从结构上杜绝。
3.3 编译后脚本与源码检出模式
预构建产物的一个隐含前提是:仓库中提供"编译后"版本的执行入口,供已下载dist的 CI lane 使用。例如 src/scripts/run-compiled-ci.ts 会根据工作区特征判断运行场景:
- 通过
src/catalog/manifest.json、docs/troubleshooting.md、.github/workflows/ci.yml三个哨兵文件判定为源码检出时,依次执行verify:native-agents、verify:plugin-bundle、verify:capabilities-lock、verify:prompt-guidance、test:node与 catalog 文档校验; - 否则按已安装包模式,运行
smoke-packed-install、nested-help-routing、mcp-parity等测试并对omx各子命令(--help、version、api、sparkshell、notepad、project-memory、trace、code-intel)做 CLI 冒烟。
该脚本的OMX_AUTO_UPDATE=0、OMX_NOTIFY_FALLBACK=0、OMX_HOOK_DERIVED_SIGNALS=0环境变量注入也保证了在 CI 上下文中不会触发自动更新或通知回退等副作用。
四、核心机制二:类型检查单运行时化 + Node 22 聚焦 smoke lane
4.1typecheck:从矩阵收敛到单运行时
改造前的 TypeScript-only 检查在多个 Node 版本上重复执行;改造后typecheckjob 移除matrix:,固定在 Node 20 上执行两条命令(.github/workflows/ci.yml):
npx tsc --noEmit npm run check:no-unused其中check:no-unused对应 package.json 的tsc -p tsconfig.no-unused.json,用于检查未使用的局部变量与参数,是原文档明确要求保留的门禁。测试 src/verification/tests/ci-rust-gates.test.ts 断言typecheckjob 不含matrix:、固定node-version: 20,同时要求testjob 中存在 Node 22 的 smoke 分支,以此锁定"类型检查只跑一遍、跨运行时验证由 smoke 承担"的契约。
4.2test矩阵:Node 20 三 lane + Node 22 smoke
testjob 的矩阵(.github/workflows/ci.yml)采用include方式精确定义:
| node-version | lane | 测试目录(test-roots) | catalog-check |
|---|---|---|---|
| 20 | team-state-runtime | dist/team/__tests__、dist/state/__tests__、dist/ralph/__tests__、dist/ralplan/__tests__、dist/runtime/__tests__ | false |
| 20 | hooks-notify-platform | dist/hooks/__tests__、dist/hooks/code-simplifier/__tests__、dist/hooks/extensibility/__tests__、dist/notifications/__tests__、dist/mcp/__tests__、dist/hud/__tests__、dist/verification/__tests__、dist/openclaw/__tests__ | false |
| 20 | cli-core-rest | dist/autopilot/__tests__、dist/cli/__tests__、dist/agents/__tests__、dist/autoresearch/__tests__、dist/catalog/__tests__、dist/compat/__tests__、dist/config/__tests__、dist/modes/__tests__、dist/planning/__tests__、dist/scripts/__tests__、dist/session-history/__tests__、dist/subagents/__tests__、dist/utils/__tests__、dist/visual/__tests__ | true |
| 22 | smoke | (不指定 test-roots,走独立冒烟步骤) | false |
smoke lane 的职责由两段步骤承担(.github/workflows/ci.yml):一是执行npm run test:team:cross-rebase-smoke:compiled验证跨 rebase 冒烟;二是通过node --test直接跑一组打包/契约/explore/sparkshell/compat 测试(如packaged-script-resolution、package-bin-contract、explore、sparkshell-cli、smoke-packed-install等)。这正是原文档 reviewer notes 中"保留 Node 22 smoke lane,避免清理掉全部跨运行时信号"的落地实现。
4.3OMX_NODE_TEST_FORCE_EXIT:自托管残留句柄的兜底
对于team-state-runtime与hooks-notify-platform两个 lane,CI 通过OMX_NODE_TEST_FORCE_EXIT=1强制 Node 测试运行器在完成后退出(.github/workflows/ci.yml),规避自托管 runner 上进程/平台句柄残留导致的挂起——这是与"超时兜底"并列的又一层稳定性设计。
五、核心机制三:覆盖率从"报告 artifact"收敛为"必需门禁"
5.1 移除 report-only 全量覆盖率 job
原文档明确:"report-only full TypeScript and Rust coverage artifact jobs"从必需 CI 状态路径中移除。所谓 report-only,是指这些 job 只生成覆盖率报告 artifact,不参与任何发布/合并门禁判定,却消耗昂贵的构建与测试时间。对应测试 src/verification/tests/ci-rust-gates.test.ts 断言:工作流中不存在coverage-ts-full:job,也不存在needs.coverage-ts-full.result引用。
需要说明的是,仓库仍然保留了全量覆盖率脚本(package.json 中的coverage:ts:full/coverage:ts:full:compiled,使用c8 --all --src dist并输出到coverage/ts-full),它们作为本地贡献者按需使用的工具存在,只是不再进入每次 CI 的必需路径。
5.2 保留的必需门禁一:team/state 覆盖率
coverage-team-criticaljob(.github/workflows/ci.yml)是强制门禁而非报告:它下载ci-dist-node20产物后执行npm run coverage:team-critical:compiled,该命令对应 package.json:
c8 --all --src dist/team --src dist/state \ --include 'dist/team/**/*.js' --include 'dist/state/**/*.js' \ --exclude '**/__tests__/**' \ --reporter=text-summary --reporter=lcov --reporter=json-summary \ --report-dir coverage/team \ --check-coverage \ --lines=78 --functions=90 --branches=70 --statements=78 \ node dist/scripts/run-test-files.js dist/team/__tests__ dist/state/__tests__要点拆解:
- 作用域:仅覆盖
dist/team与dist/state(多 Agent 团队编排与状态管理,是发布安全最敏感的区域),排除测试文件自身; - 阈值:行覆盖率 ≥ 78%、函数覆盖率 ≥ 90%、分支覆盖率 ≥ 70%、语句覆盖率 ≥ 78%,由
--check-coverage强制,未达标即失败; - 报告输出:同时产出 text-summary、lcov、json-summary 三种格式,job 随后读取
coverage/team/coverage-summary.json将 Lines/Functions/Branches/Statements 汇总表写入 GitHub Step Summary,并上传team-critical-coverageartifact 供归档; - 本地对应:CONTRIBUTING.md 记录了同一门禁的本地命令
npm run coverage:team-critical,说明 CI 与本地行为一致(本地版本先npm run build再跑 compiled 版本)。
该门禁的语义在本次清理中"未变"(原文档 validation 中注明coverage:team-critical:compiled未在本地重跑,因为它在 CI 中语义不变),只是消费方改用了预构建产物。
5.3 保留的必需门禁二:Rust 测试 + 覆盖率摘要
Rust 侧不再产出昂贵的全量覆盖率 artifact 报告,取而代之的是rust-testsjob(.github/workflows/ci.yml)中的"测试 + 摘要"模式:
mkdir -p coverage/rust cargo llvm-cov --workspace --summary-only | tee coverage/rust/workspace-summary.txt cargo llvm-cov --manifest-path crates/omx-sparkshell/Cargo.toml --summary-only | tee coverage/rust/omx-sparkshell-summary.txt两点值得注意:
--summary-only意味着只输出摘要文本、不生成 lcov/JSON 等重型 artifact;测试 src/verification/tests/ci-rust-gates.test.ts 明确断言rust-testsjob 中不得出现--lcov、output-path或"Upload Rust coverage artifact"步骤;- 显式点名
crates/omx-sparkshell/Cargo.toml:由于该 crate 不在默认 workspace 成员的常规测试路径内(其 manifest 位于 crates/omx-sparkshell/Cargo.toml,声明二进制omx-sparkshell并依赖omx-mux),必须用--manifest-path单独执行覆盖,否则其测试与覆盖率信号会静默丢失。两条摘要随后以 code block 形式写入 job summary,供 PR 审查者直接查看。
六、lane 检测与 fail-closed 门禁:让"精简"可审查
精简不是一刀切,而是让每个 job 依据改动范围决定是否激活。changesjob(.github/workflows/ci.yml)用一次git diff计算出八个输出:full_suite、docs_changed、docs_only、ts_changed、rust_changed、native_changed、shared_config_changed、reason,分类规则包括:
docs/、missions/、README/CHANGELOG 等 → docs 类;src/**/*.ts|tsx|mts|cts、src/compat/fixtures/、playground/→ TS 类;crates/、Cargo.lock、Cargo.toml→ Rust 类;dist-workspace.toml、crates/omx-(api|explore|mux|runtime-core|runtime|sparkshell)/→ native 类;.github/、templates/、prompts/、skills/、plugins/、biome.json、tsconfig*.json、package(-lock).json、Cargo.*、dist-workspace.toml→ shared config 类。
fail-closed 设计是这套系统的安全底线(测试见 src/verification/tests/ci-rust-gates.test.ts):
- 命中 shared config 或存在无法归类的路径(
unknown.length > 0)时强制full_suite=true,宁可多跑不可漏检; - 无法解析 diff 提交、空 diff 等场景同样强制全量;
- 刻意不使用workflow 级的
paths:/paths-ignore:过滤触发器(.github/workflows/ci.yml),因为路径级触发器会让"required checks"进入 pending 状态而无法满足合并要求(测试断言见 src/verification/tests/ci-rust-gates.test.ts)。
最终由ci-status聚合 job(.github/workflows/ci.yml)统一判定:对每个 lane,激活时必须是success,未激活时允许success或skipped,否则报错并输出失败清单与 lane 选择原因。这套语义在 src/verification/tests/ci-rust-gates.test.ts 中被建模为 8 种组合的确定性真值表。
七、本地验证命令与 PR 模板清单的映射
7.1 PR 模板的验证基线
.github/PULL_REQUEST_TEMPLATE.md 要求普通贡献以dev为基分支(与 CONTRIBUTING.md 一致),并在 Validation 段列出三条基线命令:npm run build、npm test、omx doctor(仅当 setup/config 行为变化时)。本 PR 的清理文档刻意补充了这些基线命令与 CI lane 的对应关系,让审查者能快速核对"本地跑过什么、CI 会跑什么"。
7.2 本 PR 的验证矩阵(摘自原文档 Validation 段)
| 命令 | 作用 | 结果 |
|---|---|---|
python3+yaml.safe_load('.github/workflows/ci.yml') | 解析工作流并确认 job 元数据存在 | 通过 |
npx tsc --noEmit | TypeScript 类型检查 | 通过 |
npm run check:no-unused | no-unused 门禁(对应 CItypechecklane) | 通过 |
npm run lint | Biome lint(src) | 通过 |
npm run build | 编译dist | 通过 |
node --test dist/verification/__tests__/ci-rust-gates.test.js dist/cli/__tests__/package-bin-contract.test.js | 工作流/包契约测试,含必需 Rust 测试/覆盖率摘要门禁 | 通过 |
python3断言 job 图 | 确认 report-only 覆盖率 job 已移除、必需门禁仍在 | 通过 |
cargo llvm-cov --workspace --summary-only与cargo llvm-cov --manifest-path crates/omx-sparkshell/Cargo.toml --summary-only | Rust 测试与覆盖率摘要(含 sparkshell 清单路径) | 通过 |
npm test | 未本地执行(本 PR 专门移除全量套件冗余,改用定向测试) | 未运行 |
npm run coverage:team-critical:compiled | 未本地执行(门禁在 CI 中语义不变) | 未运行 |
omx doctor | 未执行(setup/config 行为未变) | 不需要 |
这份矩阵本身就是"可审查性"的体现:哪些命令必须本地跑、哪些可以依赖 CI、为什么可以依赖,都被显式记录。对贡献者的实操建议是:常规改动按 CONTRIBUTING.md 的本地预部署顺序执行——npm ci→npm run lint→npm run check:no-unused→npx tsc --noEmit;涉及 team/state 的改动再追加npm run coverage:team-critical。
八、Reviewer 关注点与未来维护红线
原文档的 Reviewer notes 给出了三条维护红线,与源码证据一一对应:
- 保留 Node 22 smoke lane:跨运行时信号只能被"聚焦",不能被"清除"(对应 .github/workflows/ci.yml 与测试断言 src/verification/tests/ci-rust-gates.test.ts);
- CI 中优先使用编译后覆盖率命令:
coverage-team-critical等 lane 在下载 artifact 之后应调用*:compiled变体(如coverage:team-critical:compiled),源码级覆盖率脚本(如coverage:team-critical前段的npm run build)留给本地贡献者按需使用; npm ci移除需谨慎:若未来从消费 artifact 的 job 中移除npm ci,必须确认所需 CLI/dev 依赖均已由 artifact 或显式 setup 步骤提供,否则下载的dist将缺少运行时依赖。测试 src/verification/tests/ci-rust-gates.test.ts 当前仍要求每个消费 job 保留npm ci与 npm 缓存,且不使用actions/cache@v4直接缓存node_modules——这是"干净依赖 + 预构建产物复用"两条原则的并存结构。
九、小结
本次 CI 精简以"信号不丢、重复必除"为纲:dist只构建一次并由所有编译后 lane 复用,类型检查收敛到单一 Node 20 运行时,Node 22 以 smoke lane 形式保留跨运行时信心,覆盖率从"每次必出报告"收敛为"必需门禁 + 按需本地报告",Rust 侧则以--summary-only摘要取代重型 artifact 并显式覆盖crates/omx-sparkshell清单路径。整套拓扑由changes的 lane 分类与ci-status的 fail-closed 聚合共同守护,并配有 src/verification/tests/ci-rust-gates.test.ts 中的契约测试锁定行为。如果你正在维护一个多语言、多 lane 的仓库并苦于 CI 时长与重复构建,这套"单产物复用 + 聚焦门禁 + fail-closed 兜底"的组合可以直接借鉴:先列出每个信号的消费方与必需性,再决定哪一层负责构建、哪一层负责验证,最后用契约测试把精简后的拓扑固化下来。
【免费下载链接】oh-my-codexOmX - Oh My codeX: Your codex is not alone. Add hooks, agent teams, HUDs, and so much more.项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-codex
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考