AWS CodeBuildとGitHub Actionsを使ってテストカバレッジやサマリーを計測・表示させる

Da Vinci Studioサーバー部の徳元です。今回は私が関わっているプロジェクトでJestとGo(testing)のカバレッジやサマリーをAWS CodeBuildとGitHub Actionsを使って計測・表示した話を書いていきたいと思います。

自動テストを書きながらシステム開発をしている方も多いと思いますが、ただテストを書くだけでなくテストカバレッジを日頃から意識することで色々なメリットが得られます。例えば実装者はカバレッジの上昇がはっきりとわかることでテストを書くモチベーションが上がったり、レビュワーはカバレッジファイルを見ることでテストがされていない箇所や全体的にテストの手薄な箇所を認識できたりします。

カバレッジを見るために色々なツールを使うこともできますが、導入するのは手間やコストの面から面倒という方もいらっしゃるのではないでしょうか。実はCodeBuildとGitHub Actionsを使うことによって簡単にそれが実現できるのです。

なにをやったか

私が担当しているプロジェクトではCIにAWS CodeBuild(以下 CodeBuild)を使っています。プルリクエスト(以下 PR)を作成したり、PRへコミットを追加してCIが回った後にJestとtestingのカバレッジファイルへのリンクや、それぞれの簡単なサマリーをPR上にコメントとして表示させるようにしました。

カバレッジコメントの例
※カバレッジの値は実際のものとは異なります

青文字となっている箇所をクリックするとAmazon S3(以下 S3)に置いてあるカバレッジファイルを見ることができ、カバレッジの詳細を見ることができます。

カバレッジ計測ツールを使わなかった理由

カバレッジ計測をするとなるとCodecovなどのツールを使うこともあると思いますが、先述のとおり私が担当しているプロジェクトでは以前からCodeBuildでイメージのビルドやテストを実行しており、Jestもtestingパッケージもまあまあいい感じのカバレッジファイルを吐き出してくれる機能が既にあるので、新しいツールなどは導入せず、シンプルにCodeBuildに出力させたカバレッジファイルをGitHub Actionsに表示させる方がいいと考えこの方法を採用しました。

カバレッジ表示までの大まかな流れ

PRを作成もしくはコミットしてからカバレッジが表示されるまでの大まかな流れは以下の通りです。

カバレッジ表示までの大まかな流れ

GitHubのリポジトリにPRが作成・コミット追加されたタイミングでCodeBuildとGitHub Actionsが走り、CodeBuildによってS3にアップロードされたアーティファクトを使ってサマリーコメントを作成し、PR上に表示します。

アーティファクトがアップロードされた後じゃないとコメントが作成できないので、GitHub Status APIを使ってビルドが終わったかどうかを定期的にチェックします。

ここからは、それぞれの要素について説明していきます。まずはAWS側から説明します。

アーティファクトのアップロード

CodeBuildでは、ビルド出力ファイル等をアーティファクトとしてS3にアップロードすることができます。

まず最初に、コンソールやCloud Formationなどのインフラ定義ツールで該当ビルドプロジェクトのアーティファクトのアップロード先を設定します。そのあとbuildspec.ymlにアーティファクトに関するシーケンスを追加します(参考:CodeBuild のビルド仕様に関するリファレンス)。

具体的には、テストを実行するフェーズでJestとtestingのカバレッジファイルを吐き出させておき、artifactsシーケンスでビルド環境のビルド出力アーティファクトのパスやアップロード先のパスを指定すればOKです。アーティファクトはPOST_BUILDフェーズの後にアップロードされます(参考: ビルドフェーズの移行)。

色々省略していますが、buildspec.ymlは以下のようなイメージになります。

version: 0.2
env:
  variables:
    DOCKER_COMPOSE_YML: docker-compose.ci.yml
phases:
  install:
    runtime-versions:
      docker: 18
  build:
    commands:
      - go test -coverprofile=api.out
      - go tool cover -html=api.out -o api.html
      - npm test src -- --coverage
artifacts:
  files:
    - "coverage/lcov-report/**/*"
    - "api.html"
  base-directory: $CODEBUILD_SRC_DIR
  name: ${CODEBUILD_RESOLVED_SOURCE_VERSION}

S3にアップロードしたカバレッジファイルはBucket PolicyでIP制限しています。また、ライフサイクルルールを追加し、オブジェクトに有効期限を設けています(参考:オブジェクトのライフサイクル管理)。

AWS側の設定はこれで以上です。次に、GitHub Actions側の設定について説明します。

GitHub Actionsの追加

CodeBuildでアーティファクトをアップロードさせるよう設定したら、次はGitHub Actionsでアップロードされたアーティファクトを取得したりサマリーを表示できるようにします。

GitHub Actionsは利用可能なGitHubプランであれば.github/workflows配下にYAMLファイルでワークフローを定義することによって実行することができます。

ステップは以下の三つに大きく分かれます。

  1. AWS Credentialsの設定
  2. アーティファクトアップロードの待機
  3. アーティファクトの取得・PR上への表示

AWS Credentialsの設定

まず、S3からアーティファクトを取得するためにAWSのCredential情報を設定しておきます。

予めリポジトリに設定しておいたSecretsを使って、Workflowの中でAWS CLIを使ってS3にアクセスできるようにします。

name: Display test coverage on PR
on:
  pull_request:
    types: [opened, synchronize]
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

アーティファクトアップロードの待機

次に、アーティファクトアップロードのステータスを確認するために、GitHub Status APIへPollingを行います。

ここでGitHub Status APIにPOSTするために渡されているGITHUB_TOKENは、GitHubが自動的に生成したものとなります。(参考: GITHUB_TOKENでの認証

- name: Wait for artifcact upload
  id: commit-status
  timeout-minutes: 20
  run: |
    STATUS_API_URL=https://api.github.com/repos/$GITHUB_REPOSITORY/commits/${{ github.event.pull_request.head.sha }}/status
    CURRENT_STATUS=$(curl -v --url $STATUS_API_URL --header 'authorization:Bearer ${{ secrets.GITHUB_TOKEN }}' | jq -r '.state')

    echo "Current status is:$CURRENT_STATUS"

    while [ "$CURRENT_STATUS" = "pending" ]; do
      sleep 10
      CURRENT_STATUS=$(curl --url $STATUS_API_URL --header 'authorization:Bearer ${{ secrets.GITHUB_TOKEN }}' | jq -r '.state')
    done

    echo "Current status is:$CURRENT_STATUS"

    if [ "$CURRENT_STATUS" = "failure" ]; then
      echo "Commit status failed.Canceling execution"
      exit 1
    fi

ステータスを確認する方法を模索しながら実装していた当初は「このWorkflow自体が終わらないとステータスがsuccessになることはないんじゃないか」と思っていたのですが、そんなことはなく、CodeBuildが完了次第GitHub Statusはsuccessになってくれます。

アーティファクトの取得・PR上への表示

aws s3コマンドでアーティファクトの取得をし、Shell Scriptでアーティファクトのパースやサマリーの作成をし、最後にGitHub APIを使ってReview CommentをPOSTをすれば完了となります。 このパートはちょっと長くなるので、分割して説明していこうかと思います。

環境変数の設定

GitHub APIにコメントをPOSTするために必要となるトークンと、POST先のURL、そしてアーティファクトのオブジェクトパスに利用しているコミットハッシュをenvとして設定します。 下の例ではsecrets, githubコンテキストから値を取得しています。

コミットハッシュを取得する方法は他にWorkflowの中で使える環境変数を使うなど、色々な方法があると思いますので、こちらも参考にしてみるといいかもしれません。

- name: Comment on Pull Request
  env:
    TOKEN: ${{ secrets.GITHUB_TOKEN }}
    COMMENT_URL: ${{ github.event.pull_request.comments_url }}
    COMMIT_ID: ${{ github.event.pull_request.head.sha }}
  run: |
    # ここで色んな処理をさせる

アーティファクトの取得とパース

envで設定したCOMMIT_IDを含むプレフィックスを使ってアーティファクトを取得し、HTMLファイルの中から欲しい情報をパースします。

- name: Comment on Pull Request
  env:
    TOKEN: ${{ secrets.GITHUB_TOKEN }}
    COMMENT_URL: ${{ github.event.pull_request.comments_url }}
    COMMIT_ID: ${{ github.event.pull_request.head.sha }}
  run: |
    S3_KEY_PREFIX="s3://sample-bucket/base-directory/$COMMIT_ID"
    WEB_PATH="coverage/lcov-report/index.html"

    # SUMMARYの配列を取得しておく。Jestはデフォルトでカテゴリ別のカバレッジサマリーを用意してくれています
    WEB_SUMMARY=($(aws s3 cp $S3_KEY_PREFIX/$WEB_PATH - | grep strong | sed -e 's/<[^>]*>//g' | tr -d ' '))

    # APIの要素毎にテストカバレッジを計測しているケースを想定しています
    paths=(api.html repositories.html models.html graph.html interactors.html)

    for path in ${paths[@]}; do
      # testingパッケージのカバレッジファイルは各ファイルの中のカバレッジの割合しか表示してくれないので、全体の割合を算出しています
      COVERAGE_DATA=($(aws s3 cp $S3_KEY_PREFIX/api.html - | grep '<option value="file' | cut -d "(" -f2 | cut -d "%" -f1 | sed 's/\.//g' ))

      SUM=$(IFS=+; echo "$((${COVERAGE_DATA[*]}))")

      if [ $SUM == 0 ]; then
        AVERAGE=0
      else
        AVERAGE=$(($SUM/${#COVERAGE_DATA[@]}/10))
      fi
      # ここにサマリーをAppendしていくコードを書く
    done

PR上にコメントを表示する

最後に上記のパースした内容をReview Commentとして渡せるようテキストを修正し、POSTします。

- name: Comment on Pull Request
  env:
    TOKEN: ${{ secrets.GITHUB_TOKEN }}
    COMMENT_URL: ${{ github.event.pull_request.comments_url }}
    COMMIT_ID: ${{ github.event.pull_request.head.sha }}
  run: |
    WEB_PATH="coverage/lcov-report/index.html"
    OBJECT_URL="https://sample-bucket.s3-ap-northeast-1.amazonaws.com/base-directory/$COMMIT_ID"

    # ...中略...
    WEB_SUMMARY_TEXT="- Statements: ${WEB_SUMMARY[0]}\n- Branches: ${WEB_SUMMARY[1]}\n- Functions: ${WEB_SUMMARY[2]}\n- Lines: ${WEB_SUMMARY[3]}"

    for path in ${paths[@]}; do
      # ...中略...
      api_summary_texts+="- [$(echo $path | sed 's/\..*//g')]($OBJECT_URL/$path): $AVERAGE%\n"
    done

    curl -v POST $COMMENT_URL -H "Authorization:Bearer $TOKEN" -d \
    "{\"body\":\"## Coverage\n### Web\n- [Coverage File]($OBJECT_URL/$WEB_PATH)\n$WEB_SUMMARY_TEXT\n### API\n$api_summary_texts\"}"

全体像

これまで説明してきた処理をまとめると、このようになります。

- name: Comment on Pull Request
  env:
    TOKEN: ${{ secrets.GITHUB_TOKEN }}
    COMMENT_URL: ${{ github.event.pull_request.comments_url }}
    COMMIT_ID: ${{ github.event.pull_request.head.sha }}
  run: |
    S3_KEY_PREFIX="s3://sample-bucket/base-directory/$COMMIT_ID"
    WEB_PATH="coverage/lcov-report/index.html"
    WEB_SUMMARY=($(aws s3 cp $S3_KEY_PREFIX/$WEB_PATH - | grep strong | sed -e 's/<[^>]*>//g' | tr -d ' '))
    WEB_SUMMARY_TEXT="- Statements: ${WEB_SUMMARY[0]}\n- Branches: ${WEB_SUMMARY[1]}\n- Functions: ${WEB_SUMMARY[2]}\n- Lines: ${WEB_SUMMARY[3]}"

    OBJECT_URL="https://sample-bucket.s3-ap-northeast-1.amazonaws.com/base-directory/$COMMIT_ID"
    paths=(api.html repositories.html models.html graph.html interactors.html)

    for path in ${paths[@]}; do
      COVERAGE_DATA=($(aws s3 cp $S3_KEY_PREFIX/api.html - | grep '<option value="file' | cut -d "(" -f2 | cut -d "%" -f1 | sed 's/\.//g' ))
      SUM=$(IFS=+; echo "$((${COVERAGE_DATA[*]}))")

      if [ $SUM == 0 ]; then
        AVERAGE=0
      else
        AVERAGE=$(($SUM/${#COVERAGE_DATA[@]}/10))
      fi
      api_summary_texts+="- [$(echo $path | sed 's/\..*//g')]($OBJECT_URL/$path): $AVERAGE%\n"
    done

    curl -v POST $COMMENT_URL -H "Authorization:Bearer $TOKEN" -d \
    "{\"body\":\"## Coverage\n### Web\n- [Coverage File]($OBJECT_URL/$WEB_PATH)\n$WEB_SUMMARY_TEXT\n### API\n$api_summary_texts\"}"

以上で説明は終わりです。これで、冒頭にあったようなカバレッジサマリーが表示されるようになります。

さいごに

最後まで読んでいただきありがとうございました。Da Vinci Studioブログの技術記事の栄えある第一回の投稿をさせてもらいました。これからもインフラ、フロントエンドやサーバサイドの技術についてどんどん書いていきたいと思います。