GitHub Actionsで簡単にVerified Commitにする方法

# 概要

GitHub REST APIを使用して簡単にGitHub ActionsのコミットにVerifiedマークを付ける方法になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295261/0cfaa972-9ded-4662-aab5-8abdd29efdb5.png" alt="verified_commit" width="50%">

## ❌️ 書かれていないこと

- CLIのgitコマンドを使用してVerified Commitを作成する方法
- GitHub REST APIでコミットの作成者やコミッター情報を変更しつつVerified Commitを作成する方法

# 前提情報

[GitHub Docs](https://docs.github.com/authentication/managing-commit-signature-verification/about-commit-signature-verification#signature-verification-for-bots)に記載されているのですが、ボットによる署名はCommit APIでカスタム署名情報が含まれていなければ署名されます。
この仕組みを使用すれば比較的簡単にVerified Commitにすることができます！

ただし、GitHub REST APIでコミットを作成する際には少しGitの内部構造を知っていないと難しいところがあります。
自分もこの辺りは詳しくなかったので下記の資料を見てある程度理解しながら進めました。

- 参考記事
    - [Gitのオブジェクトの中身](https://zenn.dev/kaityo256/articles/objects_of_git)
- 参考動画
    - [Git 研修 Basic【MIXI 24新卒技術研修】](https://youtu.be/uRYg6Iw5sXs?si=LMhuEAIHTcpAkLmn&t=1915)
    - [ゆっくり中級git](https://youtube.com/playlist?list=PLxenskds9g0A5V8vJ0QrKHGaCDpN97hOy&si=XGcN6-eBLmyLjqmt)

# 実現方法

REST APIでの流れと、[go-github](https://github.com/google/go-github)ライブラリを使用した際の参考実装をそれぞれ記載しておきます。
また、各APIを実行できる権限があるトークンを使用して実行しないといけないので注意してください。

## REST APIの流れ

1. [GET /repos/{owner}/{repo}/git/ref/{ref}](https://docs.github.com/rest/git/refs#get-a-reference)でコミット先のrefを取得する
1. [GET /repos/{owner}/{repo}/commits/{ref}](https://docs.github.com/rest/commits/commits#get-a-commit)で親コミットを取得する
1. [POST /repos/{owner}/{repo}/git/trees](https://docs.github.com/rest/git/trees#create-a-tree)でツリーを作成する
1. [POST /repos/{owner}/{repo}/git/commits](https://docs.github.com/rest/git/commits#create-a-commit)で新しいコミットを作成する
    - このときに`author`と`committer`を指定しないことで署名を自動的につけることができます
1. [PATCH /repos/{owner}/{repo}/git/refs/{ref}](https://docs.github.com/rest/git/refs#update-a-reference)でコミット先にrefを更新する

## `go-github`での参考実装

`client`の初期化は省いていますが、下記の流れでVerified Commitを作成することができます。

```go
package main

import (
	"log"

	"github.com/google/go-github/v71/github"
)

func main() {
    // TODO: clientは初期化しておく
    client := github.NewClient(tc)

	// リポジトリ情報
	owner := "owner"
	repo := "repository"
	branch := "main"

	// 1. コミット先のrefを取得
	ref, _, err := client.Git.GetRef(ctx, owner, repo, "refs/heads/"+branch)
	if err != nil {
		log.Fatalf("Failed to get branch reference: %v", err)
	}

	// 2. 親のコミットを取得
	parentCommit, _, err := client.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
	if err != nil {
		log.Fatalf("Failed to get parent commit: %v", err)
	}

	// 3. ツリーを作成する
	treeEntries := []*github.TreeEntry{
		{Path: github.Ptr("new.txt"), Type: github.Ptr("blob"), Content: github.Ptr("new content"), Mode: github.Ptr("100644")},
	}
	tree, _, err := client.Git.CreateTree(ctx, owner, repo, *parentCommit.SHA, treeEntries)
	if err != nil {
		log.Fatalf("Failed to create tree: %v", err)
	}

	// 4. 新しいコミットを作成
    // NOTE: このときにAuthorとCommiterの設定を行わない
	commit := &github.Commit{
		Message: github.Ptr("new file"),
		Parents: []*github.Commit{parentCommit},
		Tree:    tree,
	}
	newCommit, _, err := client.Git.CreateCommit(ctx, owner, repo, commit, nil)
	if err != nil {
		log.Fatalf("Failed to create commit: %v", err)
	}

	// 5. コミット先のrefを更新
	ref.Object.SHA = newCommit.SHA
	_, _, err = client.Git.UpdateRef(ctx, owner, repo, ref, false)
	if err != nil {
		log.Fatalf("Failed to update new branch reference: %v", err)
	}
}
```