⚗️

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

2024/02/20に公開

実現したいこと

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

成果物

generated-sample
生成サンプル

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

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

  1. mainへPRがマージされる
    • マージされずにcloseされた時はスキップ
  2. 前回のリリースタグを取得する
  3. タグを生成する「{YYYY.MM.DD}-{count}」
    • 同日の場合 → count + 1
  4. mainへのPRのbodyを取得する
    • bodyがない場合は空文字を返す
  5. リリースノートの本文を生成する
    • 1でラベルがつけられたPRをカテゴライズする(release.yml)
  6. リリースノートを作成する
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 | sed 's/"//g' | awk '{if ($0 == "null") print ""; else print}')" >> $GITHUB_OUTPUT

      # 前回リリースからの差分をもとに、リリースノートの本文を生成する
      - name: Generate release note changes
        id: release_note
        run: |
          echo "release_note=$(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 | sed 's/"//g')" >> $GITHUB_OUTPUT

      # タグを切り、リリースノートを作成する
      - name: Create Release
        run: |
          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 }}\"}" \
            https://api.github.com/repos/${{ github.repository }}/releases

2.PRのカテゴライズ

リリースノートを生成するときに下記が自動的に割り当てられる

# 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ラベル
  • * → ラベルなし
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

      # 前回のリリースタグを取得する
      - 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をトリガーに変更します。

yaml
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 | sed 's/"//g')" >> $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 | sed 's/"//g' | awk '{if ($0 == "null") print ""; else print}')" >> $GITHUB_OUTPUT
追加したコード
| awk '{if ($0 == "null") print ""; else print}'

他にも、jq .body // emptyを試しましたが、マークダウン(複数行の文字列)に対応できず(?)正常に動作しませんでした。

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

# 前回リリースからの差分をもとに、リリースノートの本文を生成する
- name: Generate release note changes
  id: release_note
  run: |
    echo "release_note=$(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 | sed 's/"//g')" >> $GITHUB_OUTPUT

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

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

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

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

# タグを切り、リリースノートを作成する
- name: Create Release
    run: |
    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 }}\"}" \
            https://api.github.com/repos/${{ github.repository }}/releases

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

# 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にラベルを付与できるよう自動化を行います

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 }}

set-outputではなくGITHUB_OUTPUT

将来的にset-outputは非推奨になりworkflowでエラーとなるためGITHUB_OUTPUTを使用する

修正例
- name: Set output
run: echo "::set-output name={name}::{value}"
↓ 
- name: Set output
run: echo "{name}={value}" >> $GITHUB_OUTPUT
該当のコード
# 前回のリリースタグを取得する
- name: Get previous tag
  id: pre_tag
  run: |
-   echo "::set-output name=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)"
+   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
# タグを生成する 「v{バージョン番号}」
- name: Generate tag
  id: release_tag
  run: |
-   echo "::set-output name=release_tag::$today-$(($pre_release_count + 1))"
+   echo "release_tag=$today-$(($pre_release_count + 1))" >> $GITHUB_OUTPUT
# 前回リリースからの差分をもとに、リリースノートの本文を生成する
- name: Generate release note
  id: release_note
  run: |
-   echo "::set-output name=release_note::$(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 }}"}' | jq .body | sed 's/"//g')"
+   echo "release_note=$(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 }}"}' | jq .body | sed 's/"//g')" >> $GITHUB_OUTPUT

https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/#examples

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

イメージ

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://zenn.dev/kshida/articles/auto-generate-release-note-with-calver#完成形はこちら

スクラップ

https://zenn.dev/tara_is_ok/scraps/fc2b765692328c

Discussion