Go未経験なのでREADME自動更新するBOTをGoで作ってみた
ということで
Go未経験だけど何か作りたい
これの続き
- プルリクエスト作成をトリガーにGitHub Actionsから実行され、
- プルリクのdiffを取得、取得したdiffをLLMで分析してもらって、
- READMEに書く最終更新内容としてふさわしい文章を1行返してもらう。
- それをREADMEに追記(最終的に更新したい)する。
いったんMVPとして、通して動かせる状態になったのでまとメモ
前回やったこと
- リポジトリ固有のルールを読み込む処理を実装した
- go:embedでデフォルトのルール指定、環境変数でカスタムルール指定可能にしている。
- OpenAIのAPIを叩くところをopenai-goを使って実装した
- ここは正直調べたまんま使ってる。 とりあえず動いたのでよし
どうやらJSONでのレスポンスを強制するモードとかがあるらしいので調べたい
- ここは正直調べたまんま使ってる。 とりあえず動いたのでよし
今回やること
- GitHub APIを使ってプルリクエストからdiffを取得
- go-github v79.0.0(2025/12/04時点最新)を使用してCLIに頼らずやってみる
一応CLIよりライブラリ経由の方が安全らしいね。
- go-github v79.0.0(2025/12/04時点最新)を使用してCLIに頼らずやってみる
- LLMの出力をREADMEに書き戻す
やったこと
GitHub API で PR の diff を取得(go-github v79)
GitHubのクライアントを生成してプルリクからdiffを取得するだけ、、
なのだけど、いろいろ調べてみるとcontextでもっといろいろ設定した方がいいらしい 改善ポイント
createGithubClientでGitHubのアクセストークンをHTTPヘッダに付与してくれるらしい
StaticTokenSourceは常に固定トークンを使用してくれる
GitHub APIは必ず**Authorization: Bearer **を付与する必要があるとのことだが
GitHubのトークンは更新不要らしいのでStaticTokenSourceで問題ないわけですね。
// client.go
func GetDiff(info common.GitHubAccessInfo) (string, error) {
ctx := context.Background()
client, conErr := createGithubClient(info, ctx)
if conErr != nil {
return "", conErr
}
diff, diffErr := getPullRequestDiff(info, client, ctx)
if diffErr != nil {
return "", diffErr
}
return diff, nil
}
func createGithubClient(info common.GitHubAccessInfo, ctx context.Context) (*github.Client, error) {
token := info.Token
if token == "" {
return nil, errors.New("missing GITHUB_TOKEN")
}
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
tc := oauth2.NewClient(ctx, ts)
client := github.NewClient(tc)
return client, nil
}
func getPullRequestDiff(info common.GitHubAccessInfo,
client *github.Client,
ctx context.Context) (string, error) {
diff, _, err := client.PullRequests.GetRaw(
ctx,
info.Owner,
info.Repo,
info.Number,
github.RawOptions{Type: github.Diff},
)
if err != nil {
return "", fmt.Errorf("get diff failed: %w", err)
}
return diff, nil
}
取得した差分をLLMに投げてファイルを更新する
ただひたすらファイルを読み込んで書き込むだけ
ロールバックできるようにリネームしてからREADME.mdを新しく作ってるけど、
tmpでファイル作ってからリネームした方が楽だったことにあとで気付いた。 中断しても元ファイルはそのままだし
書き込み自体も今は更新orセクションごと追加してるけど、
次のヘッダ行が来るまでスキップして、完全に置き換えの方がいいなという感じ。
// file_writer.go
package utils
import (
"bufio"
"fmt"
"os"
"strings"
)
func RewriteReadme(content, header string) error {
err := os.Rename("./README.md", "./_README.md")
if err != nil {
return fmt.Errorf("rename README file failed: %v", err)
}
oldFile, err := os.Open("./_README.md")
if err != nil {
_ = os.Rename("./_README.md", "./README.md")
return fmt.Errorf("old new README file failed: %v", err)
}
defer oldFile.Close()
newFile, err := os.Create("./README.md")
if err != nil {
oldFile.Close()
_ = os.Rename("./_README.md", "./README.md")
return fmt.Errorf("create new README file failed: %v", err)
}
defer newFile.Close()
isExistHeader := false
scanner := bufio.NewScanner(oldFile)
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == strings.TrimSpace(header) {
isExistHeader = true
fmt.Fprintln(newFile, header)
fmt.Fprintln(newFile, content)
continue
}
fmt.Fprintln(newFile, line)
}
if !isExistHeader {
fmt.Fprintln(newFile, header)
fmt.Fprintln(newFile, content)
}
os.Remove("./_README.md")
return nil
}
実行してみた
とりあえずシンプルにローカル実行
ちょうどいい実行環境がないので、
ローカルのREADME.mdを適当なリポジトリのプルリク差分で書き換えることにする
実行フォルダ
PS D:\projects\readme-bot> dir
ディレクトリ: D:\projects\readme-bot
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2025/11/27 2:14 .github
d----- 2025/11/27 20:13 .vscode
d----- 2025/11/27 2:00 cmd
d----- 2025/11/28 1:25 docs
d----- 2025/11/30 14:23 internal
-a---- 2025/12/04 3:06 645 .gitignore
-a---- 2025/12/04 3:13 9571 all-go-files.md
-a---- 2025/11/30 1:56 136 CHANGELOG.md
-a---- 2025/12/03 21:29 444 go.mod
-a---- 2025/12/03 21:29 1859 go.sum
-a---- 2025/11/29 16:31 1085 LICENSE
-a---- 2025/12/04 19:15 28 README.md
コマンド(ownerとrepoは適当)
go run ./cmd/readme-bot -owner hoge -repo fuga -number 7
実行前README
# test
TEST README.md
実行後ターミナル
PS D:\projects\readme-bot> go run ./cmd/readme-bot -owner hoge -repo fuga -number 7
Hello Readme Bot!
{README を更新し、プロジェクト概要、要件、およびインストール手順を明確化しました。 {T.B.D 2023-10-11 [README.md においてプロジェクトの概要を Sandbox プロジェクトとして明確化。 ブラウザ自動化および Vision-based UI 分析に 関する目的を再定義。 依存関係として Python 3.9 以上、OpenAI API キー、Playwright 用ブラウザを追加。 インストール手順のコマンドを python -m pip の形式に変更し、Playwright のインストール方法を明示。 環境設定セクションを追 加し、OpenAI API キーの設定方法を記載。 使用例のセクションを強化し、エージェント作成とクエリ実行のサンプルコードを追加。]}}
実行後README
# test
TEST README.md
## latest change
README を更新し、プロジェクト概要、要件、およびインストール手順を明確化しました。
とまあこんな感じで無事READMEが更新された
LLM標準出力にある後半部分はCHANGELOGをやるときのための準備
感想
- 自分で作る意味ある、、?とは思いつつ、Goの勉強にはなったのかなという感じ。
- 今はOpenAI固定なので、そこはいろんなLLM APIに切り替えられると、
用途に応じて変えるみたいなことができるのでよさそう。 - あとせっかくCHANGELOGの内容出力させてるんだから、こっちの更新も実装してきます。
- Go未経験だけどかなり入りやすかったので、新しくやるには(作るのはBOTに限らずとも)結構お手軽かも?
- これさらにレビューするBOTとか作るつもりだけど、
現時点でCode Rabbitなるサービスがあるらしいので次はとりあえずそれを触ってみるつもり