GHECが使えるようになったのでActionsで作業効率化を図った話

# はじめに
こんにちは！ドワンゴ マネタイズプラットフォーム事業部所属のアズタケです。
主に、ニコニコの新しい購読システム（ [定期お支払いの管理](https://subs.nicovideo.jp/my/subscriptions) ）を開発しています。
この記事は [ドワンゴ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dwango) の 3日目の記事です。日が過ぎても空いてたのでどうせなので投稿してみました。
よければ他の記事もご覧ください！

# 今回紹介するリポジトリ
https://github.com/dwpayment/generate-release-notes
(まだREADMEまともに書いてないのはご愛敬…）
以下みたいなのが表示できるようになります。
![chrome_Zr8um0GN3T.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248339/59f45833-2796-603b-0d03-0a09f534fa5c.png)

# きっかけ
マネタイズプラットフォーム事業部では、今年からGitHub Enterprise Cloudにプロジェクトを移行していっています。
それに伴い、GitHub Actionsを活用できるようになったので、この機に作業効率化しようと思って挑戦したのが発端でした。
(これまではActionsが無効化されたGitHub Enterprise Serverを用いていました…)
ドワンゴではリリース時に必ず**承認権限保持者**（大抵マネージャとかリーダーとか）が承認することになっています。特に課金系サービスでは慎重になる必要があり、
そのため投入するリリース物を一覧化するなど、承認者が承認しやすい書類をリリースの度に作る必要がありました。
しかしリリースの度にPR全てを列挙するのって以外に大変で…負担軽減のためにリリースの簡略化することにしました。

# GitHub Actions
GitHub ActionsをCI/CDとして活用する記事はよくありますが、実はActionはJavaScriptで組むことが出来ます。
GitHub Actionsのワークフローはシェルコマンドの実行が出来るのでそれで行うことも可能なんですが、普段使い慣れてる言語で書けるのでビジネスロジックの実装にめちゃくちゃ便利でした。
しかもテンプレートをGitHubが公開してくれており、それをベースに作ることが出来ます。dependabotまで構成してくれてるのでマジで便利。

# Actionの基本的な書き方

まずAcionの定義である `action.yml` にどんなパラメータがあるのかなど指定する必要があります。
今回のリリース自動生成Actionだとどの差分を取るか指定するためにbaseブランチとheadブランチの名前を指定するようにしています。

```yml:action.yml(抜粋)
inputs:
  base:
    required: true
    description: "base branch name"
  head:
    required: true
    description: "head branch name"
  token:
    description: "GITHUB_TOKEN or a repo scoped PAT."
    default: ${{ github.token }}
```

`action.yml` で指定したinputはコード上では以下のように取得することが出来ます。

```main.ts
const base = core.getInput("base", { required: true })
const head = core.getInput("head", { required: true })
const token = core.getInput("token", { required: true })
```

`token` というパラメータを指定していることに気が付くかもしれませんが、githubではワークフローで `${{ github.token }}` とすることでaction用のトークンを取得することが出来ます(`GITHUB_TOKEN` 環境変数でも出来るらしい)。
デフォルト値としてこれを指定しておくことでActionを呼び出すだけでGitHubのAPIが呼び出せるので便利です。

ここで取得したトークンを使って以下のようにすることでAPIクライアントを取得できます

```main.ts
const kit = github.getOctokit(token)
```

このAPIクライアントを使って以下のようにするだけで超簡単にissueを取得できます。

```main.ts
await kit.rest.issues.get({
  repo: {
      owner: "dwpayment",
      repo: "hogerepo",
  },
  issue_number: 11
})
```

ちなみにGitHub Actionsは**実行時に何のissueがトリガーになった**とか、**今のリポジトリが何か**などコンテキストも一緒に付与してくれており、これはJSで実装したActionでも以下のようにして受け取ることが出来ます。

```main.ts
const { repo, issue } = github.context
await kit.rest.issues.get({
  ...repo,
  issue_number: issue.number
})
```

めちゃくちゃシンプルになりますね！
かなり色んなAPIがそのまま触れるので、これらを使って実装していくことになります。

# Gitの差分の取得

都度baseブランチとheadブランチを比較してしまうと、例えばbaseとheadが同じになった時にリリースノートが空になってしまう問題があるのでとりあえず先にbaseブランチのコミットハッシュを取得します。

Actionからシェルスクリプトを実行することも可能で、これらは

```ts
import * as exec from "@actions/exec"
```

に各種関数があります。今回のリポジトリではutility関数を作成して、それを呼び出して実行するようにしています。

```rtelease-notes.ts
await execute(`git rev-parse ${base}`)
```

コミットハッシュを取ったら、git logからマージブランチだけを取り出します。
（正規表現とかはJS側でやればいいんだけど、当初シェルスクリプトだけで作ろうとした名残で全部シェルコマンドになってます…）

```release-notes.ts
const numbers = (
await execute(
  `/bin/bash -c "git log --merges ${baseCommitId}..${head} --first-parent --grep='Merge pull request #' --format='%s' | sed -n 's/^.*Merge pull request #\\s*\\([0-9]*\\).*$/\\1/p'"`
)
).split("\n")
```

これで各種PR番号が取れるので、全てのPRをAPIから引っ張ってきます。

```release-notes.ts
const prs = (
await Promise.allSettled(
  numbers
    .map(num => Number(num))
    .map(async num =>
      kit.rest.pulls.get({
        ...repo,
        pull_number: num
      })
    )
)
)
.filter(pr => pr.status === "fulfilled")
.map(pr => {
  if (pr.status === "rejected") throw new Error() // HACK
  return pr.value
})
```

あとはPRに付いたラベルを見て分類するだけ！

```release-notes.ts
const bugs = prs.filter(pr => pr.data.labels.some(l => l.name === "bug"))
```

# コメントを一つだけ付けて以降同じコメントを更新するようにしたい

ここまでで紹介した `kit.rest ...` を使うことでissueにコメントをぶら下げるだけならとても簡単に出来るようになります。
しかし、**Action自体で状態を持たせることは出来ない**ため、例えばupsert的な動作（無ければ新規投稿し、あれば更新する）をさせるには工夫が必要です。

リリースの予定を立てた後に行う結合的な動作確認中にバグを見つけることは稀にあるのですが、**出来ればそういったものもリリースノートに乗っていてほしい**わけです。
（実はこれまで、そういったものは漏れていた場合がありました…。）

完全に投稿する内容が固定値であればそれをキーにする手がありますが、表示内容でマッチするの何か気持ち悪いですよね（~~私だけじゃないよね？？？~~）。

実はあまり知られていませんが、**IssueコメントにはHTMLコメントを使うことが出来ます**。
実際にページ上に表示されないため変なシグネチャを表示する必要も無く、とても便利で、私が普段実装する時、これをキーにする場合が多いです。

```test-procedure-comment.ts
const signature = "<!-- test procedure DO NOT REMOVE THIS LINE -->"

const commented = comments.find(
c => c.user?.type === "Bot" && c.body?.includes(signature)
)
```

`!commented` ならコメント投稿、そうでなければ更新するようにすれば以下のようにブランチに変化があったら自動で更新されるように出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248339/4d5d60ab-7aa8-0895-b291-d59172bb8ea0.png)

# まとめ
このActionを実装したことで、これまで1PRのデプロイでも超急いで１５分ぐらいはかかっていたリリース作業が、複数PRがあってもボタンポチポチで１分もあれば出来るようになりました。

他にも、これまでは手作業でチケット起票・マージ用のPR作成を行っていたものに関しても自動化しており、これによってリリースサイクルを高めることが出来ています。
このリポジトリではdependabotを構成しているので必然的にPR数も多くなるのですが、負担無く対応できています！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248339/49c947f9-d0fa-dbd7-41fc-7b9f6ceea375.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248339/2d205acc-efd8-6e4c-fb98-b2a5941476bc.png)

マネタイズプラットフォーム事業部ではGitHub Actionsを使って積極的に開発効率化を図っています。
他にもk8sでactions runnerを立てて社内に最適化したランナーの稼働なども行っていますので、機会があったらまた記事にしようと思います。

# 宣伝
株式会社ドワンゴでは、様々なサービスやコンテンツを一緒に作るメンバーを募集しています！
ドワンゴが興味ある方、応募しようか悩んでいる方がいたら気軽にご応募お待ちしています！
* [新卒採用はこちら](https://dwango.snar.jp/index.aspx)
* [キャリア採用はこちら](https://hrmos.co/pages/dwango)