GitHub Actionsでプルリクマージ時に差分を解析してslackに通知を飛ばしてみた

MoriKensukePosted by

こんにちは。運用プロジェクトでサーバーサイドエンジニアをしている森と申します。

私の所属するプロジェクトでは定期的にMySQLの特定のテーブルのデータをBigQueryに転送しているのですが、MySQLのテーブルの構造の変更を転送を管理しているSREチームに共有し忘れ、転送が失敗してデータがロストするということがしばしば発生しました。
そこで、それを防ぐためにGitHubのプルリクエストに転送対象テーブルのスキーマ変更が含まれていたら、SREチームにslackで通知を飛ばすようにしました。
本記事では、その手法について書きます。ソースコードは解説用に一部内容を変更している箇所があります。

GitHub Actionsとは?

GitHubの操作をフックとし、継続的インテグレーション (CI) と継続的デプロイメント (CD) 機能を実現できます。
例として、プルリクエストの作成時やマージ時にテストを走らせるなど、Jenkinsのようなことができます。
また、GitHub Marketplaceに有志があげている汎用的なアクションを利用することもできます。
値段も月の限度を超えなければ無料で使えるので、GitHubを使っていてCIをしたければ一考の価値ありです。

GitHub Actions

実行したい処理の流れ

処理の流れは以下のようになります。

  1. プルリクエストのマージ時に、マージするブランチとマージ先のブランチの差分からテーブルの変更を検知する
  2. SRE管理の別レポジトリにある転送対象テーブルの一覧を取得する
  3. 1と2の結果を比較して、対象テーブルが変更されていたらslackに通知を送る

1. プルリクエストのマージ時に、マージするブランチとマージ先のブランチの差分からテーブルの変更を検知する
GitHub Actionsはアクション実行時の様々な情報を持っているため、マージするブランチとマージ先のブランチのコミットハッシュを取得できます。その間のdiffをとることでテーブルの変更を検知します。私のプロジェクトではCodeIgniterのマイグレーションクラスに以下のようにSQLを記述するようにしているので、 ALTER TABLE `TABLE_NAME` が含まれているかどうかで判定します。

$sqlList[] = <<< 'QUERY_EOD'
ALTER TABLE `user` MODIFY `platform_user_id` VARCHAR(16) DEFAULT NULL COMMENT 'hoge';
QUERY_EOD;

2. SRE管理の別レポジトリにある転送対象テーブルの一覧を取得する
対象のレポジトリはプライベートレポジトリになっており、簡単には取得できません。
アクセストークンを使えばGitHubのAPIを使って取得できます。

3. 1と2の結果を比較して、対象テーブルが変更されていたらslackに通知を送る
シェルスクリプトで比較を行、通知にはslackのIncoming Webhook APIを使います。

yaml作成

GitHub Actionsの処理(ワークフローといいます)はyaml形式で記述します。
リポジトリの .github/workflows ディレクトリにファイルを作成するだけで、自動でGitHubが認識してくれます。
ここでは report_analysis_table_change.yml とします。

ワークフロー名設定

まず、ワークフロー名を記述します。このワークフロー名はGitHubのレポジトリページのActionsで参照されるので、処理の内容が分かる名前にしましょう。

name: Report Analisys Table Change

発火条件の設定

続いて、どのようなときにこのワークフローが発火するかを記述します。
今回はプルリクエストをマージしたときに発火するようにします。
また、全てのプルリクに対して発火する必要はないので、masterと、releaseから始まるブランチで、かつ差分にMySQLのスキーマ変更ファイルが含まれている場合のみに絞ります。

on:
  push:
    branches:
    - master
    - 'release/**'
    paths: codeigniter/application/migrations/*.php

処理内容の設定

次にジョブの設定をします。ワークフローの実行内容を記述します。

jobs:
  report_analisys_table_change:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
      with:
        fetch-depth: 0

    - name: report analisys table change
      env:
        MIGRATION_DIR: codeigniter/application/migrations
        REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
        SLACK_WEBHOOK_URL_TO_SRE: ${{ secrets.SLACK_WEBHOOK_URL_TO_SRE }}
      run: |
        BEFORE_SHA=$(jq -r '.before' $GITHUB_EVENT_PATH)
        AFTER_SHA=$(jq -r '.after' $GITHUB_EVENT_PATH)
        REF_URL=$(jq -r '.compare' $GITHUB_EVENT_PATH)
        REF_BRANCH=$(jq -r '.ref' $GITHUB_EVENT_PATH)
        curl -L -o repo.zip -H "Authorization: token ${REPO_ACCESS_TOKEN}" \
        https://api.github.com/repos/ORGANIZATION_NAME/SRE_REPO_NAME/zipball/master
        unzip repo.zip -d SRE_REPO_NAME
        git diff ${BEFORE_SHA} \
        -- ${MIGRATION_DIR} \
        | grep 'ALTER TABLE' \
        | while read statement ; do \
            echo $statement
            grep table: SRE_REPO_NAME/TARGET_TABLE_NAME_LIST.yaml \
            | sed -e "s/ *//g" -e "s/table:\([a-z_]*\)\s*/\1/" \
            | while read table_name ; do \
                if [[ "$statement" =~ "\`$table_name\`" ]]; then
                 report="<@SLACK_USER_ID> ${REF_BRANCH}で${table_name}テーブルに変更がかかりました\n\n内容: $(echo ${statement} | tr \' \")\n差分: ${REF_URL}"
                 echo $report
                 echo "{'attachments':[{'fallback':'${report}','color':'#ffd700','fields':[{'title':'table変更通知','value':'${report}'}]}]}" \
                 | curl -k --verbose -X POST -H 'Content-Type:application/json;' \
                 --data @- ${SLACK_WEBHOOK_URL_TO_SRE}
                fi
            done
        done

以上がジョブの設定になります。大事なところをピックアップして解説していきます。

runs-on: ubuntu-latest

処理を実行するマシンを設定します。Linux、macOS、Windowsを選択できます。
また、自分で用意したマシンを使うこともできます。
特にOS依存がなければLinuxを使うのがおすすめです(MacOSは少し値段があがります)。

- uses: actions/checkout@master
  with:
    fetch-depth: 0

usesでパブリックリポジトリにあるアクションの指定ができます。
このアクションではプルリクのレポジトリをマシンにcheckoutします。
デフォルトでは最新のコミット1つしか取得しませんが、今回は過去のコミットとの比較をしたいので、fetch-depthを0に設定して全てのコミットを持ってきます。

env:
  MIGRATION_DIR: codeigniter/application/migrations
  REPO_ACCESS_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }}
  SLACK_WEBHOOK_URL_TO_SRE: ${{ secrets.SLACK_WEBHOOK_URL_TO_SRE }}

処理内で参照したい環境変数を設定します。
アクセストークンやSSH鍵などはベタ書きにするとセキュリティ上よろしくないので、Secretsを使いましょう。
暗号化されたシークレットの作成と保存

ここからはいよいよ処理内容に入っていきます。

BEFORE_SHA=$(jq -r '.before' $GITHUB_EVENT_PATH)
AFTER_SHA=$(jq -r '.after' $GITHUB_EVENT_PATH)
REF_URL=$(jq -r '.compare' $GITHUB_EVENT_PATH)
REF_BRANCH=$(jq -r '.ref' $GITHUB_EVENT_PATH)

$GITHUB_EVENT_PATHにはこのアクションを実行する際のいろいろな情報がjson形式で格納されています。
今回はマージ前とあとの差分を出すためのSHAと、slackに転送するためにコミット差分を表すGitHubのurlと、ブランチ名を変数に入れておきます。

curl -L -o repo.zip -H "Authorization: token ${REPO_ACCESS_TOKEN}" \
  https://api.github.com/repos/ORGANIZATION_NAME/SRE_REPO_NAME/zipball/master
unzip repo.zip -d SRE_REPO_NAME

弊社はBigQueryへの転送テーブルのリストを違うレポジトリに管理しているので、これをGitHub Apiを使って取得します。
public repogitoryでない場合はアクセストークンが必要になります。

git diff ${BEFORE_SHA} \
  -- ${MIGRATION_DIR} \
  | grep 'ALTER TABLE' \
  | while read statement ; do \
    echo $statement
    grep table: SRE_REPO_NAME/TARGET_TABLE_NAME_LIST.yaml \
    | sed -e "s/ *//g" -e "s/table:\([a-z_]*\)\s*/\1/" \
    | while read table_name ; do \
      if [[ "$statement" =~ "\`$table_name\`" ]]; then
        (slackに転送する処理)
      fi
     done
  done

上記のコードではマージ前とマージ後の差分を比較し、ALTER TABLEの行が含まれていたらそのテーブル名がSREレポジトリのTARGET_TABLE_NAME_LIST.yamlファイルにそのテーブル名が含まれているか調べています。

report="<@SLACK_USER_ID> ${REF_BRANCH}で${table_name}テーブルに変更がかかりました\n\n内容: $(echo ${statement} | tr \' \")\n差分: ${REF_URL}"
echo $report
echo "{'attachments':[{'fallback':'${report}','color':'#ffd700','fields':[{'title':'table変更通知','value':'${report}'}]}]}" \
  | curl -k --verbose -X POST -H 'Content-Type:application/json;' \
  --data @- ${SLACK_WEBHOOK_URL_TO_SRE}

先ほどのコードの「slackに転送する処理」の部分が上記のコードです。slackのwebhook APIを使ってブランチ、変更されたテーブル名などの情報をSREのチャンネルに転送しています。

実行結果

プルリクエストをマージするとこんな感じでslackに飛んできます!

苦労したこと

ローカル環境でテストができない

2020年5月の段階ではアクション設定ファイルをローカルやサンドボックス環境などで実験することができないので、テストのブランチを作って何回もアクションをhookしてテストするしかありません。
間違ってmasterにpushするなど、事故が起きないように注意する必要があります。

ドキュメントが少ない、探しづらい

新しめの機能ということもあり、「これをやりたい」と思って調べてもなかなか見つけにくかったです。
自分は公式のドキュメントをみてもよくわからないところがあったので、ググった情報と組み合わせて実装を進めました。

さて、今回はGitHub Actionsを使ってプルリクエストのマージ時に差分をマージ解析し、slackに通知を飛ばす設定を作りましたが、いかがでしたか?
GitHubを使っていればすぐに利用でき、プルリクエストやマージ時に処理を走らせるのはかなり便利なので、ぜひ使ってみてください!