auto-mergeでCI待たずに自動デプロイ
記事書いたのでclose
fast forward merge縛りが課されたgithubのリポジトリでのチーム開発に対し、
- PRを出し、ready for reviewする
- なんやかんやでレビューが通る(そのうちCIも通ってる)
- チームでリリース宣言をする
- (mainブランチが先に進んでいるので)Update branchする
- CI通るのを待つ(多かれ少なかれ待つ)
- CI通ったタイミングを見計らい、PRをmergeし、デプロイする
という作業を行っているが、4,5,6が(特に5が)めんどくさい。
そこでgithubの自動マージ機能を使えばこれをいい感じにできるのでは?という気付きからの検証をする。
まずは想定環境としてサンプルリポジトリを作る。
最低限の再現要件は
- 「待ち」が発生するCIがある、かつそれがbranch protection ruleになっている
- fast forward merge縛りになっている
となる。まず1つ目として下記のactionを用意した。
name: some long test
on:
pull_request:
jobs:
long-test:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: sleep 60
そしてbranch protection ruleがこう。キャプチャに写っていない範囲は何も設定していない。
で、作ったリポジトリがこれ。
※リアルさ・シナリオテスト的にはレビュー必須も入れたいが、一人で遊ぶ以上難しいのでやめた
なお、"Require status checks to pass before merging" に設定するactionは過去一週間?以内に一度でも実行されていないとダメらしい。一度も実行していないとリストに出てこない。
ここらで従来の(auto mergeなどを使わない状態の)マージフローを確認しておく。
mainから適当なpullreqを2つ生やし、片方をmergeする。すると、もう片方がこうなる。いつものやつ。
mergeボタンあたりがやたら赤いのは自分がリポジトリオーナーだから圧倒的管理者権限を持ってるからだと思う。
で、ここからUpdate branchを押し、1分待ってCIが終われば見慣れたMergeボタンが押せる。ここまでは前提状態。
前提状態のリポジトリはできたので、auto merge機能を使ってみる。
※この機能はそもそも全く使ったことがない
リポジトリ設定の下の方のPull RequestsセクションにAllow auto-merge
という欄があるので、チェックをつければok。
適当にpullreqを生やすと、CIが終わっていない状態でこうなっている。
Enable auto-megeボタンを押すと、普通のmergeボタンのようにコミットメッセージを入れる欄が出て、そのまま進むとenabledになる。その状態でCIが終わると、勝手にmergeされる。
※enabledの状態のスクショを取り忘れた
次にupdate branch兼CI待ち->mergeを試してみる。そういえばこれが期待通りに動くのか確認してなかった。
例によって2pullreq生やして片方をmergeする。
一旦CIが通ったがUpdate branchしていない状態がこれ。
従来であれば、ここからUpdate branchしてCIを待ってmergeになる。ここでEnable auto-mergeしてみる。
...と、別にupdate branchをしてくれるわけではなかった。
とはいえ別に待ちが発生するわけではないので、Enable auto-mergeをポチった後に流れるようにUpdate branchもポチればok。
この状態であれば、待っていたら勝手にmergeされるので、この時点でCI待つのめんどくさい問題は解消される。
auto-mergeのCI待ち挙動は良い感じだったので、追加要件を実装する。やりたいのは下記。
- Enable auto-mergeした際にslack通知したい(pullreqをこのあとリリースするぞ、という意思表示ログを残す)
- 対象pullreqの名前やURLなどの情報・ポチったユーザ名が表示できるのが望ましい
- auto-mergeによってmerge完了した際に、デプロイジョブを発火する
- ジョブ自体はどこかしらに既に用意されている想定で、それを叩くだけ
- デプロイ完了通知はこのジョブ自体が行う想定
- auto-mergeしたのにCIが失敗した場合にslack通知する
- 気持ち的には「デプロイ失敗」に相当するため、通知したい
※想定運用は「リリースしてOK」という確証ができた場合のみEnable auto-mergeするもの。そのためenableされたら問答無用でslack通知だし、この状態でのCI失敗はすべて通知すべき問題とする
まずは1つ目の「Enable auto-mergeした際にslack通知したい」から。
このイベント自体はactionsで普通にキャッチできそう。on: pull_request: auto_merge_enabled
がそれか。
イベントにフックしてactionを起動することはできるので、ここではあとはslack通知するだけ。だが実際にslackを用意するのは面倒なのでechoでお茶を濁す。そしてできたのが下記。
name: notify deployment start
on:
pull_request:
types: [auto_merge_enabled]
jobs:
notify-deployment-start:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: echo -e "CI回ったらリリースします by ${{ github.event.pull_request.auto_merge.enabled_by.login }}\n[${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }})"
横長で見づらいが、真面目にコード書くときに見やすくすればok。そしてこれが実行されるとこうなる。
よい感じっぽい。pullreqのリンクはひとまずmarkdownのリンク方式にしているが、slackに応じていい感じにする想定。
これauto-mergeのキャンセル時も入れてもいいかもしれない。
が技術的には全く同じなので運用時に考える。
次に「auto-mergeによってmerge完了した際に、デプロイジョブを発火する」を実装する。
発火するデプロイジョブは外部定義想定なのでなのでよいが、ここではgithub actionsで定義されている想定とする。で作ったサンプルジョブがこれ。
name: deploy
on:
workflow_dispatch:
workflow_call:
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: echo "Start deploy"
- run: sleep 30
- run: echo "Complete deploy"
普通にworkflow_dispatchで作ってあるモノがあるとして、on.workflow_call
を足しただけ。本番リリースを想定しているので、inputsとか無いでしょ。しらんけど。
そしてこれをイベントフックで発火するジョブがこれ。
name: auto merge deploy
on:
pull_request:
types: [closed]
jobs:
auto-merge-deploy:
if: github.event.pull_request.auto_merge != null
uses: ./.github/workflows/deploy.yml
auto_mergeしてるかどうかの値がほしいので、mainブランチのpushイベントではなくpull_requestのclosedイベントを取っている。
jobs.{job_id}.if
でgithub.event.pull_request.auto_merge != null
とすればauto_mergeしてるかどうかが判定できる。
条件を満たす場合、workflow_callをそのまま呼ぶだけ。
これでauto-merge経由でmergeされた場合はactionsがこんな感じになり、auto-merge-deploy
経由でdeploy
ジョブが実行されたことがわかる。
そしてauto-mergeを使わずにmergeした場合はこう。
StatusがSkippedになっている。ヨシ!
なおデプロイジョブがactionsなんて上等なもんじゃなくて昔ながらのJenkinsなんだが?みたいな場合には下記のルートでやればok
最後に「auto-mergeしたのにCIが失敗した場合にslack通知する」を実装する。
まず「失敗した場合に」を作る必要があるので、失敗しうるテストを用意する。
failable:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
- run: sleep 30
# if './hoge' exists, this test passes
- run: ls hoge
動作テスト用pullreqでhoge
というファイルを生やしていたので、その存在を確認するテストを書いた。これでhoge
を削除するpullreqのテストがfailする。
「CIが失敗した場合に」は下記で取れそう。期待しているものか不明なので、試す。
見た感じ、event.check_suite
にconclusion
とpull_requests
があるのでなんとかなりそう。
なんとcheck_suite.pull_requests
はauto-mergeなどが含まれるようなデカいオブジェクトではなかった。ほぼリポジトリのURLとpr-numberとcommit refしかない。
ghコマンドでなんとかするか。
どうもon.check_suite
はgithub appとかで明示的に作ったステータスに対するもので、actionsでtestを作ったやつに使うのはon.status
らしい。
で、このイベントにはbranchやcommitの情報は入っているがpullreq情報が紐づいていない。さてどうするか
上も含めて確証が持てない情報が多いが、これ関連で今わかっていることは
- checksとstatusは似ているが別物(歴史的経緯的なアレ)
-
on.check_suite
はgithub actionsからchecksされた場合は起動しない -
on.status
はデフォルトブランチに対するものでしかキックされない- https://github.com/orgs/community/discussions/26169#discussioncomment-3250663
- 上記にしれっと言及があるし、質問者も検証しているので事実ではあると思われる
- が公式ドキュメントに言及がない
つまり、actionsで動くCIについて、pullreqのCIが完了したことをactions上でキャッチするストレートな方法は存在しない。
残る確認したい事項は以下
- actionsのCIもpullreqのChecksタブに表示されているが、これは自動的にchecks扱いされているのではないのか?(
on.checks_run
とか動かないか?) - workflow_runイベントからうまいことできないか?
軽く検証した。もう面倒なのであまり厳密さを求めていない。
- check_run, statusともに、actionsのCIの限りにおいては、デフォルトブランチだろうがpullreqだろうが一切起動しない
- check_runについて、自前actionから明示的にchecksするのは試してない
- statusはデフォルトブランチでは動くはずではあったが動かない
- workflow_runは元気に動く
- workflow_runはフックする対象のworkflowを明示的に指定する必要がある(無いとエラーになる、多分ワイルドカード的なことはできない?)
- workflow_runの対象はworkflowファイル単位。ジョブ単位ではない
検証リポジトリ
workflow_runは使えそう、というかそれ以外が何も使えないので、こちらの方向を模索
- workflow_runで取得できるイベントには、元となったworkflowの関連情報が入っており、そこにpull_requestsがある(pull_request詳細ではないので、auto_merge情報などは別途取得する必要がある)
- event.workflow_runにはcheck_suite_idやcheck_suite_urlなどの属性があるが、check_suiteとはいいつつもworkflowごとで個別に発行されているようだ。つまりcommitに1:1対応ではない
- つまりユーザが一般にpullreq画面を見て思い浮かべるような、CIスイートひとまとまりに対して一つ発行されるわけではない
- つまりCI全体が完了したことを取得することはできない(非常に回りくどいことをすればできるかも)
当然のことながら「CI全体が完了したことを検知する」は需要があるので、ネットを探すとえらい複雑な処理を作り込んで実装している例がいくつか見受けられる。
が、今やりたいのは「失敗した場合に通知する」なので、スイート全体をチェックする必然性はなく、auto-mergeなpullreqのjobのどれかが失敗したら即通知、で問題ない。
これだとmergeに必須ではないジョブのみが落ちた場合に変な見た目になる・複数ジョブが落ちると通知がうるさいという問題があるが、そこは運用でカバーでよかろう。
長くなったが「auto-mergeしたのにCIが失敗した場合にslack通知する」は一応できた。
name: notify deployment failed
on:
workflow_run:
types: [completed]
workflows:
- some long test
jobs:
notify-deployment-faield:
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.event.workflow_run.conclusion == 'failure'
env:
GH_TOKEN: ${{ github.token }}
steps:
- run: |
PR_URI="/repos/${{ github.event.repository.full_name }}/pulls/${{ github.event.workflow_run.pull_requests[0].number }}"
if (gh api "${PR_URI}" | jq --exit-status .auto_merge > /dev/null); then
echo -e "CIが失敗したためリリースを中断します"
fi
on.workflow_run.workflows
にはpullreq関連のCIのworkflow名を入れる。手動だが仕方ない。
またworkflow_runイベントからはpull_requestの詳細データは取れないのでghコマンドで取っている(auto_mergeの有無を確認するため)。
なおworkflowは複数のpullreqが紐づいている場合があるので配列になっているが、今回の運用ではリリース直前にしかauto-mergeしない想定なので、なんやかんや[0]
決め打ちで大丈夫だと思われる。
と、ここまでできれば当初の目的はだいたい達成できたはず。