STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

DependabotのPRをAIにマージさせよう

この記事はSTORES Advent Calendar 2025の16日目の記事です。

こんにちは。Webエンジニアをしているotariidaeです。今月は調子に乗って3つも記事を書いています。

この記事では、先日開催されたSTORES Tech Conf 2025 “What Would You Do?”でのポスター発表「LGTM, Dependabot! 見てないけど」の内容をより具体例で解説していきます。

ポスターの内容のおさらい

ポスターの内容は下記で公開しています。

www.docswell.com

要約:Dependabotなどで依存ライブラリ更新のPRが自動作成されるようになったものの、コードレビューがボトルネックとなり停滞しがちです。 この問題を解決するため、レビューからマージまでのプロセスを一部自動化するワークフローを構築しました。 安全性を担保するための拠り所として、CI、セマンティックバージョニング、AIを採用しました。

具体例

ともかく上記のフローをGitHub Actionsで構築した具体例を次に示します。

on:
  pull_request:
    branches:
      - main
    types: [opened, synchronize, reopened]

jobs:
  determine:
    runs-on: ubuntu-latest
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    outputs:
      strategy: ${{ steps.determine.outputs.strategy }}

    steps:
      - uses: dependabot/fetch-metadata@v2
        id: fetch-metadata

      - id: determine
        # パッチバージョンの更新 → 無条件で自動マージ有効
        # マイナーバージョンの更新 → AIがapproveしたら自動マージ有効
        # それ以外の更新 → AIがレビューコメントだけ投稿
        run: |
          if [ "$UPDATE_TYPE" != "version-update:semver-patch" ]; then
            echo "strategy=merge-without-review" >> $GITHUB_OUTPUT
          elif [ "$UPDATE_TYPE" != "version-update:semver-minor" ]; then
            echo "strategy=merge-with-ai-review" >> $GITHUB_OUTPUT
          else
            echo "strategy=ai-review-comment-only" >> $GITHUB_OUTPUT
          fi
        env:
          UPDATE_TYPE: ${{ steps.fetch-metadata.outputs.update-type }}

  merge-without-review:
    runs-on: ubuntu-latest
    needs: determine
    if: needs.determine.outputs.strategy == 'merge-without-review'
    steps:
      - uses: actions/create-github-app-token@v2
        id: app-token
        with:
          app-id: ${{ vars.BOT_APP_ID }}
          private-key: ${{ secrets.BOT_PRIVATE_KEY }}
          owner: heyinc
          repositories: example-repo

      - run: |
          gh pr review --approve "$PR_URL"
          gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ steps.app-token.outputs.token }}

  merge-with-ai-review:
    runs-on: ubuntu-latest
    needs: determine
    if: needs.determine.outputs.strategy == 'merge-with-ai-review'
    permissions:
      contents: write
      pull-requests: write
      checks: read
      id-token: write
      actions: read
    steps:
      - uses: actions/checkout@v5

      - name: Generate GitHub App token
        id: claude-code-app-token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ vars.CLAUDE_CODE_APP_ID }}
          private-key: ${{ secrets.CLAUDE_CODE_APP_PRIVATE_KEY }}
          owner: heyinc
          repositories: |
            example-repo
            marccket

      - name: Configure AWS Credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: ${{ vars.CLAUDE_CODE_ROLE_TO_ASSUME }}
          role-session-name: auto-merge
          aws-region: ${{ env.AWS_REGION }}

      - uses: anthropics/claude-code-action@v1
        with:
          claude_args: |
            --model global.anthropic.claude-sonnet-4-5-20250929-v1:0
          use_bedrock: "true"
          use_commit_signing: "true"
          github_token: ${{ steps.claude-code-app-token.outputs.token }}
          allowed_bots: "*"
          additional_permissions: |
            actions: read
          plugin_marketplaces: "https://x-access-token:${{ steps.claude-code-app-token.outputs.token }}@github.com/heyinc/marccket.git"
          plugins: "dependabot@marccket"
          prompt: |
            /dependabot:lgtcc-dependabot ${{ github.repository }} ${{ github.event.pull_request.number }}

      - name: check if AI approved
        id: check-ai-approved
        run: |
          APPROVED=$(gh pr view ${{ github.event.pull_request.number }} --json reviews \
            --jq '.reviews | any(.state == "APPROVED" and .author.login == "stinc-claude-code-action")')
          echo "approved=$APPROVED" >> $GITHUB_OUTPUT
        env:
          GH_TOKEN: ${{ github.token }}

      - uses: actions/create-github-app-token@v2
        id: app-token
        with:
          app-id: ${{ vars.BOT_APP_ID }}
          private-key: ${{ secrets.BOT_PRIVATE_KEY }}
          owner: heyinc
          repositories: example-repo

      - name: merge if AI approved
        id: merge
        if: steps.check-ai-approved.outputs.approved == 'true'
        run: |
          gh pr merge --auto --merge "$PR_URL"
          echo "merged=true" >> $GITHUB_OUTPUT
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ steps.app-token.outputs.token }}

  ai-review-comment-only:
    runs-on: ubuntu-latest
    needs: determine
    if: needs.determine.outputs.strategy == 'ai-review-comment-only'
    permissions:
      contents: write
      pull-requests: write
      checks: read
      id-token: write
      actions: read
    steps:
      - uses: actions/checkout@v5

      - name: Generate GitHub App token
        id: claude-code-app-token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ vars.CLAUDE_CODE_APP_ID }}
          private-key: ${{ secrets.CLAUDE_CODE_APP_PRIVATE_KEY }}
          owner: heyinc
          repositories: |
            example-repo
            marccket

      - name: Configure AWS Credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: ${{ vars.CLAUDE_CODE_ROLE_TO_ASSUME }}
          role-session-name: auto-merge
          aws-region: ${{ env.AWS_REGION }}

      - uses: anthropics/claude-code-action@v1
        with:
          claude_args: |
            --model global.anthropic.claude-haiku-4-5-20251001-v1:0
          use_bedrock: "true"
          use_commit_signing: "true"
          github_token: ${{ steps.claude-code-app-token.outputs.token }}
          allowed_bots: "*"
          additional_permissions: |
            actions: read
          plugin_marketplaces: "https://x-access-token:${{ steps.claude-code-app-token.outputs.token }}@github.com/heyinc/marccket.git"
          plugins: "dependabot@marccket"
          prompt: |
            /dependabot:review-dependabot ${{ github.repository }} ${{ github.event.pull_request.number }}

このワークフローが実行されると、セマンティックバージョニングに基づき軽微な変更もしくはAIがレビューして承認したものは自動マージが有効になります。あとはリポジトリに設定してあるブランチ保護ルールやルールセットで規定されたCI成功などの条件を満たせば勝手にマージされていくという算段です。

ジョブの構成

最初のdetermineジョブで更新のメタデータを見たうえでどのフローに進むか判定し、各フローごとにジョブを定義しています。

ステップごとのif文とかで1ジョブにまとめることも可能ではありますが、ワークフロー全体の見通しの良さを重視してジョブで分けることにしました。

determineジョブの判定基準

上記の例ではシンプルにupdate-typeだけで判定していますが、実際にはdependency-typeやpackage-ecosystemなども考慮してリポジトリの特性や取れるリスク感に合わせた判定基準にカスタマイズすると良いと思います。

Claude Codeのプラグインマーケットプレイスの利用

このワークフローを複数リポジトリで利用することを踏まえ、AIにレビューさせるためのプロンプトを一元的に管理するためにClaude Codeのプラグインマーケットプレイスを作成しました。marccket*1というのがそれです。

社内用のプライベートリポジトリなので、claude-code-actionの引数で次のように認証情報も指定する必要がありました。

plugin_marketplaces: "https://x-access-token:${{ steps.claude-code-app-token.outputs.token }}@github.com/heyinc/marccket.git"

dependabotというプラグインのなかにreview-dependabot, lgtcc-dependabot*2というカスタムスラッシュコマンドを実装しており、リポジトリ名とPR番号を渡すとレビューしてくれるようにしています。

Claude Code Actionでapproveされたかどうか判定する

Claude Code Actionのstructured outputsも使えますが、愚直にghコマンド+jq芸で判定することにしました。

APPROVED=$(gh pr view ${{ github.event.pull_request.number }} --json reviews \
            --jq '.reviews | any(.state == "APPROVED" and .author.login == "stinc-claude-code-action")')

その他Claude Code Actionの設定など

コストを追跡しやすくするためにrole-session-nameを付けたり、自動マージに直結しないレビューコメントのみならSonnetではなくHaikuを使ったり、CIの結果を読ませるためにadditional_permissionsを加えたり、もろもろの設定をしています。

まとめと今後の課題

条件付きでAIのレビュー判断を信じることで、ポスター発表でも記載した通り、このワークフローはSTORES内の複数のリポジトリで活用され累計100件以上のPRをすでにマージした実績をあげてくれています。今のところライブラリ更新起因での障害は発生していません。

一方でまだまだ課題を感じる部分もあります。自動でマージまで至らずに人間の判断に任されたPRはやはり滞留するという点です。特定のシステム専任の開発チームがおらず、プロジェクトカットのエンジニア組織構成を採用しているSTORESでは、やはりなかなかこのようなメンテナンスの類は優先度が上がりません。DependabotのPRには同時に存在する数の上限があるので、人間の判断が滞留するとPRの新陳代謝が起こらずに自動マージも動きません。現状からより一歩進めて、さらに人間に依存することなく安全かつスムーズにメンテナンス業務を回す仕組みをつくりたいなと思っているところです。

みなさんのDependabot PR捌きの参考になれば幸いです。

*1:Claude Code(CC)のマーケットだからmarccketという名前にしました

*2:lgtccはLooks good to Claude Codeの略です