この記事ははてなエンジニア Advent Calendar 2021の23日目の記事です。
昨日は @polamjag によるDevTools の Web 技術でできている部分を覗き見るでした。
今回はGithub Checks APIを活用して、プルリクエスト上でリリースまでに必要なテストを正常に終えているか確認できるようにするレシピを考えてみました!実際に手を動かして動かしてみたいので、モデルケースを想定し、そのケースに登場するプルリクエストで活用することを考えてみます。今回メインに説明したいのは、GitHub Actions周りであるのですが、AWS EventBridgeに絡めると面白いのでその内容に少し触れています。
モデルケース: 今回考えるリリースまでの流れ
モデルケースではAWS Cloudにある本番環境にデプロイする1段階前に、本番とほぼ同様の検証環境のAWS Cloud上のステージング環境にデプロイして動作確認をするケースを考えます。このモデルケースは以下の流れをたどります。
- 
GitHub上の特定のブランチstagingに対してプルリクエストを立てる - ステージング環境に
stagingブランチの内容をデプロイし、その環境に対して自動テストをする - 自動テストの結果が全て正常であれば、メインブランチ
mainにstagingブランチをマージする - 本番環境に、
mainブランチの内容をデプロイする 
ここでstaging環境は検証用環境なので開発者以外はアクセスできません。
さらに、mainブランチにstagingブランチをマージするのは、テストが正常である場合に限りたいので、GitHubのブランチの保護設定を有効にします。往々にして、GitHubではプルリクエストのコードに対するテストの実行にはGitHub Actionsを活用でき、そのテストが正常な時に限りマージ可能とすることができます。このモデルケースではGitHub Actionsで種々のテストを実行しており、どのテストが通ったか通っていないかプルリクエスト上で確認できるものとします。GitHub Actionsで容易に実施できるテストとして、プルリクエスト上のコードの静的解析や、コードがビルド可能かなどが挙げられます。
`GitHub Actions`でどのテストが通っているかプルリクエストで見て、全部通っていた場合に限りマージしたい…… https://github.co.jp/features/actions
このようにGitHubはプルリクエストに対するテストの状態を持たせることができ、テストが成功か失敗か実行中か、予定中かなどを表現できます。この状態をCheck Runsといい、各コミットに複数紐づいています。特にプルリクエストの最新のコミットのCheck Runsはプルリクエストに紐づいています。Check Runsはプルリクエストで確認することができるので、そのプルリクエストが妥当か否かを確認するのに便利なのはもちろん、前述のブランチの保護の条件にも利用できます。
ここまでイラストにすると次のように表せます。
このモデルケースでは、自動テストやブランチの保護を駆使してリリースにおいて本番環境の内容であるmainブランチが常に正常に動くものであるということ保証しようとしています。
ただし、今回のモデルケースでは次の課題を抱えていると仮定します。
モデルケースにおける課題
今回は何らかの理由でステージング環境に対する自動テストがGitHub Actionsで実行ができないと仮定します。例えば、ステージング環境にGitHub Actionsからアクセスできなかったり、ステージング環境に対する自動テストのコストがGitHub ACtionsでは高い場合などで、この仮定が現実のものになるでしょう。
この仮定のため、ブランチの保護ではステージング環境に対する自動テストが通っていなければstagingブランチのmainブランチへのマージを不可にすることができないという問題を抱えています。
迂回策として、ステージングの自動テスト結果をエンジニアの目視して、それが良しならばマージをするという手段を取れます。とはいえ、大抵のテストの状態がプルリクエストで確認できる状況ならば、プルリクエストで確認したいところです。
解決策
今回試してみる解決策は、GitHub Actions上でGitHub Checks APIを利用してCheck Runsをステージング環境に対するテストに対応するように制御し、Check Runsの成功を以ってブランチへのマージが可能となるブランチの保護を有効にすることです。
構成
Checks Runsの作成
まずはユーザーがstagingブランチのプルリクエストにプッシュした時に、ステージング環境へのテストが保留されている状態になるようにChecks Runsを更新するところから始めます。
GitHub Actionsで特定のブランチへのプッシュ起動で、最新コミットにChecks Runsを紐付けて、その状態を保留にするには、LouisBrunner/checks-actionを使って次のようなGitHub Actionsを記述します。これによってstagingブランチへのプッシュのたびにstaging-testという名前のChecks Runsが作成されます。
name: staging-test-actions
on:
  push:
    branches:
      - staging
jobs:
  # プッシュされた時はqueueにする
  init:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2.3.4
      - uses: LouisBrunner/checks-action@v1.1.1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          name: staging-test
          status: queued
Checks Runsの更新
次にGitHub Actions外でステージング環境の自動テストの実行が完了次第、成功か失敗かの結果を先ほど作ったChecks Runsに反映させます。GitHub Actionsを外部から実行するにはrepository_dispatchを利用できます。今回はAWS EventBridgeでイベントを受け取り、それをトリガーにAWS Lambdaを実行します。
まず、GitHub Actions側に成功か失敗か受け付けるrepository_dispatchトリガーを追加し、Check Runsを更新できるようにします。更新のために、名前が"staging-test"で最新のCheck RunsのIDをGitHub Actions Scriptで記述していることに注目してください。
name: staging-test-actions
on:
  push:
    branches:
      - staging
  repository_dispatch:
    types: [success, failure]
jobs:
  # プッシュされた時はqueueにする
  init:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2.3.4
      - uses: LouisBrunner/checks-action@v1.1.1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          name: staging-test
          status: queued
  # プッシュではない時の処理
  update_checks:
    if: github.event_name != 'push'
    runs-on: ubuntu-latest
    steps:
      # 最新のCheck RunsのIDを取得する
      # 最新のCheck RunsのIDは${{steps.get-staging-test-check-runs-id.outputs.result}}にstring形式で格納される
      - uses: actions/checkout@v2.3.4
      - name: Return issues
        uses: actions/github-script@v2
        id: get-staging-test-check-runs-id
        with:
          script: |
            // github-scriptでnameがstaging-testのCheck RunsのIDの一覧を新順で取得する
            const list = await github.checks.listForRef({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    ref: 'staging',
                    check_name: 'staging-test',
                    filter: 'latest'
                });
            return list.data.check_runs[0].id;
          result-encoding: string
      # repository_dispatchの結果を最新のCheck Runsに反映する
      - uses: LouisBrunner/checks-action@v1.1.1
        if: github.event_name == 'repository_dispatch'
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          check_id: ${{steps.get-staging-test-check-runs-id.outputs.result}}
          status: completed
          # successならばsuccessにし、それ以外はfailureにする
          conclusion: ${{ (github.event.action == 'success' && 'success' ) || 'failure'  }}
GitHub Actionsのrepository_dispatchトリガー
次に、GitHub Actionsのrepository_dispatchトリガーを呼び出します。少しAWS EventBridgeに絡めて具体的に考えてみます。今回のステージング環境への自動テストをAmazon CloudWatch Syntheticsを使ってスクリーンショットを撮影するテストとでもしましょう。Amazon CloudWatch SyntheticsのCanaryをstaging-snapshot-testと命名して、これの成功あるいは失敗に反応するAWS EventBridgeのルールを記載すると以下のようになります。
{
  "detail-type": [
    "Synthetics Canary TestRun Successful",
    "Synthetics Canary TestRun Failure"
  ],
  "source": [
    "aws.synthetics"
  ],
  "detail": {
    "canary-name": [
      "staging-snapshot-test"
    ]
  }
}
最後に、EventBridgeで渡ってきた成功失敗かの情報をGitHub Actionsに投げるLambdaを記述すればシステムの完成です。次のコードでは、GitHub APIのクライアントライブラリとして@octkit/restを利用し、さらに、AWS Secrets ManagerにGitHub APIを実行可能なGitHubのトークンが"repository-token"のTOKENフィールドに保存されているものとします。このLambdaのトリガーとして先ほど作成したEventBridgeを設定すると、CloudWatch Synthetics Canaryが成功、あるいは失敗するたびに、次のコードが実行されます。
const AWS = require("aws-sdk");
const { Octokit } = require("@octokit/rest");
const SecretId = "repository-token";
const region = "ap-northeast-1";
const owner = "repository-owner";
const repo = "SomeRepository";
exports.handler = async (event) => {
  const detailType = event["detail-type"];
  // Secret ManagerからGitHubのToken取得
  const client = new AWS.SecretsManager({ region });
  const data = await client.getSecretValue({ SecretId }).promise();
  const auth = JSON.parse(data.SecretString).TOKEN;
  const octokit = new Octokit({ auth });
  const getResult = async () => {
    const event_type =
      event["detail-type"] === "Synthetics Canary TestRun Successful"
        ? "success"
        : "failure";
    return await octokit.rest.repos.createDispatchEvent({
      owner,
      repo,
      event_type,
    });
  };
  const { status } = await getResult();
  const body = JSON.stringify({ status, detailType });
  const response = {
    statusCode: 200,
    body,
  };
  console.log(body);
  return response;
};
これで、stagingブランチにプッシュされた後に、ステージング環境への自動テストたるCloudWatch Synthetics Canaryが実施されない限り、テストステータスが保留となります。また、CloudWatch Synthetics Canaryが実施され、それが成功か失敗すると、プルリクエストに結果が反映されるようになりました。ここで、ブランチの保護を有効にすると、晴れてmainブランチにテストが失敗するstagingブランチをマージ不可にできます。
ここまでで、表題のGitHub Checks APIをGitHub Actionsで使う!AWS EventBridgeの結果をGitHub Checksに反映させる!を実現できました。
終わりに
はてなエンジニア Advent Calendar 2021の明日は @nabeop Hatena Developer Blog 編集部の活動の紹介です。
今年はえらい大雪でこれ書いているうちも、屋根雪が切れたりしていてえらいことなっております。
参考文献



