giftee Tech Blog

ギフティの開発を支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信しています。

Devin + GitHub Actions で Renovate PR にサマリーを自動投稿する

eyecatch

はじめに

ギフティでエンジニアをしているhsatoです。

開発や運用を行っていると、Renovate や Dependabot などの依存関係更新ツールによってバージョンアップの PR が日々作成されると思います。

OSS のライブラリが更新されてより良いものになったり、バージョンアップを検知して PR が自動で作成されることはすごくありがたいものの、「何が変わったか」「破壊的変更はないか」「テストは通るか」をひとつずつ確認するのは時間がかかり、放置しがちになる...そんな経験はないでしょうか。

本記事では、DevinGitHub Actions を組み合わせて、Renovate の PR に自動で「アップデートサマリー」をコメントさせる仕組みを導入した話を紹介します。 「PR を開いたらすでにサマリーがある」状態にすることで、レビュー効率を上げることを目指して作ってみました。

目指した状態

  • Renovate PR に、変更内容のサマリーが自動的にコメントされている
  • サマリーは 破壊的変更の有無・影響範囲 など、マージ判断に役立つ情報を含む

採用した構成:Devin + GitHub Actions

Renovate PR の作成や更新をトリガーにサマリーを PR に自動投稿するために、次の2つを組み合わせて実現しました。

具体的なフロー

具体的には以下のフローで実現しました。

  1. Renovate が PR を open / push する
    • pull_request イベント(opened / synchronize / reopened)が発火
  2. GitHub Actions が「PR 作者が renovate[bot] か」を判定
    • Renovate の PR だけに限定してジョブを実行
  3. 既存の Devin コメントがあるか確認
    • 「# アップデートサマリー by Devin」で始まるコメントを検索
  4. Devin API でセッションを作成
    • 既存コメントあり: 「この PR を分析し、既存コメントを編集して更新してください」とプロンプト
    • 既存コメントなし: 「この PR を分析し、サマリーを新規コメントしてください」とプロンプト
  5. Devin が PR を分析し、コメントを投稿 or 更新
    • セッション作成時の指示に沿って、差分の要約・破壊的変更の有無などをまとめる

詳細な実装やプロンプトについては、記事末尾のおまけ:Renovate PR 向け GitHub Actions ワークフローとプロンプトに記載しています。

どんな感じでコメントされているのか

以下は実際に Devin が生成したサマリーコメントです。

devin-comment-1 devin-comment-2 devin-comment-3 devin-comment-4

所感

Devin の出力はあくまで補助的な情報であり、情報の正確性の確認や最終的な判断は必ず人間のレビュワーが責任を持って行う必要がありますが、今まで運用してみた中で見当違いの内容が書かれていたことはなく、とても参考になっています。

パッチアップデートであっても、issue で問題が指摘されている場合はサマリーに警告が出ることもあり、マージ判断の参考になっていると感じています。

また、おすすめの新機能 セクションを用意することにより、今までは流してしまっていたような新機能にも気づけるようになりました。

まとめ

本記事では、Devin と GitHub Actions を用いて Renovate PR に アップデートサマリーを自動でコメントする仕組みについて紹介しました。PR を開いた時点でサマリーが用意されているようにすることで、マージ判断の精度向上・確認工数の削減に繋がっているかなと思います。

同じように依存関係更新 PR のレビュー効率を改善させたいチームの参考になれば幸いです。

おまけ:Renovate PR 向け GitHub Actions ワークフローとプロンプト

GitHub Actions ワークフロー

実際に使用しているワークフローは以下の通りです。

name: Renovate PR Summary

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: read

jobs:
  summarize-renovate-pr:
    if: ${{ github.event.pull_request.user.login == 'renovate[bot]' }}
    runs-on: ubuntu-latest

    steps:
      - name: Check for existing Devin comment
        id: check-comment
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          COMMENTS=$(gh api \
            "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
            --paginate \
            --jq '.[] | select(.body | startswith("# アップデートサマリー by Devin")) | {id: .id, body: .body}')

          if [ -n "$COMMENTS" ]; then
            LAST_COMMENT=$(echo "$COMMENTS" | jq -s 'last')
            COMMENT_ID=$(echo "$LAST_COMMENT" | jq -r '.id')
            COMMENT_BODY=$(echo "$LAST_COMMENT" | jq -r '.body')
            echo "existing_comment_id=$COMMENT_ID" >> $GITHUB_OUTPUT
            echo "has_existing_comment=true" >> $GITHUB_OUTPUT
            EOF_MARKER=$(openssl rand -hex 16)
            echo "existing_comment_body<<$EOF_MARKER" >> $GITHUB_OUTPUT
            echo "$COMMENT_BODY" >> $GITHUB_OUTPUT
            echo "$EOF_MARKER" >> $GITHUB_OUTPUT
          else
            echo "has_existing_comment=false" >> $GITHUB_OUTPUT
          fi

      - name: Create Devin Session for PR Summary
        id: devin-session
        env:
          DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY_V3 }}
          DEVIN_ORG_ID: ${{ vars.DEVIN_ORG_ID }}
          DEVIN_PLAYBOOK_ID: ${{ vars.DEVIN_RENOVATE_PLAYBOOK_ID }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}
          HAS_EXISTING_COMMENT: ${{ steps.check-comment.outputs.has_existing_comment }}
          EXISTING_COMMENT_ID: ${{ steps.check-comment.outputs.existing_comment_id }}
          EXISTING_COMMENT_BODY: ${{ steps.check-comment.outputs.existing_comment_body }}
        run: |
          if [ "$HAS_EXISTING_COMMENT" = "true" ]; then
            PAYLOAD=$(jq -n \
              --arg playbook_id "$DEVIN_PLAYBOOK_ID" \
              --arg repo "$REPO" \
              --arg pr_number "$PR_NUMBER" \
              --arg pr_title "$PR_TITLE" \
              --arg comment_id "$EXISTING_COMMENT_ID" \
              --arg comment_body "$EXISTING_COMMENT_BODY" \
              '{
                playbook_id: $playbook_id,
                prompt: "リポジトリ \($repo) の PR #\($pr_number) を分析してください。\nタイトル: \($pr_title)\n\n既存のコメント (ID: \($comment_id)) があります。新しいコメントを作成せず、既存のコメントを編集して更新してください。\n既存のコメント内容:\n\($comment_body)"
              }')
          else
            PAYLOAD=$(jq -n \
              --arg playbook_id "$DEVIN_PLAYBOOK_ID" \
              --arg repo "$REPO" \
              --arg pr_number "$PR_NUMBER" \
              --arg pr_title "$PR_TITLE" \
              '{
                playbook_id: $playbook_id,
                prompt: "リポジトリ \($repo) の PR #\($pr_number) を分析してください。\nタイトル: \($pr_title)"
              }')
          fi

          # Create Devin session
          RESPONSE=$(curl -s -X POST \
            -H "Authorization: Bearer $DEVIN_API_KEY" \
            -H "Content-Type: application/json" \
            -d "$PAYLOAD" \
            "https://api.devin.ai/v3/organizations/$DEVIN_ORG_ID/sessions")

          # Check for errors
          ERROR_MSG=$(echo "$RESPONSE" | jq -r '.detail // empty')
          if [ -n "$ERROR_MSG" ]; then
            echo "Error creating Devin session: $ERROR_MSG"
            exit 1
          fi

          SESSION_ID=$(echo "$RESPONSE" | jq -r '.session_id')
          SESSION_URL=$(echo "$RESPONSE" | jq -r '.url')

          if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then
            echo "Error: Failed to create Devin session."
            echo "Response: $RESPONSE"
            exit 1
          fi

          echo "session_id=$SESSION_ID" >> $GITHUB_OUTPUT
          echo "session_url=$SESSION_URL" >> $GITHUB_OUTPUT
          echo "Devin session created successfully: $SESSION_URL"

シークレットと変数

リポジトリ(または Organization)の Settings → Secrets and variables → Actions に、次のシークレットと変数を登録してください。

種別 名前 説明
Secret DEVIN_API_KEY_V3 Devin API v3 認証用
Variable DEVIN_ORG_ID Devin の組織 ID
Variable DEVIN_RENOVATE_PLAYBOOK_ID 後述 Playbook の ID

各値の取得方法については以下のとおりです。

GITHUB_TOKEN について

ワークフローでは gh api で PR コメントを取得するために secrets.GITHUB_TOKEN を使っています。これはジョブ開始時に GitHub によって自動作成される トークンで、Secrets への登録は不要です。

本ワークフローでは permissionscontents: readpull-requests: read のみ付与しています。

ワークフローの責務

このワークフローは Devin セッションの作成までを担います。PR へのコメント投稿やライブラリ調査は、起動した Devin セッションが Playbook に従って行います。そのため、Devin の環境設定 を事前に済ませておく必要があります。

プロンプトについて

この構成では、GitHub Actions から Devin API でセッションを作成する際に、分析手順や出力フォーマットまで含めた長いプロンプトを毎回渡すのではなく、Devin の Playbook 機能を利用しています。

Playbook は、繰り返し行うタスク向けに、手順・達成条件・注意点などを Markdown で定義しておける再利用可能なプロンプトです。GitHub Actions から Devin API でセッションを作成する際に playbook_id を指定することで、毎回長い指示を渡さなくても、その Playbook の内容に沿って Devin が作業を進めてくれます。

本構成では、GitHub Actions 側のプロンプトで「どの PR を分析するか」を指定し、分析の進め方やコメントの書き方は Playbook に寄せています。これにより、GitHub Actions 側の実装をシンプルに保ちつつ、分析ルールの変更は Playbook の更新だけで対応できるようにしています。

Renovate PR のアップデートサマリーを投稿するために使用している Playbook の内容は、以下のとおりです。

# ライブラリアップデート更新情報の要約

## 概要

主なタスクは dependabot や renovate が作成したバージョンアップの Pull Request(以下 PR) を元に、アップデート内容に問題が無いか確認して、その情報を PR にコメントすることです。
あなたの責任は、バージョンアップが安全に行われることを担保することと、レビュワーにその根拠を分かりやすく伝えることです。
あなたは PR に対してコメントを投稿するだけに専念し、コードの変更、PRのマージは行いません。

## 対応言語

日本語

## ユーザから必ず与えられる情報

- リポジトリ名
- PR番号

## 手順

### 1. 作業対象のPRの確認

1-1. ユーザーからリポジトリ名とPR番号を受け取ります。
1-2. 該当のPRにアクセスし、作業対象として確認します。

### 2. アップデート対象のライブラリの概要を調査

2-1. アップデート対象のライブラリを特定する(PR のライブラリのURLから判断する)
2-2. ライブラリの概要を調査する
    - 何のために導入しているライブラリなのか
    - ライブラリの役割はなんなのか
    - 対象のリポジトリではどのような使われ方をしているのか

### 3.  ライブラリのアップデート内容の確認

3-1. ライブラリの GitHub リポジトリにアクセスして、アップデート対象のバージョンまでのバージョンを洗い出す
    - 新旧のバージョン番号は PR のコード差分から確認する
3-2. リリースノート(Releases)が存在する場合はリリースノートにアクセスして内容を確認する
    - PR の `release note` のコメントは完全ではないので無視すること
    - PR の説明にある release note 内の Sourced from xxx のリンク先が正しい情報なので、必ずアクセスして確認すること

### 4. アップデート内容の危険度を調査する

4-1. 手順3で確認した内容を元にアップデートの中に破壊的変更がないか確認する
4-2. アップデート内容を確認して、デグレーションの危険度を確認する
    - High: 破壊的変更がある場合
    - Middle: 破壊的変更ではないが、確認する必要がある場合
    - Low: そのままマージしても問題無い場合

### 5. PR にコメントを投稿する

5-1. 今まで確認した内容をまとめて PR にコメントを行う
    - 既存のコメントを確認し、存在する場合は当該コメントを編集する

## コメントのフォーマット

コメントのフォーマットは以下に則って下さい。

```md
# アップデートサマリー by Devin

## xxxx(GitHub リンク付きのライブラリ名)の概要

<!-- ここに手順2で調査したライブラリの概要を記載する。-->

## 今回含まれるバージョン

<!-- 手順3でリストアップしたバージョンを記載する。-->

- xxx
- xxx

## (変更前のバージョン)から(変更後のバージョン)までのアップデート内容

### (バージョン番号)の変更点

<!--
  アップデート内容を記載していく
  バグ・パフォーマンス・機能追加などに分けて記載するのが望ましい

  セクションには絵文字を付けてください。
  例:新機能 - ✨、バグ - 🐛、パフォーマンス - 🚀 etc...

  PR 番号が分かる場合は、PR 番号と一緒に PR へのリンクを貼ってください。
  例:[#4310](https://redirect.github.com/graphql/graphql-js/pull/4310)
-->

- xxx

...

## 破壊的変更の有無

<!-- 破壊的変更の有無を記載する -->

## デグレーションの危険度: (Low(低))

<!-- デグレーションの危険度を記載する。根拠も記載する。-->

High: 破壊的変更がある場合
Middle: 破壊的変更ではないが、確認する必要がある場合
Low: そのままマージしても問題無い場合

## おすすめの新機能

<!--
  今回のアップデートに含まれる新機能の中で、対象リポジトリで活用できそうなものがあれば記載する。
  なければこのセクションは「特になし」と記載する。

  機能ごとに ### で区切って以下の形式で記載する:

  ### 機能名

  #### 概要
  機能の説明(公式ドキュメントやリリースノートへのリンク付き)

  #### 活用シーン
  どのような場面で使えそうか

  #### 導入方法
  簡単な使い方やコード例(任意)

  注意:
  - 既存コードの変更が必要な機能は、変更の影響範囲も併せて記載する
  - 実験的機能(experimental/beta)の場合はその旨を明記する
-->

## その他

<!-- その他のコメントがあれば、トピック毎にセクションを分けて記載する。-->


```

## タスク完了の条件

- アップデート対象のライブラリの概要が PR にコメントが追加されていること
    - `gh pr comment` を使って PR にコメントをすること
    - 既存のコメントが存在する場合は、当該のコメントに対して編集を行う事
    - コメント投稿前に必ず既存コメントの有無を確認すること
- アップデート対象のライブラリのアップデート情報が PR に直接コメントされていること
- GitHub コメントは**自然な日本語で書かれている**こと。

## 禁止事項

- PRブランチのコードを直接**変更しないこと**。
- プルリクエストを**マージしないこと**。
- レビューコンテキストの一部として特に指示がない限り、テストを**実行しないこと**。
- 曖昧で役に立たない、または攻撃的なコメントを**しないこと**。
- PRへのレビューコメント追加の範囲を超えて、リポジトリに**いかなる変更も加えないこと**。
- 重大な問題が未解決のままPRを**承認しないこと**(明示的に指示された場合を除く)。