Closed25

auto-mergeでCI待たずに自動デプロイ

inomotoinomoto

fast forward merge縛りが課されたgithubのリポジトリでのチーム開発に対し、

  1. PRを出し、ready for reviewする
  2. なんやかんやでレビューが通る(そのうちCIも通ってる)
  3. チームでリリース宣言をする
  4. (mainブランチが先に進んでいるので)Update branchする
  5. CI通るのを待つ(多かれ少なかれ待つ)
  6. CI通ったタイミングを見計らい、PRをmergeし、デプロイする

という作業を行っているが、4,5,6が(特に5が)めんどくさい。

そこでgithubの自動マージ機能を使えばこれをいい感じにできるのでは?という気付きからの検証をする。

https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request

inomotoinomoto

目論見としては、利用者は上記3の代わりに該当PRに対しEnable auto-mergeを押すだけで

  • slack周知
  • Update branch
  • merge
  • デプロイ

までをactionsで自動実行できるのでは?というところ。

inomotoinomoto

まずは想定環境としてサンプルリポジトリを作る。

最低限の再現要件は

  • 「待ち」が発生する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がこう。キャプチャに写っていない範囲は何も設定していない。

で、作ったリポジトリがこれ。
https://github.com/cumet04/sbox_auto-merge-and-deploy

※リアルさ・シナリオテスト的にはレビュー必須も入れたいが、一人で遊ぶ以上難しいのでやめた

inomotoinomoto

なお、"Require status checks to pass before merging" に設定するactionは過去一週間?以内に一度でも実行されていないとダメらしい。一度も実行していないとリストに出てこない。

inomotoinomoto

ここらで従来の(auto mergeなどを使わない状態の)マージフローを確認しておく。
mainから適当なpullreqを2つ生やし、片方をmergeする。すると、もう片方がこうなる。いつものやつ。

mergeボタンあたりがやたら赤いのは自分がリポジトリオーナーだから圧倒的管理者権限を持ってるからだと思う。

で、ここからUpdate branchを押し、1分待ってCIが終われば見慣れたMergeボタンが押せる。ここまでは前提状態。

inomotoinomoto

前提状態のリポジトリはできたので、auto merge機能を使ってみる。
※この機能はそもそも全く使ったことがない

リポジトリ設定の下の方のPull RequestsセクションにAllow auto-mergeという欄があるので、チェックをつければok。

inomotoinomoto

適当にpullreqを生やすと、CIが終わっていない状態でこうなっている。

Enable auto-megeボタンを押すと、普通のmergeボタンのようにコミットメッセージを入れる欄が出て、そのまま進むとenabledになる。その状態でCIが終わると、勝手にmergeされる。
※enabledの状態のスクショを取り忘れた

inomotoinomoto

次にupdate branch兼CI待ち->mergeを試してみる。そういえばこれが期待通りに動くのか確認してなかった。
例によって2pullreq生やして片方をmergeする。

一旦CIが通ったがUpdate branchしていない状態がこれ。

従来であれば、ここからUpdate branchしてCIを待ってmergeになる。ここでEnable auto-mergeしてみる。

...と、別にupdate branchをしてくれるわけではなかった。

inomotoinomoto

とはいえ別に待ちが発生するわけではないので、Enable auto-mergeをポチった後に流れるようにUpdate branchもポチればok。
この状態であれば、待っていたら勝手にmergeされるので、この時点でCI待つのめんどくさい問題は解消される。

inomotoinomoto

auto-mergeのCI待ち挙動は良い感じだったので、追加要件を実装する。やりたいのは下記。

  • Enable auto-mergeした際にslack通知したい(pullreqをこのあとリリースするぞ、という意思表示ログを残す)
    • 対象pullreqの名前やURLなどの情報・ポチったユーザ名が表示できるのが望ましい
  • auto-mergeによってmerge完了した際に、デプロイジョブを発火する
    • ジョブ自体はどこかしらに既に用意されている想定で、それを叩くだけ
    • デプロイ完了通知はこのジョブ自体が行う想定
  • auto-mergeしたのにCIが失敗した場合にslack通知する
    • 気持ち的には「デプロイ失敗」に相当するため、通知したい

※想定運用は「リリースしてOK」という確証ができた場合のみEnable auto-mergeするもの。そのためenableされたら問答無用でslack通知だし、この状態でのCI失敗はすべて通知すべき問題とする

inomotoinomoto

まずは1つ目の「Enable auto-mergeした際にslack通知したい」から。

このイベント自体はactionsで普通にキャッチできそう。on: pull_request: auto_merge_enabledがそれか。

https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows#pull_request

イベントにフックしてactionを起動することはできるので、ここではあとはslack通知するだけ。だが実際にslackを用意するのは面倒なのでechoでお茶を濁す。そしてできたのが下記。

.github/workflows/notify-deployment-start.yml
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に応じていい感じにする想定。

inomotoinomoto

これauto-mergeのキャンセル時も入れてもいいかもしれない。
が技術的には全く同じなので運用時に考える。

inomotoinomoto

次に「auto-mergeによってmerge完了した際に、デプロイジョブを発火する」を実装する。

発火するデプロイジョブは外部定義想定なのでなのでよいが、ここではgithub actionsで定義されている想定とする。で作ったサンプルジョブがこれ。

.github/workflows/deploy.yml
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とか無いでしょ。しらんけど。

そしてこれをイベントフックで発火するジョブがこれ。

.github/workflows/auto-merge-deploy.yml
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}.ifgithub.event.pull_request.auto_merge != nullとすればauto_mergeしてるかどうかが判定できる。

条件を満たす場合、workflow_callをそのまま呼ぶだけ。

inomotoinomoto

これでauto-merge経由でmergeされた場合はactionsがこんな感じになり、auto-merge-deploy経由でdeployジョブが実行されたことがわかる。

そしてauto-mergeを使わずにmergeした場合はこう。

StatusがSkippedになっている。ヨシ!

inomotoinomoto

最後に「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する。

inomotoinomoto

なんとcheck_suite.pull_requestsはauto-mergeなどが含まれるようなデカいオブジェクトではなかった。ほぼリポジトリのURLとpr-numberとcommit refしかない。

ghコマンドでなんとかするか。

inomotoinomoto

上も含めて確証が持てない情報が多いが、これ関連で今わかっていることは

つまり、actionsで動くCIについて、pullreqのCIが完了したことをactions上でキャッチするストレートな方法は存在しない。

残る確認したい事項は以下

  • actionsのCIもpullreqのChecksタブに表示されているが、これは自動的にchecks扱いされているのではないのか?(on.checks_runとか動かないか?)
  • workflow_runイベントからうまいことできないか?
inomotoinomoto

軽く検証した。もう面倒なのであまり厳密さを求めていない。

  • check_run, statusともに、actionsのCIの限りにおいては、デフォルトブランチだろうがpullreqだろうが一切起動しない
    • check_runについて、自前actionから明示的にchecksするのは試してない
    • statusはデフォルトブランチでは動くはずではあったが動かない
  • workflow_runは元気に動く
  • workflow_runはフックする対象のworkflowを明示的に指定する必要がある(無いとエラーになる、多分ワイルドカード的なことはできない?)
  • workflow_runの対象はworkflowファイル単位。ジョブ単位ではない

検証リポジトリ
https://github.com/cumet04/sbox_gh-actions

workflow_runは使えそう、というかそれ以外が何も使えないので、こちらの方向を模索

inomotoinomoto
  • 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に必須ではないジョブのみが落ちた場合に変な見た目になる・複数ジョブが落ちると通知がうるさいという問題があるが、そこは運用でカバーでよかろう。

inomotoinomoto

長くなったが「auto-mergeしたのにCIが失敗した場合にslack通知する」は一応できた。

.github/workflows/notify-deployment-failed.yml
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]決め打ちで大丈夫だと思われる。

と、ここまでできれば当初の目的はだいたい達成できたはず。

このスクラップは2023/04/16にクローズされました