Closed4

リリースの Pull Request のサマリを生成AIに作ってもらう

daisaru11daisaru11

リリース時に、ワークフローは以下の通り。

  • main ブランチから release/prd ブランチに向けた PR を作成する
  • 内容をチェックしてマージ
  • release/prd への push をトリガーにデプロイの Actions が実行される

このときの PR の description にいい感じのサマリ(リリースノート)が自動で書かれるようにしたい。
GitHub Release だとボタンポチで作れるようなやつ のイメージで、含まれる PR をリストアップしたい。

既存のツール・Actionsだと、調べた感じ下記のようなところが有名どころな気がする。

そもそも、リリースのデプロイのトリガーを GitHub Release の作成にしてしまえば標準の機能でリリースノートが作れるのでそれで良いという説はある。
が、なんとなく PR マージの方がハマる気がしていて好み(慣れの問題かもしれない)。

git-pr-release は、これまでもよく使っていて、シンプルで使いやすいんだけど、含まれる PR をグルーピングしたい。特に dependabot を運用していると、パッケージアップデートのマージが頻繁にあるので、これらは下の方にして、上の方に機能追加の PR が来るようにしたい。
テンプレートの中で、 erb が使えるので、そこでラベルとかでグルーピングするというのはありそう。

release-please は Conventional Commit と squash merge が前提になっているような空気なので、もうちょっと緩い運用でそれっぽくなるような感じが良い。

緩い運用でなんとかしたいと言えば、 AI にサマリを作らせるというのは適材適所な気もしたので試す。

daisaru11daisaru11

できれば 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
daisaru11daisaru11

↑で得られた 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すごい。

daisaru11daisaru11

これまでの結果を 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"

動くかどうかはこれから。

このスクラップは2025/02/23にクローズされました