初めまして
hashi_03と申します。
GitHubはこちら: https://github.com/db-r-hashimoto
X(旧Twitter): https://x.com/ryu_has03
初めてのGoでのGitHub Action開発 - lgtm_print
GitHubのPull RequestやIssueのコメントに "LGTM" と記載があった場合、自動でランダムなLGTM画像を追加するカスタムアクション「lgtm_print」を開発しました。
開発の過程で学んだことや実装の工夫について紹介します。
ちなみに記事のベースはChatGPTに書かせています。生成AIっぽいなという表現はご了承ください。
実装したカスタムアクションはこちら
背景
GitHubのIssueやPR上で「LGTM (Looks Good To Me)」という表現が使われることが多いため、最近仕事でよく作ってるGitHub Actionsを使ってLGTM画像を投稿できるようにしようと考えました。
また、最近勉強しているのでGoを使用して開発してみました。
LGTMはLGTMeowを使いました。猫可愛いですよね、猫欲しい、、、
開発のポイント
1. ランダムにLGTM画像を取得する
まず、ランダムなLGTM画像を提供するために、LGTMeowのAPIを使用しました。
このAPIを使ってLGTM画像のリストを取得し、その中からランダムな画像を選択する処理を実装しました。
ちなみにAPIはどう知るのかはZennのこの記事を参考にしました。
package lgtmoon
import (
"encoding/json"
"errors"
"math/rand"
"net/http"
"time"
)
type LGTMImage struct {
ID int `json:"id"`
ImageURL string `json:"imageUrl"`
}
func GetRandomLgtmImageURL() (string, error) {
url := "https://lgtmeow.com/api/lgtm-images"
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.New("failed to fetch images from LGTMeow API")
}
var images []LGTMImage
if err := json.NewDecoder(resp.Body).Decode(&images); err != nil {
return "", err
}
if len(images) == 0 {
return "", errors.New("no images found")
}
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
randomIndex := rnd.Intn(len(images))
return images[randomIndex].ImageURL, nil
}
2. コメントの編集でLGTM画像を追加する
次に、"lgtm"(大文字小文字分けなく)とコメントされた内容に対して、LGTM画像を既存のコメントに追加する処理を実装しました。
GitHub APIを活用し、既存コメントの内容を編集することで、画像を新しいコメントとして追加するのではなく、既存コメントに挿入する形にしています。
また、GitHub Actionsとして、コメントのIDとBodyを持ってくるようにしています。
package main
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"github.com/google/go-github/v48/github"
"golang.org/x/oauth2"
"github.com/db-r-hashimoto/lgtm_print/internal/lgtmoon"
)
func main() {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
log.Fatal("GITHUB_TOKEN is not set")
}
ctx := context.Background()
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
client := github.NewClient(tc)
commentBody := os.Getenv("COMMENT_BODY")
commentIDStr := os.Getenv("COMMENT_ID")
owner := os.Getenv("OWNER")
repo := os.Getenv("REPO")
commentID, err := strconv.ParseInt(commentIDStr, 10, 64)
if err != nil {
log.Fatalf("Invalid COMMENT_ID: %v", err)
}
if strings.Contains(commentBody, "![LGTM]") {
log.Println("LGTM image already present in the comment.")
return
}
imageUrl, err := lgtmoon.GetRandomLgtmImageURL()
if err != nil {
log.Fatalf("Failed to fetch LGTM image: %v", err)
}
updatedComment := fmt.Sprintf("%s
![LGTM](%s)", commentBody, imageUrl)
_, _, err = client.Issues.EditComment(ctx, owner, repo, commentID, &github.IssueComment{
Body: &updatedComment,
})
if err != nil {
log.Fatalf("Failed to edit comment: %v", err)
}
log.Println("LGTM image added to the existing comment successfully!")
}
実際テストで動かしたGitHub Actionsのyaml
コメントのBodyの内容でlgtm
とある場合に動きます。
ただし、画像のaltをLGTMにしているため、![LGTM]
という文字列の場合は処理自体Skipする仕組みを入れています。(Go側でも弾いているが念の為)
name: LGTM Bot
on:
issue_comment:
types: [created, edited]
pull_request_review:
types: [submitted, edited]
pull_request_review_comment:
types: [created, edited]
jobs:
lgtm:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check if comment contains 'lgtm'
id: check_comment
run: |
echo "Comment body: ${{ github.event.comment.body }}"
COMMENT_BODY="${{ github.event.comment.body }}"
COMMENT_BODY_LOWER=$(echo "$COMMENT_BODY" | tr '[:upper:]' '[:lower:]')
if [[ "$COMMENT_BODY_LOWER" != *"lgtm"* || "$COMMENT_BODY" == *"![LGTM]"* ]]; then
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Run LGTM Bot Action
if: steps.check_comment.outputs.skip == 'false'
uses: db-r-hashimoto/lgtm_print@v0.0.3
with:
github-token: ${{ secrets.GIT_TOKEN }}
owner: db-r-hashimoto
repo: lgtm_print
comment_body: ${{ github.event.comment.body }}
comment_id: ${{ github.event.comment.id }}
学び
-
Goでの開発の勉強になった
新しく言語を学ぶ際は、何作ろうって考えてしまうことが多いですが、カスタムアクションなら日々使っているGitHubをもっと楽にできないかなと考えるので開発コストも高くないしとっつきやすかったですね。 -
GoでのAPIリクエストとJSONパース
GoでのHTTPリクエストの送信とレスポンスのハンドリング、そしてJSONのデコードの基礎を学びました。
net/httpパッケージで簡潔にAPIリクエストを扱えることや、encoding/jsonでレスポンスのJSONデータをGo構造体にマッピングできる便利さを実感しました。 -
GitHub APIクライアントライブラリの使用
google/go-githubライブラリを利用することで、GoからGitHubのIssueやコメントの操作がスムーズに行えることを学びました。OAuth認証をセットアップしてからAPI操作を実行するまでの一連の流れを理解できました。
今後の展望
PRなどコメントに対して、ラベル貼りなどのカスタムアクションも作っていきたいですね。
例えばこんな感じ
あとはIssue含めてマイルストーンなどラベル貼れたら面白いのかなと思います。
それでは今回はここまでです。読んでいただきありがとうございました。