AIにプルリクを作成させて個人開発のモチベを上げる
1.モチベーション
久々の投稿です。
今回は個人開発のモチベーションを上げるために「自分の好きなキャラを作って、そのキャラにPRを自動生成する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 を作成する
- GitHubのApp作成ページ にアクセス
- 「New GitHub App」をクリック
- 以下のように入力します:
項目 | 設定例 |
---|---|
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 |
差分取得のため |
.pem
を生成・ダウンロード
4-3.秘密鍵 - App 作成後の画面 or 左サイドバーの「Private keys」に移動
- 「Generate a private key」をクリック
-
.pem
ファイルがダウンロードされる
.pem
は再ダウンロードできません。紛失した場合は再生成が必要です。
セキュリティのため安全な場所に保管してください。
このタイミングでやらなければいけない訳ではなく、WebhookのURLを取得するタイミングでも問題ありません。
4-4.GitHub App をインストールする
- App の設定画面の「Install App」タブを開く
- 表示される自分のアカウントに「Install」ボタンがあるのでクリックして使用できるようにします
- *リポジトリのアクセス範囲を「Only select repositories」にし、Botを使いたいリポジトリだけを選択
*歯車マークをクリックすると、Repository accessという項目があり、ここでどのリポジトリに対してBotを動作させたいかを指定することができます
5.Cloud Functions側の設定
Functionsは提供される無料枠を使うことができますが、クレジットの登録は必要です。
参考サイト:
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
を使うためインストールします。
import * as functions from "firebase-functions";
import { app } from "./app";
export const githubWebhook = functions
.region("asia-northeast1")
.https.onRequest(app);
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 };
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