浏览 1.0.0 文档
1.0.0 文档修订 2

ait-runner:原生 CI 执行面

通过确切的 Worker Job 认领、Snapshot 物化、隔离的执行尝试、有界证据、租约和部署控制,运维 1.0.0 原生 runner。

适用人群: CI 运维、Repository 所有者与集成方

ait-runner 是干什么的#

ait-runner 是原生的执行面,跑的是 ait-server 接纳下来的 CI 工作。 服务端拥有 Repository 权威、Worker Job 记录、租约、状态迁移和被接纳 的结果。runner 只拥有一次有界的执行尝试:认领兼容的工作,把确切的 已记录源码重建出来,调用 Repository 的 CI 入口,返回证据,然后把自己 的尝试目录删掉。

CI 必须离开开发者的 worktree 执行时、不同机器服务不同 Repository 集合时,或者运维得重放某一对已知的 Worker Job 时,runner 就派上用场。 它不是 Repository 备份,不是策略引擎,不是独立于 ait-server 的调度 器,也不是某种语言专属的构建系统。

1.0.0 里对外可认领的 Binary Worker Job 只有两类:

固定 kind公开名称执行目的
7patchset.ci为受管的就绪判定校验选定的 Patchset Snapshot。
11repo.ci针对某一个确切的已记录 Snapshot 跑 Repository CI。

持久的 job 身份是 (repository_index, worker_job_index) 这一对。诊断 或重放一个 Binary Worker Job 时,绝对不要拿 Repository 名字来顶这 一对值。

四种使用场景#

常驻的执行服务#

想让一个 worker 持续轮询、维持租约、执行兼容的 job、投递终态结果时, 把 serve 放在服务管理器下面跑:

Code · bash
ait-runner serve \
  --server https://ait.example.internal \
  --worker-id runner-taipei-01 \
  --attempt-root /srv/ait/runner-attempts

--worker-id 是必填的运维身份,不是环境层面的设置。服务端 URL、源码 根和尝试根同样是显式的命令参数,这样起出来的进程自己就把运行边界讲 清楚了。

按 Repository 划分的 worker 池#

重复给 --repository-index,就能把一个服务实例限制在确切的一组 Repository 权威上。重复的索引会被归一:

Code · bash
ait-runner serve \
  --server https://ait.example.internal \
  --worker-id runner-linux-arm64-02 \
  --repository-index 3 \
  --repository-index 8 \
  --attempt-root /srv/ait/runner-attempts

不加这个过滤,兼容队列就会在所有已注册的 Repository 之间挑。过滤器 是用来表达一条有意划下的机群边界的,不是拿来猜哪个 Repository 拥有 某个出错 job 的。

只跑一个确切的 Worker Job#

在受控的诊断或恢复过程里,用 run-job 处理一对已知的 Binary 值。它 只对那一对做认领、执行、心跳和收尾:

Code · bash
ait-runner run-job \
  --server https://ait.example.internal \
  --repository-index 3 \
  --worker-job-index 42 \
  --attempt-root /srv/ait/runner-attempts

服务端必须声明当前的 ait.runner.native-job.v3 契约。这一对不可认领、 租约凭证无效,或者请求和认领到的 Repository 对不上,命令都会失败。

本地请求校验#

execute 跑一个带类型的本地目录请求,不认领任何服务端 job。这对 校验 runner 边界本身很有用;它不会创建也不会更新 Worker Job:

Code · json
{
  "contract": "ait.runner.native-job.v3",
  "label": "local-repository-ci",
  "source": {"kind": "local_directory", "path": "."},
  "command": {
    "argv": ["./ci/run"],
    "working_directory": ".",
    "environment": {}
  },
  "timeout_ms": 900000,
  "suite_id": "repository-ci"
}
Code · bash
ait-runner execute \
  --request request.json \
  --source-root /srv/ait/sources \
  --attempt-root /srv/ait/runner-attempts

--request - 从标准输入读同样的 JSON,这也是默认行为。服务端下发的 remote_snapshot 请求,需要 run-jobserve 用的那个服务端物化 provider;独立跑的 execute 走的是本地源码路径。

1.0.0 完整命令面#

Code · text
ait-runner doctor --server <SERVER>

ait-runner execute \
  [--request <REQUEST>] \
  [--source-root <SOURCE_ROOT>] \
  [--attempt-root <ATTEMPT_ROOT>]

ait-runner run-job \
  --server <SERVER> \
  --repository-index <REPOSITORY_INDEX> \
  --worker-job-index <WORKER_JOB_INDEX> \
  [--source-root <SOURCE_ROOT>] \
  [--attempt-root <ATTEMPT_ROOT>]

ait-runner serve \
  --server <SERVER> \
  --worker-id <WORKER_ID> \
  [--repository-index <REPOSITORY_INDEX>]... \
  [--once] \
  [--poll-interval-ms <MILLISECONDS>] \
  [--heartbeat-interval-ms <MILLISECONDS>] \
  [--source-root <SOURCE_ROOT>] \
  [--attempt-root <ATTEMPT_ROOT>]
命令确切行为
doctor读取服务端的实时健康状态并协商 runner 契约,不认领任何 job。
execute解析一个有界的 v3 请求,在隔离的一次尝试里执行它的本地源码。
run-job认领并完成确切的一对 Binary Worker Job;需要当前的 v3 支持。
serve持续轮询;带上 --once 时,在一次空闲检查、一次 job 投递或一次出错之后退出。

executerun-jobserve--source-root 默认是 .。不给 --attempt-root 就用平台临时目录下的 ait-runner/attemptsserve 默认轮询间隔 1,000 毫秒,请求的心跳间隔 30,000 毫秒。两个间隔都不能 为零;实际生效的 Binary 心跳还会被当前租约进一步收紧。

端到端的 job 生命周期#

  1. 一个远程工作流或 Repository CI 请求,促使 ait-server 为某个 Repository 提交一个带类型的 Worker Job。
  2. runner 检查服务端健康度,选出当前的 Binary runner 契约。doctor 到这一步就停了,什么都不认领。
  3. serve 认领下一个兼容的 job,或者 run-job 认领确切的 (repository_index, worker_job_index) 这一对。服务端返回租约凭证、 尝试次数、固定的 job kind 和带类型的运行时请求。
  4. 执行之前,runner 会校验凭证、job kind、Repository 索引、请求大小、 路径、参数、环境、平台和 CI 入口。
  5. remote_snapshot,它会把确切的 Snapshot Tree、Blob 包和锁定的 外部 Repository Snapshot 下载下来、验过,放进一次新的尝试里。
  6. 逻辑上的 argv 选择器 ./ci/run,在 Unix 上解析成 ci/run.sh,在 Windows 上解析成 ci/run.ps1。runner 用直接的 argv 启动它,不是 拼一条 shell 命令。
  7. 进程跑着的时候,一条单独的心跳维持着服务端租约。超时会在证据定稿 之前,把它拥有的那个进程组终止掉。
  8. runner 把 stdout 和 stderr 收成有界的证据,记下退出和物化的事实, 删掉尝试目录,再校验终态结果的大小。
  9. runner 带着原来那份租约凭证提交 complete 或 fail。这次状态迁移还 能不能被接受,只有服务端说了算,被接纳的终态也由它存下来。

整个尝试期间,选定的 Snapshot 始终不可变。对一个远程 Binary job, runner 永远不会去执行开发者 worktree 里的脏内容。

请求契约#

当前的请求 schema 是 ait.runner.native-job.v3。未知字段一律拒收。 它的顶层字段是:

字段契约
contract必填,必须正好是字符串 ait.runner.native-job.v3
label可选的非空标签,最多 256 字节。
source必填,local_directory 或当前的 remote_snapshot source 对象。
command必填的直接 argv 命令对象。
timeout_ms可选;默认 900,000 毫秒,取值范围必须是 1 到 86,400,000 毫秒。
suite_id可选的非空套件标签,最多 256 字节。

服务端下发的 source 里,remote_snapshot 带着确切的 repository_indexrepository_namesnapshot_id,以及一张从外部 Repository 名字到数字索引的映射。请求里的 Repository 索引,必须和认领 到的那一对 Worker Job 对得上。command 对象要求有 argv,而 working_directory 默认是 .environment 默认是空映射。

argv 的第一个值永远是逻辑选择器 ./ci/run。后面的值会直接传给平台 入口。工作目录必须是相对路径,而且不能跑出物化出来的工作区。

物化与尝试隔离#

每次执行都用一个名字唯一的 attempt-* 目录,里面装着工作区、下载 下来的包和临时日志。serve 会在它配置的尝试父目录上拿一把排他锁, 而且只回收确切属于本 runner 的陈旧尝试目录。清理时会处理只读文件, 并且不会跟着符号链接跑到尝试目录之外。

远程物化在这些情况下会直接失败:清单格式不对、校验和对不上、记录未知 或过多、Tree 有环或过深、路径越界、source 或入口是符号链接,以及内容 超出声明的上限。锁定的外部 Repository,是从它们各自确切的 Snapshot 物化出来的,不是从某个现成的 checkout 里拿的。

CI 子进程会收到这几个由 runner 提供的值:

名称含义
AIT_RUNNER_ATTEMPT_ROOT当前这次 runner 所属尝试的确切根目录。
AIT_RUNNER_WORKSPACE物化出来的主 Repository 工作区的确切路径。
AIT_EXTERNAL_<NAME>_REPO_ROOT某个归一化的、锁定的外部 Repository 物化出来的确切根目录。

这些是注入到进程边界上的值,不是启动 runner 用的设置。runner 唯一 支持的凭据环境变量,是可选的 AIT_SERVER_TOKEN。服务端 URL 和 worker 身份仍然走 --server--worker-idAIT_SERVER_URLAIT_RUNNER_WORKER_ID 不是输入。

有界的执行与证据#

边界1.0.0 上限
带类型的请求1 MiB。
终态结果JSON 编码之后 64 KiB。
超时默认 15 分钟;最长 24 小时。
命令 argv256 个值;每个 16 KiB;总共 128 KiB。
命令环境变量256 条;键和值加起来共 256 KiB。
stdout 与 stderr 证据每条流取 8 KiB 尾部。

每条流的记录里包含总字节数、所有捕获字节的 SHA-256、base64 编码的 尾部、尾部字节数,还有一个截断标记。完整的临时日志会随尝试目录一起 删掉,所以终态证据是有意做成有界的,不是一份藏起来的日志归档。

远程 Snapshot 导入还会强制这些上限:清单 64 MiB、物化文件 10,000,000 个、物化字节 512 GiB、单个包下载 16 GiB,以及 8 个并发的包下载/解码 操作。这些是安全天花板,不是推荐的 job 规模;运维给真实 worker 主机 定容量预期时,应该定得低得多。

结果与诊断契约#

命令执行成功会写出一个 ait.runner.native-result.v1 对象。它的终态 statussucceededcommand_failedtimed_out,而 tests_status 只有在 succeeded 时才会是 pass。结果里包含套件 记录、退出码或信号、耗时、物化的文件数和字节数、有界的 stdout 与 stderr 证据,以及清理证据。

面向服务端的命令,会把被接纳的终态迁移包在 ait.runner.delivery.v1 里。其他对外的输出契约有:

契约什么时候发出
ait.runner.doctor.v1doctor 确认健康,并报出选定的 runner 契约。
ait.runner.service.v1serve --once 没找到兼容的 job,返回 idle
ait.runner.serve-event.v1常驻的 serve 把一次有界的 job 失败报到 stderr,然后继续轮询。
ait.runner.error.v1命令在能返回正常结果之前就失败了。

--once 是快速失败的,出了那一个结果就返回。常驻的 serve 在一次 有界的单 job 失败事件之后会继续跑。轮询连不上服务端时,常驻服务模式 会做有界的重连退避,最长约 30 秒。当前的 Binary 契约一旦选定,进程 就不会悄悄降级。心跳失败会让认领到的 job 失败;runner 不会把语义 含糊的心跳重试叠在一起。

部署#

1.0.0 为 macOS、Linux/glibc 和 Windows 发布原生 runner 可执行文件, arm64 和 x86_64 都有。Linux 的 OCI 索引覆盖 amd64arm64

Code · text
ghcr.io/weita2026/ait-runner:1.0.0

镜像入口是 ait-runner,工作目录是 /workspace,以数字 UID/GID 65532 运行。给尝试路径挂一个属于该身份、可写的卷;跑远程 Snapshot 的 worker 并不需要一份可变的本地 Repository checkout:

Code · bash
docker volume create ait-runner-attempts
docker run --rm \
  --network ait-native-rc \
  --volume ait-runner-attempts:/var/lib/ait-runner \
  ghcr.io/weita2026/ait-runner:1.0.0 \
  serve \
  --server http://ait-server:8088 \
  --worker-id runner-container-01 \
  --repository-index 0 \
  --attempt-root /var/lib/ait-runner \
  --once

AIT_SERVER_TOKEN 用服务或容器的 secret 来传;别把 token 嵌进镜像、 命令、Repository 或者留存的日志里。上生产之前先核验发布的索引,然后 把不可变的 1.0.0 镜像摘要钉死。

安全与运维清单#

  • 每个 worker 都跑在专用的操作系统身份或容器下,文件系统和网络权限 给到最小。
  • 收紧服务端入站,在受信任的边界上用带认证的 TLS,并且别让 AIT_SERVER_TOKEN 出现在 argv 里或尝试根下的文件里。
  • 尝试用的存储放在 Repository 权威之外,也放在任何源码根之外。
  • 别让两个同时跑的 serve 进程共用一个尝试根;排他租约会拒掉它。
  • ci/run.shci/run.ps1 当成 Repository 经过评审的 CI 边界 来写。别指望 runner 自己猜出包管理器或测试命令。
  • 盯着服务端的 Worker Job 状态、runner 的失败事件、租约丢失、磁盘 容量、清理证据和版本契约不匹配。
  • command_failed 当作仓库 CI 的证据,把 timed_out 当作执行 超限,把 ait.runner.error.v1 当作 runner 或请求边界的失败。

先做诊断,不要认领工作:

Code · bash
ait-runner --version
ait-runner doctor --server https://ait.example.internal
ait repo jobs --remote origin --json
ait repo ci-capabilities --remote origin --json

然后逐项对:确切的 Repository 和 Worker Job 索引、选定的 runner 契约、 尝试次数、租约时间、终态操作、流的摘要,以及清理证据。服务端那边的 job 证据要留好;清理了一半的尝试目录别留着,更别把它当成权威源码 再用一次。

HTTP 的认领、心跳、complete、fail、Snapshot 导入和包下载这些路由,见 ait-server REST API 参考。 服务端权威和异地恢复,见 ait-server:远程权威与恢复。 完整的公开环境变量清单在 附录:环境变量

Version authority

Checked against the exact 1.0.0 source

This page is public documentation, not a second product contract. Use the exact source and distribution contract for release authority.

Owning component Snapshots
  • ait-coreSNP-B06A48DA0245
  • ait-serverSNP-E90456E6425E
  • ait-runnerSNP-6B0A1BB3AFAD
  • ait-pythonSNP-973E3BFAF3DE
  • ait-nodeSNP-F962CC66AA62