rulesは実行ではなく生成を決める
一言でいうと
rulesは、ジョブが始まるときに「動かすか動かさないか」を問う仕組みではなく、パイプラインが作られる瞬間に「このジョブを一覧に入れるか」を決める仕組みです。
なぜ必要なのか
1つのパイプラインで複数の状況をまかなわなければならないというのが、問題の出発点です。機能ブランチではテストだけを動かしたく、マージリクエストではそこにセキュリティスキャンを加えたく、デフォルトブランチではステージングまで出したく、タグが付いたら本番デプロイを準備しつつ、人にボタンを押させたい。この4つをファイル4つに分けると、共通部分が4つに分かれ、やがて互いに違うものになります。
従来の文法であるonly/exceptは、この要求を中途半端にしか受け止められませんでした。条件を列挙することはできましたが、条件ごとに異なる動作(自動実行、手動承認、失敗の許容)を付けることができず、2つのキーを1つのジョブで一緒に使うこともできませんでした。rulesは条件と動作を1つの項目にまとめ、この限界を取り払った文法で、今では新しい設定でonly/exceptを使う理由がありません。2つを1つのジョブに混ぜて使うと、GitLabが設定を拒否します。
どう動くのか
rulesは項目のリストで、上から読んでいき、条件に最初に引っかかった項目1つだけを適用して止まります。この「最初のマッチだけ」という性質がすべてです。そのため、条件のない項目は常に引っかかり、その下に何を書いても二度と読まれません。条件のないwhen: neverをリストの途中に置くと、その後ろのルールがまるごと死にますが、この事故は何のエラーも出さないため、デプロイが出ない理由をなかなか見つけられなくなります。
項目が持てる条件は3つです。ifは変数の式で、$CI_COMMIT_BRANCH == "main"のように書きます。changesは、今回の変更に特定のパスが含まれているかを見ます。existsは、リポジトリに特定のファイルがあるかを見ます。
引っかかったときの動作はwhenが決めます。on_successは前がすべて成功していれば自動で、manualはジョブは作るが人が押さないと開始せず、alwaysは前が失敗しても、neverはジョブをそもそも作りません。manualと一緒に使うallow_failureは意味が紛らわしいですが、これがfalseであって初めて、その手動ジョブが本当の承認ゲートになります。trueだと、押さなくてもパイプラインが成功で終わってしまいます。
ここでもう一度確認しておくのは、評価の時点です。rulesはパイプラインを作るときに一度だけ評価され、ジョブが始まるときに再び見ることはありません。そのため、前のジョブが実行中に作った値で、後ろのジョブを実行するかどうかを決めることはできません。そのような条件分岐は、rulesではなくジョブの中のscriptで処理する必要があります。
現場での姿
最もよくある症状は、コミット1つでパイプラインが2つできることです。ブランチパイプラインとマージリクエストパイプラインが同じ条件に両方とも引っかかるためで、ランナーのリソースを2倍食い、状態表示も2つになります。解決は、ジョブごとに条件を足すことではなく、workflow:rulesでパイプライン自体を1つだけ作るように止めることです。
2番目によくあるのは、本番デプロイのジョブをwhen: manualにしておいて、allow_failureをデフォルトに任せることです。承認ゲートだと信じていたのに、誰も押していないパイプラインが緑色で終わっている場面を、いつか見ることになります。
続けて見ること
ジョブとジョブの間でファイルを渡すartifacts、そして実行と実行の間で時間を節約するcacheを見ます。名前が似ていてよく混同されますが、まったく別のものです。