CI によるテスト
私たちの CI システムの主な目的は、テストスイートに合格することによって、
rust-lang/rust の main ブランチが常に有効な状態にあることを保証することです。
大まかに言うと、rust-lang/rust でプルリクエストを開くと、
次のことが起こります。
- PR に push されるたびに、テストとチェックの小さなサブセットが実行されます。 これは一般的なエラーの検出に役立つはずです。
- PR が承認されると、bors bot がその PR を[マージキュー]にエンキューします。
- PR がキューの先頭に到達すると、bors はマージコミットを作成し、 その上で完全なテストスイートを実行します。 マージコミットには特定の PR が 1 つだけ含まれる場合もあれば、CI コストとマージ遅延を削減するために、 複数の PR をまとめた “rollup” である場合もあります。
- テストスイート全体が完了すると、2 つのことが起こり得ます。
CI が失敗して開発者による対処が必要なエラーが発生するか、CI が成功して
マージコミットが
mainブランチに push されます。
CI で実行される内容を変更したい場合は、CI ジョブの変更を参照してください。
CI ワークフロー
私たちの CI は主に GitHub Actions 上で実行され、.github/workflows/ci.yml で定義された単一のワークフローを使用します。
このワークフローには、実行するすべての CI ジョブに共通化された多数のステップが含まれています。
対応するブランチまたは PR にコミットが push されると、ワークフローは
src/ci/citool crate を実行し、実行すべき具体的な CI ジョブを動的に生成します。
このスクリプトは、私たちのすべての CI ジョブの宣言的な設定を含む
jobs.yml ファイルを入力として使用します。
ほぼすべてのビルドステップは、個別のスクリプトを shell out します。これにより、CI はかなり プラットフォーム非依存になります(つまり、GitHub Actions に過度に依存していません)。 GitHub Actions に依存しているのは、CI プロセスのブートストラップと、 プロセスを駆動するスクリプトのオーケストレーションだけです。
本質的には、すべての CI ジョブは、さまざまなオペレーティングシステム、ターゲット、プラットフォームにわたって、
異なる設定で ./x test、./x dist、またはその他のコマンドを実行します。
実行されるジョブには、dist ジョブと非 dist ジョブという 2 つの大きなカテゴリがあります。
- Dist ジョブは、特定のプラットフォーム向けにコンパイラの完全なリリースをビルドします。
これには、rustup 経由で配布するすべてのツールが含まれます。
それらのビルドはその後
rust-lang-ci2S3 バケットにアップロードされ、rustup-toolchain-install-master ツールを使って ローカルにインストールできるようになります。 同じビルドは実際のリリースにも使用されます。私たちのリリースプロセスは基本的に、 これらのアーティファクトをrust-lang-ci2から本番エンドポイントへコピーし、署名することで構成されています。 - 非 dist ジョブは、そのプラットフォーム上で私たちの完全なテストスイートと、 rustup 経由で配布するすべてのツールのテストスイートを実行します。 テストする内容の量は プラットフォームに依存します(たとえば、一部のテストは Tier 1 プラットフォームでのみ実行されます)。また、 一部のより高速なプラットフォームは、CI リソースの浪費を避けるために同じ builder 上にまとめられます。
入力イベント(通常はブランチへの push)に基づいて、3 種類のビルド(ジョブの集合)のいずれかを実行します。
- PR ビルド
- Auto ビルド
- Try ビルド
Pull Request ビルド
プルリクエストに push されるたびに、一連の pr ジョブが実行されます。
現在、これらは Linux 上で実行される x86_64-gnu-llvm-X、x86_64-gnu-tools、pr-check-1、pr-check-2
および tidy ジョブを実行します。
これらは、一般的な問題を検出するはずの、比較的短時間
(約 40 分)で軽量なテストスイートを実行します。
より具体的には、一連の lint を実行し、Windows mingw 向けのクロスコンパイルチェック
ビルド(アーティファクトは生成しない)を試み、LLVM の system バージョンを使って
コンパイラをテストします。
残念ながら、すべての PR の各コミットに対して完全なテストスイートを実行するには、
リソースが多すぎます。
doc comment に関する注意
2024 年 10 月時点の PR CI は、デフォルトでは
./x doc xxxを実行しようとしないことに注意してください。これは、./x doc xxxの失敗につながる壊れた intradoc link がある場合、 完全なマージキュー CI パイプラインのかなり遅い段階でそれが発生することを意味します。そのため、doc comment の変更については、早期に検出できるように
./x doc xxxをローカルで実行することをお勧めします。
PR ジョブは jobs.yml の pr セクションで定義されています。
その結果は、PR ページ下部の “CI checks” セクションで、
PR 上から直接確認できます。
Auto ビルド
コミットを main ブランチにマージできるようにする前に、完全なテストスイートに合格する必要があります。
これを auto ビルドと呼びます。
このビルドは、オペレーティングシステムやターゲットをまたいでさまざまなテストを実行する、数十個の CI ジョブを実行します。
完全なテストスイートはかなり低速です。
すべての auto CI ジョブが完了するまでに数時間かかることがあります。
ほとんどのプラットフォームではビルドステップのみを実行し、一部は制限されたテストセットを実行します。 完全なテストスイートを実行するのは一部のみです(Rust の platform tiers を参照)。
Auto ジョブは jobs.yml の auto セクションで定義されています。
これらは rust-lang/rust リポジトリの automation/bors/auto ブランチ上で実行され、
最終結果は、対応する PR に bors が作成するコメントを通じて報告されます。
ライブ結果は the GitHub Actions workflows page で確認できます。
任意の時点で、実行中の auto ビルドは最大 1 つです。
詳細は bors による PR の逐次マージ を参照してください。
通常、auto ジョブが失敗すると、CI ワークフロー全体が直ちに終了します。 しかし、CI 上で一定期間テストしてからマージをブロックするようにするために、 「非ブロッキング」または任意の auto ジョブを作成すると有用な場合があります。 これは、それらのジョブが flaky になり得る場合に有用です。
そのためには、そのようなジョブに optional- というプレフィックスを付け、jobs.yml で continue_on_error: true を設定します。
Try ビルド
特定の PR について CI 上でテストスイートのサブセットを実行したり、
その PR からコンパイラアーティファクトのセットをビルドしたりしたい場合があります。
これはマージを試みずに行います。
これを “try build” と呼びます。
try build は、適切な権限を持つユーザーが @bors try コマンドを含む PR コメントを投稿した後に開始されます。
try build にはいくつかのユースケースがあります。
- 私たちの rustc-perf ベンチマークスイートを使用して、一連のパフォーマンスベンチマークを実行する。
これには動作するコンパイラビルドが必要であり、Linux 上で最適化された
コンパイラのバージョンをビルドする dist-x86_64-linux CI ジョブを実行する
try build によって生成できます(このジョブは現在、try build を開始するとデフォルトで実行されます)。
try build を作成して
パフォーマンスベンチマークのためにスケジュールするには、
@bors try @rust-timer queueコマンドの組み合わせを使用できます。 - Crater run を使用して、Rust エコシステム全体に対する PR の影響を確認する。 この場合も、動作するコンパイラビルドが必要であり、 dist-x86_64-linux CI ジョブによって生成できます。
- 特定の CI ジョブ(例: Windows テスト)を PR 上で実行し、そのジョブによって実行されるテストスイートに
合格するかどうかを素早くテストする。
デフォルトでは、
@bors tryを含むコメントを送信すると、jobs.ymlのtryセクションに定義されているジョブが実行されます。 このモードを「高速 try ビルド」と呼びます。 このような try ビルドではテストは一切実行されず、コンパイル警告も許容されます。 これは、Crater 実行やパフォーマンスベンチマークのために、 完全に正しく動作していない可能性があっても、 できるだけ速く最適化済みツールチェーンを取得したい場合に便利です。
高速 try ビルドで実行される CI ジョブには、デフォルトの try ジョブのフルビルドと区別するために、
特別なサフィックス(-quick)が付きます。
代わりにフルビルドを行いたい場合は、
ジョブパターン(後述)にそのジョブ名を指定してください。
try ビルドでカスタム CI ジョブを実行し、それらがすべてのテストに合格し、 コンパイル警告を一切生成しないことを確認したい場合は、ジョブパターンを指定して実行する CI ジョブを選択できます。 これは次の 2 つの方法のいずれかで使用できます。
- PR の説明に
try-job: <job pattern>ディレクティブのセットを追加し(後述)、 単に@bors tryを実行できます。 CI はこれらのディレクティブを読み取り、指定したジョブを実行します。 これは、 PR を段階的に変更した後で、同じ try ジョブのセットを複数回再実行したい場合に便利です。 - try コマンドの
jobsパラメーターを使用してジョブパターンを指定できます:@bors try jobs=<job pattern>。 これは、特定のジョブを指定した一回限りの try ビルドに便利です。jobsパラメーターは PR の説明内のディレクティブよりも優先度が高いことに注意してください。- たとえば
@bors try jobs=job1,job2,job3のように、複数のパターンを指定することもできます。
- たとえば
各ジョブパターンは、ジョブの正確な名前、または複数のジョブに一致する glob パターンのいずれかにできます。
たとえば *msvc* や *-alt です。
1 回の try ビルドで開始できるジョブは最大 20 個です。
PR の説明で glob パターンを使用する場合、
たとえばアスタリスクが含まれている場合に GitHub がそのパターンを Markdown として描画するのを避けるため、
必要に応じてバッククォート(`)で囲むことができます。このエスケープは、
@bors jobs= パラメーターを使用する場合には機能しないことに注意してください。
ジョブパターンは、jobs.yml の auto または optional セクションに定義されている 1 つ以上のジョブに一致する必要があります。
autoジョブは、コミットがmainブランチにマージされる前に実行されます。optionalジョブは、try ビルドを通じて明示的に要求された場合にのみ実行されます。 通常、tier 2 および tier 3 ターゲットに使用されます。
try ビルドを行う理由の 1 つは、上記のように @rust-timer queue を使って perf 実行を行うことです。
この perf ビルドは、その後 main 上の何らかのコミットと比較されます。
@bors try parent=<sha> を使用すると、try ビルドとその後の perf 実行を main 上の特定のコミットに基づかせることができ、
perf 比較をできるだけ公平にするのに役立ちます。
try-jobPR 説明ディレクティブの使用
実行したい try-job のセットを特定します。CI ジョブの名前は
jobs.ymlで確認できます。PR の説明を修正して、パターンのセットを含めます(通常は PR の説明の末尾)。 例:
This PR fixes #123456. try-job: x86_64-msvc try-job: test-various try-job: `*-alt`各
try-jobパターンは、それぞれ独立した行に記述する必要があります。
@bors tryで指定された try ジョブを実行します。前述のとおり、これにはユーザーが (1)try権限を持っているか、(2)try権限を持つ誰かによって@bors delegate=tryでtry権限を委譲されている必要があります。通常、これは
jobs.ymlを手動で編集するよりも簡単です。 ただし、この方法で実行されるテストのセットを調整できないため、 柔軟性は低くなる場合があります。
try ビルドは rust-lang/rust リポジトリ配下の automation/bors/try ブランチで実行され、
その結果は GitHub Actions ワークフローページ で確認できますが、
通常は対応する PR に bors が投稿するコメントによって結果が通知されます。
異なる PR 間では複数の try ビルドを並行して実行できますが、任意の時点で 1 つの PR 上で実行できる try ビルドは最大 1 つです。
CI ジョブの変更
CI で実行される内容を変更したい場合は、jobs.yml ファイルの
pr、auto、または try セクションを単に変更できます。
一時的に実行内容を変更することもできます。たとえば、ローカルでのテストが難しい 特定のプラットフォームや構成をテストする場合です(たとえば、Windows ビルドが失敗したが、 Windows マシンにアクセスできない場合など)。 そのような状況では、ためらわずに CI リソースを使用してください。
任意の CI ジョブを実行するには、次の 2 つの方法があります。
- try ビルド機能を使用し、try ビルドで実行したい CI ジョブを PR の説明で指定します。
jobs.ymlのprセクションを変更して、 PR への各 push 後にどの CI ジョブを実行するかを指定します。 これは、try ビルドを繰り返し開始するよりも速い場合があります。
PR への各 push 後に実行されるジョブを変更するには、auto セクションから
ジョブ定義の 1 つを pr セクションへ単にコピーできます。
たとえば、x86_64-msvc ジョブは 64 ビット MSVC テストを実行する役割を担っています。
これを pr セクションにコピーすると、次のように、PR にコミットが push された後に
そのジョブが実行されるようになります。
pr:
...
- image: x86_64-gnu-tools
<<: *job-linux-16c
# この項目は `auto` セクションからコピーされました
# vvvvvvvvvvvvvvvvvv
- image: x86_64-msvc
env:
RUST_CONFIGURE_ARGS: --build=x86_64-pc-windows-msvc --enable-profiler
SCRIPT: make ci-msvc
<<: *job-windows-8c
その後、ファイルをコミットし、GitHub 上の PR ブランチへ push できます。 すると GitHub Actions は、PR への各 push 後にこの CI ジョブを実行するはずです。
実験が終わったら、一時的な変更であるはずの jobs.yml への変更を削除することを忘れないでください!
try ジョブをまだ実行している間は PR タイトルの先頭に [WIP] を付け、
テスト目的で CI ジョブを変更するコミットには [DO NOT MERGE] を付けるのが良い習慣です。
CI を使用することは歓迎されますが、これは並行実行数に限りがある共有リソースであることを意識してください。 一度にあまり多くのジョブを有効にしないようにしてください。 ほとんどの場合、1 つか 2 つで十分なはずです。
bors による PR の直列マージ
CI サービスは通常、ブランチの最後のコミットを main の最後のコミットとマージしたものをテストします。
これは機能が単独で動作するかどうかを確認するには優れていますが、
マージされた後にコードが動作することを保証するものではありません。
このような破損は通常、ビルドの実行後に、互換性のない別の PR がマージされたときに発生します。
常に動作する main ブランチを確保するため、手動マージは禁止しています。
代わりに、すべての PR は私たちのボットである bors を通じて承認されなければなりません。
承認されたすべての PR は マージキュー に入れられ
(優先度と作成日時でソートされ)、1 つずつ自動的にテストされます。
すべてのビルダーが green であれば PR はマージされ、そうでなければ失敗が
記録され、PR は再度承認される必要があります。
Bors は CI サービスと直接やり取りするわけではありませんが、テストしたい
マージコミットを特定のブランチ(automation/bors/auto や automation/bors/try など)にプッシュすることで動作します。
これらのブランチは CI チェックを実行するように設定されています。
その後、Bors は Commit Statuses または Check Runs のいずれかをリッスンすることでビルド結果を検出します。
マージコミットは最新の main に基づいており、一度にテストできるのは 1 つだけなので、
結果が green であれば、main はそのマージコミットへ fast-forward されます。
残念ながら、1 度に 1 つの PR しかテストできないことに加え、CI が長時間(フル実行で約 2 時間)かかるため、1 日に多くの PR をマージすることはできず、 1 回の失敗がスループットに大きく影響します。 1 日にマージできる PR の最大数はおよそ ~10 です。
CI の実行時間が長く、大規模なビルダープールが必要になる主な理由は、
完全なリリース成果物が dist- ビルダーでビルドされるためです。
これらのリリース成果物には次の利点があるため、その価値があります。
- 後日であっても perf テストを可能にする。
- 後でバグが見つかったときに二分探索を可能にする。
- 常にリリースしている状態であれば問題を早期に発見できるため、リリース品質を確保できる。
ロールアップ
一部の PR では、フルテストスイートを実行する必要はありません。たとえば、 タイポ修正や README の改善のような些細な変更はビルドを壊すべきではなく、それらすべてを 2 時間以上かけてテストするのは無駄です。 これを解決するために、私たちは定期的に「ロールアップ」を作成します。これは、保留中の些細な PR を複数マージして、 まとめてテストできるようにした PR です。 ロールアップは、チームメンバーが merge queue の “create a rollup” ボタンを使って手動で作成します。 チームメンバーは自身の判断で、PR にリスクがあるかどうかを決定します。
Docker
macOS と Windows 上のものを除くすべての CI ジョブは、そのプラットフォーム専用の Docker container 内で実行されます。 これには私たちにとって多くの利点があります。
-
基盤となるイメージの変更に関係なく、ビルド環境が一貫している (trusty イメージから xenial への切り替えは、私たちにとって問題なく行えました)。
-
最大限のバイナリ互換性を確保するために、古いビルド環境を使用できる。 たとえば、Linux ビルダーでは 古い CentOS リリースを使用しています。
-
Docker イメージキャッシュのおかげで、ツール(QEMU や Android エミュレーターなど)を毎回再インストールせずに済む。
-
ユーザーは次のコマンドを実行するだけで、同じ環境で同じテストをローカルに実行できる。
cargo run --manifest-path src/ci/citool/Cargo.toml run-local <job-name>これは失敗のデバッグに役立ちます。 ライセンスやその他の制約により、ローカルで利用できるのは Linux Docker イメージのみであることに注意してください。
dist- という接頭辞が付いた Docker イメージは成果物のビルドに使用され、
その接頭辞がないものはテストやチェックを実行します。
また、CI ではあまり一般的でないアーキテクチャ(主に Tier 2 および Tier 3 プラットフォーム)のテストも実行します。 これらのプラットフォームは x86 ではないため、すべてを QEMU 内で実行するか、 そのプラットフォームのテストを実行したくない場合はクロスコンパイルのみを行います。
これらのビルダーは、GitHub が私たちのためにセットアップし保守している特別なビルダープール上で実行されています。
キャッシュ
私たちの CI ワークフローでは、主に 2 つの用途でさまざまなキャッシュ機構を使用しています。
Docker イメージのキャッシュ
Linux ベースのビルダーの大半を実行するために使用している Docker イメージは、完全にビルドするのに長い時間がかかります。 ビルドを高速化するために、Docker registry caching を使用してこれらをキャッシュし、中間成果物は ghcr.io に保存しています。 また、ビルド済みの Docker イメージも ghcr にプッシュしているため、他のツール (rustup)や、Docker ビルドをローカルで実行している開発者がそれらを再利用できます(ビルドを高速化するため)。
複数の分岐したブランチ(main、beta、stable)をテストしているため、
イメージに対して単一のキャッシュに依存することはできません。そうしないと、あるブランチ上のビルドが
他のブランチのキャッシュを上書きしてしまいます。
代わりに、すべての Dockerfile と関連スクリプトの内容から作成した
カスタムハッシュで識別される、異なるタグの下にイメージを保存しています。
CI はハッシュキーを計算し、次のいずれかが変更された場合に Docker イメージのキャッシュが 無効化されるようにしています。
- Dockerfile
- Dockerfile 内で Docker イメージにコピーされるファイル
- GitHub runner のアーキテクチャ(x86 または ARM)
Sccache による LLVM キャッシュ
私たちはさまざまな CI ジョブで C/C++ のものをいくつかビルドしており、中間 LLVM 成果物をキャッシュするために Sccache に依存しています。 Sccache は Mozilla が開発した分散 ccache であり、 オブジェクトストレージバケットをストレージバックエンドとして使用できます。
Sccache では、ハッシュキーを自分たちで計算する必要はありません。 Sccache は、ソースコード、コンパイラーのバージョン、重要な環境変数など、 関連する入力の変更を検出すると、自動的にキャッシュを無効化します。 そのため、私たちは Cargo の上に Sccache ラッパーを渡すだけで、残りは Sccache が処理します。
永続的な成果物は S3 バケット rust-lang-ci-sccache2 に保存しています。
そのため CI が実行されると、Sccache は、LLVM が同じ C/C++ コンパイラーでコンパイルされており、
LLVM ソースコードが同一であることを確認した場合、個々のコンパイル済み翻訳単位を
S3 から取得します。
CI 周辺のカスタムツール
長年にわたり、私たちは CI 体験を改善するためのカスタムツールをいくつか開発してきました。
PR にエラーメッセージを表示する Rust Log Analyzer
rust-lang/rust のビルドログは非常に大きく、ログを見て
ビルド失敗の原因を見つけるのは現実的ではありません。
そのため、私たちは Rust Log Analyzer(RLA)というボットを開発しました。このボットは
失敗時にビルドログを受け取り、エラーメッセージを自動的に抽出して、
PR スレッドに投稿します。
このボットはエラー文字列を探すようにハードコードされているわけではなく、多数の ビルド失敗を使って、ビルド間で共通している行とそうでない行を認識するように訓練されています。 生成されるスニペットがときどき奇妙になることはありますが、このボットは、 これまで見たことのないエラーであっても、関連する行を特定するのにかなり優れています。
許可された失敗をサポートする Toolstate
rust-lang/rust リポジトリは CI でコンパイラーだけをテストしているわけではなく、
さまざまなツールやドキュメントもテストしています。
一部のドキュメントは git サブモジュール経由で取り込まれます。
ドキュメントが修正されるまで rustc PR のマージをブロックしてしまうと、私たちは
鶏と卵の問題に陥ってしまいます。なぜなら、そのドキュメントの CI は、
テスト対象としてまだマージされていないバージョンの rustc を必要とするため、更新しても通らないからです
(そして通常、CI が通っていることを要求しています)。
この問題を避けるため、サブモジュールの失敗は許可され、その状態は rust-toolstate に記録されます。 サブモジュールが壊れると、ボットが自動的に メンテナーに ping して破損を知らせ、その失敗を toolstate リポジトリに記録します。 その後、リリースプロセスは nightly 上の壊れたツールを無視し、 配布される nightly からそれらを削除します。 ツールの失敗はほとんどの場合許容されますが、リリースの 1 週間前には自動的に禁止されます。nightly でツールが壊れているかどうかは問題にしませんが、beta と stable では動作しなければならないため、nightly を beta に昇格させる数日前には nightly でも動作している必要があります。
詳細は toolstate ドキュメントで確認できます。
公開 CI ダッシュボード
Rust CI を監視するには、infra チームが管理している公開ダッシュボードを確認できます。
以下は、ダッシュボード内の便利なパネルです。
- パイプライン所要時間: auto ビルドの実行にどれくらい時間がかかるかを確認します。
- 最も遅いジョブ: 実行に最も時間がかかっているジョブを確認します。
- ジョブ所要時間の中央値の変化: 以前より遅くなっているジョブを確認します。 これはリグレッションの検出に役立ちます。
- 最も失敗しているジョブ: 最も多く失敗しているジョブを確認します。
ダッシュボードの詳細については、Datadog CI ドキュメントを参照してください。
CI 設定の判定
特定のジョブについて、CI でどの bootstrap.toml 設定が使用されているかを判定したい場合は、ビルドログを見るのが最も簡単でしょう。
そのためには、次のようにします。
- Rust CI の成功したワークフロー実行ページに移動し、 最新のものをクリックします。
- 左側で、関心のあるジョブを選択します。
- 歯車アイコンをクリックし、“View raw logs” を選択します。
- 文字列 “Configure the build” を検索します。
- すべてのビルド設定は、
build.configure-argsというテキストがある行に一覧表示されています。