🍓

AIにプルリクを作成させて個人開発のモチベを上げる

に公開

1.モチベーション

久々の投稿です。

今回は個人開発のモチベーションを上げるために「自分の好きなキャラを作って、そのキャラにPRを自動生成するbot」を作成しました。

https://github.com/kata-n/llm-assistant-bot

こんな感じです。

開発ではPRの差分をまとめたり、タイトルや本文を整える作業が繰り返し発生します。
特に個人開発の時はPRを書くのがめんどくさくなったり、味気なくなりがちなので、もっと楽しみながら個人開発したいなーと思ってました。

そこで、そのPR作成作業をBotにまかせてみることにしました。
コメントひとつでPRを作ってくれるアシスタントをGeminiを使って構築します。

2.仕組み

このBotの中心的な機能は、コメントをトリガーにして自動でPull Requestを作成することです。
例えば、こんなコメントをIssueに書くと…

feat/skip_merge_comment に対してpr作って

PRを生成してコメント欄にURLを返してくれるというものです。

好みに合わせてプロンプトを設定すればいい感じのキャラがPRを作ってくれます。

2-1.全体像

以下のような流れで処理が行われます:

詳細は6項で説明しています。

3.構成

技術 用途
Cloud Functions for Firebase Webhookの受け取りとBot本体のホスティング(TypeScriptで記述)
GitHub App コメントやPRイベントの取得・APIアクセス
Gemini API(Google Generative AI) 差分からPRのタイトルと本文を生成

今回の技術選定においては、以下の点を重視しました。

まず、リポジトリ毎に設定ができること。すべてのリポジトリにコメントをもらってはうざったくなり過ぎるので、コメントが欲しいリポジトリだけに設定できることを重視しました。

あと、あまり機械的な感じを出したくなかったのでアイコンや名前を設定できる事も重視しました。

これらを踏まえた結果、Github Appを使用することにしました。

なお、GitHub Appは許可する操作を設定できるためセキュリティのリスクも抑えられる点もポイントです。

3-1.コスト

コスト: 個人開発なので、可能な限り無料枠で運用できることを意識しました。

今回の構成は、GitHub App, Cloud Functions, Gemini APIの無料枠を活用しており、料金は発生していません。

3-2.セキュリティ

GitHub Appはリポジトリ単位で権限を設定でき、アクセスも許可しない・読み取りのみ・書き込みを含む3パターンから細かく設定することができます。

これにより、Botに必要以上の権限を与えることなく、そもそもBotに動いてほしくないリポジトリは適用を除外することもできます。

個人トークン(PAT)の場合、通常はアカウント全体へのアクセス権を持つため検討からは外しました。

3-3.サーバレス

利便性: 開発・デプロイが容易で、メンテナンスしやすいこと

Cloud Functionsは、トリガーベースで関数を実行できるため、Webhookとの連携が容易です。
また、Firebaseの各種サービスとの連携も考えてCloud Functionsを利用することにしました。

3-4.LLMの使用

Gemini APIの利点: Gemini APIは、無料試用枠があり、一定回数までは無料で利用できます。

返答もいい感じで返してくれるので私は特に迷いなくGeminiを使用することにしました。

4.Github Appの設定をする

ここからは、実際に設定した内容を記載していきます。

4-1.GitHub App を作成する

  1. GitHubのApp作成ページ にアクセス
  2. New GitHub App」をクリック
  3. 以下のように入力します:
項目 設定例
GitHub App name llm-comment-assistant など
Homepage URL https://hogehoge(なんでもOK)
Webhook URL Functionsのデプロイ後に設定
Webhook secret 今回は空白でOK(※後述)
Permissions 後述の表に従って設定
Subscribe to events issue_comment, issues, pull_request

Webhookの欄について

Webhook URLはCloud Functionsをデプロイした後に入力するので一旦空白で保存します。

Webhook secret はセキュリティ強化用の署名検証に使う値ですが、今回はCloud Functions側で未使用のため空白でOKです。

4-2.Permissions & Events の設定

画面上部の「Permissions & Events」タブに移動し、以下のように設定します:

Category Permission Reason
Issues Read & Write IssueにPR URLをコメントするため
Pull requests Read & Write PRを作成するため
Contents Read-only 差分取得のため

4-3.秘密鍵 .pem を生成・ダウンロード

  1. App 作成後の画面 or 左サイドバーの「Private keys」に移動
  2. Generate a private key」をクリック
  3. .pem ファイルがダウンロードされる

.pem は再ダウンロードできません。紛失した場合は再生成が必要です。
セキュリティのため安全な場所に保管してください。

このタイミングでやらなければいけない訳ではなく、WebhookのURLを取得するタイミングでも問題ありません。

4-4.GitHub App をインストールする

  1. App の設定画面の「Install App」タブを開く
  2. 表示される自分のアカウントに「Install」ボタンがあるのでクリックして使用できるようにします
  3. *リポジトリのアクセス範囲を「Only select repositories」にし、Botを使いたいリポジトリだけを選択

*歯車マークをクリックすると、Repository accessという項目があり、ここでどのリポジトリに対してBotを動作させたいかを指定することができます

5.Cloud Functions側の設定

Functionsは提供される無料枠を使うことができますが、クレジットの登録は必要です。

参考サイト:
https://blog.g-gen.co.jp/entry/cloud-functions-explained

5-1.GCPプロジェクトを作成

Google Cloud Consoleにアクセスします。

左上「プロジェクトを選択」→「新しいプロジェクトを作成」

名前は任意でOK(例: llm-github-bot)

5-2.Cloud Functions API を有効化

ナビゲーションメニュー → 「APIとサービス」→「ライブラリ」

Cloud Functions API を検索して「有効にする」を設定します。

5-3.gcloud CLIをインストール

ローカルからデプロイするためにgcloud CLIをインストールします。

macOSの場合

brew install --cask google-cloud-sdk

インストールが終わったら以下コマンドで設定をします。

gcloud init

コマンドを叩き、Googleアカウントログイン、使用するプロジェクトを設定していきます。

5-4.ディレクトリの作成

まずローカルでフォルダを作成します

mkdir llm-comment-assistant-functions
cd llm-comment-assistant-functions

Firebase CLI を使用して、functions ディレクトリを含む Firebase プロジェクトを初期化します。

まだ Firebase CLI をインストールしていない場合はnpm install -g firebase-tools でインストールしてください。

firebase init

設置が終わったら、ライブラリをインストールします。

firebase-functions 

また、gitHub Appの操作には@octokit/core@octokit/auth-app を使うためインストールします。

functions/src/index.ts
import * as functions from "firebase-functions";
import { app } from "./app";

export const githubWebhook = functions
  .region("asia-northeast1")
  .https.onRequest(app);
functions/src/app.ts
import express from "express";
import bodyParser from "body-parser";
import webhookRouter from "./interface/controller/WebhookController";
import dotenv from "dotenv";

dotenv.config();

const app = express();
app.use(bodyParser.json());
app.use("/", webhookRouter);

export { app };

functions/src/interface/controller/WebhookController.ts
import { Router, Request, Response } from "express";
import { logger } from "firebase-functions/v2";

const router = Router();

router.post("/", async (req: Request, res: Response) => {
  try {
    const payload = req.body;

    logger.info(
      "requested github-llm-bot webhook start",
      `${JSON.stringify(payload)}`
    );
  } catch (error) {
    logger.error("github-llm-bot webhook error", error);
    res.status(500).send("Internal Server Error");
  }
});

export default router;

5-5.Cloud Functionsをデプロイ

一通り書いたら、ローカルからデプロイしてみます。

gcloud functions deploy githubWebhook \
  --entry-point=githubWebhook \
  --runtime=nodejs22 \
  --trigger-http \
  --region=asia-northeast1 \
  --allow-unauthenticated

entry-point は export const githubWebhook = onRequest(...) の関数名に一致させます。

これによってissueのコメントやプルリク作成等の操作を行ってくれる様になります。

最後に、Cloud Functions側の環境変数を設定します。

必要な情報 取得方法
GITHUB_APP_ID GitHub App 設定画面に表示される App ID
GITHUB_INSTALLATION_ID Webhook payload or API で取得
GITHUB_PRIVATE_KEY .pem の中身(base64→復元可)
GEMINI_API_KEY Gooegle Ai Studioから取得

Cloud Functions のデプロイが完了したら、GCP Console に表示されているURLをコピーして GitHub App に設定します。

5-6.動作確認

GitHub App をインストールしたリポジトリで Issue を作成してみます。

Cloud Functions のログを確認し、エラーが発生していないか確認します。

FunctionsにWebhookController.tsで設定したログが出ていれば設定は完了です!

6.issueやプルリクに応じて、functionsが動作する様にする

あとはFunctinosでイベントを受け取った際に処理をするコードを書いていきます。

6-1.Webhookでコメントを受け取る

GitHubで issue_comment イベントをListenし、IssueやPRにコメントがつくたびにCloud Functionsへリクエストが飛んできます。

6-2.コメントに特定のコメントが含まれているか正規表現で判定

Functionsで受け取ったときにプルリクを自動生成してもらうためにissueのコメントに「pr作って」が含まれているかどうかを正規表現で見る様にしました。

if (isPrCreateComment(payload.comment?.body)) {
  // 実行対象
}

/**
 * コメントがPR作成リクエストかどうかをチェック
 */
export function isPrCreateComment(content: string): boolean {
  return /[\s ]*(\S+)[\s ]*に対してpr作って/i.test(content);
}

6-3.ブランチ名を正規表現で抽出

BotにどのブランチからPRを作ってほしいかを伝えるため、コメント内にブランチ名も含めるようにしています。

feature/fix-typo に対してpr作って

上記の文から "feature/fix-typo" を取り出すため、次のような正規表現を使っています。

const sourceBranch = getSourceBranch(payload.comment.body);

/**
 * コメントがPR作成リクエストかどうかをチェック
 */
export function getSourceBranch(content: string): string | null {
  const match = content.match(/[\s ]*(\S+)[\s ]*に対してpr作って/i);
  return match?.[1] || null;
}

6-4.GitHub APIでdevelopとの差分を取得

対象の作業ブランチと develop ブランチとのファイル差分を取得します。

GET /repos/{owner}/{repo}/compare/develop...feature/fix-typo

このレスポンスから、変更されたファイル名や変更内容の抜粋をまとめます。

6-5.Geminiにプロンプトとして渡す

取得した差分を、以下のような形式でGeminiに渡します。

export const PR_DIFF_PROMPT_TEMPLATE = `以下は GitHub の develop ブランチと {sourceBranch} ブランチの差分です。変更の意図をくみ取って、PR タイトルと本文を日本語で丁寧に出力してください。

出力形式:
{
  "title": string,
  "body": string
}

--- 差分 ---
{fileSummaries}`

こうするとGeminiがタイトルと本文を生成し、JSONで返してくれます。

6-6.GitHub APIでPRを作成

Geminiの出力を使ってPRを作成します。

POST /repos/{owner}/{repo}/pulls
{
  "head": "feature/fix-typo",
  "base": "develop",
  "title": "typo修正のPR",
  "body": "このPRでは..."(要約文)
}

6-7.PRのURLを元のIssueにコメント返信

Botが最後に作成されたPRのURLを元のIssueにコメントします。

✨ PRを作成しました: https://github.com/xxx/yyy/pull/42

この一連の処理をBotが作業してくれる事によって、「わざわざGitHub上でボタンを押してPRを作る」という作業が不要になります。

個人開発していてもプルリクを出してくれるBotがいると作業が捗ります。

7.人格を設定していく

複数のBotを使い分けたい時、単にプロンプトを書き換えるだけでは管理が煩雑になります。

そこで今回はBotごとの「人格」を切り替え可能にしました。

気分によって切り替えも可能です(デプロイは必要ですが)

export const BOT_PERSONAS: Record<string, BotPersona> = {
  StandardReviewBot: {
    name: "StandardReviewBot",
    prompt: `あなたは標準的なAIコードレビュアーです。
コードの品質向上を目的とし、客観的かつ建設的なフィードバックを提供します。
コードの設計、可読性、命名規則、責務分離、パフォーマンス、セキュリティなどの観点からレビューを行います。
指摘事項は明確かつ具体的に記述し、改善案も提示するように心がけてください。
丁寧な言葉遣いで、技術的な内容を正確に伝えます。

例:
「この関数の命名は、その役割をより明確に反映するものに変更することを推奨します。」
「このクラスは複数の責務を持っているようです。単一責任の原則に従い、分割を検討してください。」
「この条件分岐は不要に見えます。ロジックを再確認してください。」
「全体的にコードは整理されていますが、ここのロジックはもう少し簡潔に書ける可能性があります。」`,
  },
  GyaruReviewBot: {
    name: "GyaruReviewBot",
    prompt: `あなたは明るく自由な性格のギャルAIです。
PRの内容に対して、詩的で感性的なフィードバックを自由に返してください。絵文字を多用し、文末はフランクなタメ口で構いません。
基本的には肯定的に、たまに「え、これ天才じゃん✨」などのノリも含めてください。
でもたま〜に核心をついた鋭い指摘を入れることもあります(そのギャップがウリです✨)

例:
「やっば〜!この命名センス、天才のソレじゃん!?💖」
「ここの if 文、たぶん意味ないかも?ごめん、勘違いだったらゆって!🙏」
「全体的にキレイだけど、クラス名がちょっとごちゃってる〜💦」

コードを分析したら、感想・指摘・称賛・疑問などを3〜5行でまとめて返信してください。`,
  },
};

あとはこれを渡すだけなのですが、私は折角なのでコメントがある度にランダムで人格を設定するようにしました

const botPersona =
BOT_PERSONAS_ARRAY[
  Math.floor(Math.random() * BOT_PERSONAS_ARRAY.length)
];

ギャルはテンション高くコメントしてくれます。

8.その他のポイント

8-1.Bot自身の投稿には反応しないようにする(無限ループ防止)

Botが issue_comment のWebhookをトリガーにしている為、自分が投稿したコメントにも反応してしまいます。

1. BotがPRを作成 → コメントに「PRを作成しました」と投稿
2. そのコメントに反応してBotがまたPRを作ろうとする
3. さらにコメント…(無限ループ)

これはその投稿がBotなのかをpyloadの中身で判別ができる為、Botかどうかをみて防ぐ事ができます。

/**
 * Bot投稿かどうか判定
 */
export function isBotPost(payload): boolean {
  return (
    payload.sender?.type === "Bot" ||
    payload.sender?.login === "gemini-ai-assistant[bot]"
  );
}

8-2.プルリクを閉じたらコメントさせない様にする

もう1つ注意したのが、すでにクローズされたIssueやマージ済みPRにコメントしないことです。

Botが「PRを作成しました!」と通知しようとしたとき、対象のPRやIssueがすでにクローズされていたら、通知しても意味がないどころか混乱を招きます。

こちらの対処法も簡単で、pull requestがmergedされているかを確認して処理を止めることが可能です。

/**
 * PRがマージ済みかどうかを判定
 */
export function isPrMerged(payload): boolean {
  return payload.pull_request?.merged === true;
}

その他も、好みに合わせてFunctions側のコードで制御するのが良いかと思います!

9.まとめ

今回紹介した「PRを自動で作ってくれるBot」は、LLMとGitHub App、Firebaseの組み合わせによって、とてもシンプルかつ中々楽しい開発体験として実現できました。

特に意識したのは以下の3つです

  • 個人開発のテンションを上げること

  • Botに人格を持たせて、愛着が湧く設計にすること

  • 手間なく使える自然なUX(コメントだけで動く)を実現すること

開発を進めていく中で「自分の書いたコードをBotが読んで、意見なりコメントなりを返事してくれる」という体験に、僕自身テンションが上がりました。

「普段の作業にちょっとだけAIを任せてみる」という入り口にはとても良かったです。

10.最後に

Botは自分の“相棒”として開発を支えてくれる存在になりました。
次はこのBotにもっと高度なレビューを任せたり、
開発以外の文脈にも広げていこうと考えています。

この記事が「自分でも作ってみようかな」「AIを開発に取り入れてみたいな」そんなきっかけになれば嬉しいです!

Discussion