⚗️

【Github Actions】リリースノートとタグを自動生成する

2024/09/25に公開

はじめに

Rehab for JAPAN フロントエンドエンジニアの @tara_is_ok です!🧢
私が所属しているチームではリリースを行う際、リリースノートを毎回作成しています。
以前はリポジトリのメインページから Releases をクリック → タグを生成 → リリースノートを記入・・・など手動で行っている、かつリポジトリが複数あるためかなり手間がかかっていました。(作成フロー)

今回はそれを解消するため、リリースノートとタグを自動生成する方法を紹介していきます 🫶

実現したいこと

  • 😙 main ブランチにマージ後に自動でタグとリリースノートが生成されること
  • 😙 リリースに含まれる PR の一覧が表示されること
    • 😙 ラベルに応じてカテゴライズ出来ること
  • 😙 リリースノートに Summary 的な内容が追加出来ること
  • 🥶 release-drafter などでリリースノートの Draft を生成する
    • 基本的に人の手を使わない方針
  • 🥶 手動でリリースノートを作成、タグを切る

成果物

generated-sample
生成サンプル

1.タグとリリースノートの自動生成

リリースノートとタグを自動生成する(.github/workflows/release.yml)

処理の流れは次の通りです

  1. main へ PR がマージされる
    • マージされずに close された時はスキップ
  2. 前回のリリースタグを取得する
  3. タグを生成する「{YYYY.MM.DD}-{count}」
    • 同日の場合 → count + 1
  4. main への PR の body を取得する
    • body がない場合はスキップ
  5. 前回リリースからの差分をもとに、変更点を取得する
    • 1 でラベルがつけられた PR をカテゴライズする(.github/release.yml)
  6. リリースノートの本文を生成する
    • 4, 5 で取得したテキストをマージする
  7. リリースノートを作成する
.github/workflows/release.yml
name: Create release tag and release note.

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

jobs:
  create-release-tag:
    # PRがマージされたときのみ実行
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest

    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      TZ: "Asia/Tokyo"

    steps:
      - uses: actions/checkout@v3

      # 前回のリリースタグを取得する
      - name: Get previous tag
        id: pre_tag
        run: |
          echo "pre_tag=$(curl -H 'Accept: application/vnd.github.v3+json' -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name)" >> $GITHUB_OUTPUT

      # タグを生成する 「{YYYY.MM.DD}-{当日リリース回数}」
      - name: Generate tag
        id: release_tag
        run: |
          today=$(date +'%Y.%m.%d')
          pre_release_date=$(echo ${{ steps.pre_tag.outputs.pre_tag }} | awk -F'-' '{print $1}')
          pre_release_count=$(echo ${{ steps.pre_tag.outputs.pre_tag }} | awk -F'-' '{print $2}')
          if [[ ! $pre_release_date = $today ]]; then
            pre_release_count=0
          fi
          echo "release_tag=$today-$(($pre_release_count + 1))" >> $GITHUB_OUTPUT

      # PRのDescriptionを取得しマークダウン形式に変換する
      - name: Get pr description
        id: pr_description
        run: |
          echo "pr_description=$(curl -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
            'https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number}}' \
            | jq .body | awk '{if ($0 == "null") print ""; else print}')" >> $GITHUB_OUTPUT

      # 前回リリースからの差分をもとに、変更点を取得する
      - name: Generate release note changes
        id: changes
        run: |
          echo "changes=$(
          curl -X POST \
            -H 'Accept: application/vnd.github.v3+json' \
            -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' \
            https://api.github.com/repos/${{ github.repository }}/releases/generate-notes \
            -d '{
              "tag_name": "${{ steps.release_tag.outputs.release_tag }}",
              "previous_tag_name": "${{ steps.pre_tag.outputs.pre_tag }}",
              "target_commitish": "main"
            }' | jq .body
          )" >> $GITHUB_OUTPUT

      # リリースノートの本文を作成する
      - name: Create release note body
        id: release_note_body
        run: |
          echo "release_note_body=$(echo \
            ${{ steps.pr_description.outputs.pr_description }} \
            ${{ steps.changes.outputs.changes }} \
            | sed 's/\\"//g' | sed 's/["“]//g')" >> $GITHUB_OUTPUT

      # タグを切り、リリースノートを作成する
      # PRのラベルに応じてカテゴライズする場合は、使用先のリポジトリで下記を定義する
      # https://docs.github.com/ja/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes
      - name: Create Release
        run: |
          response=$(curl -X POST \
            -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
            -d "{ \
              \"tag_name\": \"${{ steps.release_tag.outputs.release_tag }}\", \
              \"target_commitish\": \"main\", \
              \"name\": \"${{ steps.release_tag.outputs.release_tag }}\", \
              \"body\": \"${{ steps.release_note_body.outputs.release_note_body }}\" \
            }" \
            -w "%{http_code}" \
            -o response_body.txt \
            https://api.github.com/repos/${{ github.repository }}/releases)
            status_code=$(tail -n1 <<< "$response")
            echo "Status Code: $status_code"
            body=$(cat response_body.txt)
            echo "Response Body: $body"
            if [ $status_code -ne 201 ]; then
              echo "Failed to create release"
              exit 1
            fi

2.PR のカテゴライズ

「1.タグとリリースノートの自動生成 5」に該当するコードです
リリースノートを生成するときに下記の Title が自動的に割り当てる

.github/release.yml
# PRのラベルに応じてリリースノートのカテゴリを分ける
changelog:
  exclude:
    authors:
      - github-actions
  categories:
    - title: New Features
      labels:
        - "feature"
    - title: Bug Fixes
      labels:
        - "bug"
    - title: Hot Fixes
      labels:
        - "hotfix"
    - title: Other Changes
      labels:
        - "*"

3.PR のラベル自動付与

ブランチ名に基づいて PR にラベルを付与する

  • feature/~ → feature ラベル
  • fix/~ → bug ラベル
  • hotfix/~ → hotfix ラベル
  • * → ラベルなし
.github/workflows/label.yml
name: Automatically labeling pull request.

on:
  pull_request:
    types: [opened]

jobs:
  auto-labeling-pr:
    runs-on: ubuntu-latest

    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      # ラベル名を取得する
      - name: Get label name
        id: label_name
        run: |
          branch_type=$(echo ${{ github.head_ref }} | cut -d "/" -f1)
          if [ $branch_type == 'feature' ]; then
            label_name="feature"
          elif [ $branch_type == 'fix' ]; then
            label_name="bug"
          elif [ $branch_type == 'hotfix' ]; then
            label_name="hotfix"
          else
            label_name=""
          fi
          echo "label_name=$label_name" >> $GITHUB_OUTPUT

      # PRにラベルを付与する
      - name: Auto labeling
        if: ${{ steps.label_name.outputs.label_name != '' }}
        run: |
          number=$(echo ${{ github.event.pull_request.number }})
          gh pr edit $number --add-label ${{ steps.label_name.outputs.label_name }}

以上が成果物です 👩‍💻
ここからは要点を絞って実装を見ていきます!

前回のリリースタグを取得する

こちらで取得可能です。
GET /repos/{owner}/{repo}/releases/latest

.github/workflows/release.yml
# 前回のリリースタグを取得する
- name: Get previous tag
  id: pre_tag
  run: |
    echo "pre_tag=$(curl -H 'Accept: application/vnd.github.v3+json' -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name)" >> $GITHUB_OUTPUT

リクエストやレスポンスは下記に書いてあります
https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release

PR の Summary を任意でつける

PR の一覧を全てカテゴライズして表示する場合、リリースの規模が大きかったりするとノイズとなる PR が出てきます。
そこで、main への PR の body にリリース内容のまとめを書き、それをリリースノートに含める運用とすることでリリースノートで強調したい機能が分かりやすく表示できるようにしました。

流れは次のようにします。

  1. PR の番号を取得する
  2. get-a-pull-requestをリクエストし、description を取得
  3. 2 で取得した description を整形する(null 判定)
  4. release note の body に Summary を入れる

既に完成で見せていますが、次のイメージです


main への PR


PR から body を取得して表示

対象の PR 番号を取得する(準備)

${{github.event.pull_request.number}}で取得が可能です。

作成当初は、main への push をトリガーに workflow を走らせていました。
しかし、push をトリガーでは pr 番号が取得できなかったため、PR の close をトリガーに変更します。

.github/workflows/release.yml
name: Create release tag and release note.

on:
- push:
-   branches: [main]
+ pull_request:
+   types: [closed]
+   branches:
+     - main

jobs:
 create-release-tag:
     # PRがマージされたときのみ実行
+    if: github.event.pull_request.merged == true
   runs-on: ubuntu-latest

https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow

PR の情報を取得し、body が空の場合に表示をスキップする

GET /repos/{owner}/{repo}/pulls/{pull_number}を使うことで情報を取得することができます。

リクエスト方法やレスポンスのサンプルはこちらで確認できます。
https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request

nullが文字列で返る
# PRのDescriptionを取得しマークダウン形式に変換する
- name: Get pr description
   id: pr_description
   run: |
      echo "pr_description=$(curl -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' 'https://api.github.com/repos/${{ github.repository }}/pulls/${{github.event.pull_request.number}}' | jq .body)" >> $GITHUB_OUTPUT

しかし PR の body が null の場合に、リリースノートに文字列の nullが表示されてしまいます。
そこで今回は、文字列の null が返ってきた時に、空文字を返すようにしました。

# PRのDescriptionを取得しマークダウン形式に変換する
- name: Get pr description
  id: pr_description
  run: |
    echo "pr_description=$(curl -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
      'https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number}}' \
      | jq .body | awk '{if ($0 == "null") print ""; else print}')" >> $GITHUB_OUTPUT
追加したコード
| awk '{if ($0 == "null") print ""; else print}'

https://www.tohoho-web.com/ex/jq.html#empty

リリースノートを作成する

流れとしては次のようになります。

  1. リリースノートの本文を生成
  2. リリースノートを作成する
    • PR で記載した内容をのせる

前回リリースからの差分をもとに、変更点を取得する

こちらで生成が可能です
POST /repos/{owner}/{repo}/releases/generate-notes

https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#generate-release-notes-content-for-a-release

.github/workflows/release.yml
# 前回リリースからの差分をもとに、変更点を取得する
- name: Generate release note changes
  id: changes
  run: |
    echo "changes=$(
    curl -X POST \
      -H 'Accept: application/vnd.github.v3+json' \
      -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' \
      https://api.github.com/repos/${{ github.repository }}/releases/generate-notes \
      -d '{
        "tag_name": "${{ steps.release_tag.outputs.release_tag }}",
        "previous_tag_name": "${{ steps.pre_tag.outputs.pre_tag }}",
        "target_commitish": "main"
      }' | jq .body
    )" >> $GITHUB_OUTPUT

本文をを作成する

取得したリリース PR の description と前回からの差分をマージしていきます。
また、"は特殊文字として扱われてしまいエラーになるので省きました。

# リリースノートの本文を作成する
- name: Create release note body
  id: release_note_body
  run: |
    echo "release_note_body=$(echo \
      ${{ steps.pr_description.outputs.pr_description }} \
      ${{ steps.changes.outputs.changes }} \
      | sed 's/\\"//g' | sed 's/["“]//g')" >> $GITHUB_OUTPUT

リリースノートを作成する

生成した本文を使用しリクエストを行います。
release_note_bodyで output したもの(${{steps.release_note_body.outputs.release_note_body}})をリクエストボディに含めます。

POST /repos/{owner}/{repo}/releases

https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release

また、status codeが成功(201)以外の場合はエラーを出力し処理を終了させるようにします。

.github/workflows/release.yml
# タグを切り、リリースノートを作成する
- name: Create Release
  run: |
    response=$(curl -X POST \
      -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
      -d "{ \"tag_name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"target_commitish\": \"main\", \"name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"body\": \"${{steps.pr_description.outputs.pr_description}} ${{ steps.release_note.outputs.release_note }}\"}" \
      -w "%{http_code}" \
      -o response_body.txt \
      https://api.github.com/repos/${{ github.repository }}/releases)
      status_code=$(tail -n1 <<< "$response")
      echo "Status Code: $status_code"
      body=$(cat response_body.txt)
      echo "Response Body: $body"
      if [ $status_code -ne 201 ]; then
        echo "Failed to create release"
        exit 1
      fi

リリースノートをカテゴライズ

リリースノートを自動生成する際に、Pull Request をラベルに応じてカテゴライズすることが可能です。

.github/release.yml
# PRのラベルに応じてリリースノートのカテゴリを分ける
changelog:
  exclude:
    authors:
      - github-actions
  categories:
    - title: New Features
      labels:
        - "feature"
    - title: Bug Fixes
      labels:
        - "bug"
    - title: Hot Fixes
      labels:
        - "hotfix"
    - title: Other Changes
      labels:
        - "*"

https://docs.github.com/ja/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes

また、ここで定義するラベルは事前に定義する必要があります。

PR にラベルを自動付与

テンプレートは用意できたが、前セクションの段階では手動で PR にラベルを付与しなければなりません。
なので、ブランチに応じて PR にラベルを付与できるよう自動化を行います

.github/workflows/label.yml
name: Automatically labeling pull request.

on:
  pull_request:
    types: [opened]

jobs:
  auto-labeling-pr:
    runs-on: ubuntu-latest

    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      # ラベル名を取得する
      - name: Get label name
        id: label_name
        run: |
          branch_type=$(echo ${{ github.head_ref }} | cut -d "/" -f1)
          if [ $branch_type == 'feature' ]; then
            label_name="feature"
          elif [ $branch_type == 'fix' ]; then
            label_name="bug"
          elif [ $branch_type == 'hotfix' ]; then
            label_name="hotfix"
          else
            label_name=""
          fi
          echo "label_name=$label_name" >> $GITHUB_OUTPUT

      # PRにラベルを付与する
      - name: Auto labeling
        if: ${{ steps.label_name.outputs.label_name != '' }}
        run: |
          number=$(echo ${{ github.event.pull_request.number }})
          gh pr edit $number --add-label ${{ steps.label_name.outputs.label_name }}

セマンティックバージョン

今回はカレンダーバージョンを採用したのですが、その経緯について触れたいと思います。

イメージ

type increment reset
major 1.0.0 minor+patch
minor 0.1.0 patch
patch 0.0.1 -
その他 - -

当初は上記のようなセマンティックバージョンでのタグ方針で考えていたが、次の懸念点からカレンダーバージョンとしました

  • main への PR のタイトルに v.1.0.1 のように書いて github action に伝搬させる必要がある?
  • PR に major,minor,patch などのラベルをつける
    • github actions 側で major の場合+1 などを定義
  • Conventional Commits などでコミットからバージョンを特定する(+1 か+0.1 のような)
    • 私が参画したプロジェクトでは commitzen などでコミットメッセージを整備できていなかったので今回は見送りとしました

https://semver.org/lang/ja/

https://calver.org/overview.html

おわりに

今回は GitHub Actions でリリースノートとタグを自動生成する方法を紹介しました。
この方法により、リリース作業を効率化し、手動でのミスを減らすことができました!🐥

複数のリポジトリ間でアクションを共有することも可能なので、興味のある方は是非こちらもご覧ください!

https://zenn.dev/tara_is_ok/articles/dd4b0f8a1ce3b5

最後までありがとうございました!💆‍♂️

参考

こちらの記事がなければ実装できていなかったです。ありがとうございます!
https://zenn.dev/kshida/articles/auto-generate-release-note-with-calver#完成形はこちら

Rehab Tech Blog

Discussion