赤いビルドが十件、誰もログを読んでいなかった
目標
パイプラインの失敗を表で分類し、分類ごとに異なる対応へ振り分け、不安定であることを測定で証明し、失敗した実行を記録だけでもう一度再現し、決定的な判定スクリプトで原因のコミットを自動的に見つけ出す、調査の流れを最初から最後まで作ります。
なぜ重要なのか
失敗が1日に10回あると、人はログを読みません。リトライボタンが先に押され、通れば忘れられます。その習慣がついたパイプラインでは、本物の欠陥も、2、3回リトライされているうちにたまたま通ればそのままマージされ、赤信号はシグナルであることをやめます。そのため必要なのは、よりよいログではなく分類です。インフラの問題はリトライしてもよく、不安定なものはリトライで覆い隠すと永遠に残り、本物の欠陥は何回実行しても同じ答えなので、リトライは時間をむだにするだけです。分類を決めてはじめて、「いつ壊れたのか」という問いが成り立ち、その問いに答える二分探索は、判定が決定的なときだけ信頼できます。1回の誤った判定が、残りの探索全体を見当違いの区間へ追いやるからです。このラボは、その一連の流れをまるごと作ります。表で分類し、測定で証明し、記録で再現し、終了コードだけで語る判定スクリプトで原因を見つけます。
ステップ
/root/triage/rules.tsvに、分類の規則を表で書いてください。タブで区切られた3列규칙id<TAB>갈래<TAB>확장정규식(プレースホルダーは、規則id、分類、拡張正規表現です)で、上から順に最初に一致したものが優先されます。規則6つを、次のidと分類で置きます。dns(infra)、disk(infra)、oom(infra)、timeout(flaky)、assert(defect)、syntax(defect)。続いて、/root/triage/logs/に、失敗ログのサンプル6つrun-01.logからrun-06.logまでを自分で書いてください。順に、名前解決の失敗(dns)・ディスク不足(disk)・メモリ超過による終了(oom)・タイムアウト(timeout)・アサーションの失敗(assert)・構文エラー(syntax)がわかる内容でなければなりません。最後に、/root/triage/classify.sh <로그파일>(プレースホルダーはログファイルです)を作ってください。規則の表の場所は、環境変数RULES_FILEで変更でき、既定値は/root/triage/rules.tsvです。1つ目の語で分類を、2つ目の語で規則idを、1行で出力します。どの規則にも一致しなければunknown -を出力します。ログファイルを読み取れなければ、標準出力には何も出さず、終了コード2で終了します。/root/triage/policy.tsvに、分類ごとの対応を表で書いてください。タブで区切られた3列갈래<TAB>권장대응<TAB>재시도가능(プレースホルダーは、分類、推奨する対応、リトライの可否です)で、4行です。infra requeue yes、flaky measure no、defect bisect no、unknown read no(列の間はタブ)。続いて、/root/triage/classify.shを直して、1行に4つの語<갈래> <규칙id> <권장대응> <재시도가능>(プレースホルダーは、分類、規則id、推奨する対応、リトライの可否です)を出力させてください。ポリシーの表の場所は、環境変数POLICY_FILEで変更でき、既定値は/root/triage/policy.tsvです。そして、分類を終了コードでも伝えるようにしてください。infraは0、flakyは3、defectは4、unknownは5、ログを読み取れなければ2です。最後に、/root/triage/logs/の6つを順に実行して、その出力をそのまま/root/triage/triage.txtに6行で保存してください(run-01からrun-06の順)。/root/triage/tests/に、テストスクリプトを3つ作ってください。always-pass.sh(常に0)、always-fail.sh(常に0以外)、flip.sh(奇数回目の実行でだけ失敗)です。flip.shが数える値は、自分のファイルの隣($(dirname "$0")の下)に置いて、スクリプトをコピーして移しても、一緒についてくるようにしてください。続いて、/root/triage/flaky-probe.sh <시험스크립트> <횟수>(プレースホルダーは、テストスクリプトと回数です)を作ってください。受け取ったスクリプトをその回数だけ実行して、1行でruns=<횟수> pass=<성공> fail=<실패> verdict=<판정>(プレースホルダーは、回数、成功、失敗、判定です)を出力し、判定はstable-pass・flaky・stable-failのいずれかです。終了コードは、stable-passが0、flakyが3、stable-failが4で、引数がないか、回数が1以上の整数でなければ2です。最後に、3つのスクリプトをそれぞれ8回ずつ実行した出力を、/root/triage/flaky-report.txtに3行で保存してください(always-pass、flip、always-failの順)。/root/triage/tests/seed-test.shを作ってください。環境変数SEEDを読んで、同じシードなら常に同じ結果を出しながら、シードによって成功と失敗が分かれるテストです。SEEDが空なら、終了コード2で終了します。続いて、/root/triage/record-failure.sh <시험스크립트> <기록파일>(プレースホルダーは、テストスクリプトと記録ファイルです)を作ってください。環境変数SEEDに毎回異なる値を入れて、最大30回まで実行し、最初の失敗で止まり、記録ファイルに5行をSEED=、TZ=、LC_ALL=、SCRIPT_SHA256=(テストスクリプトのsha256の先頭64桁)、EXIT_CODE=で書いて、0で終了します。30回のうちに失敗がなければ、記録を残さず、0以外の値で終了します。最後に、/root/triage/replay.sh <기록파일> <시험스크립트>(プレースホルダーは、記録ファイルとテストスクリプトです)を作ってください。記録のSCRIPT_SHA256が今のスクリプトのハッシュと違っていたら、テストを実行せず、終了コード2で終了し、同じなら、記録のSEED・TZ・LC_ALLを入れてテストを実行したあと、そのテストの終了コードで終了します。3つをつなげて、/root/triage/repro/case.envを作ってください。record-failure.sh /root/triage/tests/seed-test.sh /root/triage/repro/case.envです。/root/triage/repoに、練習用のgitリポジトリを作ってください。コミットは20個で、件名はchange 1からchange 20です。各コミットはapp/rate.pyとapp/notes.txtを含み、python3 app/rate.pyは、コミット1からコミット11までは100、コミット12からは250を出力します(コミット12が、欠陥を入れたコミットです)。Podにはgitの身元がないので、リポジトリの中でgit config user.nameとgit config user.emailを先に設定してください。/root/triage/expected.txtには、期待値100を1行で書きます。続いて、/root/triage/oracle.shを作ってください。チェックアウトされた作業ツリーのルートで実行され、python3 app/rate.pyの出力が期待値と同じなら0、違えば1で終了します。期待値ファイルの場所は、環境変数EXPECT_FILEで変更でき、既定値は/root/triage/expected.txtです。標準出力には何も出力しません。/root/triage/repoで、最初のコミットをgood、HEADをbadとして、git bisect runで原因のコミットを探してください。判定には/root/triage/oracle.shを使います。見つけたら、/root/triage/bisect/culprit.txtに2行を書いてください。culprit=<40자리 커밋 해시>(プレースホルダーは40桁のコミットハッシュです)とsubject=<그 커밋의 제목>(プレースホルダーはそのコミットの件名です)です。そして、/root/triage/bisect/steps.txtに3行を書いてください。revisions=<good 뒤부터 bad 까지의 커밋 수>(プレースホルダーは、goodのあとからbadまでのコミット数です)、tests=<판정 스크립트가 실제로 불린 횟수>(プレースホルダーは、判定スクリプトが実際に呼ばれた回数です)、reason=<왜 그 횟수인지 한 줄 설명>(プレースホルダーは、なぜその回数なのかの1行の説明です。20文字以上)です。終わったら、git bisect resetでリポジトリを元のブランチに戻しておいてください。/root/triage/repo-skipに、2つ目のリポジトリを作ってください。コミットは20個で、件名は同じ形ですが、今回はコミット10のapp/rate.pyが構文エラーで実行すらできず、コミット18からは250を出力します(コミット1からコミット17までは、コミット10を除いてすべて100)。続いて、/root/triage/oracle-skip.shを作ってください。oracle.shと同じですが、app/rate.pyがないか、構文が通らなければ、終了コード125で終了します。同じリポジトリで、最初のコミットをgood、HEADをbadとして、二分探索を2回実行してください。1回は/root/triage/oracle.shで、もう1回は/root/triage/oracle-skip.shでです。結果を/root/triage/bisect/skip-report.txtに4行で書いてください。naive=<oracle.sh 가 지목한 40자리 해시>(プレースホルダーは、oracle.shが指したコミットの40桁のハッシュです)、skip=<oracle-skip.sh 가 지목한 40자리 해시>(プレースホルダーは、oracle-skip.shが指した40桁のハッシュです)、true=<진짜 범인의 40자리 해시>(プレースホルダーは、本当の原因のコミットの40桁のハッシュです)、limit=<건너뛰기의 한계를 적은 한 줄>(プレースホルダーは、スキップの限界を書いた1行です。20文字以上)です。終わったら、git bisect resetを忘れないでください。/root/triage/investigate.sh <로그파일> <저장소> <보고서파일>(プレースホルダーは、ログファイル、リポジトリ、レポートファイルです)を作ってください。ログを/root/triage/classify.shで分類し、分類がdefectのときだけ、リポジトリを、git cloneでクローンして、最初のコミットからHEADまで、/root/triage/oracle-skip.shで二分探索して、原因のコミットを見つけます。受け取ったリポジトリは1文字も変わってはならず、そのリポジトリで誰かが直していたファイルも、そのままでなければなりません。レポートは5行です。category=、action=、culprit=(defectでなかったか、見つけられなかったときは-)、subject=(同様)、reproduce=(再び起こす方法を1行で)。レポートファイルの親ディレクトリがなければ作り、正常に終わったら0で、引数が足りないか、ログやリポジトリを読み取れなければ2で終了します。作ったら、2回実行してください。/root/triage/logs/run-05.logと/root/triage/repo-skipで/root/triage/report/case-defect.txtを、/root/triage/logs/run-01.logと/root/triage/repo-skipで/root/triage/report/case-infra.txtを作ってください。
参考
- このラボのPodでは、コンテナを起動できません。seccompが、新しいユーザーの名前空間の作成を防いでいるためです。
podman run・podman build・buildah・unshare -Uは使いません。使うのは、bash・git 2.43・python3.12(標準ライブラリ)・jq・tar・sha256sumです。yq・make・go・pytestはなく、インターネットもありません。 - Podにはgitの身元が設定されていません。練習用のリポジトリを作ったら、その中で
git config user.nameとgit config user.emailを先に設定してください。しないと、最初のgit commitがPlease tell me who you areで止まります。 - このラボでいう「再現」は、失敗の再現です。同じ入力が同じバイト列を出すか(アーティファクトの再現)ではなく、同じ条件を再び作って、同じ失敗を起こせるかどうかです。そのため、記録するものも、アーティファクトのハッシュではなく、シード・環境・入力のハッシュです。
- よくあるミスは、判定スクリプトをリポジトリの中に置いてしまうことです。二分探索がコミットを移動すると、そのスクリプトまで一緒に変わります。
- よくあるミスは、構文の確認に
py_compileを使って、__pycache__を作業ツリーに残し、そのあとのチェックアウトが止まってしまうことです。ast.parseは、何も残しません。 - よくあるミスは、
git bisect resetを忘れて、リポジトリをデタッチ状態のHEADに置いたまま出てきてしまうことです。 - git bisect・git rev-list・Continuous Integration (Martin Fowler)
赤信号が10個あるのに、読んだ人は誰もいない
/root/triage/rules.tsvに、分類の規則を表で書いてください。タブで区切られた3列규칙id<TAB>갈래<TAB>확장정규식(プレースホルダーは、規則id、分類、拡張正規表現です)で、上から順に最初に一致したものが優先されます。規則6つを、次のidと分類で置きます。dns(infra)、disk(infra)、oom(infra)、timeout(flaky)、assert(defect)、syntax(defect)。続いて、/root/triage/logs/に、失敗ログのサンプル6つrun-01.logからrun-06.logまでを自分で書いてください。順に、名前解決の失敗(dns)・ディスク不足(disk)・メモリ超過による終了(oom)・タイムアウト(timeout)・アサーションの失敗(assert)・構文エラー(syntax)がわかる内容でなければなりません。最後に、/root/triage/classify.sh <로그파일>(プレースホルダーはログファイルです)を作ってください。規則の表の場所は、環境変数RULES_FILEで変更でき、既定値は/root/triage/rules.tsvです。1つ目の語で分類を、2つ目の語で規則idを、1行で出力します。どの規則にも一致しなければunknown -を出力します。ログファイルを読み取れなければ、標準出力には何も出さず、終了コード2で終了します。
規則をコードではなく表に置く理由は、新しい失敗の形が現れたときに、直す場所がスクリプトではなくデータであれば、レビューと履歴が残るからです。タブで区切られた行は、while IFS=$'\t' read -r a b cで読みます。最後の行に改行がないと、readがその行を取りこぼすので、|| [ -n "$a" ]を付け足すほうが安全です。拡張正規表現は、grep -Eq -- "$pattern"で照会します。採点は、自分で作った規則の表と、自分で作ったログでも、このスクリプトを実行します。ログのファイル名を見て答えていると、そこで引っかかります。
リトライしてもよい失敗と、絶対にリトライしてはいけない失敗
/root/triage/policy.tsvに、分類ごとの対応を表で書いてください。タブで区切られた3列갈래<TAB>권장대응<TAB>재시도가능(プレースホルダーは、分類、推奨する対応、リトライの可否です)で、4行です。infra requeue yes、flaky measure no、defect bisect no、unknown read no(列の間はタブ)。続いて、/root/triage/classify.shを直して、1行に4つの語<갈래> <규칙id> <권장대응> <재시도가능>(プレースホルダーは、分類、規則id、推奨する対応、リトライの可否です)を出力させてください。ポリシーの表の場所は、環境変数POLICY_FILEで変更でき、既定値は/root/triage/policy.tsvです。そして、分類を終了コードでも伝えるようにしてください。infraは0、flakyは3、defectは4、unknownは5、ログを読み取れなければ2です。最後に、/root/triage/logs/の6つを順に実行して、その出力をそのまま/root/triage/triage.txtに6行で保存してください(run-01からrun-06の順)。
分類を終了コードでも出力する理由は、あとに続く自動化が、文字列を改めて解析しなくて済むようにするためです。このラボの最後のステップが、その値を使います。リトライしてもよいのはインフラだけです。不安定なものはリトライで覆い隠すと永遠に残り、本物の欠陥は何回実行しても同じ答えなので、時間をむだにするだけです。採点は、自分で作ったポリシーの表をPOLICY_FILEで渡して実行します。対応の語をスクリプトの中に埋め込んでいると、そこで引っかかります。
1回失敗したものを、不安定と呼ぶことはできない
/root/triage/tests/に、テストスクリプトを3つ作ってください。always-pass.sh(常に0)、always-fail.sh(常に0以外)、flip.sh(奇数回目の実行でだけ失敗)です。flip.shが数える値は、自分のファイルの隣($(dirname "$0")の下)に置いて、スクリプトをコピーして移しても、一緒についてくるようにしてください。続いて、/root/triage/flaky-probe.sh <시험스크립트> <횟수>(プレースホルダーは、テストスクリプトと回数です)を作ってください。受け取ったスクリプトをその回数だけ実行して、1行でruns=<횟수> pass=<성공> fail=<실패> verdict=<판정>(プレースホルダーは、回数、成功、失敗、判定です)を出力し、判定はstable-pass・flaky・stable-failのいずれかです。終了コードは、stable-passが0、flakyが3、stable-failが4で、引数がないか、回数が1以上の整数でなければ2です。最後に、3つのスクリプトをそれぞれ8回ずつ実行した出力を、/root/triage/flaky-report.txtに3行で保存してください(always-pass、flip、always-failの順)。
同じ条件で結果が分かれるか。これが、不安定の定義であり、判別法です。1回の失敗は、どの種類なのかを教えてくれません。flip.shの状態を/tmpや固定したパスに置くと、コピーを作ったときに元と状態を共有してしまい、測定が狂います。回数の検査には、case "$N" in ''|*[!0-9]*)が短く済みます。採点は、学生のtests/をまるごとコピーして、コピーで実行します。
再現できない失敗は、調査できない
/root/triage/tests/seed-test.shを作ってください。環境変数SEEDを読んで、同じシードなら常に同じ結果を出しながら、シードによって成功と失敗が分かれるテストです。SEEDが空なら、終了コード2で終了します。続いて、/root/triage/record-failure.sh <시험스크립트> <기록파일>(プレースホルダーは、テストスクリプトと記録ファイルです)を作ってください。環境変数SEEDに毎回異なる値を入れて、最大30回まで実行し、最初の失敗で止まり、記録ファイルに5行をSEED=、TZ=、LC_ALL=、SCRIPT_SHA256=(テストスクリプトのsha256の先頭64桁)、EXIT_CODE=で書いて、0で終了します。30回のうちに失敗がなければ、記録を残さず、0以外の値で終了します。最後に、/root/triage/replay.sh <기록파일> <시험스크립트>(プレースホルダーは、記録ファイルとテストスクリプトです)を作ってください。記録のSCRIPT_SHA256が今のスクリプトのハッシュと違っていたら、テストを実行せず、終了コード2で終了し、同じなら、記録のSEED・TZ・LC_ALLを入れてテストを実行したあと、そのテストの終了コードで終了します。3つをつなげて、/root/triage/repro/case.envを作ってください。record-failure.sh /root/triage/tests/seed-test.sh /root/triage/repro/case.envです。
失敗を再び起こせなければ、不安定なのか本物の欠陥なのかを分ける方法がありません。そのため、記録には、その実行を再現するのに必要なものだけが入ります。シード、タイムゾーン、ロケール、そして入力がそのままかどうかを確認するハッシュです。ハッシュを照合する理由は、入力が変わったあとの実行は、再現ではなく新しい実験だからです。黙って通してしまうと、「再現できた」という誤った結論が残ります。シードは、$RANDOMをつなげたり、繰り返しの変数を混ぜたりして作ります。同じシードで30回実行しても、同じ結果が30回出るだけです。採点は、シードが何種類あったかを数えます。
二分探索が信頼できる判定スクリプトを、先に作る
/root/triage/repoに、練習用のgitリポジトリを作ってください。コミットは20個で、件名はchange 1からchange 20です。各コミットはapp/rate.pyとapp/notes.txtを含み、python3 app/rate.pyは、コミット1からコミット11までは100、コミット12からは250を出力します(コミット12が、欠陥を入れたコミットです)。Podにはgitの身元がないので、リポジトリの中でgit config user.nameとgit config user.emailを先に設定してください。/root/triage/expected.txtには、期待値100を1行で書きます。続いて、/root/triage/oracle.shを作ってください。チェックアウトされた作業ツリーのルートで実行され、python3 app/rate.pyの出力が期待値と同じなら0、違えば1で終了します。期待値ファイルの場所は、環境変数EXPECT_FILEで変更でき、既定値は/root/triage/expected.txtです。標準出力には何も出力しません。
判定基準をリポジトリの中に置くと、コミットを移動するときに基準まで一緒に変わって、何も判定できなくなります。そのため、期待値はリポジトリの外に置き、場所を環境変数で開きます。そうすれば、同じ判定スクリプトを別のリポジトリにも使えます。標準出力を空にする理由は、二分探索が読むのは終了コードだけだからです。画面に判定を混ぜて出力すると、自動化した側がその文字列をもう一度解析することになります。履歴を作る繰り返しは、git add -Aとgit commit -qmで短く書きます。採点は、クローンでコミットを1つずつたどりながら、正常から問題ありに切り替わる場所がちょうど1か所であるかを確認します。
19個のコミットを、4回で絞り込む
/root/triage/repoで、最初のコミットをgood、HEADをbadとして、git bisect runで原因のコミットを探してください。判定には/root/triage/oracle.shを使います。見つけたら、/root/triage/bisect/culprit.txtに2行を書いてください。culprit=<40자리 커밋 해시>(プレースホルダーは40桁のコミットハッシュです)とsubject=<그 커밋의 제목>(プレースホルダーはそのコミットの件名です)です。そして、/root/triage/bisect/steps.txtに3行を書いてください。revisions=<good 뒤부터 bad 까지의 커밋 수>(プレースホルダーは、goodのあとからbadまでのコミット数です)、tests=<판정 스크립트가 실제로 불린 횟수>(プレースホルダーは、判定スクリプトが実際に呼ばれた回数です)、reason=<왜 그 횟수인지 한 줄 설명>(プレースホルダーは、なぜその回数なのかの1行の説明です。20文字以上)です。終わったら、git bisect resetでリポジトリを元のブランチに戻しておいてください。
判定スクリプトをリポジトリの外に置く理由が、ここで明らかになります。二分探索はコミットを移動するので、リポジトリの中に置くと、そのスクリプトまでコミットごとに変わってしまいます。区間のコミット数は、git rev-list --count <good>..<bad>で数えます。呼ばれた回数は、git bisect runが画面に出力するrunning ...の行を数えるか、判定スクリプトを1行ずつ記録するラッパーで包んで数えれば済みます。回数がコミット数に比例せず、そのログに比例する理由を、reason=に書いてください。採点は、クローンで同じ二分探索を自分で実行して、答えと回数を照合します。
実行すらできないコミット1つが、原因を入れ替えてしまった
/root/triage/repo-skipに、2つ目のリポジトリを作ってください。コミットは20個で、件名は同じ形ですが、今回はコミット10のapp/rate.pyが構文エラーで実行すらできず、コミット18からは250を出力します(コミット1からコミット17までは、コミット10を除いてすべて100)。続いて、/root/triage/oracle-skip.shを作ってください。oracle.shと同じですが、app/rate.pyがないか、構文が通らなければ、終了コード125で終了します。同じリポジトリで、最初のコミットをgood、HEADをbadとして、二分探索を2回実行してください。1回は/root/triage/oracle.shで、もう1回は/root/triage/oracle-skip.shでです。結果を/root/triage/bisect/skip-report.txtに4行で書いてください。naive=<oracle.sh 가 지목한 40자리 해시>(プレースホルダーは、oracle.shが指したコミットの40桁のハッシュです)、skip=<oracle-skip.sh 가 지목한 40자리 해시>(プレースホルダーは、oracle-skip.shが指した40桁のハッシュです)、true=<진짜 범인의 40자리 해시>(プレースホルダーは、本当の原因のコミットの40桁のハッシュです)、limit=<건너뛰기의 한계를 적은 한 줄>(プレースホルダーは、スキップの限界を書いた1行です。20文字以上)です。終わったら、git bisect resetを忘れないでください。
125は、「このコミットでは判定できない」という意味です。判定不能を1(問題あり)にまとめてしまうと、探索はその前のほうに収束して、無実のコミットを1つに絞って自信たっぷりに指し示します。わからないと言わないほうが、より悪い結果になります。構文だけを確認するときにpy_compileを使うと、__pycache__が作業ツリーに残って、次のチェックアウトが止まります。python3 -c 'import ast,sys; ast.parse(open(sys.argv[1]).read())'は、何も残しません。本当の原因は、古いコミットから1つずつたどって、最初に問題ありになる場所です。リポジトリをクローンして走査すれば、元のリポジトリに触れずに済みます。limit=には、スキップしたコミットが原因のコミットと隣り合っているときに、探索がどこまでしか答えられないのかを書いてください。
ログを1つ入れると、原因のコミットが出てくるようにする
/root/triage/investigate.sh <로그파일> <저장소> <보고서파일>(プレースホルダーは、ログファイル、リポジトリ、レポートファイルです)を作ってください。ログを/root/triage/classify.shで分類し、分類がdefectのときだけ、リポジトリを、git cloneでクローンして、最初のコミットからHEADまで、/root/triage/oracle-skip.shで二分探索して、原因のコミットを見つけます。受け取ったリポジトリは1文字も変わってはならず、そのリポジトリで誰かが直していたファイルも、そのままでなければなりません。レポートは5行です。category=、action=、culprit=(defectでなかったか、見つけられなかったときは-)、subject=(同様)、reproduce=(再び起こす方法を1行で)。レポートファイルの親ディレクトリがなければ作り、正常に終わったら0で、引数が足りないか、ログやリポジトリを読み取れなければ2で終了します。作ったら、2回実行してください。/root/triage/logs/run-05.logと/root/triage/repo-skipで/root/triage/report/case-defect.txtを、/root/triage/logs/run-01.logと/root/triage/repo-skipで/root/triage/report/case-infra.txtを作ってください。
調査スクリプトが、他人のリポジトリでそのまま二分探索を実行すると、その人が作業していた場所を奪ってしまいます。git cloneした一時ディレクトリで実行し、trap ... EXITで片付けてください。判定スクリプトは、期待値ファイルの場所を環境変数から読みます。調査スクリプトは、その変数を消さず、そのまま引き継げば済みます。採点は、自分の期待値ファイルを渡して実行します。最初のコミットは、git rev-list --max-parents=0 HEADで探します。インフラの分類では、二分探索をそもそも実行してはいけません。コードと無関係な失敗のためにコミットを調べるのは、時間のむだです。