GitHubのリリースノートを自動化する仕組み
GitHub のAutomatically generated release notesを使ってリリースノートの内容を PR に基づいて自動生成するフローを作りました。
今までは、コミットメッセージのルールであるConventional Commitsとconventional-github-releaserを使って、コミットからリリースノートを自動生成していました。 他の人の PR でも、squah merge でコミットメッセージを書き換えることで、リリースノートに反映されるようにしていました。
ただ GitHub に仕組みは違うけどほぼ似たことをするAutomatically generated release notesという機能が実装されているので、これをベースに移行しようと思って、そのワークフローを作っていました。
目的
目的としては、次を目標にしていました。
- Convential commit でやっているものをAutomatically generated release notesベースに移行する
自分は大量のリポジトリを持っているので、段階的に移行できるようにしました。 Convential commitとAutomatically generated release notesは同時に使っても競合はしないので、実際にはConventional Commitsでコミットしながら、リリースするときに Convential commit かAutomatically generated release notesどちらを使うかを選択できるようにしています。 (たまに、PR を経由せずにコミットしちゃうことがあるので、それをリリースするときはConventional Commitsベースに切り替える)
また、今回は CI からの自動リリースへと切り替えるのは、目的にしませんでした。 npm パッケージなどを CI からリリースするにはまだトークン管理が必要になってしまうので、npm <-> GitHub 間の OIDC が実装されてから考えることにしました。
- RFC for linking packages to their source and build by feelepxyz · Pull Request #626 · npm/rfcs
- Automated publishing of npm packages from CI/CD · Issue #625 · npm/statusboard
ただし、textlintやSecretlintなどの大きな monorepo では、今回作ったワークフローを CI から実行するフローにしています。これは、monorepo だとローカルからリリースするよりも、CI からリリースした方が楽だったりすることが多いためです(単純に時間がかかったり、クリーンな環境でリリースしたいため)。
GitHubのAutomatically generated release notes
GitHubにはAutomatically generated release notesという、Pull Requestから自動的にリリースノートを生成する機能があります。
この機能は、次のような手順で利用できます。
- リポジトリに
.github/release.yml
というラベルをカテゴライズする設定ファイルを追加する - PR にラベルを貼る
- GitHub Releaseで”Generate release notes”を実行 or
gh release create --generate-notes
コマンドを実行する - リリースノートにPRのラベルに基づいたカテゴライズされたリリースノートが生成される
実際に生成されるリリースノートは、次のような感じです。
- Release v13.3.1 · textlint/textlint
- Release v4.2.0 · textlint-rule/sentence-splitter
- Release v2.0.0 · textlint-ja/textlint-rule-no-hankaku-kana
基本的に事前に準備するのは.github/release.yml
というファイルだけです。
.github/release.yml
の設定
元からgithub-label-setup
というラベルをいい感じにセットアップするツールを作って使っていました。
- azu/github-label-setup: 📦 Setup GitHub label without configuration.
- GitHubのラベルをいい感じにセットアップするツール | Web Scratch
github-label-setup v5.0.0で.github/release.yml
の設定ファイルの生成もサポートしました。
そのため、次のコマンドでリポジトリにラベルのプリセットを追加して、そのラベルに対応した.github/release.yml
を生成できます。
npx @azu/github-label-setup --token "${AZU_GITHUB_TOKEN}" --allow-added-labels
npx @azu/github-label-setup --addReleaseYml
あとは、このラベルをPull Reuqestにつけていくだけで、リリースノートの自動生成機能が利用できます。
📝 自分の場合は、このリポジトリのセットアップは次のようなスクリプトで行っています。
ラベルのセットアップ、.github/release.yml
の生成、リポジトリのauto-merge/auto-branch-delete/discussionsの有効化を行っています。
local GITHUB_TOKEN="GITHUB TOKEN" # $(op read "op://Private/GITHUB_TOKEN/token")
npx @azu/github-label-setup --token "${AZU_GITHUB_TOKEN}" --allow-added-labels
echo "✓ Add .github/release.yml"
npx @azu/github-label-setup --addReleaseYml
if git ls-files --others --exclude-standard | grep --color=auto ".github/release.yml" -q
then
git add .github/release.yml
git commit -m "CI: add .github/release.yml"
fi
echo "✓ Enable auto-branch-delete/auto-merge/discussions"
gh repo edit --delete-branch-on-merge --enable-auto-merge --enable-discussions
gh repo edit --enable-discussions
はなかったので実装してv2.22.0で追加されています。
ラベルのカテゴライズ
.github/release.yml
の設定ファイルは、次のような感じです。
Type: *
のラベルをPRの変更内容に合わせてつけていくことで、リリースノートのカテゴライズができます。
changelog:
exclude:
labels:
- 'Type: Meta'
- 'Type: Question'
- 'Type: Release'
categories:
- title: Security Fixes
labels: ['Type: Security']
- title: Breaking Changes
labels: ['Type: Breaking Change']
- title: Features
labels: ['Type: Feature']
- title: Bug Fixes
labels: ['Type: Bug']
- title: Documentation
labels: ['Type: Documentation']
- title: Refactoring
labels: ['Type: Refactoring']
- title: Testing
labels: ['Type: Testing']
- title: Maintenance
labels: ['Type: Maintenance']
- title: CI
labels: ['Type: CI']
- title: Dependency Updates
labels: ['Type: Dependencies', "dependencies"]
- title: Other Changes
labels: ['*']
例:
- 機能追加:
Type: Feature
- バグ修正:
Type: Bug
- ドキュメントの変更:
Type: Documentation
- リファクタリング:
Type: Refactoring
リポジトリの種類
Automatically generated release notesはGitHubのリリースノートの機能なのでリポジトリの中身は関係ないです。 しかし、リリースと同時にnpmパッケージも公開するため、扱うパッケージによって微妙にリリースフローが変わってきます。
自分が扱うリポジトリは大きく分けて、2 種類のリポジトリがあります。 ここでは、どちらもnpmパッケージを公開するリポジトリとして扱います。
- 単体のパッケージを扱うリポジトリ
- monorepoなパッケージを扱うリポジトリ
どちらも同じ流れで、Automatically generated release notesベースでリリースできるように移行しています。
単体のパッケージを扱うリポジトリ
単体のパッケージを扱うリポジトリは、次のような形でnpmへのpublishと同時にリリースノートを公開できます。
# patchアップデート
$ npm version patch && npm publish && gh release create --generate-notes "$(git describe --tags --abbrev=0)"
# minorアップデート
$ npm version minor && npm publish && gh release create --generate-notes "$(git describe --tags --abbrev=0)"
# majorアップデート
$ npm version major && npm publish && gh release create --generate-notes "$(git describe --tags --abbrev=0)"
npm-versionコマンドを使うと、package.json
のバージョン更新とGit Tagの作成が同時に行われます。
バージョンを更新したら、npm publish
でnpmへの公開を行い、gh release create --generate-notes
でGitHubリリースを行います。
gh release createにはタグ名が必要なので、git describe --tags --abbrev=0
でタグ名を取得しています。
monorepoなパッケージを扱うリポジトリ
monorepoのパッケージも基本的な流れは同じです。 パッケージの公開にLernaを使ってる例を紹介します。
$ npx lerna version && npx lerna publish from-package && gh release create --generate-notes "$(git describe --tags --abbrev=0)"
lerna version
で対話的にバージョンが指定できます。
バージョンを決めたら、lerna publish from-package
でnpmへの公開を行い、gh release create --generate-notes
でGitHubリリースを行います。
CIから公開するmonorepoなパッケージを扱うリポジトリ
先ほどの例はどちらも、ローカルからコマンドを叩いて公開する方法でした。
しかしCIから公開する場合には、どのバージョン(patch, minor, major)にアップデートするかをどう選ぶかが難しい問題になります。 Conventional Commitsの場合は、コミットによって決まりますが、Automatically generated release notesではその仕組みはありません。
そのため、リリース時に選ぶ形 or ラベルによって決めるかという選択肢があります。 今回は、リリース時にバージョンを選ぶ形で実装しています。
次のリポジトリに、CIからAutomatically generated release notesを使ってパッケージの公開とリリースノートを作る実装があります。
リポジトリのREADMEにも書いてあるように、次のようなフローでリリースできるようになっています。
ステップ:
- PRを作成するためのcreate-release-pr.ymlワークフローをディスパッチする
- バージョンを選択できる
- [CI] PRを作成する
lerna.json
のversion
とpackages/*/package.json
のversion
が更新される- PRの本文にAutomatically generated release notesが自動的に入る(APIを叩いて取得してる)
- PRをレビューする
- PRの本文を修正できて、その内容がリリースノートに反映される
- PRをマージする
- [CI] npmとGitHub Releaseに公開する
- リリースノートの内容はPRの本文と同じになる
- CIでPRの本文を取得して、リリースノートに反映する
5のpublishはnpm registryやパッケージのビルド時に失敗することがあります。 その場合は、.github/workflows/release.ymlワークフローをDispatchすることで、5だけを再実行できます。 また、緊急時にはローカルからも次のコマンドで公開できます(これもリリースノートは自動生成されます)。
$ npm run versionup && npm run release && gh release create --generate-notes "$(git describe --tags --abbrev=0)"
実装は、先ほどのステップからもわかるように2つのワークフローになっています。
- バージョンを選んでリリース用のPRを作成するワークフロー: https://github.com/azu/monorepo-github-releases/blob/main/.github/workflows/create-release-pr.yml
- npm publishとGitHubリリースをするワークフロー: https://github.com/azu/monorepo-github-releases/blob/main/.github/workflows/release.yml
この実装を使ってる例としては、次のリポジトリがあります。
サンプルリポジトリはlernaを使ったmonorepoなパッケージですが、単独のパッケージでもlerna.json
-> package.json
への変更とci:release
のスクリプトの中身を変えるだけで対応できます
- 単体のパッケージをCIから公開してる例: pkgdeps/update-github-actions-permissions: A CLI that update GitHub Actions’s
permissions
automatically
リリースする際に一度PRを経由することで、PRを送った人にどのバージョンに含まれるかを明示できます(PRに書かれたリリースノートの内容にmentionが入ってるため通知される)。
面倒な人用に、既存のリポジトリにこのCIからのリリースフローを導入するためのマイグレーションスクリプトを用意しています。
curl -fsSL https://raw.githubusercontent.com/azu/monorepo-github-releases/main/migrate.sh | bash
このマイグレーションスクリプトは、次のような処理をやってくれます。
- ワークフローの追加
npm run ci:*
スクリプトの追加(CIからpublishするときのコマンドを定義している)- monorepoと単独のパッケージに対応
- npmとyarnに対応
実際に利用してるローカルからpublishするスクリプト
ここまでで、基本的なAutomatically generated release notesベースのリリースフローを紹介しました。
しかし、実際にはもっと色々なことをローカルでは実装しています。 最後に、実際に使ってるスクリプトを紹介します。
# conventional-github-releaserを使ってリリース
function releaseConventialGitHub(){
local GITHUB_TOKEN=$(op read "op://Private/GITHUB_TOKEN/Section_xxx/token")
CONVENTIONAL_GITHUB_RELEASER_TOKEN="${GITHUB_TOKEN}" conventional-github-releaser -p angular
}
# .github/release.ymlがあるかで分岐
function releaseGitHub(){
# if FORCE_CONVENTIONAL is defined
if [[ -n "${FORCE_CONVENTIONAL}" ]]; then
echo "🤖 Use conventional-github-releaser"
releaseConventialGitHub
return 0
fi
# .github/release.ymlがあるかで分岐
declare hasGitHubReleaseYaml
hasGitHubReleaseYaml=$(([[ -f .github/release.yml ]] || [[ -f .github/release.yaml ]]) && echo "true" || echo "false")
if [[ "${hasGitHubReleaseYaml}" == "true" ]]; then
echo "🤖 Use gh release"
gh release create --generate-notes "$(git describe --tags --abbrev=0)" --discussion-category "announcements"
else
echo "🤖 Use conventional-github-releaser"
releaseConventialGitHub
fi
}
# npm
function _confirm-npm(){
# .github/release.ymlがあるかで分岐
declare hasGitHubReleaseYaml
hasGitHubReleaseYaml=$(([[ -f .github/release.yml ]] || [[ -f .github/release.yaml ]]) && echo "true" || echo "false")
if [[ "${hasGitHubReleaseYaml}" == "true" ]]; then
echo "🔎 前回のリリース以降にマージされて、ラベルがついてないPRを検索中…"
echo "" # 空改行
searchQuery="no:label merged:>$(gh release view --json createdAt --jq .createdAt)"
noLabelPRs=$(gh pr list -s merged -S "${searchQuery}")
if [[ -z "${noLabelPRs}" ]]; then
echo "🎉 ラベルがついてないPRはありません"
echo ""
else
echo "🚨 ラベルがついてないPRがあります"
echo "🚨 ラベルをつけてください"
gh pr list -s merged -S "${searchQuery}" -w
echo "" # 空改行
return 1
fi
else
echo "🤖 Use conventional-github-releaser"
fi
# Enterを連打してても無視されるようにする
while true; do
echo -n "🤔 npm publish to \033[036m$1\033[0m(y/N/c)?"
read yn
case $yn in
[Yy]* ) return 0;;
[Nn]* ) return 1;;
# c を入力すると conventional-github-releaser を使う
[Cc]* ) export FORCE_CONVENTIONAL=1; echo "🤖 Use conventional-github-releaser"; return 0;;
* ) echo " Please answer Y or N or C";;
esac
done
}
# Worflow
# 1. Check
# 2. Tagを貼る
# 3. Publishする
# 3. Release Noteを作成
# npmに公開しないリポジトリ用
alias release-node-patch='_confirm-npm "Public" && pre-version-no-npm && npm version patch && post-version && releaseGitHub'
alias release-node-minor='_confirm-npm "Public" && pre-version-no-npm && npm version minor && post-version && releaseGitHub'
alias release-node-major='_confirm-npm "Public" && pre-version-no-npm && npm version major && post-version && releaseGitHub'
# npmをpublicで公開する
alias npm-patch='_confirm-npm "Public" && pre-version && npm version patch && post-version && npm publish --access=public --otp=$(npm-get-otp) && releaseGitHub'
alias npm-minor='_confirm-npm "Public" && pre-version && npm version minor && post-version && npm publish --access=public --otp=$(npm-get-otp) && releaseGitHub'
alias npm-major='_confirm-npm "Public" && pre-version && npm version major && post-version && npm publish --access=public --otp=$(npm-get-otp) && releaseGitHub'
# npmをprivateで公開する - https://github.com/azu/scoped-modules-checker
alias private-npm-patch='_confirm-npm "Private" && scoped-modules-checker && pre-version && npm version patch && post-version && npm publish --access=restricted --otp=$(npm-get-otp) && releaseGitHub'
alias private-npm-minor='_confirm-npm "Private" && scoped-modules-checker && pre-version && npm version minor && post-version && npm publish --access=restricted --otp=$(npm-get-otp) && releaseGitHub'
alias private-npm-major='_confirm-npm "Private" && scoped-modules-checker && pre-version && npm version major && post-version && npm publish --access=restricted --otp=$(npm-get-otp) && releaseGitHub'
# npmをbetaで公開する
alias npm-beta-release='_confirm-npm "Public" && pre-version && npm version prerelease --preid=beta && post-version && npm publish --tag beta --access=public --otp=$(npm-get-otp) && releaseGitHub'
alias npm-beta-patch='_confirm-npm "Public" && pre-version && npm version prepatch --preid=beta && post-version && npm publish --tag beta --access=public --otp=$(npm-get-otp) && releaseGitHub'
alias npm-beta-minor='_confirm-npm "Public" && pre-version && npm version preminor --preid=beta && post-version && npm publish --tag beta --access=public --otp=$(npm-get-otp) && releaseGitHub'
alias npm-beta-major='_confirm-npm "Public" && pre-version && npm version premajor --preid=beta && post-version && npm publish --tag beta --access=public --otp=$(npm-get-otp) && releaseGitHub'
# monorepo用のコマンド
# require "versionup" && "release" command
alias npm-monorepo-release='_confirm-npm "Public" && opr npm run versionup && npm run release -- --yes --otp=$(npm-get-otp) && gh release create --generate-notes "$(git describe --tags --abbrev=0)" --discussion-category "announcements"'
# publish前に色々チェックする処理 - 自由に追加できる
alias pre-version='git diff --exit-code && npm run --if-present build && git diff --exit-code && npm test && git diff --exit-code'
alias post-version='git push && git push --tags'
alias pre-version-no-npm='git diff --exit-code && npm prune && npm install -q --no-shrinkwrap && npm test && git diff --exit-code'
# 1password経由でTOTPを取得
alias npm-get-otp='op item get --otp XXXX'
めちゃくちゃ色々定義していますが、npm-{patch,minor,major}
が単独のリポジトリからパッケージを公開するときのコマンドです。
やっていることは、次のようなことをやっています
- Publicに公開するかの確認
- ラベルがついてないPRの確認 -> ある場合はラベルがないPRの一覧が開かれる
- Automatically generated release notes or Conventional Commitsのどっちでリリースするか選択
- git diffがないか、ビルドやテストが通るかのチェック
- バージョンアップとリリース
- リリースノートの作成(Discussionにも投稿)
monorepo用のnpm-monorepo-release
やprivate package用のprivate-npm-{patch,minor,major}
もやっていることは大体同じです。
細かい処理を自分で書きたかったので書いていますが、大体の人は次のコマンドとかでも問題ないかもしれません。 (これらのツールが出る前から、このコマンドを使っていて、中身を適宜書き換えている)
- sindresorhus/np: A better
npm publish
- release-it/release-it: 🚀 Automate versioning and package publishing
まとめ
GitHubのAutomatically generated release notesベースのリリースフローについて紹介しました。
最初に書いていたように既存のリポジトリを全て移行する予定はないので、必要になったらその都度 .github/release.yml
の設定で書いていたスクリプトで移行しています(冪等性のあるマイグレーションコマンドを実装して、何も考えずにコマンドを叩くようにしてる)。
実際に使ってるスクリプトで実装していましたがAutomatically generated release notesとConventional Commitsは共存できます。
そのため、実際にはリポジトリごとにどちらの方法を使うかを選択しています。
npmパッケージの公開するときのリリースノートを、GitHubのリリースノート自動生成機能で書くための仕組みを解説した記事を書きました。
— azu (@azu_re) March 11, 2023
- 単体のパッケージ
- monorepoパッケージ
- CIから公開するフロー
"GitHubのリリースノートを自動化する仕組み | Web Scratch"https://t.co/M2JIBaAYkd pic.twitter.com/VSDzL6eOa5
Automatically generated release notesベースはPRを経由しないとリリースノートに入らないという問題があるので、mainブランチにpushしまくっているようなリポジトリだとConventional Commitsの方が適切だったりします。
将来、npmのOIDC連携などがきたらまた変わる気がします。
そのため変更に対応しやすように、基本的にはコマンドを&&
で繋いで実装できるようなものを書いて使っています。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。