GitHub Actions でプルリクエストがコンフリクトしたらSlackで通知する

## はじめに

プルリクエストがコンフリクトしてマージができなくなったときにSlackに通知してくれるGitHub Actionsを作成しました。

## 動作

毎朝8時に、リポジトリのプルリクエストの中でコンフリクトが発生してマージができない状態になっているものがあれば、画像のようにSlackのチャンネルにURLを通知します。
![2023-02-23_14h46_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1062427/267a10f8-0e0e-a5b1-dd30-bb499b46423a.png)

マージできないプルリクエストがない場合は通知しません。

## 作成したYAMLファイル
以下のページのコードをほぼほぼお借りして作成しました。
https://xuwei-k.hatenablog.com/entry/2021/12/30/164125
元のコードはユーザーのプルリクエストを取得していますが、リポジトリのプルリクエストを取得するように変更しています。

以下のyamlファイルを.githubフォルダ以下に配置します。
リポジトリのプルリクエストを取得してくるため、上記のGitHub Actionsは次の章で示すSlackで通知するための設定を実施すれば動くはずです。

```yaml
name: PullRequest_Conflict
on:
  schedule:
  - cron: '0 23 * * *'
  workflow_dispatch:
jobs:
  check-conflict:
    timeout-minutes: 2
    runs-on: ubuntu-latest
    steps:
    - uses: actions/github-script@v6
      id: conflict_pull_requests
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}
        script: |
            const pulls = await github.rest.pulls.list({
              owner: context.repo.owner,
              repo: context.repo.repo,
            });
            let pullsCount = pulls.data.length;
            let mergeableCount = 0;
            let unmergeableCount = 0;
            const conflicts = [];
            for (const pull of pulls.data) {
                let pull_request = await github.rest.pulls.get({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  pull_number: pull.number
                });
                while(pull_request.data.mergeable === null){
                    pull_request = await github.rest.pulls.get({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    pull_number: pull.number
                  });
                }
               if (pull_request.data.mergeable === false) {
                  conflicts.push(pull.html_url);
                  unmergeableCount++;
                }
                else{
                  mergeableCount++;
                }
              }
             let summary = "";
             if(conflicts.length == 0) {
              core.setOutput('value', '');
              summary += pullsCount + "件のプルリクエストにコンフリクトがありませんでした。";
              } else {
              core.setOutput('value', '<!channel>\\n' + pullsCount + '件のプルリクエストのうち、以下がマージできないので、コンフリクトを解消してください\\n' + conflicts.join("\\n"));
              summary += pullsCount + '件のプルリクエストのうち、以下がマージできないので、コンフリクトを解消してください\\n' + conflicts.join("\\n");
              }
              await core.summary
              .addHeading('conflict report')
              .addRaw(summary)
              .write()
    - uses: slackapi/slack-github-action@v1.16.0
      if: steps.conflict_pull_requests.outputs.value != ''
      with:
        payload: "{\"text\":\"${{ steps.conflict_pull_requests.outputs.value }}\"}"
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.NOTIFICATION_PULLREQUEST_CONFLICT }}

```
### マージできないプルリクエストのURLを取得する方法

`script:`内でやっていることは以下です。
1. リポジトリのプルリクエスト一覧を取得
1. プルリクエスト一覧から各プルリクエストを取得し`mergeable`ステータスが`false`であればプルリクエストのURLを`conflicts`配列に追加
1. `conflicts`配列が空でない(=マージできないプルリクエストが1つ以上存在する)場合は、説明文+配列の要素を改行区切りで表示するメッセージを出力する

プルリクエストは`mergeable`属性を持っているため、これを利用してプルリクエストがマージできる状態か否かを判定しています。
ただし、`mergeable`属性は取得に時間がかかるため、取得した`mergeable`が`null`の場合はループして再取得するようにしています。
(参考：https://docs.github.com/ja/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests)

```yaml
while(pull_request.data.mergeable === null){
    pull_request = await github.rest.pulls.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    pull_number: pull.number
    });
```

### Slackで表示するメッセージの設定

`conflicts`配列が空でない(=マージできないプルリクエストが1つ以上存在する)場合に、説明文+配列の要素を改行区切りで表示するメッセージを出力しています。
`<!channel>`は、チャンネルに`@channel`でメンションするためのSlack apiの記法です。
(参考:https://api.slack.com/reference/surfaces/formatting#special-mentions)

```yaml
if(conflicts.length == 0) {
              core.setOutput('value', '');
              summary += pullsCount + "件のプルリクエストにコンフリクトがありませんでした。";
              } else {
              core.setOutput('value', '<!channel>\\n' + pullsCount + '件のプルリクエストのうち、以下がマージできないので、コンフリクトを解消してください\\n' + conflicts.join("\\n"));
              summary += pullsCount + '件のプルリクエストのうち、以下がマージできないので、コンフリクトを解消してください\\n' + conflicts.join("\\n");
              }
```

`if:`で、`conflicts`配列が空でない場合のみSlackで通知する条件文を記述しています。
`payload:`に出力するメッセージを設定しています。
`SLACK_WEBHOOK_URL`には、次の章で説明するWebhook URLから作成したシークレットの名前を指定します。
```yaml
- uses: slackapi/slack-github-action@v1.16.0
  if: steps.conflict_pull_requests.outputs.value != ''
  with:
    payload: "{\"text\":\"${{ steps.conflict_pull_requests.outputs.value }}\"}"
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.NOTIFICATION_PULLREQUEST_CONFLICT }}

```

### 定期実行の設定

定期実行の設定は以下の部分で行っています。
`cron:`に実行したい日時をUTC(日本時間-9h)で設定します。
(今回は午前8時に動いてほしいため、時間を23で設定しています。)
```yaml
on:
  schedule:
  - cron: '0 23 * * *'
  workflow_dispatch:
```
(参考：https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows#schedule)
なお、`workflow_dispatch:`を記述しているので、手動でも実行できます。

## Slackで通知するための設定

Slackでの通知にはIncomming Webhookを利用します。
任意のチャンネルにIncomming Webhook を追加し、Webhook URLを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1062427/2a998457-b799-6591-3d4c-bf3d1d898c1d.png)

リポジトリの[Setting]-[Secrets and variables]-[New repository secret]からシークレットを作成します。
[Name]にはYAMLファイルで使う際の名前(上のコードでは `NOTIFICATION_PULLREQUEST_CONFLICT`としています)を、
[Secret]には先程取得したWebhook URLを設定します。
![2023-02-22_16h18_32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1062427/adfe9acf-eb3c-086c-0e9f-733213854753.png)

Slack側に通知するための準備は以上です。

## おわりに

`script:`の記述はJavaScriptなので、あまり書いたことがなく苦労しました。
ただ、これでプルリクエストがコンフリクトしてもすぐに気づけるようになるので、コンフリクトをほったらかして解消にいっぱい時間がかかる……ということが無くなりそうで良かったです。