放置されたPRを自動的に閉じるGithubActionsを作成する

更新日から1週間以上経過したPR（Pull Request）を自動的に閉じるGithubActionを作成しました。

https://github.com/matometaru/pr-deadline

## なぜ作ったか

長期間放置されているPRがあり、スッキリさせたかったから。

## 期待される効果

PRの生存期限を設けることで以下の効果を期待しています。

|問題  |期待される効果  |
|---|---|
|属人化による、レビュワー不足によるPR放置  |ペアプロ |
|PRの単位が大きくレビュワーへの負担よるPR放置  |小さな単位でのPRを意識 |
|タスクの優先度が変わることによるPR放置  |今必要ないPRはClose  |

## 作る

- リポジトリにある更新してからX日以上経ったPRをクローズする
- クローズしたPull Requestを標準出力で返す

作る前にGithubActionsを探してみましたが、なかったので作ります。
[GitHub Marketplace · Actions to improve your workflow · GitHub](https://github.com/marketplace?type=actions&query=pr)

### テンプレ

今回はTypeScriptのテンプレートを使わせていただきました。

- https://github.com/actions/typescript-action
- https://github.com/actions/javascript-action

テンプレートに加え、以下のライブラリを追加しています。

|ライブラリ  |概要  |
|---|---|
|@octokit/rest  |GithubのRestAPI Client  |
|jest-date-mock  |Dateのモック（テストで使用） |

### 処理の流れ

入力: トークン（リポジトリのアクセストークン）
入力: 有効期限（デフォルトは7日）
↓
更新日から7日以上経過したPRを取得
↓
取得したプルリクエストをClose
↓
出力: メッセージ
出力: プルリクエストのリスト（Json.stringfyで文字列で出力）

https://github.com/matometaru/pr-deadline/blob/master/src/main.ts#L7-L21

### 作成中に悩んだ箇所

#### テストでモックが効かない？

[child_process.execSync](https://nodejs.org/api/child_process.html#child_process_child_process_execfile_file_args_options_callback) でNodeからshellコマンドを実行できるんですが、以下のようなモックを使ったテストは失敗します。

``` main.test.ts
test('test runs', async () => {
  jest.spyOn(githubService, 'getDeadlinePullRequest').mockImplementation(() => {
    return Promise.resolve([
      {
        number: 1,
        title: 'テスト1',
        htmlUrl: 'http://test1.com'
      },
    ])
  })
  jest.spyOn(githubService, 'closePullRequests').mockImplementation(() => {
    return Promise.resolve()
  })

  const ip = path.join(__dirname, '..', 'lib', 'main.js')
  const options: cp.ExecSyncOptions = {
    env: process.env
  }
　// Command failed: node xxx/main.js
  console.log(cp.execSync(`node ${ip}`, options).toString())
})
```

xxx/main.jsを実際に叩いているので、jestのモックは無効となります。

#### 配列の出力について

setOutputの第二引数は`string`なので、配列を返すため`JSON.stringify`で文字列にして出力した後、使う側でparseして使うことを考えました。

```
core.setOutput('closedPulls', JSON.stringify(deadlinePullList))
```

ただ使う側の[Slack Notify](https://github.com/marketplace/actions/slack-notify)ではそのような入力は受け付けていません。

また、GithubActionにもループ処理の構文がありませんでした。

https://help.github.com/ja/actions/reference/context-and-expression-syntax-for-github-actions

仕方なくCloseしたPull Requestのリストをメッセージとして結合して返すことにしました。

``` main.ts
let message = "";
for (const pull of deadlinePullList) {
  message += `* [${pull.title}](${pull.htmlUrl})\n`
}

core.setOutput('closedPulls', JSON.stringify(deadlinePullList))
core.setOutput('message', message)
```

## 使用例

毎日09:00にcronで実行。クローズされたPRがあればSlackに通知。

``` /.github/workflows/pr.yml
name: Close PR by deadline and slack notification
on:
  schedule:
  # UTC
  - cron: '0 0 * * *'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Close PR by deadline
        uses: matometaru/pr-deadline@1.0.0
        with:
          token: ${{ secrets.PR_TOKEN }}
          expirationDate: 7
        id: prDeadline
      - name: Slack Notification
        if: steps.prDeadline.outputs.closedPulls != '[]'
        uses: rtCamp/action-slack-notify@master
        env:
          SLACK_CHANNEL: self
          SLACK_ICON: https://github.com/rtCamp.png?size=48
          SLACK_MESSAGE: ${{ steps.prDeadline.outputs.message }}
          SLACK_TITLE: pr-deadline
          SLACK_USERNAME: rtCamp
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
```

![スクリーンショット 2020-04-12 20.29.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510204/72442eea-c3c5-3237-6455-352f9f140228.png)


## 参考

https://help.github.com/ja/actions/reference/context-and-expression-syntax-for-github-actions
https://octokit.github.io/rest.js/v17