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ファイルでワークフローを定義することによって実行することができます。
ステップは以下の三つに大きく分かれます。
- AWS Credentialsの設定
- アーティファクトアップロードの待機
- アーティファクトの取得・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ブログの技術記事の栄えある第一回の投稿をさせてもらいました。これからもインフラ、フロントエンドやサーバサイドの技術についてどんどん書いていきたいと思います。
We are hiring
Da Vinci Studio では一緒に働ける仲間を絶賛募集中です。募集職種と詳細に関しては、以下のリンクからそれぞれ確認できます。