Rust 仓库 CI 工具 citool 完全解析:任务矩阵计算、try-job 调度与本地复现
2026/9/11 6:44:23 网站建设 项目流程

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.

翻译过来即:

  1. CI 任务矩阵计算:根据触发场景(PR 推送、try job、merge attempt)决定本次 CI 应该执行哪些任务;
  2. 本地任务执行:提供在本地运行(部分)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.rsCLI 入口与子命令分发、GitHub 上下文加载、本地执行流程
jobs.rsjobs.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_NAMEGITHUB_REF和(仅 push 事件)COMMIT_MESSAGE中读取上下文,get_run_type()(main.rs)按如下规则映射:

事件分支引用(GITHUB_REF运行类型含义
pull_request任意PullRequestPR 推送触发的常规 CI
pushrefs/heads/automation/bors/tryTryJob@bors try触发的试运行(可携带自定义任务模式)
pushrefs/heads/automation/bors/try-perfTryJob(无自定义模式、不设限)性能测试专用试运行
pushrefs/heads/automation/bors/autoAutoJob@bors r+后的合并尝试
pushrefs/heads/mainMainJobmain 分支推送,返回空矩阵,仅用于共享 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-msvc

jobs.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.04macos-15windows-2025、AWS EC2 实例等)与free_disk等公共属性;
  • envs:按运行类型(pr/try/auto)声明共享环境变量,例如pr注入PR_CI_JOB: 1production锚点注入DEPLOY_BUCKET、AWS 密钥 ID 与TOOLSTATE_PUBLISH
  • jobs:声明dist-*(发布构建)任务,随后是pr:try:auto:optional:四个任务列表。

Job 数据模型

每个任务对应 Job 结构体(serde(deny_unknown_fields)严格反序列化):

字段类型说明
nameString任务名,如test-pr-check-1dist-x86_64-linux
osString执行任务的 runner 标签
envBTreeMap<String, Value>任务级环境变量(值支持字符串/布尔/数字)
only_on_channelOption<String>仅在指定 channel 执行(如beta),否则被skip_jobs过滤
continue_on_errorOption<bool>失败时不取消整个工作流
free_diskOption<bool>是否先释放磁盘空间
doc_urlOption<String>排障文档链接
codebuildOption<bool>是否在 AWS CodeBuild 上执行

JobDatabase(jobs.rs)聚合pr_jobstry_jobsauto_jobsoptional_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)按运行类型选择任务来源与共享环境,然后依次执行四步变换:

  1. 选择任务集PullRequestpr_jobs(前缀PR,注入pr_env);TryJob→ 有自定义模式则按 glob 展开auto_jobs+optional_jobs,否则用try_jobsAutoJobauto_jobs(注入auto_env);
  2. 替换 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)得以唯一化的原因;
  3. channel 过滤skip_jobs()(jobs.rs)剔除only_on_channel与当前 channel 不匹配的任务;
  4. 合并环境变量并输出:共享环境与任务环境合并(任务级优先),生成带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中展开,结果去重;
  • 未匹配到任何任务的模式会直接报错:"Patternsxxxdid 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.ymlpr:段落注释也明确说明:自动注册时以continue_on_error=false复制,显式覆盖时则逐字段校验等价性。

2. 数据库校验——validate_job_database()(jobs.rs)包含四类约束:

  • 四个任务列表中都不允许出现重名任务;
  • PR 任务与同名 Auto 任务必须等价(除continue_on_errorenv外的所有字段一致),例如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)接收任务名和可选的--typeautopr,默认auto),在本地复现 CI 任务:

cargo run --manifest-path src/ci/citool/Cargo.toml run-local x86_64-gnu-llvm-21-1

其实现run_workflow_locally()(main.rs)的关键步骤:

  1. auto_jobspr_jobs中按名字查找任务(find_linux_job只允许 Linux 任务,否则报错并列出可用任务清单);
  2. 复刻setup-environment.sh的行为:任务名以dist-开头时注入DEPLOY=1(以-alt结尾则注入DEPLOY_ALT=1);
  3. 把任务env中布尔/数字/字符串类型的环境变量注入子进程;
  4. src/llvm-project/子模块为空目录,则自动执行git submodule update --initinit_submodule_if_needed,utils.rs);
  5. 最终调用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_diffsreport_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_NAMECOMMIT_MESSAGEGITHUB_REF等环境变量(env_clear()清空其余环境),分别验证:

  • auto(push 到automation/bors/auto):PR 任务被自动注册进 auto 列表且continue_on_error=falsetest-tidy等任务带doc_url
  • try(push 到automation/bors/try):默认 try 任务被注入DIST_TRY_BUILD=1
  • 自定义 try-job:两条try-job:模式精确展开为对应任务;
  • prpull_request事件):仅执行 PR 任务,注入PR_CI_JOB=1
  • main:输出空矩阵。

这些快照同时验证了环境变量合并、full_name前缀(auto -/try -/PR -)、free_diskcontinue_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询