0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go未経験なのでREADME自動更新するBOTをGoで作ってみた

Last updated at Posted at 2025-12-04

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よりライブラリ経由の方が安全らしいね。
  • 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なるサービスがあるらしいので次はとりあえずそれを触ってみるつもり
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?