🔖
GitHub Apps名義でPullRequestを作らせる
記事では、GitHub App を使用して、リポジトリへのコミットとプルリクエスト (PR) 作成を自動化する方法を紹介します。
背景
私たちは、開発プロセスを効率化するために、GitHub App を導入して、以下のタスクを自動化することにしました。
- 特定のユーザー(bot)名義でのコミット
- PR の作成
- PR コメントのチェックと対応
前提条件
- GitHub アカウント
- Node.js 環境
- GitHub App の作成権限
手順
1. GitHub App の作成
まず、GitHub App を作成します。
-
GitHub.com → Settings → Developer settings → GitHub Apps へ移動
-
"New GitHub App" をクリック
-
以下の設定で作成:
- GitHub App name: happyo-bot(または他のご希望の名前)
- Homepage URL: リポジトリのURL
- Webhook: Active のチェックを外す
- Repository permissions:
- Contents: Read and write
- Pull requests: Read and write
- Where can this GitHub App be installed?: Only on this account
-
アプリ作成後、以下の手順を実行します:
- Generate a private keyボタンをクリックして秘密鍵をダウンロード
- Install Appボタンをクリックし、リポジトリにインストール
2. 認証情報の設定
GitHub App の App ID と秘密鍵を安全に保管します。
- App ID:
1172985
(例) - 秘密鍵:
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAuHYR2bjl...
-----END RSA PRIVATE KEY-----
秘密鍵は、ソースコードにコミットせず、環境変数として扱うことを推奨します。
3. 必要なパッケージのインストール
以下のパッケージをインストールします。
npm install @octokit/auth-app @octokit/rest
4. コミットとPR作成スクリプトの作成 (scripts/commit-and-create-pr.js)
以下のスクリプトを作成し、scripts/commit-and-create-pr.js
として保存します。
import { createAppAuth } from '@octokit/auth-app';
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const appId = [YOUR_APP_ID]; // GitHub AppのApp ID
const privateKey = fs.readFileSync('.github-app-key.pem', 'utf8');
async function commitAndCreatePR() {
// Initialize auth
const auth = createAppAuth({
appId,
privateKey,
});
// Get installation ID
const appOctokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId,
privateKey,
},
});
const { data: installations } = await appOctokit.rest.apps.listInstallations();
const installationId = installations[0].id;
// Get installation token
const { token } = await auth({
type: 'installation',
installationId,
});
// Create Octokit instance with installation token
const octokit = new Octokit({
auth: token,
});
// Get current branch name and commit message from env
const branch = process.env.BRANCH_NAME;
const message = process.env.COMMIT_MESSAGE;
if (!branch || !message) {
console.error('BRANCH_NAME and COMMIT_MESSAGE environment variables are required');
process.exit(1);
}
try {
// Get the latest commit SHA on the current branch
const { data: ref } = await octokit.rest.git.getRef({
owner: 'hiragram',
repo: 'happyo',
ref: `heads/${branch}`,
});
const latestCommitSha = ref.object.sha;
// Get the current tree
const { data: commit } = await octokit.rest.git.getCommit({
owner: 'hiragram',
repo: 'happyo',
commit_sha: latestCommitSha,
});
const treeSha = commit.tree.sha;
// Create the new tree
const files = process.env.FILES ? process.env.FILES.split(',') : [];
const newTree = [];
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
const { data: blob } = await octokit.rest.git.createBlob({
owner: 'hiragram',
repo: 'happyo',
content,
encoding: 'utf-8',
});
newTree.push({
path: file,
mode: '100644',
type: 'blob',
sha: blob.sha,
});
}
const { data: tree } = await octokit.rest.git.createTree({
owner: 'hiragram',
repo: 'happyo',
base_tree: treeSha,
tree: newTree,
});
// Create the commit
const { data: newCommit } = await octokit.rest.git.createCommit({
owner: 'hiragram',
repo: 'happyo',
message,
tree: tree.sha,
parents: [latestCommitSha],
author: {
name: 'hiragram-bot',
email: 'hiragram+bot@gmail.com',
},
committer: {
name: 'hiragram-bot',
email: 'hiragram+bot@gmail.com',
},
});
// Update the reference
await octokit.rest.git.updateRef({
owner: 'hiragram',
repo: 'happyo',
ref: `heads/${branch}`,
sha: newCommit.sha,
});
console.log(`Changes committed: ${newCommit.sha}`);
// Create PR if PR_TITLE is set
if (process.env.PR_TITLE) {
const { data: pr } = await octokit.rest.pulls.create({
owner: 'hiragram',
repo: 'happyo',
title: process.env.PR_TITLE,
head: `hiragram:${branch}`,
base: 'main',
body: process.env.PR_BODY || '',
});
console.log(`Pull request created: ${pr.html_url}`);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}
commitAndCreatePR().catch(console.error);
5. PRコメントチェックスクリプトの作成 (scripts/check-pr-comments.js)
以下のスクリプトを作成し、scripts/check-pr-comments.js
として保存します。
import { createAppAuth } from '@octokit/auth-app';
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const appId = [YOUR_APP_ID]; // GitHub AppのApp ID
const privateKey = fs.readFileSync('.github-app-key.pem', 'utf8');
async function checkPRComments() {
// Initialize auth
const auth = createAppAuth({
appId,
privateKey,
});
// Get installation ID
const appOctokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId,
privateKey,
},
});
const { data: installations } = await appOctokit.rest.apps.listInstallations();
const installationId = installations[0].id;
// Get installation token
const { token } = await auth({
type: 'installation',
installationId,
});
// Create Octokit instance with installation token
const octokit = new Octokit({
auth: token,
});
const prNumber = process.env.PR_NUMBER;
if (!prNumber) {
console.error('PR_NUMBER environment variable is required');
process.exit(1);
}
try {
// Get PR comments
const { data: comments } = await octokit.rest.issues.listComments({
owner: 'hiragram',
repo: 'happyo',
issue_number: prNumber,
});
// Get the PR details to know the head branch
const { data: pr } = await octokit.rest.pulls.get({
owner: 'hiragram',
repo: 'happyo',
pull_number: prNumber,
});
const branch = pr.head.ref;
// Process each comment
for (const comment of comments) {
// Skip bot's own comments
if (comment.user.type === 'Bot') continue;
// Simple heuristic: If comment ends with "?" it's a question
const isQuestion = comment.body.trim().endsWith('?');
// Check if comment contains specific keywords indicating a request for changes
const isChangeRequest = comment.body.toLowerCase().includes('please change') ||
comment.body.toLowerCase().includes('should be') ||
comment.body.toLowerCase().includes('should be') ||
comment.body.toLowerCase().includes('needs to be') ||
comment.body.toLowerCase().includes('修正してください') ||
comment.body.toLowerCase().includes('変更してください');
if (isQuestion) {
// Reply to question
await octokit.rest.issues.createComment({
owner: 'hiragram',
repo: 'happyo',
issue_number: prNumber,
body: `@${comment.user.login} ご質問ありがとうございます。内容を確認させていただき、対応を検討いたします。`,
});
}
if (isChangeRequest) {
// Reply to change request
await octokit.rest.issues.createComment({
owner: 'hiragram',
repo: 'happyo',
issue_number: prNumber,
body: `@${comment.user.login} 修正リクエストありがとうございます。内容を確認の上、対応させていただきます。`,
});
}
}
// Check review comments (inline comments)
const { data: reviewComments } = await octokit.rest.pulls.listReviewComments({
owner: 'hiragram',
repo: 'happyo',
pull_number: prNumber,
});
for (const comment of reviewComments) {
// Skip bot's own comments
if (comment.user.type === 'Bot') continue;
// Reply to review comments
await octokit.rest.pulls.createReplyForReviewComment({
owner: 'hiragram',
repo: 'happyo',
pull_number: prNumber,
comment_id: comment.id,
body: `コメントありがとうございます。内容を確認の上、対応させていただきます。`,
});
}
console.log('Comments processed successfully');
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}
checkPRComments().catch(console.error);
6. .gitignore の設定
秘密鍵をリポジトリにコミットしないように、.gitignore
ファイルに以下の行を追加します。
*.pem
7. 開発フローの変更
今後は、以下の手順でコミットとPR作成を行います。
- 変更をコミットする。
- 以下の環境変数を設定する。
- BRANCH_NAME: ブランチ名
- COMMIT_MESSAGE: コミットメッセージ
- FILES: 変更したファイル (カンマ区切り)
-
node scripts/commit-and-create-pr.js
を実行する。
PR のコメントを確認するには、以下の手順を実行します。
- 以下の環境変数を設定する。
- PR_NUMBER: PR の番号
-
node scripts/check-pr-comments.js
を実行する。
まとめ
GitHub App を使用することで、リポジトリへのコミットとPR 作成を効率的に自動化できます。
このブログ記事が、GitHub App の導入を検討している方の参考になれば幸いです。
Discussion