Go言語でのGitHub Actions（カスタムアクション）の初めての開発


## 初めまして

hashi_03と申します。
GitHubはこちら: [https://github.com/db-r-hashimoto](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っぽいなという表現はご了承ください。

実装したカスタムアクションは[こちら](https://github.com/db-r-hashimoto/lgtm_print)

## 背景

GitHubのIssueやPR上で「LGTM (Looks Good To Me)」という表現が使われることが多いため、最近仕事でよく作ってるGitHub Actionsを使ってLGTM画像を投稿できるようにしようと考えました。
また、最近勉強しているのでGoを使用して開発してみました。

LGTMは[LGTMeow](https://lgtmeow.com/)を使いました。猫可愛いですよね、猫欲しい、、、

## 開発のポイント

### 1. ランダムにLGTM画像を取得する

まず、ランダムなLGTM画像を提供するために、LGTMeowのAPIを使用しました。
このAPIを使ってLGTM画像のリストを取得し、その中からランダムな画像を選択する処理を実装しました。

ちなみにAPIはどう知るのかは[Zennのこの記事](https://zenn.dev/miyasic/articles/lgtm-shellscript)を参考にしました。

```go
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を持ってくるようにしています。

```go
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側でも弾いているが念の為）

```yaml
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などコメントに対して、ラベル貼りなどのカスタムアクションも作っていきたいですね。
例えば[こんな感じ](https://dev.classmethod.jp/articles/github-actions-pull-request-labeler/)

あとはIssue含めてマイルストーンなどラベル貼れたら面白いのかなと思います。

それでは今回はここまでです。読んでいただきありがとうございました。