GitHub Actionsでリリースノートをいい感じに自動生成する

# 結果から
![スクリーンショット 2023-06-13 15.29.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3467633/9b3b2d64-854b-5746-9d63-3fa1fc3c2c4c.png)
こんな感じのリリースノートが生成されます！

# 前提
- GitHubの[Automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes)という仕組みを使います
- 今回はライブラリのリリースノート自動生成を行います。[セマンティックバージョニング](https://semver.org/lang/ja/)に対応して、リリースノートの内容が変わるようにします。（この辺の独自要件をどうするかは[後半に纏めてます](#packagejsonのバージョンに連動してプルリクエストにラベルを付与したい)）

# Automatically generated release notes とは
[公式ドキュメント](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes)

要約すると、`.github/release.yml`ファイルをリポジトリに置いて、プルリクエストにラベルを貼ればいい感じのリリースノートが作れるぞってことです！

`release.yml`でリリース内容をカテゴリ分けして、対応するラベルを定義します。
実際見た方がわかりやすいので、今回作ったyamlファイルを以下に載せます。

```yaml
changelog:
  categories:
    - title: Breaking Changes 🛠
      labels:
        - Semver-Major
    - title: Exciting New Features 🎉
      labels:
        - Semver-Minor
    - title: Fixes and improvements 🐛
      labels:
        - Semver-Patch
    - title: Dependencies
      labels:
        - dependencies
    - title: Other Changes
      labels:
        - "*"
```
説明すると、一番上の設定はプルリクエストに`Semver-Major`ラベルが貼ってあったら、リリースノートに`Breaking Changes 🛠`セクションを設けて、そのセクション内に対象のプルリクエストを記載するという意味です。

他の設定も同様で、もちろんtitleやlabelsは好きに変更できます。
今回はセマンティックバージョニングに対応させる為にそれっぽい名前にしてます。

ちなみにdependenciesは[dependabot](https://docs.github.com/ja/code-security/dependabot/working-with-dependabot)が生成するプルリクエストに自動で付与されるラベルです。

# プルリクエストにラベルを貼る
`release.yml`が用意できたら、次はラベル貼りです。
手動で貼っても良いのですが、どうせなら自動化したいですよね。

GitHub Actionsから[github-script](https://github.com/actions/github-script)を使うことで実現可能です。
github-scriptは、JavaScriptでGitHub APIを叩けるやつです。

実際作ったyamlファイルはこんな感じ。
```yaml
name: Set Label to Pull Request

inputs:
  semver:
    required: true

runs:
  using: "composite"
  steps:
    - uses: actions/github-script@v6
      with:
        script: |
          const { SEMVER } = process.env
          github.rest.issues.setLabels({
            owner: context.repo.owner,
            repo: context.repo.repo,
            issue_number: context.payload.pull_request.number,
            labels: [SEMVER]
          });
      env:
        SEMVER: ${{ inputs.semver }}
```
いくつか今回の要件に合わせた記法が含まれてますが(※後ほど解説します)
ラベルの付与は`github.rest.issues.setLabels`APIを呼び出せばOK。

APIの呼び出しに使っている`github`は、github-scriptが提供している引数で、デフォルトで使えます。

`setLabels`APIの[リファレンスはこちら](https://docs.github.com/ja/rest/issues/labels?apiVersion=2022-11-28#set-labels-for-an-issue)
なお、`addLabels`という似たAPIがありますが、こちらはその名の通りラベルを追加します。`setLabels`は今付与されているラベルがあったら削除してから貼る挙動。

# リリースノートの生成
今回導入したリポジトリではリリースの作成に[action-gh-release](https://github.com/softprops/action-gh-release)というライブラリを使っていたので、これの`generate_release_notes`という設定を`true`にするだけでした。

他の手段としては、GUI上でリリースを作成する際に`Generate release notes`ボタンを押下するか、`gh release create --generate-notes`コマンドを実行する方法もあるようです。

# package.jsonのバージョンに連動してプルリクエストにラベルを付与したい
さて、ここからが独自要件です。まずは前提から。

- 対象とするリポジトリは、社内で使っている共通ライブラリ
- 一つのリポジトリ内で複数のパッケージを管理している
- セマンティックバージョニングを採用しており、パッケージに何らかの変更を加えたら、package.jsonのversionを手動で変更してプルリクエストに含める運用をしている

ここで改めて`setLabels`を実行するyamlファイルを見てみましょう。
```yaml
name: Set Label to Pull Request

inputs:
  semver:
    required: true

runs:
  using: "composite"
  steps:
    - uses: actions/github-script@v6
      with:
        script: |
          const { SEMVER } = process.env
          github.rest.issues.setLabels({
            owner: context.repo.owner,
            repo: context.repo.repo,
            issue_number: context.payload.pull_request.number,
            labels: [SEMVER]
          });
      env:
        SEMVER: ${{ inputs.semver }}
```

`using: "composite"`という記述がありますね。
これは複数のワークフローで同じアクションを実行したい場合に一連のステップを切り出しておいて、ワークフローから利用することで処理を共通化する仕組みです。
複数のパッケージで使いたかったので、採用しています。

compositeの詳細は[こちら](https://docs.github.com/ja/actions/creating-actions/creating-a-composite-action)。構文の詳細は[こちら](https://docs.github.com/ja/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions)も参照。


`inputs`構文は、他のstep（利用する側のワークフロー）から値を受け取る為に記載しています。
ここでは、ラベルに記載する文字列を受け取っていますね。
この文字列が「Semver-Major」とか「Semver-Minor」になります。
package.jsonのversionが`1.0.0`だったとして、これを`1.0.1`に変えたら、「Semver-Patch」が渡ってくるようにしたいわけです。

では、どうするか。
やるべきことは、以下のフローになります。

1. package.jsonから現在のversionと変更後のversionを取得する
1. それらを比較し、メジャー・マイナー・パッチのどのバージョンが上がっているかを検出する
1. 検出結果から、付与するラベルを決定する

この一連の流れをアクションyamlにすると、以下のようになります。
```yaml
name: utility/version-check

on:
  pull_request:
    branches:
      - main
    paths:
      - "utility/**"

jobs:
  version-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Get new version
        id: new-package-version
        uses: martinbeentjes/npm-get-version-action@v1.3.1
        with:
          path: utility

      - uses: actions/checkout@v3
        with:
          ref: "main"

      - name: Get current version
        id: current-package-version
        uses: martinbeentjes/npm-get-version-action@v1.3.1
        with:
          path: utility

      - uses: actions/checkout@v3

      - name: Check version update
        id: check-version-update
        uses: ./.github/workflows/composite/check-version-update
        with:
          new-version: ${{ steps.new-package-version.outputs.current-version }}
          current-version: ${{ steps.current-package-version.outputs.current-version }}

      - name: Set Label to Pull Request
        uses: ./.github/workflows/composite/set-label
        with:
          semver: ${{ steps.check-version-update.outputs.semver }}
```

実は元々`version-check`という「package.jsonのversionをちゃんとインクリメントしているか」チェックするワークフローが作られており、ラベル付与においてもこの仕組みを利用しました。

`npm-get-version-action`という[ライブラリ](https://github.com/martinbeentjes/npm-get-version-action)で、現在のブランチのpackage.jsonからversionを取得します。

最初は作業ブランチ側のversionを取得して、その後`actions/checkout@v3`の対象を`main`にすることでマージ対象のブランチからversionを取得しているわけですね。

そして、それらを`check-version-update`に渡す。ここでもcompositeを使っています。

その内容がこちら
```yaml
name: check-version-update

inputs:
  new-version:
    required: true
  current-version:
    required: true
outputs:
  semver:
    description: "Semantic Versioning"
    value: ${{ steps.check-version-update.outputs.semver }}
runs:
  using: "composite"
  steps:
    - name: Check version update
      id: check-version-update
      shell: bash
      run: |
        new_version=${{ inputs.new-version }}
        new_ver_array=(${new_version//./ })
        current_version=${{ inputs.current-version }}
        current_ver_array=(${current_version//./ })
        if [ ${new_ver_array[0]} -gt ${current_ver_array[0]} ]; then echo "semver=Semver-Major" >> "$GITHUB_OUTPUT"; exit 0; fi
        if [ ${new_ver_array[0]} -eq ${current_ver_array[0]} ] && [ ${new_ver_array[1]} -gt ${current_ver_array[1]} ]; then echo "semver=Semver-Minor" >> "$GITHUB_OUTPUT"; exit 0; fi
        if [ ${new_ver_array[0]} -eq ${current_ver_array[0]} ] && [ ${new_ver_array[1]} -eq ${current_ver_array[1]} ] && [ ${new_ver_array[2]} -gt ${current_ver_array[2]} ]; then echo "semver=Semver-Patch" >> "$GITHUB_OUTPUT"; exit 0; fi
        echo "Please update version in package.json"
        exit 1
```
bashで`1.0.0`などの文字列をドット区切りで配列にする書き方が珍しいですね。
あとは条件分岐でラベルに使いたい文字列を出力してます。`echo "semver=Semver-Major" >> "$GITHUB_OUTPUT";`とかがそれです。

stepの出力仕様については[こちら](https://docs.github.com/ja/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)

ここまで出来たら、あとは度々掲示している`setLabels`を実行しているアクションと繋いで完了です！

# 感想
リリースノートの自動生成を行う技術記事は他にも色々あったものの、GitHub Actionsの更新が多いからか記述方法が古かったり、そもそもやり方が違うものも多い状況でした。
ということで本記事を書いてみましたが、これもすぐに古くなるんだろうなあ。。

# 参考
[actions/github-script と composite アクションでカスタムアクションをかんたんに作れる](https://gotohayato.com/content/557/)
[GitHubのリリースノートを自動化する仕組み](https://efcl.info/2023/03/11/auto-release-note/)