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ブログの技術記事の栄えある第一回の投稿をさせてもらいました。これからもインフラ、フロントエンドやサーバサイドの技術についてどんどん書いていきたいと思います。

開発者が成長・変化できるような組織アーキテクチャ

Da Vinci Studio 代表の吉川です。

Da Vinci Studio は、くふうカンパニーグループ内にある開発会社です。 グループ内外の受託開発の他、自社でもサービス開発に取り組んでいます。

異色なのはその成り立ちです。

くふうカンパニーグループはオウチーノ、みんなのウェディング、Zaim、RCUBEといった、もともと自社に開発組織を持ち自社で提供するサービスを持つ会社が集まりました。 そんな中で全く新規の会社として設立され、当初からグループ外の案件の受託を事業内容に入れて組織されました。

自社でしっかりとした事業体があるにも関わらず何故あえてリソースを他に割くようなグループ外の受託を始めたのでしょうか。

自立・自律する組織という考え方

前提として、くふうカンパニーはこの考え方を土台に据えています。 親会社はあくまで各事業会社を支援する立場であり、ユーザーの対面に立つ事業会社が主体的に意思決定をする。 お互い助け合いますし、グループ内での転籍・出向も積極的に行われていますが、グループ内の各社で人事制度などもバラバラです。

そんな中にある開発組織をどう設計するか。

開発者が成長・変化できるための土壌をつくる

Da Vinci Studioを考える際に一番重視したポイントがここです。

例えば会社で一つのアプリケーションのみを開発している場合、仮に個々の機能についてまかせられたとしても、全体の技術選定や設計、あるいはデザインのトンマナなどはトップに近い人間が判断することになります。トップのレベルが全体のキャップになってしまう可能性もありますし、トップのレベルが高かったとすればそれはそれで後進が最終判断をする機会が訪れない可能性もあります。 一方で判断の経験が浅いメンバーにいきなり新規事業等をまかせたとしても、開発プロセスの取捨選択がうまくできず迷走してしまっては誰もメリットがありません。

変化についても同様です。色々な案件を行う受託開発から、一つの開発に集中したいということで事業会社に転職する人もいますし、その逆もいます。 また自身のライフステージが変わったことで取り組む仕事も変えたいという人もいます。 できるだけ長く働いてほしいと考えるのはどこの会社も同じかもしれませんが、本人の志向性の変化は福利厚生だけではカバーできません。

その人のフェーズにあった開発の場が存在することが重要そうですが、どういった場があればよいのでしょうか。

開発プロセスにおける意思決定・判断ができる場を増やす

開発プロセスにおいては大小様々な粒度の意思決定が必要となります。

ひとつのメソッドにどの程度までの責務をもたせるのか、といったことも判断ですし、MVPをどのように定義するのかも判断です。 プロジェクトを進める、あるいは事業開発に近いことをやりたいのであれば、様々な案件で判断の経験を重ねることで引き出しが増えていくでしょうし、 ある特定技術の専門性を高めたい場合、その技術に絡む部分の判断を多くすることで専門性は高まるでしょう。

必ずしもすべての領域において判断ができる必要はなく、一人ひとりで見た場合は、軸となる領域は深まるように、隣接領域には幅が広がるように、 山なりの経験値を得ていくことで、より大きな判断ができるようになるはずです。

開発プロセスにおける取捨選択の精度を高める

判断とは、何かの選択肢を取捨選択することに他なりません。 取捨選択する精度が高まると、開発プロセスの時間が短縮されます。

同じアウトプットにかかる時間が短縮できれば、同じ時間内により多くの試行錯誤を追加して品質をより高めることもできますし、 逆に品質はそのままにアウトプットの数を増やすこともできます。

開発者、あるいは開発組織にとってアウトプットの品質はケースバイケースで一義的に決めるのが難しいポイントですが、 同じアウトプットにかかる時間を短縮することは一義的にプラスに働くことが期待できます。

判断力を高めるために必要なもの

判断判断言っていますが、ランダムに決めてしまえばそれは判断ではなく賭けです。

設計を行う際は、ベストプラクティスとされるものを知っていればそれを利用するべきですし、それがなくとも自身の開発経験から近しいものを組み合わせることで的はずれなことに時間を使うことはなくなるはずです。 何かエラーでハマってしまったという際も、エラーメッセージをちゃんと読むのは当然ながら、そもそもこの構成でXというエラーは起こり得ないので、可能性としてはYかZだ、という風にアタリをつければ、闇雲に色々変えて試すよりはるかに効率が良いはずです。

そのためには

  • スキルセットや知見の幅を広め、あるいは深める
  • それらを活用して判断を行う機会を増やす

ということを念頭に置かなければなりません。

幅を広げるという意味では、幅広い種類の開発案件があると良さそうです。であれば受託開発という形が向いていそうですね。 受託開発の方が新しい技術に取り組みづらい・・というイメージを持つ人もいるかもしれませんが、 自社開発で運用年数が増えると気軽にリプレイスはできませんし、例えばフロントをReactにしたいと思っても事業メリットが無いためそこにリソースを割きづらい・・・ということもあります。 新規で開発する場合はそういった縛りがありません。そのため必然的に新規が増える受託の方が新しい技術を導入しやすいと考えています。 もちろんあまりにピーキーな技術を試すのは憚られますが、それは自社サービスでも同じはずです。

ただこれは表裏一体で、長年の運用を経ることで様々な応用ケースが発生しやすいのは自社サービスです。新規の場合はミニマムで開発する場合が多く、その場合シンプルな構成で済んでしまいます。 それが時間を経ると当初のユースケースでは対応できないためフレームワークの拡張を行ったり自社で何かしら開発したりと複雑になっていきます。そういった場面でしか得られない経験もあるはずです。

受託開発と自社開発の機能を併せ持つ組織

Da Vinci Studioでは冒頭に述べたようにグループ内外の受託と自社開発を行っています。 グループ内事業の既存の年季が入ったサービスもありますし、グループ外で運用されていたシステムを引き継ぐケースもあります。 またグループ内・外においてスクラッチで開発を始めたものもあります。

ただそれらだけだとカジュアルに新規技術を試しづらかったり、あるいはディレクション等まで入る場合にある程度要求レベルも高くなるため(それが良い人ももちろんいます) 自社開発しているものでそこをカバーしています。

例えばリーダーサポートのもと様々な開発案件の実装を行い、その後受託案件で一つのプロジェクトをリードする経験を得る。 あるいはプロジェクト横断で特定の技術領域についてカバーしていく。といった様々な場ができてきます。

自身のロールを宣言する

さらにこういった経験の場を最大限に活用するため、評価制度にも少し工夫があります。

Da Vinci Studioの目標設定フォーマットは

  • 組織の中で自分が解決すべきIssue
  • そのために担うRole
  • 具体的に担えている状態

という3つから構成されており、Approveされた後に毎月これらに対応する形で実績を追加していきます。 なおこれらはGitHub上でプルリクエストを出し、上長でなく全員が見えてコメントできる形をとっています。

初めてこのフォーマットでやるメンバーは面食らう人も多いのですが、それでも出てくる自身のRoleは千差万別です。 例えばRailsについてのスキル・知識を増やすというメンバーもいれば、アプリもサーバーサイドもシームレスに開発できることを目指すメンバーもいます。 しっかりと言葉にすることで、本人だけでなく周囲もそのメンバーの志向性を認識します。 周囲が認識していることで、そのRoleに関連するアサインがされやすくなりますし、相談やトピックも集まりやすくなります。

出る杭は引っこ抜いてエースに

ここまで成長軸を考えているのは、それこそが最終的に開発組織のパフォーマンスを最大化すると考えているためです。 プロダクト開発がうまく進む場合は、組織体制やテクニック的なところももちろんあるでしょうが、やはり中心となるキーマンが存在する場合が多いと考えています。 その人が入るだけで何故かプロジェクトがうまく進む、という人を見かけたことはあるのではないでしょうか。 それはおそらくその人が高い専門性を持ち、固有の専門性を活用して判断できるからだと思います。

もちろん採用で連れてくるという手はあるのですが、そのプロダクトや組織課題にぴったりマッチする人材が見つかることは稀だと思っています。 近しい人は見つかるかもしれませんが、その場合には組織がその人を中心として多少なりとも再構成される必要があります。

つまりいずれにせよ組織は成長・変化できる土壌がなければどこかで行き詰まってしまう可能性が高いということです。

そこで、成長・変化する土壌を最大限に形成するために、あえて既存の開発組織とは別に立ち上げたのです。

実際のところ、当初は実験的な意味合いも含まれていましたが、今となっては当初の規模より3倍ほどに開発者も増え、また様々な開発組織の土壌が混じり合い始めています。 まだまだ課題もありますが、すごい開発者集団になれるように日々頑張っています。

社外へのアウトプットの場をつくる

さてさて場をつくるという意味では、社外へアウトプットしていく経験も重要ですね。 ということでDa Vinci Studio ブログが始まりました。 今後Da Vinci Studioのメンバーが得た経験をどんどんアウトプットしていきたいと思います。

これからどうぞよろしくお願いいたします。