GitHub Actionsでいい感じのリリースノートを完全自動で作成する
きっかけ
スプリントで実装した内容をリリースする際、リリースノートを毎回作成しています。
GitHub のリリースノート自動生成機能も便利なのですが、それでも「毎回ボタンをクリックする一手間が面倒だな。自動化したいな〜」と思っていました。
そこで、結構前に勉強も兼ねてリリースノート自動作成のアクションを自作したところ、チーム内で好評だったのでご紹介したいと思います。
(色々あってすっかり記事にするのが遅れてしまいました・・)
要件
- main ブランチにマージされたら自動でタグとリリースノートが生成されること
- リリースノートには前回リリースとの差分が表示されること
- 同日に複数回リリースしても識別できること
- リリースノートのテンプレートを指定できること
完成形はこちら
いきなりですが、生成されるリリースノートはこんな感じです。
完成形のアクションはこちらになります。
name: Create release tag and release note.
on:
push:
branches: [ main ]
jobs:
create-release-tag:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TZ: 'Asia/Tokyo'
steps:
- uses: actions/checkout@v2
# 前回のりリースタグを取得する
- 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)"
# タグを生成する 「{YYYY.MM.DD}-{当日リリース回数}」
- name: Generate release 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
echo "init count"
pre_release_count=0
fi
echo "::set-output name=release_tag::$today-$(($pre_release_count + 1))"
# 前回リリースからの差分をもとに、リリースノートの本文を生成する
- 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')"
# タグを切り、リリースノートを作成する
- name: Create Release
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-d "{ \"tag_name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"body\": \"${{ steps.release_note.outputs.release_note }}\"}" \
https://api.github.com/repos/${{ github.repository }}/releases
内容について順に確認していきましょう。
バージョニングについて
タグ・リリースノートの自動化にあたり、本アクションではカレンダーバージョニング(CalVer)を採用しています。
具体的には {YYYY.MM.DD}-{当日リリース回数}
という形式です。
自動生成するタグについて当初はSemVerで管理しようと思っていたのですが、それだと「どのリリースでどのバージョンを上げるか」を人間が判断しなければなりません。
有名なrelease-drafterを使うのであればコミットを工夫することで自動化できるのですが、でも、どうせ自作するなら完全自動化したいなと思いました。
少し悩んだものの、私が関わっているサービスでは SemVer でなければならない理由が特になかったため、チーム内で相談して CalVer で運用することにしました。
リリースタグを生成する
まずはタグを生成します。
今回は {YYYY.MM.DD}-{当日リリース回数}
という形で運用するので、前回のリリースと今回のリリースが同日かどうか判断しなければなりません。
(同日だったら当日リリース回数
をインクリメントする)
前回リリース分のタグを見ればリリース日が分かるので、前回(=最新)のリリースが取得できれば良いですよね。
そこで利用するのが、GitHub の Get the latest release
API です。
GET /repos/{owner}/{repo}/releases/latest
公式のサンプルとなりますが、以下のようにリクエストすると、
curl \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer <YOUR-TOKEN>" \
https://api.github.com/repos/OWNER/REPO/releases/latest
このようなレスポンスが返ってきます。(簡便の為、一部省略)
tag_name
も含まれていますね。
{
"url": "https://api.github.com/repos/octocat/Hello-World/releases/1",
"html_url": "https://github.com/octocat/Hello-World/releases/v1.0.0",
"assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/1/assets",
"upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}",
"tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0",
"zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0",
"discussion_url": "https://github.com/octocat/Hello-World/discussions/90",
"id": 1,
"node_id": "MDc6UmVsZWFzZTE=",
"tag_name": "v1.0.0", // <-- これがタグ名
"target_commitish": "master",
"name": "v1.0.0",
"body": "Description of the release",
"draft": false,
"prerelease": false,
"created_at": "2013-02-27T19:35:32Z",
"published_at": "2013-02-27T19:35:32Z",
"author": {
"login": "octocat",
"id": 1,
...(省略)...
},
"assets": [
{
"url": "https://api.github.com/repos/octocat/Hello-World/releases/assets/1",
"browser_download_url": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/example.zip",
"id": 1,
...(省略)...
}
]
}
この API で前回のタグを取得して今回のタグ名を生成しているのが、以下の部分になります。
# 前回のりリースタグを取得する
- 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)"
# タグを生成する 「{YYYY.MM.DD}-{当日リリース回数}」
- name: Generate release 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
echo "init count"
pre_release_count=0
fi
echo "::set-output name=release_tag::$today-$(($pre_release_count + 1))"
前回と同日のリリースだった場合は、タグの{当日リリース回数}
の部分をインクリメントしています。
以下の$(($pre_release_count + 1))
の部分です。
echo "::set-output name=release_tag::$today-$(($pre_release_count + 1))"
当日初回の場合はpre_release_count=0
としているので、タグの{当日リリース回数}
の部分は 1 となります。
if [[ ! $pre_release_date = $today ]]; then
echo "init count"
pre_release_count=0
fi
前回からの差分をもとにリリースノートの本文を生成する
次にリリースノートを生成したいです。
リリースノートの生成と一口で言っても、実は 2 段階に分けられます。
- リリースノートの本文を生成する
- リリースノートを生成する
ということで、まずはリリースノート本文の生成からいきましょう。
ここでは、GitHub の Generate release notes content for a release
API を利用します。
POST /repos/{owner}/{repo}/releases/generate-notes
公式のサンプルを確認しましょう。
以下のようにリクエストすると、
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer <YOUR-TOKEN>" \
https://api.github.com/repos/OWNER/REPO/releases/generate-notes \
-d '{"tag_name":"v1.0.0","target_commitish":"main","previous_tag_name":"v0.9.2","configuration_file_path":".github/custom_release_config.yml"}'
このように、自動生成されたリリースノートの本文が body
に入って返ってきます。
{
"name": "Release v1.0.0 is now available!",
"body": "##Changes in Release v1.0.0 ... ##Contributors @monalisa"
}
この API の素敵なところは、今回のタグと前回のタグを指定することで差分を取得してくれるというところです!
tag_name string Required
The tag name for the release. This can be an existing tag or a new one.
previous_tag_name string
The name of the previous tag to use as the starting point for the release notes. Use to manually specify the range for the set of changes considered as part this release.
今回のタグと前回のタグは先ほど取得したので、そちらを使いましょう。
# 前回リリースからの差分をもとに、リリースノートの本文を生成する
- 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')"
リリースタグとリリースノートを作成する
最後はリリースノートの生成ですね。
リリースノートの生成には、GitHub の Create a release
API を使います。
POST /repos/{owner}/{repo}/releases
公式のサンプルは以下の通りです。
リクエストでは、タグ名やリリースノートの本文を指定します。
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer <YOUR-TOKEN>" \
https://api.github.com/repos/OWNER/REPO/releases \
-d '{"tag_name":"v1.0.0","target_commitish":"master","name":"v1.0.0","body":"Description of the release","draft":false,"prerelease":false,"generate_release_notes":false}'
レスポンスは先ほど Get the latest release
API で取得したものと同様なので省略します。
それでは、これまでに作成したタグ名と本文を指定しましょう。
# タグを切り、リリースノートを作成する
- name: Create Release
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-d "{ \"tag_name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"body\": \"${{ steps.release_note.outputs.release_note }}\"}" \
https://api.github.com/repos/${{ github.repository }}/releases
これでデフォルトのリリースノートは生成できるようになりました。
折角なので、最後にもう少し見やすくなるように工夫していきましょう。
テンプレートを用意してリリースノートを見やすくする
.github/release.yml
ファイルを用意することで、リリースノートにテンプレートを設定できます。
今回は機能追加と不具合修正を分けて表示したいので、以下のようにしてみました。
マージされた PR のラベルに応じて振り分けます。
changelog:
categories:
- title: New Features 🎉
labels:
- "enhancement"
- title: Bug Fix 💊
labels:
- "bug"
- title: Other Changes 🛠
labels:
- "*"
exclude
の authors
を設定することで、リリースノートから除外したいユーザーを指定できます。
GitHub Actions で生成した PR がある場合はノイズになるので除外してしまいましょう。
changelog:
exclude:
# リリースノートから除外したいユーザー
authors:
- github-actions # <-- 追加
categories:
- title: New Features 🎉
labels:
- "enhancement"
- title: Bug Fix 💊
labels:
- "bug"
- title: Other Changes 🛠
labels:
- "*"
私の場合はリリース用 PR を自動生成するアクションを設定しているので、github-actions
を除外します。
PR にラベルを自動付与できるようにする
これでテンプレートは用意できましたが、今のままでは PR にラベルを手動で付与しなければなりません。
ついでにここも自動化しましょう。
Gitflow に則っている場合、PR の種別はブランチ名で判断できます。
今回は、ブランチ名が feature/~
の場合はenhancement
ラベル、fix/~
or hotfix/~
の場合はbug
ラベルを付与するようにしましょう。
ラベル自動付与アクションの完成形はこちらです。
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:
- uses: actions/checkout@v2
# ラベル名を取得する
- name: Get label name
id: label_name
run: |
branch_type=$(echo ${{github.head_ref}} | cut -d "/" -f1)
if [ $branch_type == 'feature' ]; then
label_name=$(echo "enhancement")
elif [ $branch_type == 'fix' ] || [ $branch_type == 'hotfix' ]; then
label_name=$(echo "bug")
else
label_name=""
fi
echo "::set-output name=label_name::$label_name"
# PRにラベルを付与する
- name: Auto labeling
if: ${{ steps.label_name.outputs.label_name }}
run: |
number=$(echo $GITHUB_REF | sed -e 's/[^0-9]//g')
gh pr edit $number --add-label ${{ steps.label_name.outputs.label_name }}
簡単に解説します。
以下箇所では、ラベル名を決定するためにブランチ名を取得しています。
# ラベル名を取得する
- name: Get label name
id: label_name
run: |
branch_type=$(echo ${{github.head_ref}} | cut -d "/" -f1)
if [ $branch_type == 'feature' ]; then
label_name=$(echo "enhancement")
elif [ $branch_type == 'fix' ] || [ $branch_type == 'hotfix' ]; then
label_name=$(echo "bug")
else
label_name=""
fi
echo "::set-output name=label_name::$label_name"
マージする側のブランチ名は ${{github.head_ref}}
で取得できます。
ラベル付与の条件を変更したい場合は、こちらの if 文に条件を追加することで対応可能です。
そして、ラベル付与部分ですね。
# PRにラベルを付与する
- name: Auto labeling
if: ${{ steps.label_name.outputs.label_name }}
run: |
number=$(echo $GITHUB_REF | sed -e 's/[^0-9]//g')
gh pr edit $number --add-label ${{ steps.label_name.outputs.label_name }}
ラベルの付与には GitHub CLI を使います。
gh pr edit [<number> | <url> | <branch>] [flags]
GitHub CLI でラベルを付与するには PR の番号が必要なので、以下で番号を取り出しています。
number=$(echo $GITHUB_REF | sed -e 's/[^0-9]//g')
それでは実際に試してみましょう。
まずは feature
の場合。
そして fix
の場合。
先ほど指定した条件に従ってラベルが付与されています。
これでラベルを自動で付与できるようになりましたね。
完成したリリースノート
実際に生成されたリリースノートを再掲します。
タグもリリースノートも{YYYY.MM.DD}-{当日リリース回数}
の形式で生成されていますね。
当日 2 回目以降の場合は、以下のように{当日リリース回数}
の部分がインクリメントされます。
これでリリースノートの生成が自動化されました。
このアクションは今でもスプリントリリースのたびに活躍してくれています!
おわりに
やはり実際に自分でアクションを作成してみると理解が深まりますね!
色々確認しながら進めるのはとても楽しかったです。
今年も残り僅かですが、小さな改善を積み重ねていけるように精進していこうと思います。
今回作ったアクションを再掲して終了です。
name: Create release tag and release note.
on:
push:
branches: [ main ]
jobs:
create-release-tag:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TZ: 'Asia/Tokyo'
steps:
- uses: actions/checkout@v2
# 前回のりリースタグを取得する
- 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)"
# タグを生成する 「{YYYY.MM.DD}-{当日リリース回数}」
- name: Generate release 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
echo "init count"
pre_release_count=0
fi
echo "::set-output name=release_tag::$today-$(($pre_release_count + 1))"
# 前回リリースからの差分をもとに、リリースノートの本文を生成する
- 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')"
# タグを切り、リリースノートを作成する
- name: Create Release
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-d "{ \"tag_name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"name\": \"${{ steps.release_tag.outputs.release_tag }}\", \"body\": \"${{ steps.release_note.outputs.release_note }}\"}" \
https://api.github.com/repos/${{ github.repository }}/releases
changelog:
exclude:
# リリースノートから除外したいユーザー
authors:
- github-actions
categories:
- title: New Features 🎉
labels:
- "enhancement"
- title: Bug Fix 💊
labels:
- "bug"
- title: Other Changes 🛠
labels:
- "*"
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:
- uses: actions/checkout@v2
# ラベル名を取得する
- name: Get label name
id: label_name
run: |
branch_type=$(echo ${{github.head_ref}} | cut -d "/" -f1)
if [ $branch_type == 'feature' ]; then
label_name=$(echo "enhancement")
elif [ $branch_type == 'fix' ] || [ $branch_type == 'hotfix' ]; then
label_name=$(echo "bug")
else
label_name=""
fi
echo "::set-output name=label_name::$label_name"
# PRにラベルを付与する
- name: Auto labeling
if: ${{ steps.label_name.outputs.label_name }}
run: |
number=$(echo $GITHUB_REF | sed -e 's/[^0-9]//g')
gh pr edit $number --add-label ${{ steps.label_name.outputs.label_name }}
参考にさせていただいた記事
素晴らしい記事をありがとうございました!
Discussion
記事参考になりました、ありがとうございます。
2点ほど指摘があります。
::set-output
は depricated されているので、$GITHUB_OUTPUT
を使うようにするといいと思います。 cf https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/とありますが、
sed 's/"//g'
これをしてしまうと body の中に"
が含まれている場合、jq
コマンドがすでに"
を\"
にエスケープするので、余分な\
が残ってしまうかと思います。sed
を削除した場合は、後続のAPI呼び出しの部分の修正も必要かと思います。