GitHub Actions ですべてのCIの完了をチェックして PR を Auto-Merge する
こんにちは ミクシィの 開発本部 SREグループ の riddle です。
今回も自分の所属するチームで行った活動について紹介します。
※こちらで自己紹介を簡単にしていますのでよろしければご覧ください。
◆ モチベーション
◆ 方法
◆ 補足
◆ YAML の解説
∘ GitHub Actions を動作させる条件
∘ 全体的な動き
◆ さいごに
◆ 参考
モチベーション
GitHub と なんらかの CI を組み合わせて、マージ前にテストを通すことがよくあると思います。
ただ「CI が通るのを待ってマージするのはめんどくさいです!!!!」
GitHub には マージ条件がすべてクリアされていれば勝手にマージしてくれる Auto-Merge という機能があります。(楽ちん)
しかしこれには 落とし穴 がありました。
リポジトリの構成にもよりますが、ドキュメントやコードなど様々なファイルがリポジトリに存在していると思います。
ファイルによってチェック観点が様々なため、ファイルによって走らせる CI を変更するのが一般的でしょう。
例えばこの図は、私達のリポジトリにあるアプリケーションコードの修正ですが、Kubernetes manifest ファイル自体の変更がこの Pull Request には含まれていないため CI が動いていません。
※ アプリケーションを変更したのだから Pod としてデプロイするところまでやるべきでは?という考えもあると思いますが、本筋から逸れるため今回は扱いません
この時、困ったことが1つあります。
GitHub では、PR をマージする前に特定の CI が通っていることを強制することができます。
しかし、ファイルによって動く CI ジョブが異なる場合、どれか1つを強制してしまうと、そのCI ジョブが動かない変更(例えばドキュメントなど)の際に、いつまでたってもマージできなくなってしまいます。
これにより Auto-Merge がうまいこと使えず、やきもきしている方もいるのではないでしょうか?
今回はこれを解消してAuto-Merge できるようにする方法をまとめます。
方法
方法としては単純で「すべての CI が成功したことをチェックする CI を用意し、その CI が成功することを強制する」というやり方をとります。
この YAML ファイルを対象の Git リポジトリに保存してください。
※パスは .github/workflows/好きな名前.yaml
# 特定のコミットに対する各CIの実行結果をチェックしすべて成功or実行されていない(=neutral)の状態であれば成功とみなします
#
# 用途としてはフォルダごとにCIが独立している場合にGitHub上でそれぞれのチェックを強制にすると
# 対象のファイルを変更しない場合にチェックが行われずマージができなくなります。
# そこで、このアクションを通じて全てのCIが完了して成功または実行されていない(=neutral)であることをチェックします。
# これにより、このアクションを必須とすることで結果的に必要なCIが全てパスすることを担保できます。
name: check-all-ci-result
on:
push:
# https://stackoverflow.com/questions/57699839/github-actions-how-to-target-all-branches-except-master
branches:
- '**' # matches every branch
- '!main' # excludes main
defaults:
run:
shell: bash
jobs:
check-another-ci:
name: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-20.04
steps:
- name: check another ci
run: | # このジョブ自身は無視したいのでIDを取得する
CHECK_SUITE_ID=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | jq -r '.check_suite_id')
SECONDS=0 # 対象のコミットハッシュに紐づくcheck-suitesをみてCIが全て完了するまでループします
while [ ${SECONDS} -lt 1800 ] # 30分
do # 対象のコミットに紐づくcheck-suitesをみてCIの状況を取得する
# https://docs.github.com/ja/rest/reference/checks#get-a-check-suite
# dependabot を有効にしていると引っかかってしまうので除外する
STATUS=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/check-suites" \
| jq -r ".check_suites[] \
| select(any(.app;.slug != \"dependabot\")) \
| select(.id != ${CHECK_SUITE_ID}) \
| .status" \
| sort \
| uniq) # 結果が全てcompletedだったら抜け出す
[ "${STATUS}" = "completed" ] && break sleep 30
done # 結果が全てsuccessかneutralなら成功
# https://docs.github.com/ja/rest/guides/getting-started-with-the-checks-api#about-check-suites
# dependabot を有効にしていると引っかかってしまうので除外する
gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/check-suites" \
| jq -r ".check_suites[] \
| select(any(.app;.slug != \"dependabot\")) \
| select(.id != ${CHECK_SUITE_ID}) \
| .conclusion" \
| sort \
| uniq \
| grep -v -E "(success|neutral)" && exit 1 || exit 0
※ branches のところで main を指定しているので、お使いの環境のブランチ名に合わせてこの CI を動かしたくないブランチを指定してください
あとは [Setting] → [Branches] → [Branch protection rule] → [対象のブランチのEdit] に移動し以下を設定します。
- Require status checks to pass before merging の有効化
- Require branches to be up to date before merging の有効化
- Status checks that are required. のところで 「check」を選択
なお、Protect Branch は公式サイトにもある通り限定的に利用できる機能のため、ご自身で利用の可否を確認してください。
Managing a branch protection rule — GitHub Docs
補足
上記の GitHub Action は最長で30分の間ずっと動き続けます。
つまり GitHub Action の課金対象となりますので、ご自身の環境の Pull Request の数や使われ方に応じて使ってみてください。
Feature Request としてジョブでの sleep や wait 機能を望む声もありますので、気長に待ちましょう。
YAML の解説
GitHub Actions を動作させる条件
on:
push:
# https://stackoverflow.com/questions/57699839/github-actions-how-to-target-all-branches-except-master
branches:
- '**' # matches every branch
- '!main' # excludes main
ここでは push 時にこの CI を動かすが、main ブランチへの Push ではない時 = 「Pull Request などで main 以外のブランチに Push したとき」に動作するようになります。
全体的な動き
以下のステップで処理をしています
- Pull Request の最新の Commit ID に紐づく CI ジョブの状況を取得
- 1 で取得した CI ジョブが完了するまで待つ
- すべての CI ジョブが正常に終了していれば成功、それ以外なら失敗とする
1 と 2 をここで実施しています。
# このジョブ自身は無視したいのでIDを取得する
CHECK_SUITE_ID=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | jq -r '.check_suite_id')
SECONDS=0# 対象のコミットハッシュに紐づくcheck-suitesをみてCIが全て完了するまでループします
while [ ${SECONDS} -lt 1800 ] # 30分
do# 対象のコミットに紐づくcheck-suitesをみてCIの状況を取得する
# https://docs.github.com/ja/rest/reference/checks#get-a-check-suite
# dependabot を有効にしていると引っかかってしまうので除外する
STATUS=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/check-suites" \
| jq -r ".check_suites[] \
| select(any(.app;.slug != \"dependabot\")) \
| select(.id != ${CHECK_SUITE_ID}) \
| .status" \
| sort \
| uniq)# 結果が全てcompletedだったら抜け出す
[ "${STATUS}" = "completed" ] && breaksleep 30
done
CHECK_SUITE_ID で取得しているのは、ここで紹介しているすべての CI 結果をチェックする ジョブの ID です。こいつを除外しないと、自分自身が完了するまで待つという無限ループになってしまいます。
dependabot に関して無視する処理をしています。これは GitHub で dependabot を有効にしている場合に処理は走らないにもかかわらず check-suites にでてきてしまうので意図的に無視しています
SECONDS はシェルで使用できる変数で、シェルが起動してからの時間を表示してくれます。直前に SECONDS=0 とすることで、その行が実行されてからの経過時間を常に格納してくれます。
3 をここで実施しています。
# 結果が全てsuccessかneutralなら成功
# https://docs.github.com/ja/rest/guides/getting-started-with-the-checks-api#about-check-suites
# dependabot を有効にしていると引っかかってしまうので除外する
gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/check-suites" \
| jq -r ".check_suites[] \
| select(any(.app;.slug != \"dependabot\")) \
| select(.id != ${CHECK_SUITE_ID}) \
| .conclusion" \
| sort \
| uniq \
| grep -v -E "(success|neutral)" && exit 1 || exit 0
ここでは単純にジョブの実行結果をみて「正常かどうか?」を判定しているだけですね。
さいごに
いかがでしたでしょうか!
GitHub Actions ではいろんな事ができるのでとてもおもしろいですね。
これで自分のチームでは Auto Merge がきちんと動くようになり、Pull Request のマージが楽になりましたので皆さんもぜひ使ってみてください!
(GitHub Actioins の Auto-update を併用するとよい)