Rust 仓库 CI 工具 citool 完全解析:任务矩阵计算、try-job 调度与本地复现
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
导读
citool是 Rust 官方仓库rust-lang/rust中用于驱动 GitHub Actions CI 的核心 Rust 工具,它以极简的方式解决了两个关键问题:根据当前场景(Pull Request、@bors try试运行、@bors r+合并尝试、main 分支推送)动态计算应该执行哪些 CI 任务,以及把(部分)CI 任务在本地完整复现。本文以 src/ci/citool/README.md 为切入点,深入其源码,覆盖运行类型判定、jobs.yml配置格式、try-job 提交信息语法、任务矩阵输出、数据库校验、本地执行与指标分析等全部环节,读完即可理解 rust-lang/rust 的 CI 是如何做"动态裁剪"的,并掌握在本地复现任意 Linux CI 任务的方法。
citool 是什么:README 定义的两大职责
官方 README 用两句话定义了它的全部职责(原文):
This is a simple Rust script that determines which jobs should be executed on CI based on the situation (pull request, try job, merge attempt). It also provides a simple way of executing (some) CI jobs locally.
翻译过来即:
- CI 任务矩阵计算:根据触发场景(PR 推送、try job、merge attempt)决定本次 CI 应该执行哪些任务;
- 本地任务执行:提供在本地运行(部分)CI 任务的简单途径。
"simple" 只是谦虚的说法——从源码看,它承担了 CI 定义(jobs.yml)的解析、校验、展开、环境变量合并、GitHub API 查询、指标下载与 Datadog 上报等大量工作。它被设计为一个独立的 Cargo 工作区(见 Cargo.toml 中的[workspace]注释:citool 独立于仓库其他 crate,避免被误并入根工作区),使用edition = "2024",依赖clap(CLI 解析)、serde_yaml(配置解析)、askama(HTML 模板)、ureq(HTTP 客户端)等。
源码结构一览
citool 的全部代码位于 src/ci/citool/src 目录:
| 模块 | 职责 |
|---|---|
| main.rs | CLI 入口与子命令分发、GitHub 上下文加载、本地执行流程 |
| jobs.rs | jobs.yml数据模型、加载/校验、任务矩阵计算核心 |
| analysis.rs | 构建步骤耗时、测试套件结果、测试差异的报告输出 |
| metrics.rs | 从 CI 制品站点下载 / 缓存metrics-*.json |
| github.rs | 查询 GitHub Actions 工作流任务信息(耗时、摘要链接) |
| test_dashboard.rs | 生成测试结果 HTML 仪表盘(配合 templates 下的 askama 模板) |
| cpu_usage.rs | 解析collect-cpu-stats.sh生成的 CPU 使用 CSV |
| datadog.rs | 向 Datadog 上报 CI 自定义指标 |
| utils.rs | 环境变量读取、子模块初始化等工具函数 |
场景判定:RunType 与 GitHub 上下文的映射
任务矩阵计算的第一个步骤是判定当前 CI 属于哪种运行类型。GitHubContext(main.rs)从环境变量GITHUB_EVENT_NAME、GITHUB_REF和(仅 push 事件)COMMIT_MESSAGE中读取上下文,get_run_type()(main.rs)按如下规则映射:
| 事件 | 分支引用(GITHUB_REF) | 运行类型 | 含义 |
|---|---|---|---|
pull_request | 任意 | PullRequest | PR 推送触发的常规 CI |
push | refs/heads/automation/bors/try | TryJob | @bors try触发的试运行(可携带自定义任务模式) |
push | refs/heads/automation/bors/try-perf | TryJob(无自定义模式、不设限) | 性能测试专用试运行 |
push | refs/heads/automation/bors/auto | AutoJob | @bors r+后的合并尝试 |
push | refs/heads/main | MainJob | main 分支推送,返回空矩阵,仅用于共享 GitHub Actions 缓存 |
| 其他 | 其他 | None | 无法判定,直接报错退出 |
其中RunType枚举定义在 jobs.rs。值得注意MainJob的设计:它故意不执行任何任务,但会在 CI 上触发一次工作流,从而"预热"缓存,供后续任务共享。
TryJob变体携带两个可选字段:job_patterns(自定义任务 glob 模式列表)与nolimit(是否跳过最多 20 个自定义任务的限制),二者均从提交信息中解析而来。
从提交信息解析 try-job 模式与 nolimit
get_try_job_metadata()(main.rs)逐行扫描提交信息,支持两种语法:
1. 自定义任务模式—— 形如:
try-job: <job-pattern>或(为了避免 GitHub 把 glob 当作 Markdown 渲染,可以用反引号包裹):
try-job: `<job-pattern>`反引号会在解析时被trim_matches('')` 剥离。
2. 取消任务数量限制—— 提交信息中出现try-nolimit行即可。
例如(取自 tests/jobs.rs 的测试用例):
This is a test PR try-job: test-aarch64-gnu try-job: dist-i686-msvcjobs.yml:CI 任务的单一数据源
所有任务的声明集中在 src/ci/github-actions/jobs.yml(约 889 行),citool 在 CI 中动态读取它。顶层结构包含三个部分:
runners:定义可复用的 YAML 锚点(&base-job、&job-linux-4c、&job-macos-15、&job-windows等),声明 runner 的os(如ubuntu-24.04、macos-15、windows-2025、AWS EC2 实例等)与free_disk等公共属性;envs:按运行类型(pr/try/auto)声明共享环境变量,例如pr注入PR_CI_JOB: 1,production锚点注入DEPLOY_BUCKET、AWS 密钥 ID 与TOOLSTATE_PUBLISH;jobs:声明dist-*(发布构建)任务,随后是pr:、try:、auto:、optional:四个任务列表。
Job 数据模型
每个任务对应 Job 结构体(serde(deny_unknown_fields)严格反序列化):
| 字段 | 类型 | 说明 |
|---|---|---|
name | String | 任务名,如test-pr-check-1、dist-x86_64-linux |
os | String | 执行任务的 runner 标签 |
env | BTreeMap<String, Value> | 任务级环境变量(值支持字符串/布尔/数字) |
only_on_channel | Option<String> | 仅在指定 channel 执行(如beta),否则被skip_jobs过滤 |
continue_on_error | Option<bool> | 失败时不取消整个工作流 |
free_disk | Option<bool> | 是否先释放磁盘空间 |
doc_url | Option<String> | 排障文档链接 |
codebuild | Option<bool> | 是否在 AWS CodeBuild 上执行 |
JobDatabase(jobs.rs)聚合pr_jobs、try_jobs、auto_jobs、optional_jobs四个列表,以及按运行类型共享的envs。加载时有两个值得注意的细节:
- YAML merge key 展开:
load_job_db()(jobs.rs)先对serde_yaml::Value连续两次调用apply_merge(),因为 serde_yaml 无法直接处理<<合并键,两次调用可展开最多嵌套两层的合并; - 默认镜像名:
Job::image()(jobs.rs)中,Docker 镜像名默认等于任务名,但可被env.IMAGE覆盖。
任务矩阵计算流程
calculate_jobs()(jobs.rs)按运行类型选择任务来源与共享环境,然后依次执行四步变换:
- 选择任务集:
PullRequest→pr_jobs(前缀PR,注入pr_env);TryJob→ 有自定义模式则按 glob 展开auto_jobs+optional_jobs,否则用try_jobs;AutoJob→auto_jobs(注入auto_env); - 替换 GitHub 上下文变量:
substitute_github_vars()(jobs.rs)把os中的$github.run_id、$github.run_attempt替换为真实环境变量值——这正是jobs.yml中 AWS EC2 实例名(如ec2-x86_64ami-m8a.2xlarge-x64-linux-$github.run_id-$github.run_attempt)得以唯一化的原因; - channel 过滤:
skip_jobs()(jobs.rs)剔除only_on_channel与当前 channel 不匹配的任务; - 合并环境变量并输出:共享环境与任务环境合并(任务级优先),生成带
full_name(如auto - dist-x86_64-linux,即{run_type} - {job_name})的GithubActionsJob。
此外还有一个隐藏的优化:默认@bors try(无自定义模式)时,会给任务注入DIST_TRY_BUILD=1(jobs.rs)。该变量告诉opt-dist跳过某些构建步骤和测试,让 try 构建更快完成——jobs.yml顶部注释也说明了这一点。
try-job 模式的 glob 展开与数量限制
当提交信息指定了自定义模式时(jobs.rs):
- 用
glob_match::glob_match(如dist-*匹配所有dist-前缀任务)在auto_jobs+optional_jobs中展开,结果去重; - 未匹配到任何任务的模式会直接报错:"Patterns
xxxdid not match any auto jobs"; - 展开后的任务数超过
MAX_TRY_JOBS_COUNT = 20(jobs.rs)且未指定nolimit时,报错并提示使用@bors try jobs=... nolimit。
矩阵输出
calculate_job_matrix()(jobs.rs)把结果按任务名排序后,以jobs=<JSON>与run_type=<pr|try|auto|main>两行输出到 stdout,供 GitHub Actions 的后续步骤消费;同时向 stderr 打印jobs=与run_type=便于排障。空矩阵(非MainJob)会触发错误:"Computed job list is empty"。
数据完整性校验:防止 PR 红灯被合并进 main
citool 在加载数据库后执行两轮校验,这是它最体现工程严谨性的部分。
1. PR 任务自动注册为 Auto 任务——register_pr_jobs_as_auto_jobs()(jobs.rs):为保证"PR 任务必须是 Auto 任务的子集",所有未在auto列表出现的 PR 任务会被自动克隆进auto_jobs,并将continue_on_error强制覆写为false(fail-fast,避免浪费 Auto CI 资源)。jobs.yml中pr:段落注释也明确说明:自动注册时以continue_on_error=false复制,显式覆盖时则逐字段校验等价性。
2. 数据库校验——validate_job_database()(jobs.rs)包含四类约束:
- 四个任务列表中都不允许出现重名任务;
- PR 任务与同名 Auto 任务必须等价(除
continue_on_error和env外的所有字段一致),例如test-x86_64-gnu-tools在 Auto 环境下会多出DEPLOY_TOOLSTATES_JSON环境变量,这属于允许的 carve-out; - Auto 任务若
continue_on_error: true,则名字必须以optional-开头,否则报错; - 所有 Auto 任务名必须以
test-或dist-开头(允许optional-前缀),保证命名约定统一。
这些约束从机制上杜绝了"PR 只跑部分任务、红灯却合入 main,导致后续所有 PR 全红"的隐患(见 jobs.rs 的注释)。
本地执行 CI 任务:run-local 子命令
这是 README 提到的第二大能力。run-local(main.rs)接收任务名和可选的--type(auto或pr,默认auto),在本地复现 CI 任务:
cargo run --manifest-path src/ci/citool/Cargo.toml run-local x86_64-gnu-llvm-21-1其实现run_workflow_locally()(main.rs)的关键步骤:
- 从
auto_jobs或pr_jobs中按名字查找任务(find_linux_job只允许 Linux 任务,否则报错并列出可用任务清单); - 复刻
setup-environment.sh的行为:任务名以dist-开头时注入DEPLOY=1(以-alt结尾则注入DEPLOY_ALT=1); - 把任务
env中布尔/数字/字符串类型的环境变量注入子进程; - 若
src/llvm-project/子模块为空目录,则自动执行git submodule update --init(init_submodule_if_needed,utils.rs); - 最终调用
src/ci/docker/run.sh <image>启动对应的 Docker 镜像执行任务。
与 Docker 执行器的配合
src/ci/docker/README.md 补充了本地执行的关键细节:
- 镜像与任务名的关系:一个 Docker 镜像可被多个任务复用,任务名才是关键;镜像名取自
env.IMAGE或默认等于任务名; - 输出目录:本地执行时构建产物输出到仓库根目录的
obj/<image-name>/(CI 中则直接输出到obj/),这是为了避免多个 Docker 镜像交替使用时产生奇怪的链接器错误; - 需要 DOCKER_SCRIPT 的复杂工作流:对于
x86_64-gnu-llvm-21-N这类任务,需要从 jobs.yml 中查得该任务执行的脚本,再手动传入,例如:DOCKER_SCRIPT=x86_64-gnu-llvm3.sh ./src/ci/docker/run.sh x86_64-gnu-llvm-21
辅助子命令:指标、仪表盘与可观测性
除矩阵计算与本地执行外,citool 还提供四个面向 CI 指标分析的子命令(CLI 定义见 main.rs):
| 子命令 | 作用 |
|---|---|
postprocess-metrics <metrics_path> [--parent <sha> --job-name <name>] | 处理 bootstrap 生成的metrics.json,输出构建步骤耗时表与测试结果汇总;若提供--parent与--job-name,还会下载父提交的指标做 diff(见 analysis.rs 的format_build_step_diffs与report_test_diffs,含 Markdown 表格形式的步骤耗时变化、最多 100 条测试差异、按 job 分组索引、按 stage 分组等) |
upload-build-metrics <cpu_usage_csv> | 解析collect-cpu-stats.sh生成的 CSV(每行两列,第二列为 idle 值,换算为100 - idle得到 CPU 使用率),计算平均值后上传 Datadog 指标avg-cpu-usage(见 cpu_usage.rs 与 datadog.rs) |
post-merge-report <parent> <current> | 对比父/当前提交,输出测试差异、任务耗时 Top10 变化(output_largest_job_duration_changes),并提示生成测试仪表盘的命令 |
test-dashboard <current> --output-dir <dir> | 下载该提交所有 auto 任务的指标,生成包含全部 compiletest 测试结果的 HTML 仪表盘(test_dashboard.rs 借助 askama 模板按目录层级递归组织测试分组,并为每个测试标注"在哪些任务上通过"的 jobset 编号) |
其中指标下载(metrics.rs)从https://ci-artifacts.rust-lang.org/rustc-builds/{sha}/metrics-{job_name}.json拉取(-alt任务走-alt制品桶),并在.citool-cache/{sha}/{job_name}.json建立本地缓存以加速重复执行;github.rs 则通过 GitHub API 查询工作流任务的started_at/completed_at以计算耗时,并对 workflow run id 做内存缓存以减少 API 调用。post-merge-report的注释明确提示该报告主要面向 t-infra 成员,用于排查 CI 变慢,因为任务耗时会受 runner 实例、系统噪声、缓存失效等因素干扰。
测试保障:快照测试锁定矩阵行为
citool 的矩阵计算行为由 tests/jobs.rs 中的 insta 快照测试锁定。测试通过cargo run -q calculate-job-matrix --jobs-file src/ci/citool/tests/test-jobs.yml并注入GITHUB_EVENT_NAME、COMMIT_MESSAGE、GITHUB_REF等环境变量(env_clear()清空其余环境),分别验证:
auto(push 到automation/bors/auto):PR 任务被自动注册进 auto 列表且continue_on_error=false,test-tidy等任务带doc_url;try(push 到automation/bors/try):默认 try 任务被注入DIST_TRY_BUILD=1;- 自定义 try-job:两条
try-job:模式精确展开为对应任务; pr(pull_request事件):仅执行 PR 任务,注入PR_CI_JOB=1;main:输出空矩阵。
这些快照同时验证了环境变量合并、full_name前缀(auto -/try -/PR -)、free_disk、continue_on_error等字段的序列化结果,是理解矩阵输出的最佳活文档。
总结
citool 虽然 README 只有两句话,但它实际是 rust-lang/rust CI 体系的"大脑":以 jobs.yml 为单一数据源,通过运行类型判定(PR / try / auto / main)、glob 模式展开、环境变量分层合并与严格的数据库校验,动态生成每次 CI 的任务矩阵;同时又以run-local把 Docker 化的 CI 任务平移到本地,配合指标分析、测试仪表盘与 Datadog 上报,构成一套完整、可观测、可本地复现的 CI 工作流。对任何想理解大型开源项目 CI 工程化实践的开发者而言,src/ci/citool 是一个值得通读的范本。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考