瀏覽 1.1.1 文件
1.1.1

ait-runner:原生 CI 執行面

透過確切的 Worker Job 認領、Snapshot 物化、隔離的執行嘗試、有界證據、租約和部署控制,維運 1.1.1 原生 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.1.1 裡對外可認領的 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 放在服務管理器下面跑:

程式碼 · 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 權威上。重複的索引會被歸一:

程式碼 · 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 值。它 只對那一對做認領、執行、心跳和收尾:

程式碼 · 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:

程式碼 · 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"
}
程式碼 · 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.1.1 完整命令面#

程式碼 · 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.1.1 上限
帶型別的請求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.1.1 為 macOS、Linux/glibc 和 Windows 發布原生 runner 可執行檔案, arm64 和 x86_64 都有。Linux 的 OCI 索引覆蓋 amd64arm64

程式碼 · text
ghcr.io/weita2026/ait-runner:1.1.1

映象入口是 ait-runner,工作目錄是 /workspace,以數字 UID/GID 65532 執行。給嘗試路徑掛一個屬於該身份、可寫的卷;跑遠端 Snapshot 的 worker 並不需要一份可變的本地 Repository checkout:

程式碼 · bash
docker volume create ait-runner-attempts
docker run --rm \
  --network ait-native \
  --volume ait-runner-attempts:/var/lib/ait-runner \
  ghcr.io/weita2026/ait-runner:1.1.1 \
  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.1.1 映象摘要釘死。

安全與維運清單#

  • 每個 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 或請求邊界的失敗。

先做診斷,不要認領工作:

程式碼 · 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:遠端權威與恢復。 完整的公開環境變數清單在 附錄:環境變數

版本權威

對照 1.1.1 原始碼逐條核對

這一頁是公開文件,不是第二份產品契約。要確認發布權威,請以確切的原始碼和分發契約為準。

所屬元件 Snapshot
  • ait-coreSNP-ED7593DBF982
  • ait-serverSNP-0CCD7DD2A077
  • ait-runnerSNP-35C9C133D2EE
  • ait-pythonSNP-756C731A4CC0
  • ait-nodeSNP-55E90D0A81F1