GitHub Checks APIをGitHub Actionsで使う！AWS EventBridgeの結果をGitHub Checksに反映させる！

この記事は[はてなエンジニア Advent Calendar 2021](https://qiita.com/advent-calendar/2021/hatena)の23日目の記事です。

昨日は @polamjag  による[DevTools の Web 技術でできている部分を覗き見る](https://polamjag.hatenablog.jp/entry/2021/12/22/100000)でした。

今回は[Github Checks API](https://docs.github.com/ja/rest/guides/getting-started-with-the-checks-api)を活用して、プルリクエスト上でリリースまでに必要なテストを正常に終えているか確認できるようにするレシピを考えてみました！実際に手を動かして動かしてみたいので、モデルケースを想定し、そのケースに登場するプルリクエストで活用することを考えてみます。今回メインに説明したいのは、`GitHub Actions`周りであるのですが、`AWS EventBridge`に絡めると面白いのでその内容に少し触れています。

## モデルケース: 今回考えるリリースまでの流れ

モデルケースでは`AWS Cloud`にある本番環境にデプロイする1段階前に、本番とほぼ同様の検証環境の`AWS Cloud`上のステージング環境にデプロイして動作確認をするケースを考えます。このモデルケースは以下の流れをたどります。

1. `GitHub`上の特定のブランチ`staging`に対してプルリクエストを立てる
1. ステージング環境に`staging`ブランチの内容をデプロイし、その環境に対して自動テストをする
1. 自動テストの結果が全て正常であれば、メインブランチ`main`に`staging`ブランチをマージする
1. 本番環境に、`main`ブランチの内容をデプロイする

ここで`staging`環境は検証用環境なので開発者以外はアクセスできません。


さらに、`main`ブランチに`staging`ブランチをマージするのは、テストが正常である場合に限りたいので、`GitHub`の[ブランチの保護設定](https://docs.github.com/ja/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule)を有効にします。往々にして、`GitHub`ではプルリクエストのコードに対するテストの実行には[GitHub Actions](https://github.co.jp/features/actions)を活用でき、そのテストが正常な時に限りマージ可能とすることができます。このモデルケースでは`GitHub Actions`で種々のテストを実行しており、どのテストが通ったか通っていないかプルリクエスト上で確認できるものとします。`GitHub Actions`で容易に実施できるテストとして、プルリクエスト上のコードの静的解析や、コードがビルド可能かなどが挙げられます。


> ![スクリーンショット 2021-12-27 23.12.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/2b074a3b-f67a-c793-1002-6b8dd231349d.png)    
> <figcaption>`GitHub Actions`でどのテストが通っているかプルリクエストで見て、全部通っていた場合に限りマージしたい……　https://github.co.jp/features/actions </figcaption>
    

このように`GitHub`はプルリクエストに対するテストの状態を持たせることができ、テストが成功か失敗か実行中か、予定中かなどを表現できます。この状態を[Check Runs](https://docs.github.com/ja/rest/guides/getting-started-with-the-checks-api)といい、各コミットに複数紐づいています。特にプルリクエストの最新のコミットの`Check Runs`はプルリクエストに紐づいています。`Check Runs`はプルリクエストで確認することができるので、そのプルリクエストが妥当か否かを確認するのに便利なのはもちろん、前述の`ブランチの保護`の条件にも利用できます。

ここまでイラストにすると次のように表せます。

![デプロイ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/e3b624e4-a3c0-66ba-d4c9-867727df9b2e.png)

このモデルケースでは、自動テストや`ブランチの保護`を駆使してリリースにおいて本番環境の内容である`main`ブランチが常に正常に動くものであるということ保証しようとしています。

ただし、今回のモデルケースでは次の課題を抱えていると仮定します。

## モデルケースにおける課題

今回は何らかの理由でステージング環境に対する自動テストが`GitHub Actions`で実行ができないと仮定します。例えば、ステージング環境に`GitHub Actions`からアクセスできなかったり、ステージング環境に対する自動テストのコストが`GitHub ACtions`では高い場合などで、この仮定が現実のものになるでしょう。

この仮定のため、`ブランチの保護`ではステージング環境に対する自動テストが通っていなければ`staging`ブランチの`main`ブランチへのマージを不可にすることができないという問題を抱えています。

迂回策として、ステージングの自動テスト結果をエンジニアの目視して、それが良しならばマージをするという手段を取れます。とはいえ、大抵のテストの状態がプルリクエストで確認できる状況ならば、プルリクエストで確認したいところです。

# 解決策

今回試してみる解決策は、`GitHub Actions`上で[GitHub Checks API](https://docs.github.com/ja/rest/guides/getting-started-with-the-checks-api)を利用して`Check Runs`をステージング環境に対するテストに対応するように制御し、`Check Runs`の成功を以ってブランチへのマージが可能となる`ブランチの保護`を有効にすることです。


# 構成

## `Checks Runs`の作成
まずはユーザーが`staging`ブランチのプルリクエストにプッシュした時に、ステージング環境へのテストが保留されている状態になるように`Checks Runs`を更新するところから始めます。

![push時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/8007c979-408e-b0d0-0c2c-d3d2b69d6b54.png)

`GitHub Actions`で特定のブランチへのプッシュ起動で、最新コミットに`Checks Runs`を紐付けて、その状態を保留にするには、[LouisBrunner/checks-action](https://github.com/LouisBrunner/checks-action)を使って次のような`GitHub Actions`を記述します。これによって`staging`ブランチへのプッシュのたびに`staging-test`という名前の`Checks Runs`が作成されます。

```.github/workflows/staging-test-actions.yaml
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](https://docs.github.com/ja/actions/learn-github-actions/events-that-trigger-workflows)を利用できます。今回は[AWS EventBridge](https://aws.amazon.com/jp/eventbridge/)でイベントを受け取り、それをトリガーに`AWS Lambda`を実行します。

![event発生時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/7492c20c-8a7d-da3b-0c5e-4282eaf2f699.png)

まず、`GitHub Actions`側に成功か失敗か受け付ける`repository_dispatch`トリガーを追加し、`Check Runs`を更新できるようにします。更新のために、名前が`"staging-test"`で最新の`Check Runs`のIDを[GitHub Actions Script](https://docs.github.com/ja/rest/guides/getting-started-with-the-checks-api)で記述していることに注目してください。


```.github/workflows/staging-test-actions.yaml
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](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html)を使ってスクリーンショットを撮影するテストとでもしましょう。`Amazon CloudWatch Synthetics`の`Canary`を`staging-snapshot-test`と命名して、これの成功あるいは失敗に反応する`AWS EventBridge`のルールを記載すると以下のようになります。


```rule.json
{
  "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](https://github.com/octokit/octokit.js)を利用し、さらに、[AWS Secrets Manager](https://aws.amazon.com/jp/secrets-manager/)に`GitHub API`を実行可能な`GitHub`のトークンが`"repository-token"`の`TOKEN`フィールドに保存されているものとします。この`Lambda`のトリガーとして先ほど作成した`EventBridge`を設定すると、`CloudWatch Synthetics Canary`が成功、あるいは失敗するたびに、次のコードが実行されます。

```handler.js
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](https://qiita.com/advent-calendar/2021/hatena)の明日は @nabeop  [Hatena Developer Blog 編集部の活動の紹介](https://developer.hatenastaff.com/entry/2021/12/24/090000)です。

今年はえらい大雪でこれ書いているうちも、屋根雪が切れたりしていてえらいことなっております。

**参考文献**

https://aws.amazon.com/jp/eventbridge/

https://aws.amazon.com/jp/secrets-manager/

https://docs.github.com/ja/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule

https://docs.github.com/ja/rest/guides/getting-started-with-the-checks-api

https://github.com/marketplace/actions/github-script

https://github.com/LouisBrunner/checks-action

https://github.com/octokit/octokit.js