Open24

GitHub CLI Extensionを作る

tasshitasshi

もう結構作っちゃってるけど色々工夫したところをメモする

tasshitasshi

プロジェクトの作成は公式ドキュメントに沿ってやる。

言語が3つ選べるけどGoでやるのが長期的にはメンテナンスしやすそう。

  • シェルスクリプト
  • 実行ファイル(プリコンパイル済み拡張機能)
    • Go
    • その他の開発言語
gh extension create --precompiled=go EXTENSION-NAME

https://docs.github.com/ja/github-cli/github-cli/creating-github-cli-extensions#creating-a-precompiled-extension-in-go-with-gh-extension-create

tasshitasshi

Cobra: コマンドの宣言方法について

cobra-cli で生成したコードでは、グローバル変数にコマンド(*cobra.Command)やフラグの値を宣言している。
これだと Lintで怒られる 変数がどこからでもアクセスできてプログラムの見通しが悪くなる。

そのため、以下のようにフラグなども含めてコマンドを生成するコンストラクタを作成する。

// フラグの値を格納する型
type Options struct {
	Verbose bool
}

// *cobra.CommandをNewするコンストラクタ
func NewCmd() *cobra.Command {
	opts := new(Options)

	cmd := &cobra.Command{
		Use: "gh-iteration", // パッケージ名
		Run: func(_ *cobra.Command, _ []string) {
			run(opts)
		},
	}

	cmd.PersistentFlags().BoolVar(
		&opts.Verbose,
		"verbose",
		false,
		"Output verbose logs",
	)
	cmd.AddCommand(NewSubCmd()) // サブコマンドは親から読み込む

	return cmd
}

// コマンドの処理内容
func run(opts Options) {
	fmt.Println("command called")
}

これはGitHub CLI (gh)の設計を参考にした。

https://github.com/cli/cli/blob/fcb1576d26f8fbf1304c6381591ae33c991d8025/pkg/cmd/project/list/list.go#L32

tasshitasshi

Cobra: コマンド名について

Usageなどにリポジトリ名の gh-拡張機能名 が表示されてしまう。

Usage:
  gh-iteration list [flags]

gh extensionなどのプラグインを作成する場合は、cobra.CommandDisplayNameAnnotationを指定する。

// 簡略化のためもろもろ省略
var rootCmd = &cobra.Command{
	Annotations: map[string]string{
		cobra.CommandDisplayNameAnnotation: "gh iteration",
	},
}
Usage:
  gh iteration list [flags]

https://github.com/spf13/cobra/blob/main/site/content/user_guide.md#creating-a-plugin

tasshitasshi

Cobra: フラグの指定について

必須かどうかや、組み合わせて使うオプションなどを表現できる。

https://github.com/spf13/cobra/blob/bcfcff729ecdeb4072ef6f2a9c0b5e4ca1853206/site/content/user_guide.md#working-with-flags

今回は利用してないけど設定ファイルから取り込むようにすることもできるらしい。


特定のフラグがある場合に他のフラグを操作する、みたいな柔軟な挙動変更をしたい場合はPreRunで指定する。

https://github.com/spf13/cobra/issues/1595


フラグが変更されたかどうかはflag.Changedを見ると良い。

https://github.com/cli/cli/blob/b07f955c23fb54c400b169d39255569e240b324e/pkg/cmdutil/json_flags.go#L149

tasshitasshi

Lint: golangci-lint

GoのLinterはgolangci-lintがよく使われている。

https://github.com/golangci/golangci-lint

こちらの記事が分かりやすかった。

https://zenn.dev/sanpo_shiho/books/61bc1e1a30bf27

CIで実行するときは公式アクションを実行する。

https://github.com/golangci/golangci-lint-action

go installgo get でインストールするのは非推奨らしい。)

https://golangci-lint.run/usage/install/#install-from-source

tasshitasshi

Lint: golangci-lintの設定

設定ファイル .golangci.yml を作成する。
外部プリセットを読み込む機能はなさそうだった。

利用できる全設定は↓で確認できる。

https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml

自分はざっくり以下を指定した

.golangci.yml
run:
  tests: true # テストファイルを対象に含める
  allow-serial-runners: true # 複数インスタンス(直列実行)を許可する

linters: # ルールの設定
  disable-all: true # 全てのルールを無効化
  enable: # 個別のルールを有効化
    - (省略)

linters-settings:
  lll:
    line-length: 140 # デフォルト値だと短いので調整

完成系はこれ

https://github.com/mshrtsr/gh-iteration/blob/main/.golangci.yml

tasshitasshi

API: go-gh

GitHub CLIの実行やREST API/GraphQL APIの実行にはgo-ghを使う。
GitHub CLIに設定した認証情報を自動的に使ってくれるので簡単。

https://github.com/cli/go-gh

利用サンプルは↓で確認できる
https://github.com/cli/go-gh/blob/trunk/example_gh_test.go#L1

GraphQLの場合は公式ドキュメントのオブジェクトやらクエリやらミューテーションやらと睨めっこする。

https://docs.github.com/ja/graphql/reference/objects

tasshitasshi

CI/CD: cli/gh-extension-precompile を使わなかった

公式で簡単にGitHub Extensionsをビルド・公開するActionがある。

https://github.com/cli/gh-extension-precompile

以下の理由で使えなかった。

ルートディレクトリ以外をサポートしていない。

ビルド対象ディレクトリを指定するオプションがないので必ずリポジトリルートがビルド対象になる。

GoのCLIツールでよくあるcmdディレクトリ下にエントリーポイントを置く構成だとビルドできない。

https://github.com/cli/gh-extension-precompile/blob/b0da21c1042c79394bfb66a6c320bb1e360b876a/build_and_release.sh#L48

no Go files in /home/runner/work/gh-iteration/gh-iteration
https://github.com/mshrtsr/gh-iteration/actions/runs/7952663306/job/21707568223#step:3:112

※Actionsのworking-directoryはusesには効かないので解決しない。

対策として build_script_override オプションに独自のビルドスクリプトを指定するとそれを実行してくれるので、その中でビルド対象ディレクトリを指定すれば良い。

android/amd64向けビルドにCGOが必須

デフォルトでは互換性重視のため、CGOがoffになっている。
僕はこの方針に同意なのでCGOをoffで実行したいんだけど、ビルド対象に含まれているandroid/amd64がCGOを有効にしないとビルドできない。

android/amd64 requires external (cgo) linking, but cgo is not enabled

https://github.com/mshrtsr/gh-iteration/actions/runs/7952732732/job/21707738151#step:3:129

ビルドするOS/アーキテクチャのリストは変更できないので、こちらも独自のビルドスクリプトを作成する必要がある。

やってることがビルドとアップロード

上記2つの問題のため、ビルドには独自のビルドスクリプトを指定する必要があった。
cli/gh-extension-precompileは大きく、ビルドしてアップロードするという2つの処理を行っている。

https://github.com/cli/gh-extension-precompile/blob/b0da21c1042c79394bfb66a6c320bb1e360b876a/build_and_release.sh

アップロード部分はGitHub CLIを呼ぶだけなので、複雑なのはビルド部分なのだけど、ビルド部分が使えないなら、もう自分でスクリプト・ワークフロー書けばいいじゃんとなってしまった。

最終系

make build-allでマルチプラットフォームビルド、make upload tag=<TAG>でアップロードできるようにして、ワークフローからこれらを呼び出すようにした。
その気になれば手元からもリリースできるし、これで良かったと思う。

tasshitasshi

シェルスクリプト

シェルスクリプト書くのは慣れない、、、

変数展開の記事は何度か見た。

https://qiita.com/t_nakayama0714/items/80b4c94de43643f4be51

ディレクトリ内のファイル一覧を変数に格納する

files=($(find "${dir}" -type f))

コミット前のファイルがあったらエラーを出す

if test -n "$(git status -s)"; then \
  echo "Uncommitted files found"; \
  git status -s; \
  exit 1; \
fi
tasshitasshi

CI/CD: release-pleaseのYoshiって何?

release-pleaseのstrategiesには*-yoshiってのがあって謎だった。

https://github.com/googleapis/release-please/tree/3569f158158e2bd096a94c145bdb1a7f455829f0/src/strategies

どうやら開発元のGoogleのチームが使う用に調整されたやつらしい。

The Java release strategies that are currently implemented are tailored towards Google Cloud's Java client libraries team.

https://github.com/googleapis/release-please/blob/3569f158158e2bd096a94c145bdb1a7f455829f0/docs/java-releases.md?plain=1#L4

tasshitasshi

ドキュメント: ドキュメントページの自動生成

cobraにはコマンドのドキュメントをMarkdownで出力する機能がある。
doc.GenMarkdownTreeで全コマンドのドキュンメントを生成できる。

https://ygithub.com/spf13/cobra/blob/bcfcff729ecdeb4072ef6f2a9c0b5e4ca1853206/site/content/docgen/md.md

ただ、トップページ相当のものも欲しかったのでそちらは自分で自動生成するコードを作った。
GitHub Pagesでトップページになるようにindex.mdを生成している。

https://github.com/mshrtsr/gh-iteration/blob/2330e10f372aeece38c144e2bcea3e82e94a995a/pkg/docs/docs.go#L73


ページはGitHub Pagesで公開している。
手間をかけず、mainにPushするとデプロイされる。

https://docs.github.com/ja/pages/getting-started-with-github-pages/creating-a-github-pages-site


完成系

https://mshrtsr.github.io/gh-iteration/

tasshitasshi

OwnerがUserかOrgか見分ける方法

GraphQLでGitHub ProjectをProject numberから取得しようとした場合、以下の方法で取得する必要がある。

ProjectV2Owner -> projectV2(number)

ProjectV2Ownerが実装されているのはUserかOrganization(かIssueかPullRequest)

クエリから辿るとuser|viewer->Userまたはorganization->Organizationで取得する。

https://docs.github.com/ja/graphql/reference/interfaces#projectv2owner

つまり、

  • ProjectをProject numberから取得するにはOwnerの情報が必要
  • CLIから受け取ったOwnerがUserかOrgかは実行してみないとAPIを叩いて調べる必要がある。

めんどー。

GitHub CLIでも同様の処理をしている。

https://github.com/cli/cli/blob/8948ee8c3beaf432676061aaaf481d2f41039483/pkg/cmd/project/shared/queries/queries.go#L971

tasshitasshi

ドキュメント: トップページを index.md から README.md に変更

GitHub Pagesはindex.html, index.md, README.mdを探索してトップページにする。

README.md にしておくと、GitHub上でディレクトリを開いたときに表示されて良い。

https://github.com/mshrtsr/gh-iteration/tree/main/docs