テーブル変更したら回帰テスト全部実行するようにした話

テーブル変更したら回帰テスト全部実行するようにした話

えらい人:今うちらさ、Git上でデータベースのテーブル構成を管理してるでしょ。

私:あーはいはい。

え:それに変更が入った時にさ、アプリの単体テスト実行したいんだよね。DB使ってるの全部。

私:はい?

え:いやほら、今までの仕組みだとJenkinsでそういうことをやっていたのよ。テーブル構成変更で悪影響でてないか確認したくてね。で、それと同じことをECS使った新しい仕組みのアプリでもやりたいんだよね。Github 使ってなんとかならないかな?

私:んーー?とりあえずやり方調べないとわからないですね。各アプリ側のCIは、Github Actions でやってるからそれを動かせばよいのかな?

え:とにかくやってみてよ。じゃ、よろしくーー。

私:りょ、了解しました。。。

現在の状況

データベースのテーブル構成のSQLファイルは、table_def というGitHubのリポジトリで管理されている。これは、開発PC上に開発環境を作るときや、Jenkins環境でのDB初期化の時にこのリポジトリ内のSQLを使ってデータベースを作っている。

テーブル構造を更新したいときはtable_def上で、masterからブランチ作ってやりたい更新をコミットしてプルリクエストを作る。この時に目視によるレビューをして、問題ないならmasterにマージされる。(ブランチ名はご容赦を…)

アプリ開発の要件でテーブルが増えたり定義が変わったりする。 このことをきっかけとして、発生するであろう既存アプリへの影響を検知したい。そこで、単体テストを動かして結果を知りたいというわけだ。

ECS使った新しい仕組みのアプリでは、CIを Github Actions でやってる。PRにコミット入った時にアプリ側のリポジトリの Github Actions を叩けばよいな。

あるいは、table_def の Github Actions で全部のアプリのテストを実行してもいいかな。 けどアプリ数これから増えそうだし、全部が1個に集中すると管理が大変かもな。やめとこう。

やってみよう

別リポジトリの Github Actions ってどうやって実行すれば良いんだ?そういえばリトアニアにすごいエンジニアがいるから相談してみよう。

リトアニア在住の同僚氏: Found a solution for you

https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#repository_dispatch

run: |
    curl -XPOST -u "${{ secrets.PAT_USERNAME}}:${{secrets.PAT_TOKEN}}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/YOURNAME/APPLICATION_NAME/dispatches --data '{"event_type": "build_application"}'

おーなるほど。repository_dispatch というイベントを用意して、Github Actions 内で curlでAPIを叩けばよいのだな。やってみよう!リトアニアの同僚氏ありがとーう。

<<ドタドタ・バタリ>>

ちょっと調べて構成考えてみた。まず処理全体はこんな感じになる。

table_defでのテーブル修正PRでのコミット発生きっかけで、その時点でのSQLファイルを使ってMySQLデータベースのイメージを build して ECR に pushする。この時点での hash値でタグを付けておく。

その後、各アプリにhash値と返信に必要な情報を client_payload で送って、 repository_dispatch を実行する。 各アプリでは、hash値が付いたイメージでデータベースを起動して、単体テストを実行する。結果レポートをS3にアップロードしつつ、成否だけ table_def 上にあるPRにコメントとして残せば良いな。失敗だったら、テスト結果レポートのリンクも入れると。

コード例

テーブル定義リポジトリ

table_defでのgithub Actionsはこんな感じだ。(途中のECRログインの処理は省略)

name: notify another repo when candidate database created

on:
  pull_request:
    branches:
      - master

jobs:
  build_candidate_database_and_notify:
    env:
      REPOSITORY_URI: 1234dummy5678.dkr.ecr.ap-northeast-1.amazonaws.com
      TARGET_APPLI: appli1,appli2,appli13,appliN
    outputs:
      tag_commit_hash: ${{ steps.prepare_parameter.outputs.tag_commit_hash }}
      target_appli_json: ${{ steps.prepare_parameter.outputs.target_appli_json }}
    steps:
      - name: prepare_parameter 
        id: prepare_parameter
        run: |
          set -e -x
          commit_hash=$(echo ${{ github.event.pull_request.head.sha }} | cut -c -7 )
          echo ::set-output name=tag_commit_hash::${commit_hash}
          tg_json=$(jq -nc --arg t "$TARGET_APPLI" '($t|split(","))')
          echo ::set-output name=target_appli_json::${tg_json}

      - name: checkout code
        uses: actions/checkout@v2

      # データーベースイメージをビルドし、コミットハッシュから取得したタグを付与してpushする。
      - name: build and push images
        run: |
          set -e -x
          docker build -t $REPOSITORY_URI/database:latest .
          docker tag $REPOSITORY_URI/database:latest $REPOSITORY_URI/database:${{ steps.prepare_parameter.outputs.tag_commit_hash }}
          docker push $REPOSITORY_URI/database:latest
          docker push $REPOSITORY_URI/database:${{ steps.prepare_parameter.outputs.tag_commit_hash }}

  notify_repos:
    needs: build_candidate_conti_db
    strategy:
      matrix: 
        target-appli: ${{fromJSON(needs.build_candidate_conti_db.outputs.target_appli_json)}}
    steps:
      # 各アプリのリポジトリのrepository_dispatch なイベントを実行する。
      # イメージのタグ、返信用リポジトリ名、返信用PR番号を client_payload で送信する。
      - name: notify repository that new database candidate created
        run: |
          curl  -H "Authorization: token ${{ secrets.PAT_FOR_CI }}"  \
            -H "Accept: application/vnd.github.everest-preview+json"    \
            "https://api.github.com/repos/GMORBLOGDUMMY/${{ matrix.target-appli }}/dispatches"  \
            -d '{"event_type": "candidate_database_created", "client_payload":   
              { 
                "commit_hash" : "${{ needs.build_candidate_conti_db.outputs.tag_commit_hash }}" ,
                "repository_name":"${{ github.event.repository.name }}" ,
                "pull_request_number":"${{ github.event.pull_request.number }}"  
              }}'

各アプリへ通知をする処理は繰り返しになるから matrix を使ってみた。イメージのビルドは繰り返されたくないから別のjobに分ける必要がある。ちょっと面倒だぜ。例えば単純に分けただけだとjobが2個とも同時に動き出してしまう。イメージのビルドのjobが終わった後に通知のjobを実行したいので、yaml に以下の記述を入れる。

needs: build_candidate_conti_db

さらに、ハッシュ値や通知対象アプリのリストをjob間で受け渡すために、以下の記述も入れる。

outputs:
  tag_commit_hash: ${{ steps.prepare_parameter.outputs.tag_commit_hash }}
  target_appli_json: ${{ steps.prepare_parameter.outputs.target_appli_json }}

通知対象のアプリのリストは環境変数から設定するようにした。matrix では配列オブジェクトが必要らしいから、アプリ名をカンマ区切り文字列にして環境変数に設置し、 jq と、fromJSON を使って配列に変換できた。

# jqでカンマ区切り文字列をJSON文字列に変換
tg_json=$(jq -nc --arg t "$TARGET_APPLI" '($t|split(","))')
echo ::set-output name=target_appli_json::${tg_json}
# fromJSONでJSON文字列を配列に変換
strategy:
  matrix: 
    target-appli: ${{fromJSON(needs.build_candidate_conti_db.outputs.target_appli_json)}}

これもっと端的にできないだろうか? しかしとりあえず設定を環境変数に置くことができたからこれで良しとするか。

各アプリのリポジトリ

アプリのリポジトリでのGithub Actionsはこんな感じだ。Kotlinのアプリケーションだとこうなる。 動作のパラメータのようなものは、client_payload で受け取る。(途中のECRログインやテストの前準備の処理は省略)

name: test for candidate database
on:
  repository_dispatch:
    types: [candidate_database_created]

jobs:
  test_candidate_database:
    steps:
      - uses: actions/checkout@v2

      # データーベースを起動してテストを実行する。環境変数でイメージのタグを設定することで起動するイメージを指定する。
      - name: start backing service
        run: |
          set -x
          docker-compose -p ci_network -f .github/docker-compose.database.yml up -d
          sleep 10
          gradlew test
        env:
          DATABASE_TAG: ${{ github.event.client_payload.commit_hash }}

      # テスト結果レポートをS3にアップロード(テスト失敗時のみ)
      - name: Upload report artifact to S3
        if: failure()
        id: upload-to-s3
        run: |
          now=$(date "+%Y%m%d-%H%M%S")
          echo ::set-output name=result::${now}
          aws s3 cp ./build/reports/ s3://GMORBLOGDUMMY/dated/${{ github.event.repository.name }}/${now}/ --recursive

      # テスト失敗を table_defリポジトリのPRにコメントする。
      - name: notify report html url on failure
        if: failure()
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          GITHUB_TOKEN: ${{ secrets.PAT_FOR_CI }}
          recreate: true
          header: ${{ github.event.repository.name }}
          message: |
            ${{ github.event.repository.name }}でテスト失敗してます!
            [テストレポート](https://GMORBLOGDUMMY.s3-ap-northeast-1.amazonaws.com/dated/${{ github.event.repository.name }}/${{ steps.upload-to-s3.outputs.result }}/tests/test/index.html)
          repo: ${{ github.event.client_payload.repository_name }}
          number: ${{ github.event.client_payload.pull_request_number }}

      # テスト成功を table_defリポジトリPRにコメントする。
      - name: notify test success
        if: success()
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          GITHUB_TOKEN: ${{ secrets.PAT_FOR_CI }}
          recreate: true
          header: ${{ github.event.repository.name }}
          message: |
            ${{ github.event.repository.name }}でテスト成功しました!
          repo: ${{ github.event.client_payload.repository_name }}
          number: ${{ github.event.client_payload.pull_request_number }}

if: success() と if: failure()を使えば成功時失敗時のそれぞれの処理を設定できる。

それと、table_def のPRへのコメントは最新の結果だけ表示されるように設定する。 実利用上は複数リポジトリからコメントを受けるので最新のコメントだけが残る形になってしまう。 (どこのリポジトリからのコメントなのかわからないので。) それだと使えないのでメッセージ送信元リポジトリの名前で識別できるようにする。

yamlとしてはこう書く。

recreate: true # 最新の結果だけ表示されるように設定
header: ${{ github.event.repository.name }} # メッセージ送信元リポジトリ名でメッセージを区別できるように設定

ここを参考にした。

https://github.com/marocchino/sticky-pull-request-comment#keep-more-than-one-comment

まとめ

うん。これでできた。これで ECS使った新しい仕組みのアプリでは repository_dispatch を受け取ったら指定されたデータベースのイメージで単体テストやって結果を table_def リポジトリに返せば良いな。テーブル構成変更PRを書いた人がアプリの単体テスト失敗に気がつけるようになるからこれは良いことだ。

各アプリのGithub Actions は既存の単体テストのworkflowをいくつか変更して別に作った形になる。うまく1本のymlファイルに纏めれたらよいけどそれは今後の課題としよう。きれいにメンテしやすく纏められるに越したことはないが、あまりに複雑だったら分けたほうが良いかもしれない。いずれにせよ、CIのコードってどれも似たりよったりでパターンになっていくだろうから、あまり気にしなくても良いかもね。

前の記事
«
次の記事
»

技術カテゴリの最新記事