チケット起票するだけで、リポジトリを跨いでプルリクエストを自動生成できるようにしてみた

この記事はコインチェック株式会社(以下、コインチェック)のアドベントカレンダー3日目の記事です。 qiita.com

自己紹介

みなさん、こんにちはこんばんは。

コインチェックのプロダクトエンジニアリング部のリスティング&プロトコル GでエンジニアをしているYotaです。

普段はステーキング関連の開発を主にしています。

たまに勉強会に登壇しています。 coincheck.connpass.com

今回は、チケット作成からプルリクエスト作成までを自動化するツールを作成してみたので紹介します。

はじめに

バックログ、気づいたら増えていませんか?

ある課題を見つけてチケット起票をしても、結局開発リソースの関係などで手付かずのチケットがバックログに取り残されてませんでしょうか。

チケットをこまめに立てるのはいいこと。でも、実装に結びつかないまま放置されたチケットが積み上がっていくと、 それはもはや「バックログ管理」ではなく「チケットの墓場」になってしまいます。

この問題を解決するために、チケットを作った瞬間にPRが自動で生まれる仕組みを作ってみました。 Issueを作るだけでPRが自動生成され、開発者はレビューに集中できます。

今回は、そんな「チケット駆動開発の自動化」の仕組みをリポジトリを跨いでPoCとして実装してみた話を書きます。

今回実現したいこと

今回はGitHubのProjectでIssueを作成したときに、そのIssueを元に別リポジトリに存在するソースコードを修正するPRの自動生成を行います。

自動生成の流れ

  1. 手動でリポジトリAにIssueを作成
  2. 手動でIssueにready-for-copilotラベルを付与
  3. 2をトリガーにリポジトリAのGitHub Actionsが起動
  4. Github Actionにより、リポジトリBにIssueを作成
  5. Github Actionにより、リポジトリBに作成したIssueにGithub Copilotをアサイン
  6. Githubの機能により、5をトリガーに自動でPull RequestがリポジトリBに作成される

リポジトリA, Bの役割を整理すると下記のようになります。

  • リポジトリA(チケット起票側)
    • チケット起票
    • Github Action実行用のymlファイル管理
    • Github Action実行
  • リポジトリB(ソースコードが置いてあるリポジトリ)
    • PR作成

なぜこのようなリポジトリを跨ぐ構成になったかというと、以下のような運用をしているためです:

  • リポジトリA:チーム専用のプロジェクト管理リポジトリ

    • GitHub Projectでのバックログ管理
    • スクラム関連のドキュメント
    • チーム独自の自動化ツール
  • リポジトリB:実際のプロダクトコードが存在するリポジトリ

    • 複数チームが共同で開発
    • 厳格なレビュープロセス

このような分離により、チームの自由度を保ちながら、プロダクトコードの品質管理の維持に努めています。そこで、リポジトリAでのIssue管理からリポジトリBでのPR作成を自動化する、今回のような仕組みを実装しました。

実現するための前提条件

本記事を参考に実際に試す場合、Githubの機能を利用するため、下記を満たしていないと実現できません。

Github Actionsを利用できるリポジトリが最低一つあること Github Copilotを利用できるリポジトリが最低一つあること

やってみる

それでは実際にやってみましょう

ステップ1:Issue作成で起動するワークフローを作成する

まず、リポジトリAでGithub Actionsの基本構造を定義します。

.github/workflowsにauto_copilot_pr.ymlを作成します。

これでリポジトリAでIssueを作成した時に動くGithub Workflowの内容を定義しています。

name: Auto assign Copilot Coding Agent

on:
  issues:
    types: [opened, labeled]

jobs:
  handle-issue:
    name: Create Issue in repository-b and assign to Copilot
    runs-on: ubuntu-latest
    if: (github.event.action == 'opened' || github.event.action == 'labeled') && (github.event.label.name == 'ready-for-copilot')
    steps:
      - name: Check GitHub CLI version
        run: gh --version

ステップ2:認証情報設定

今回のGithub Actionsを実行するにあたり、リポジトリの操作権限をワークフローに与えるためのPersonal Access Token(PAT)が必要になるので、発行してください。

発行するPATには下記の権限を付与しておきます

  • アクセス可能なリポジトリ
    • Repository A
    • Repository B
  • Permission
    • Issues: Read and Write
    • Metadata: Read-only

発行したPATはリポジトリAのActions Secretsに登録します。

PATのsecret登録

ステップ3:Issue情報の抽出と環境変数への保存

次に、STEP2でActions Secretsに設定した認証情報を利用してIssue情報を環境変数として記録する処理を記述します。

注意:GH_PATはActions Secretsに設定したRepository Secrets名に合わせて変更してください。

ここでは、リポジトリAでIssueを作るたびにリポジトリBにIssueが作成されないように、ready-for-copilotというラベルが付与されたissueに対してのみ、本ロジックが動くように制御しています。

name: Auto assign Copilot Coding Agent

on:
  issues:
    types: [opened, labeled]

jobs:
  handle-issue:
    name: Create Issue in repository-b and assign to Copilot
    runs-on: ubuntu-latest
    if: (github.event.action == 'opened' || github.event.action == 'labeled') && (github.event.label.name == 'ready-for-copilot')
    env:
      GH_TOKEN: ${{ secrets.GH_PAT }}
    steps:
      - name: Extract issue info
        id: issue_data
        run: |
          echo "title<<EOF" >> "$GITHUB_OUTPUT"
          echo "${GITHUB_EVENT_TITLE}" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
      
          echo "body<<EOF" >> "$GITHUB_OUTPUT"
          echo "${GITHUB_EVENT_BODY}" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
      
          echo "number=${GITHUB_EVENT_NUMBER}" >> "$GITHUB_OUTPUT"
        shell: bash
        env:
          GITHUB_EVENT_TITLE: ${{ github.event.issue.title }}
          GITHUB_EVENT_BODY: ${{ github.event.issue.body }}
          GITHUB_EVENT_NUMBER: ${{ github.event.issue.number }}

      - name: Check GitHub CLI version
        run: gh --version

ステップ4:リポジトリBへのIssue作成

記録したIssue情報を使って、リポジトリBに新規Issueを作成します。 注意:owner-orgの部分は、実際にリポジトリを所有している組織名やユーザー名に置き換えてください

name: Auto assign Copilot Coding Agent

on:
  issues:
    types: [opened, labeled]

jobs:
  handle-issue:
    name: Create Issue in repository-b and assign to Copilot
    runs-on: ubuntu-latest
    if: (github.event.action == 'opened' || github.event.action == 'labeled') && (github.event.label.name == 'ready-for-copilot')
    env:
      GH_TOKEN: ${{ secrets.GH_PAT }}
    steps:
      - name: Extract issue info
        id: issue_data
        run: |
          echo "title<<EOF" >> "$GITHUB_OUTPUT"
          echo "${GITHUB_EVENT_TITLE}" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
      
          echo "body<<EOF" >> "$GITHUB_OUTPUT"
          echo "${GITHUB_EVENT_BODY}" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
      
          echo "number=${GITHUB_EVENT_NUMBER}" >> "$GITHUB_OUTPUT"
        shell: bash
        env:
          GITHUB_EVENT_TITLE: ${{ github.event.issue.title }}
          GITHUB_EVENT_BODY: ${{ github.event.issue.body }}
          GITHUB_EVENT_NUMBER: ${{ github.event.issue.number }}

      - name: Check GitHub CLI version
        run: gh --version

      - name: Create corresponding issue in repository-b
        id: new_issue
        env:
          REPO_B_TITLE: ${{ steps.issue_data.outputs.title }}
          REPO_B_NUMBER: ${{ steps.issue_data.outputs.number }}
        run: |
          gh issue create \
            --repo owner-org/repository-b \
            --title "$REPO_B_TITLE" \
            --body $'Originally from:\n- owner-org/repository-a#'"${REPO_B_NUMBER}" > created_issue.txt

          cat created_issue.txt

          ISSUE_URL=$(grep -Eo 'https://github.com/[^ ]+/issues/[0-9]+' created_issue.txt | head -n 1)
          echo "url=$ISSUE_URL" >> "$GITHUB_OUTPUT"

      - name: Get new issue number from URL
        id: issue_number
        run: |
          issue_number=$(echo "${{ steps.new_issue.outputs.url }}" | grep -oE '[0-9]+$')
          echo "number=$issue_number" >> "$GITHUB_OUTPUT"

ステップ5:GraphQL APIでGithub Copilotを検索してIssueにアサインする

Github Copilotのユーザー情報と作成したIssueを取得し、Github CopilotをIssueにアサインします。

注意:copilot-agentの部分は、実際のCopilot Botのログイン名に置き換えてください

name: Auto assign Copilot Coding Agent

on:
  issues:
    types: [opened, labeled]

jobs:
  handle-issue:
    name: Create Issue in repository-b and assign to Copilot
    runs-on: ubuntu-latest
    if: (github.event.action == 'opened' || github.event.action == 'labeled') && (github.event.label.name == 'ready-for-copilot')
    env:
      GH_TOKEN: ${{ secrets.GH_PAT }}
    steps:
      - name: Extract issue info
        id: issue_data
        run: |
          echo "title<<EOF" >> "$GITHUB_OUTPUT"
          echo "${GITHUB_EVENT_TITLE}" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
      
          echo "body<<EOF" >> "$GITHUB_OUTPUT"
          echo "${GITHUB_EVENT_BODY}" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
      
          echo "number=${GITHUB_EVENT_NUMBER}" >> "$GITHUB_OUTPUT"
        shell: bash
        env:
          GITHUB_EVENT_TITLE: ${{ github.event.issue.title }}
          GITHUB_EVENT_BODY: ${{ github.event.issue.body }}
          GITHUB_EVENT_NUMBER: ${{ github.event.issue.number }}

      - name: Check GitHub CLI version
        run: gh --version

      - name: Create corresponding issue in repository-b
        id: new_issue
        env:
          REPO_B_TITLE: ${{ steps.issue_data.outputs.title }}
          REPO_B_NUMBER: ${{ steps.issue_data.outputs.number }}
        run: |
          gh issue create \
            --repo owner-org/repository-b \
            --title "$REPO_B_TITLE" \
            --body $'Originally from:\n- owner-org/repository-a#'"${REPO_B_NUMBER}" > created_issue.txt

          cat created_issue.txt

          ISSUE_URL=$(grep -Eo 'https://github.com/[^ ]+/issues/[0-9]+' created_issue.txt | head -n 1)
          echo "url=$ISSUE_URL" >> "$GITHUB_OUTPUT"

        

      - name: Get new issue number from URL
        id: issue_number
        run: |
          issue_number=$(echo "${{ steps.new_issue.outputs.url }}" | grep -oE '[0-9]+$')
          echo "number=$issue_number" >> "$GITHUB_OUTPUT"

      - name: Get Copilot user ID
        id: get_copilot_id
        run: |
          copilot_id=$(gh api graphql -f query='
            query {
              repository(owner: "owner-org", name: "repository-b") {
                suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 10) {
                  nodes {
                    login
                    ... on Bot {
                      id
                    }
                  }
                }
              }
            }' | jq -r '.data.repository.suggestedActors.nodes[] | select(.login == "copilot-agent") | .id')
          echo "id=$copilot_id" >> $GITHUB_OUTPUT

      - name: Get new issue global node ID
        id: get_issue_node_id
        run: |
          issue_node_id=$(gh api graphql -f query='
            query {
              repository(owner: "owner-org", name: "repository-b") {
                issue(number: ${{ steps.issue_number.outputs.number }}) {
                  id
                }
              }
            }' | jq -r '.data.repository.issue.id')
          echo "id=$issue_node_id" >> "$GITHUB_OUTPUT"

      - name: Assign Copilot to issue
        run: |
          gh api graphql -f query='
            mutation {
              replaceActorsForAssignable(input: {
                assignableId: "${{ steps.get_issue_node_id.outputs.id }}",
                actorIds: ["${{ steps.get_copilot_id.outputs.id }}"]
              }) {
                assignable {
                  ... on Issue {
                    assignees(first: 5) {
                      nodes {
                        login
                      }
                    }
                  }
                }
              }
            }'

完成です!

ここまできたら、あとはリポジトリAでIssueを作成して、ready-for-copilotラベルを付与してあげれば、自動でリポジトリBにプルリクが生成されているはずです。

今後の課題

ここまでで一旦PoCとして最低限の、Issueを作成して別リポジトリにプルリクを出すことができるようになりました。

しかし、Githubの機能に依存している箇所が多かったり、自動生成されるプルリクの精度など、実運用に至らせるためにはまだまだ改善の余地がたくさんあったため、見つけた課題を述べていきます。

ブランチ名の制約

ブランチ名にcopilot/というプレフィックスが自動的に付与されてしまいます。 実際の運用ルールではブランチ命名規則が定められている現場もあると思います。そのような場合に、今回は対応できません。

Githubのみで完結するため、基本的にできることはGithubが提供してくれいている機能に制限されます。現在Github Copilotを利用して生成したプルリクのブランチ名を変えられる機能やGithub Commandは用意されていません。

プルリクの精度

自動生成されるプルリクエストはGithub Copilotが生成するため、精度はGithub Copilotの性能に依存します。

Github CopilotをアサインするIssueの書き方も最適化する余地が残っています。

今回試した範囲では、簡単なカラム追加や数行のコード追加レベルであれば十分、そのままプロダクションコードにマージできそうでしたが、高度なドメイン知識が求められるような複雑な内容は難しそうです。 Github Copilotにcontextを食わせる上手なやり方があれば、もっと精度を上げられると思います。

他のツールとの比較

「そもそもDevinとかでよくない?」という声もあるでしょう。 それはそう。MPCとか使って、JIRAやDevinを連携すれば自動化できると思います。 ただ、今回は複数のサービスを利用せず、Githubのみで完結している点が嬉しいです。

まとめ

今回は、GitHub Issueから自動でPRを作成する仕組みを実装しました。完璧な自動化とはいきませんが、簡単なタスクであれば十分実用的なレベルのPRが作成できることが確認できました。 この仕組みにより、開発者は「実装」ではなく「レビュー」に集中でき、バックログの消化速度向上が期待できます。 皆さんの開発現場でも、このような自動化の仕組みを導入してみてはいかがでしょうか。