リリースの Pull Request のサマリを生成AIに作ってもらう
リリース時に、ワークフローは以下の通り。
-
main
ブランチからrelease/prd
ブランチに向けた PR を作成する - 内容をチェックしてマージ
-
release/prd
への push をトリガーにデプロイの Actions が実行される
このときの PR の description にいい感じのサマリ(リリースノート)が自動で書かれるようにしたい。
GitHub Release だとボタンポチで作れるようなやつ のイメージで、含まれる PR をリストアップしたい。
既存のツール・Actionsだと、調べた感じ下記のようなところが有名どころな気がする。
- https://github.com/x-motemen/git-pr-release
- https://github.com/googleapis/release-please
- https://github.com/release-drafter/release-drafter
そもそも、リリースのデプロイのトリガーを GitHub Release の作成にしてしまえば標準の機能でリリースノートが作れるのでそれで良いという説はある。
が、なんとなく PR マージの方がハマる気がしていて好み(慣れの問題かもしれない)。
git-pr-release は、これまでもよく使っていて、シンプルで使いやすいんだけど、含まれる PR をグルーピングしたい。特に dependabot を運用していると、パッケージアップデートのマージが頻繁にあるので、これらは下の方にして、上の方に機能追加の PR が来るようにしたい。
テンプレートの中で、 erb が使えるので、そこでラベルとかでグルーピングするというのはありそう。
release-please は Conventional Commit と squash merge が前提になっているような空気なので、もうちょっと緩い運用でそれっぽくなるような感じが良い。
緩い運用でなんとかしたいと言えば、 AI にサマリを作らせるというのは適材適所な気もしたので試す。
できれば PR の番号を渡したら、自分で情報を取りに行って欲しいけど、それくらいは自分でやるかということで、 GPT に gh と jq のコマンドを聞きながら、 リリース PR に含まれるマージ済み PR の情報を抽出するシェルコマンドを作る。
PR_NUMBER=[PR番号]
gh pr view $PR_NUMBER --json commits| jq -r '[.commits[]
| select(.messageHeadline | startswith("Merge pull request #"))
| (.messageHeadline | capture("#(?<pr_number>\\d+)") | .pr_number)]
| unique | .[]' > /tmp/prs.txt
cat /tmp/prs.txt | xargs -I {} gh pr view {} --json title,labels,author,number > /tmp/pr_info.txt
↑で得られた PR 情報を、 ChatGPT に投げてプロンプトを調整する。
以下のようなプロンプトができた。
下記の GitHub の PR 情報の json のリストを元に、PR の title, author, PR番号をリストにしてサマリを作ってください。
- テンプレートを参考に PR はグルーピングする
- title の内容は要約せずそのまま出力する
- author が bot の場合は、 `app/` などのプレフィクスは取り除く
- パッケージの更新は、対象のディレクトリごと(api, web)でさらに分類する
出力は、作成したサマリのみとしてください。
テンプレート:
```
## 機能
- #0: title (author)
## 不具合修正
- #0: title (author)
## リファクタリング
- #0: title (author)
## パッケージの更新
### api
- #0: title (author)
### web
- #0: title (author)
## その他
- #0: title (author)
```
PRの一覧:
```
(上で得られたPR情報のjson)
```
- 前提として、 PR のタイトルには
feat:
のような prefix がついているケースがほとんどなので、それで分類してくれそう。一応 label の情報も渡している。- もう少し分類の基準を指示しても良いかも?
- まあ大体あってればOK
- バックエンドとフロントエンドのモノレポで、それぞれで dependabot が動いているので、それは分けるように
- dependabot は gh コマンドから得られる author の名前が
app/dependabot
みたいになるようだったので、app/
は取り除くように - 絵文字とかを使ってもう少しめでたい感じにしても良いかもしれない
細かい config をいじらずに出力を調整できるのはツールにはない良さに感じる。生成AIすごい。
これまでの結果を Cursor の Composer に貼り付けて GitHub Actions にしてくれと指示した。
ちょっと調整して下記のような感じに。
---
name: Update Release PR Description
on:
pull_request:
types: [opened, synchronize]
branches:
- "release/prd"
jobs:
update-pr-description:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Install OpenAI package
run: npm install openai
- name: Get merged PRs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh pr view $PR_NUMBER --json commits | jq -r '[.commits[]
| select(.messageHeadline | startswith("Merge pull request #"))
| (.messageHeadline | capture("#(?<pr_number>\\d+)") | .pr_number)]
| unique | .[]' > merged_pr_numbers.txt
cat merged_pr_numbers.txt | xargs -I {} gh pr view {} --json title,labels,author,number > merged_pr_details.json
- name: Generate summary with OpenAI
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL: "gpt-4o-mini-2024-07-18"
run: |
cat << 'EOF' > generate_summary.js
const fs = require('fs');
const { OpenAI } = require('openai');
const openai = new OpenAI();
async function main() {
const prInfo = fs.readFileSync('merged_pr_details.json', 'utf8');
const prompt = `下記の GitHub の PR 情報の json のリストを元に、PR の title, author, PR番号をリストにしてサマリを作ってください。
- テンプレートを参考に PR はグルーピングする
- title の内容は要約せずそのまま出力する
- author が bot の場合は、 app/ などのプレフィクスは取り除く
- パッケージの更新は、対象のディレクトリごと(api, web)でさらに分類する
出力は、作成したサマリのみとしてください。
テンプレート:
\`\`\`
## 機能
- #0: title (author)
## 不具合修正
- #0: title (author)
## リファクタリング
- #0: title (author)
## パッケージの更新
### api
- #0: title (author)
### web
- #0: title (author)
## その他
- #0: title (author)
\`\`\`
PRの一覧:
\`\`\`
${prInfo}
\`\`\``;
const completion = await openai.chat.completions.create({
model: process.env.OPENAI_MODEL,
messages: [{ role: "user", content: prompt }],
});
const summary = completion.choices[0].message.content;
fs.writeFileSync('pr_summary.md', summary);
}
main().catch(console.error);
EOF
node generate_summary.js
- name: Update PR description
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ ! -f pr_summary.md ]; then
echo "Error: Summary file not found"
exit 1
fi
if [ ! -s pr_summary.md ]; then
echo "Error: Summary file is empty"
exit 1
fi
SUMMARY=$(cat pr_summary.md)
gh pr edit $PR_NUMBER --body "$SUMMARY"
動くかどうかはこれから。