GitHub Actionsで自動リリースノートを作成

# はじめに
この記事は[ディップ株式会社 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dip-dev)の6日目の記事です！
GitHub Actionsを使用し、リリースノートの自動生成ワークフローを構築したので、その方法を紹介します！

# この記事について

みなさんも本番リリースした後にタグ付けを行い、リリースノートを作成することは多いのではないでしょうか？
私は思ったんです。これって煩雑だなって。
ということでこの作業を自動化するようにGitHub Actionsを使って自動で作成しました！
方法としては、本番リリースを行うブランチ（弊チームではmasterブランチ）へのPRにmajor、minor、patchのラベルをつけ、
その情報を基にマージ後に自動でタグをバージョンアップし、リリースノートを自動生成するというものです。

# 環境情報 & 使用ツール

- GitHub Enterprise
- GitHub Actions
- GitHub CLI

# 手順
1. masterブランチへのPR作成時にタグチェック
1. masterブランチへマージ後にタグ付けとリリースノートの自動作成


# masterブランチへのPR作成時にタグチェック

masterブランチへのPRを作成する際には、そのPRにタグのバージョンアップ方法に関する情報を記載する必要があります。
今回はそのPRのラベルに`major`か`minor`か`patch`の3つのラベルを用意して、そのラベルをもとに`major`か`minor`か`patch`のバージョンをあげようと思います。
そのPRに上記の3つのラベルがPRについていなかった場合にはマージできないようにするワークフローを作成しました。

ワークフローのトリガーはPRの作成や閉じるときだけでなく、ラベルを付け替えたときにも起動するようにしています。
また、ラベルをつけていない場合、ラベルを複数つけた場合にはエラーになるようにしています。

```yaml
name: check label of PR to master

on:
  pull_request:
    branches:
      - master
    types:
      - synchronize
      - labeled
      - unlabeled
      - opened
      - reopened

jobs:
  check_label:
    if: github.base_ref == 'master'
    runs-on: [ self-hosted ]
    # secrets.GITHUB_TOKENの権限が大きいので、セキュリティリスク軽減のために制限。
    permissions:
      contents: write
      pull-requests: write
    # デフォルトでは最大6時間実行されるため制限。
    timeout-minutes: 10
    env:
      MASTER_BRANCH: master

    steps:
      - uses: actions/checkout@v3
      - name: gh auth login
        env:
          # secrets.GITHUB_TOKENはworkflowが実施されるたびに自動で生成される
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: echo ${GH_TOKEN} | gh auth login --hostname {ホスト名} --with-token

      - name: check label of PR to master
        # エラーの場合でも次のログアウトの処理を実行。結果ではエラーのマークは出ないので、PRが作成されなかった場合はActionsのページから詳細を確認。
        continue-on-error: false
        env:
          TZ: "Asia/Tokyo"
        run: |
          # forループを使用して配列内の各要素をチェック
          i=0
          items=$(gh pr view ${{ github.event.number }} --json labels -q '.labels[]')
          # 配列をループで処理
          IFS=$'\n' # 改行で区切る
          for item in $items; do
            name=`echo $item | jq -r '.name'`
            echo ${name}
            case "${name}" in
                  "major"|"minor"|"patch")
                          i=`expr $i + 1`
                      ;;
              esac
          done
          if [ ${i} -eq 0 ]; then
              echo "更新するリリースバージョンのlabelを付けてください( major or minor or patch )"
              exit 1;
          fi
          if [ ${i} -gt 1 ]; then
              echo "リリースバージョンのlabelを2つ以上つけないでください"
              exit 1;
          fi
          echo "finish"
      - name: gh auth logout
        # ランナーのインスタンスは他のプロダクトでも使用されているのでログアウトする。
        run: gh auth logout --hostname {ホスト名}
```

# masterブランチへマージ後にタグ付けとリリースノートの自動作成

DIPでは本番環境にリリースされたらタグ付けを行ってリリースノートを作成する必要があります。
しかし、リリースするたびに次のバージョンを入力して、タグを作成し、タグを作成後にリリースノートを作成する。ということを行っているのは煩雑だなと思いました。
そこでこれらもワークフローで自動で行うようにするということを考えました。

```yaml
name: Create Pull requests released contents

on:
  pull_request:
    branches:
      - master
    types: [closed]

jobs:
  create-auto-tag:
    if: github.ref == 'master' && github.event.pull_request.merged == true
    runs-on: [ self-hosted ]
    # secrets.GITHUB_TOKENの権限が大きいので、セキュリティリスク軽減のために制限。
    permissions:
      contents: write
      pull-requests: write
    # デフォルトでは最大6時間実行されるため制限。
    timeout-minutes: 10
    env:
      MASTER_BRANCH: master

    steps:
      - uses: actions/checkout@v3
      - name: gh auth login
        env:
          # secrets.GITHUB_TOKENはworkflowが実施されるたびに自動で生成される
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: echo ${GH_TOKEN} | gh auth login --hostname {ホスト名} --with-token
      
      - name: create release tag
          # エラーの場合でも次のPR作成の処理を実行。結果ではエラーのマークは出ないので、タグが作成されなかった場合はActionsのページから詳細を確認。
        continue-on-error: true
        env:
          # 独自でタグを指定する場合はtrueにする。
          USE_ORIGINAL_TAG: false
          # 独自のタグを指定する場合（例: v2.0.0rc）、もしくは最新のタグをvX.Y.Zの形に直したい場合（例: v2.0.0rc→v2.0.0）に指定。
          NEW_TAG_NAME: "original_tag"
          # major minor patchのいずれかを指定。バージョンを上げるように指定した部分以下は0にする。他のバージョン部分も一緒に変更する際は独自でタグを指定。（例: v1.1.1→v2.2.2）
          # major: メジャーバージョンを上げる。（例: v1.1.1→v2.0.0）
          # minor: マイナーバージョンを上げる。（例: v1.1.1→v1.2.0）
          # patch: パッチバージョンを上げる。（例: v1.1.1→v1.1.2）
          UP_VER: minor
        run: |
          NEW_TAG=${NEW_TAG_NAME}
          # タグの履歴を取得。would clobber existing tagのエラーを避けるため、-fのオプションを追加。
          git fetch --prune --unshallow --tags -f
          # 最新のタグを取得
          NOW_TAG=(`git describe --tags $(git rev-list --tags --max-count=1)`)
          echo "NOW_TAG is ${NOW_TAG}"
          if [ "${USE_ORIGINAL_TAG}" == false ]; then
            # 取得したタグのバリデーション。vX.Y.Zの形になっていることを確認。各数字部分3桁まで対応。
            if [[ ! ${NOW_TAG} =~ ^v[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$ ]]; then
                echo "${NOW_TAG} does not match regular expression ^v[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$"
                exit 1
            fi
            # 先頭のvを取り除く
            MAJOR_VER=(`echo ${NOW_TAG#v}| awk '{split($0, version, "."); print version[1]}'`)
            MINOR_VER=(`echo ${NOW_TAG}| awk '{split($0, version, "."); print version[2]}'`)
            PATCH_VER=(`echo ${NOW_TAG}| awk '{split($0, version, "."); print version[3]}'`)
            UP_VER=""
            items=$(gh pr view ${{ github.event.number }} --json labels -q '.labels[]')
            # 配列をループで処理
            IFS=$'\n' # 改行で区切る
            for item in $items; do
                name=`echo $item | jq -r '.name'`
                case "${name}" in
                    "major"|"minor"|"patch")
                            # 事前のワークフローでチェック済みなのでとりあえず変数を詰める
                            UP_VER=${name}
                        ;;
                esac
            done
            # バージョンを上げるように指定した部分以下は0にする。大文字の指定でも小文字に変換して対応。
            case `echo "${UP_VER}"|tr A-Z a-z` in
                "major")
                  MAJOR_VER=`expr ${MAJOR_VER} \+ 1`
                  MINOR_VER=0
                  PATCH_VER=0
                  ;;
                "minor")
                  MINOR_VER=`expr ${MINOR_VER} \+ 1`
                  PATCH_VER=0
                  ;;
                "patch")
                  PATCH_VER=`expr ${PATCH_VER} \+ 1`
                  ;;
                *)
                  echo "UP_VER is invalid: ${UP_VER}"
                  ;;
            esac
            NEW_TAG=v${MAJOR_VER}.${MINOR_VER}.${PATCH_VER}
          fi
          echo "NEW_TAG is ${NEW_TAG}"
          # 前のタグとの差分を基にリリースノートの自動生成、およびlatestのマークの付与を行う。
          echo `gh pr view ${{ github.event.number }} --json body | jq -r '.body'` >> body.txt
          today=`date +'%Y/%m/%d'`
          gh release create ${NEW_TAG} --notes-start-tag ${NOW_TAG} --latest --target ${{env.MASTER_BRANCH}} -F body.txt -t "${today}リリース"
      - name: gh auth logout
        # ランナーのインスタンスは他のプロダクトでも使用されているのでログアウトする。
        run: gh auth logout --hostname {ホスト名}
```

流れとしては下記になります。
- 現在の最新のタグ名を取得
- PRに貼られているラベル名を取得（major or minor or patch)
- 取得したPRをもとに次のタグ名を作成
    - ex: minorの場合　1.20.1 → 1.21.0
- 作成したタグに対してリリースノートを作成
    - リリースノート内容はmasterへのPRの内容をリリースノートに貼り付けるという処理を行っています。
    - リリースノートのタイトルはリリース日にするというルールがチーム内にあるのでマージ日の日付をタイトルにする

ポイントとしては事前にラベルチェックワークフローを実行しているのでラベルがかならず1つついているという保証ができていることです。
これによって、条件式を追加することなく、ラベルを取得してそのラベル情報をもとにタグのバージョンアップをできるようになります。


# まとめ
今回はGitHub Actionsを用いてリリースノートの自動生成を行う方法について紹介しました。
この方法により、リリース後のタグ付けやリリースノートの作成作業を効率化し、時間とリソースの節約が可能となります。
また、この自動化によって、人的ミスを減少させると同時に、開発チームがより重要なタスクに集中できるようになります。
このワークフローは他の開発プロジェクトでも使えると思いますのでぜひ使ってみてください！