ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

qwen-code 发布质量门禁分片改造:基于 Vitest Shard 的 Release Workflow DAG 设计与实践

qwen-code 发布质量门禁分片改造:基于 Vitest Shard 的 Release Workflow DAG 设计与实践 qwen-code 发布质量门禁分片改造基于 Vitest Shard 的 Release Workflow DAG 设计与实践【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读qwen-code 的 Release 工作流此前在单个 runner 上串行执行全部 workspace 测试质量步骤已占满发布关键路径。本文基于 2026-08-31-release-quality-sharding.md 设计文档结合 release.yml 与 release-workflow.test.js 的源码级实现完整讲解如何把单体质量 Job 拆分为“静态检查独立、构建一次、消费方并行”的 DAG并通过 Vitest 原生--shard把 workspace 测试扇出为三个分片同时保持发布门禁 fail-closed、force_skip_tests紧急开关与构建产物可重放。读完本文你将掌握一套可在任意多包仓库复用的发布质量门禁分片范式。背景质量步骤如何成为发布关键路径设计文档给出了一个具体的时间线证据Release 工作流的 workspace 测试步骤在 7 月 31 日至 8 月 29 日之间从 19:30 增长到 27:17同期执行的测试数量增长了 44%。由于该步骤在每个 workspace 上串行执行且只用一个 runner它已经主导了发布关键路径release critical path。与之对应发布门禁必须继续覆盖以下全部检查项缺一不可静态格式化formatting与 lintserve fast-path bundle 检查完整构建full build类型检查typecheck每个 workspace 的测试scripts 测试套件scripts suite。同时两个既有契约不允许被破坏force_skip_tests必须保留现有的紧急行为紧急跳过后仍允许发布发布必须继续依赖一个 fail-closed 的单一质量结果stablequalitydependency。设计文档的结论是把“一次跑完所有检查”的单体质量 Job重构成一个有明确依赖关系的有向无环图DAG。DAG 设计从单体质量 Job 到分阶段并行整体拓扑设计文档给出的拆分方案如下静态格式化与 lint 检查独立于构建运行serve fast-path 检查与完整构建只跑一次并把生成的dist目录打包进单个 artifact类型检查、scripts 套件、三个 workspace 测试分片共同消费该构建 artifactworkspace 分片使用 Vitest 原生--shardindex/count选项通过一个可复用的根 package 脚本暴露一个轻量的聚合 Job检查所有结果并保留发布与失败通知所依赖的稳定quality依赖。在实际的 release.yml 中这一拓扑落实为以下 Job 集合源码行号见文件Job角色needs关键行为prepare元数据准备—解析 ref 为不可变 commit SHA输出release_sha等release.ymlquality_static静态格式化 lintprepare独立于构建可调超时release.ymlquality_buildfast-path 检查 完整构建prepare唯一构建生产者产出release-quality-buildartifactrelease.ymlquality_typecheck类型检查preparequality_build下载并解包构建产物release.ymlworkspace_tests3 个 Vitest 分片preparequality_buildmatrixshard: [1, 2, 3]release.ymlquality_scriptsscripts 测试套件preparequality_build同样消费构建产物release.ymlquality聚合门禁上述全部fail-closed 汇总release.yml一个值得注意的实现细节quality_typecheck、workspace_tests、quality_scripts三个消费方必须显式地在needs中声明quality_build。这不仅是拓扑要求也是时序保证——测试用例明确断言“下载只在needs边存在时才等待上传完成缺少该边会导致消费者与生产者竞争”见 release-workflow.test.js。构建一次产物打包与消费契约quality_build是唯一执行完整构建的 Job其步骤顺序在测试中被严格钉死release-workflow.test.jsCheck Serve Fast Path Bundle执行npm run check:serve-fast-path-bundle这一步会把 esbuild bundle 输出到仓库根目录dist——注释明确说明“这一步物化了下面要打包的仓库根 dist”release.ymlBuild Projectnpm run buildPack Build Outputs调用提取出的脚本run-release-step.sh pack-build打包dist与packages/web-templates/src/generated两个路径Upload Build Outputs上传 artifactrelease-quality-build。上传参数中蕴含两个重要的可重放性设计release.ymlretention-days: 3设计文档要求产物保留三天。原因在测试注释中写得很清楚——Re-run failed jobs超过一天后重跑时已成功的生产者quality_build不会被重跑其消费者仍必须能下载到原始 artifact若保留期只有 1 天artifact 已过期所有消费者都会下载失败release-workflow.test.jsoverwrite: true上传显式允许覆盖同名 artifact从而让Re-run all jobs场景也可恢复release-workflow.test.js。打包脚本还有一个防静默欠打包的校验find必须剪除嵌套依赖的node_modules目录只让真正的构建产物进入压缩包并且通过printf打印打包路径、用${#build_paths[]} -gt 2断言路径数任何一步缺失都会让 tar 只携带硬编码路径而静默出错release-workflow.test.js。三个消费者quality_typecheck、workspace_tests、quality_scripts都必须经历“下载 → 解包”两步解包命令为tar -xzf ${RUNNER_TEMP}/release-quality-build/release-build.tgz且解包步骤必须位于下载之后release-workflow.test.js。Workspace 分片复用根脚本 Vitest 原生 Shard根脚本与 workspace 自动发现设计文档强调“Workspace sharding 使用 Vitest 原生--shardindex/count选项通过一个可复用的根 package 脚本实现”并且“每个带有test:ci脚本的 workspace 仍会被 npm 自动选中因此新增 workspace 不会静默地漏出发布门禁”。根脚本定义在 package.jsontest:release: npm run test:release:workspaces npm run test:scripts, test:release:workspaces: cross-env NODE_OPTIONS\--max-old-space-size3072\ npm run test:ci --workspaces --if-present -- --coverage.enabledfalseWorkspace 的自动发现由 scripts/workspaces.js 中的getTestCiWorkspacePackageJsonPaths(root)完成它读取根package.json的workspaces字段展开 glob 后过滤出定义了test:ci脚本的 workspace。这一解析函数同时被发布门禁测试和单元测试消费见 release-workflow.test.js 的注释确保“分片完整性钉死”与“零测试棘轮”作用于同一 workspace 集合。分片执行脚本的完整实现分片由提取出的 run-release-workspace-tests.sh 驱动其核心命令为npm run test:release:workspaces -- --shard${shard}/3 --passWithNoTests ${retry_arg[]} 21 | tee ${log} || { # 失败分类与 transport timeout 放行逻辑 }关键点逐一拆解--shard${shard}/3Vitest 原生分片选项index/count语义。workspace_testsJob 的 matrix 为shard: [1, 2, 3]fail-fast: falserelease.yml因此三个分片都会被完整执行、各自报告结果--passWithNoTests让没有分到任何测试文件的小 workspace 也能以退出码 0 参与——这是设计文档明确要求的--retry的语义默认通过VITEST_RETRY环境变量传入取值默认2即vars.QWEN_RELEASE_VITEST_RETRY || 2。测试用例特别强调off必须省略该参数而不是传--retry0否则会覆盖 workspace 自身配置级的重试release-workflow.test.jsset -eo pipefail$?取的是 npm 的退出码保证 fail-closed 与本地执行、Actions 执行使用同一个 shellrelease-workflow.test.js。针对 Vitest transport timeout 的放行守卫这是脚本中最精妙的部分对应设计文档“失败语义”章节的实现细节。run-release-workspace-tests.sh通过tee把完整日志落盘当命令失败时按以下顺序分类日志中出现^[[:space:]]*FAIL→ 是真实测试失败不加注释直接保持退出码日志中出现[vitest-worker]: Timeout callingVitest worker RPC 超时→ 进入“transport timeout 放行”分支其他情况 → 打印::error提示日志中既无 FAIL 行也无 transport timeout。transport timeout 放行分支不是简单看错误头而是比对 Vitest 自己打印的未处理错误计数Errors N errors与日志中 transport 消息的条数只有满足以下全部条件才放行为通过run-release-workspace-tests.sherrors$(awk /^[[:space:]]*Errors[[:space:]][0-9] errors?$/ { total $2 } END { print total 0 } ${log}) timeouts$(grep -cE \[vitest-worker\]: Timeout calling ${log} || true) if [ ${status} -lt 128 ] \ grep -qE ^[[:space:]]*Tests[[:space:]][0-9] passed ${log} \ ! grep -qE ^[[:space:]]*(Tests|Test Files)[[:space:]][0-9] failed ${log} \ [ ${errors} -eq ${timeouts} ]; then # 打印 ::warning 并 exit 0设计文档没有展开这一层但它正是“失败语义”能被安全执行的关键--retry只能重跑失败的测试无法覆盖 worker RPC 超时导致的未处理错误这类错误会直接让整个 run 失败。测试文件用 20 组日志形态逐一验证了放行与拒绝边界例如“真实测试失败必须保持退出码”“transport 计数与错误计数不一致必须拒绝放行”“测试自己打印的Timeout calling字样不得误判为 transport 死亡”等release-workflow.test.js。分片完整性的双保险--passWithNoTests有一个副作用如果某个 workspace 的测试文件全部丢失三个分片都会以 0 退出而保持绿色。设计文档要求在 scripts 套件中加一道“棘轮”ratchet来兜底。测试用例discovers at least one test file in every test:ci workspace实现此保障它用与 Vitest 默认 include 模式一致的 glob**/*.{test,spec}.?(c|m)[jt]s?(x)排除node_modules、dist、e2e扫描每个test:ciworkspace发现任何 workspace 匹配不到测试文件即断言失败release-workflow.test.js。由于该测试运行在发布门禁的quality_scriptslane 上测试发现为空的 workspace 会再次阻断发布弥补--passWithNoTests带来的盲区。失败语义fail-closed 聚合与紧急开关聚合 Job 的 fail-closed 逻辑quality聚合 Job 的if条件与结果校验共同构成发布门禁if: ${{ !cancelled() needs.prepare.result success github.event.inputs.force_skip_tests ! true }}测试注释解释了为什么用!cancelled()而非always()被取消的运行会让聚合 Job 保持 skipped从而notify_failure的needs.quality.result failure门保持关闭——这是为了让操作员主动中止的运行不触发失败通知而失败完成的组件仍会运行聚合 Job 并将其判为失败release-workflow.test.js。prepare门的存在则是为了避免 fork dispatch 上五个 skipped lane 被!cancelled()误判为 quality 失败、进而错误打开notify_failure。结果校验步骤通过环境变量接收五个组件的 result任何一个不为success即输出::error::A release quality check did not succeed.并退出 1release.yml。测试对整个结果映射做了全量钉死release-workflow.test.js删除循环中的任何一项或把某个环境变量错映射到其他 Job 的 result都必须在此失败而不能让失败的组件发布出去。matrix 不 fail-fast 的语义设计文档指出“matrix 不 fail-fast因此所有分片都会报告各自的结果”。fail-fast: false保证三个 workspace 分片即使某一个失败其余分片仍会跑完并报告聚合 Job 在所有分片完成无论成功、失败还是取消之后运行且任一组件未成功即整体失败。publishJob 只依赖quality而不直接依赖workspace_testsrelease-workflow.test.js从而保持“一个 fail-closed 质量结果”的稳定接口。force_skip_tests 紧急开关force_skip_tests是workflow_dispatch的布尔输入描述为“跳过发布验证 Jobquality、integration_none、integration_docker允许在缺少它们的情况下继续发布正式发布应运行验证”release.yml。设计文档要求它保持“现有紧急行为”实现上有两个层次聚合层qualityJob 的if中排除force_skip_tests true的分发组件层quality_static、quality_build、quality_typecheck、workspace_tests、quality_scripts五个组件各自在if中声明github.event.inputs.force_skip_tests ! true。测试明确断言仅靠聚合层门是不够的——紧急分发时每个组件 lane 也必须被直接跳过否则一个红色的 lane 仍然会运行并阻塞这个本应由开关放行的发布release-workflow.test.js。检出策略与不可变 commit 解析验证 Job 使用浅检出设计文档规定验证 Job 使用浅检出shallow checkout因为它们只测试一个选定的 ref不需要检查 tag 或历史元数据准备与发布则保留完整历史。测试uses shallow history only for validation jobs将这一契约钉死release-workflow.test.jsprepare与publishfetch-depth: 0完整历史quality_static、quality_build、quality_typecheck、workspace_tests、quality_scripts、integration_none、integration_dockerfetch-depth: 1。prepare 解析不可变 commit SHA设计文档强调“preparation job 把选定的 ref 解析为一个不可变 commit SHA每个验证检出与发布都使用该 SHA防止移动分支把不同 commit 的源码与构建产物混在一起。”实现路径为prepare输出release_sha: ${{ steps.source.outputs.release_sha }}而source步骤执行run-release-step.sh resolve-commit脚本内含git rev-parse HEAD随后所有验证 Job 与publish的 checkout 都使用ref: ${{ needs.prepare.outputs.release_sha }}且每个消费方都必须在needs中声明prepare。测试注释点出这个时序陷阱ref 表达式只有在消费者声明了prepare依赖时才被解析否则会渲染为空字符串checkout 静默回退到事件 ref即 checkout 时刻的移动分支 tiprelease-workflow.test.js。可调运行时参数不经过 PR 的运维旋钮分片与质量 lane 的超时不是硬编码的而是通过 GitHub Actionsvars暴露的运维旋钮测试逐一定死了默认值与表达式形态变量作用 Job默认值表达式QWEN_RELEASE_WORKSPACE_TIMEOUT_MINUTESworkspace_tests45fromJSON(vars.QWEN_RELEASE_WORKSPACE_TIMEOUT_MINUTES || 45)release.ymlQWEN_RELEASE_STATIC_TIMEOUT_MINUTESquality_static60fromJSON(... || 60)release.ymlQWEN_RELEASE_BUILD_TIMEOUT_MINUTESquality_build45fromJSON(... || 45)release.ymlQWEN_RELEASE_VITEST_RETRYworkspace_tests2vars.QWEN_RELEASE_VITEST_RETRY || 2release.ymlQWEN_CI_VITEST_MAX_WORKERSworkspace/scripts 测试4仅自托管 runnervars.QWEN_CI_VITEST_MAX_WORKERS || 4release.yml测试注释还记录了这些旋钮的历史教训分片运行时长跟随宿主机负载而非测试套件本身变化同一第三个分片在空闲宿主机上 6.7 分钟、在争用宿主机上 36 分钟超时边界上的 45 分钟曾误杀过全部测试都绿的分片release-workflow.test.js。fromJSON而非裸变量是因为timeout-minutes需要数字类型未设置的变量必须回退到默认值而不能渲染为空字符串。此外每个可调 lane 都有一个Report timeout budget步骤通过::notice打印它解析到的预算及变量是否到达 Job避免“拼错的变量名渲染为空、lane 静默按默认值死亡且日志无从对账”release-workflow.test.js。预期效果与上线验证设计文档给出的预期收益之前 27 分钟的 workspace 阶段在约 8 分钟构建之后变成三个大致均衡的分片基于近期计时质量关键路径应从约 42 分钟降到约 20–25 分钟。同时文档给出了审慎的验证要求首次非发布分发first non-publishing dispatch应对比测试文件数与测试总数与单体基线的差异再依赖该计时估计。这体现了“先验证、后信任”的工程纪律——分片均衡性取决于测试文件在三个分片间的分配不能仅凭估算就宣称优化成立。后续工作Daily CI 的分片化设计文档在“Daily CI follow-up”中划定了边界daily CI 与 release 工作流在覆盖率coverage、JUnit 报告、fork 权限、required-check 契约上各不相同不应共享同一次改动。它应当在独立的变更中采用相同的 build-once 与 Vitest-shard 原语每个分片的覆盖率产物在现有 report Job 之前合并使用稳定的聚合检查名stable aggregate check name。单独推进的理由是缩小爆炸半径limits the blast radius并让 runner 容量影响可度量makes runner-capacity effects measurable——即把“发布流程加速”与“日常 CI 改造”两个变量解耦避免一次改动引入多个不可归因的回归源。参考实现索引以下仓库文件与本文各节直接对应可继续深入阅读设计文档docs/design/2026-08-31-release-quality-sharding.md工作流定义.github/workflows/release.yml分片执行脚本.github/scripts/run-release-workspace-tests.sh工作流契约测试分片、聚合、产物、超时旋钮等scripts/tests/release-workflow.test.jsworkspace 自动发现scripts/workspaces.js根脚本定义package.json【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表