GitHub CLI Extensionを作る
もう結構作っちゃってるけど色々工夫したところをメモする
プロジェクトの作成は公式ドキュメントに沿ってやる。
言語が3つ選べるけどGoでやるのが長期的にはメンテナンスしやすそう。
- シェルスクリプト
- 実行ファイル(プリコンパイル済み拡張機能)
- Go
- その他の開発言語
gh extension create --precompiled=go EXTENSION-NAME
GoのCLI開発にはCobraを使うと良い。
ただ、色々と追加でやったことがあるのでそれは後で書く。
参考にした記事
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
)の設計を参考にした。
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]
Cobra: フラグの指定について
必須かどうかや、組み合わせて使うオプションなどを表現できる。
今回は利用してないけど設定ファイルから取り込むようにすることもできるらしい。
特定のフラグがある場合に他のフラグを操作する、みたいな柔軟な挙動変更をしたい場合はPreRun
で指定する。
フラグが変更されたかどうかはflag.Changed
を見ると良い。
Cobra: ヘルプコマンドの非表示
デフォルトでhelp
コマンドが自動追加される。
--help
だけでいいので消したい。
以下の設定でヘルプオプションを非表示にできる。
cmd.SetHelpCommand(&cobra.Command{Hidden: true})
Lint: golangci-lint
GoのLinterはgolangci-lintがよく使われている。
こちらの記事が分かりやすかった。
CIで実行するときは公式アクションを実行する。
(go install
や go get
でインストールするのは非推奨らしい。)
Lint: golangci-lintの設定
設定ファイル .golangci.yml
を作成する。
外部プリセットを読み込む機能はなさそうだった。
利用できる全設定は↓で確認できる。
自分はざっくり以下を指定した
run:
tests: true # テストファイルを対象に含める
allow-serial-runners: true # 複数インスタンス(直列実行)を許可する
linters: # ルールの設定
disable-all: true # 全てのルールを無効化
enable: # 個別のルールを有効化
- (省略)
linters-settings:
lll:
line-length: 140 # デフォルト値だと短いので調整
完成系はこれ
Lint: IntelliJのプラグイン
IntelliJ上でLintを有効化する場合、以下のプラグインを利用する。
(ただ、ちょっと重たい。適用するルールを減らせば軽減されるかも。)
API: go-gh
GitHub CLIの実行やREST API/GraphQL APIの実行にはgo-ghを使う。
GitHub CLIに設定した認証情報を自動的に使ってくれるので簡単。
利用サンプルは↓で確認できる
GraphQLの場合は公式ドキュメントのオブジェクトやらクエリやらミューテーションやらと睨めっこする。
Makefile
タスクランナーとしてMakefileを使う。
このブログを見ながら書いたら最高のMakefileができる。
ヘルプコマンドを追加するのもやった。(神)
CI/CD: リリース
リリースにはrelease-pleaseとGitHub CLIを使った。
- リリースタグをrelease-pleaseで生成
- ビルドした成果物をGitHub CLIでリリースにアップロード
完成系↓
後述の理由でcli/gh-extension-precompileは使わなかった。
CI/CD: cli/gh-extension-precompile を使わなかった
公式で簡単にGitHub Extensionsをビルド・公開するActionがある。
以下の理由で使えなかった。
ルートディレクトリ以外をサポートしていない。
ビルド対象ディレクトリを指定するオプションがないので必ずリポジトリルートがビルド対象になる。
GoのCLIツールでよくあるcmdディレクトリ下にエントリーポイントを置く構成だとビルドできない。
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
ビルドするOS/アーキテクチャのリストは変更できないので、こちらも独自のビルドスクリプトを作成する必要がある。
やってることがビルドとアップロード
上記2つの問題のため、ビルドには独自のビルドスクリプトを指定する必要があった。
cli/gh-extension-precompileは大きく、ビルドしてアップロードするという2つの処理を行っている。
アップロード部分はGitHub CLIを呼ぶだけなので、複雑なのはビルド部分なのだけど、ビルド部分が使えないなら、もう自分でスクリプト・ワークフロー書けばいいじゃんとなってしまった。
最終系
make build-all
でマルチプラットフォームビルド、make upload tag=<TAG>
でアップロードできるようにして、ワークフローからこれらを呼び出すようにした。
その気になれば手元からもリリースできるし、これで良かったと思う。
CI/CD: 実行時間の可視化
これでやってる。神。
シェルスクリプト
シェルスクリプト書くのは慣れない、、、
変数展開の記事は何度か見た。
ディレクトリ内のファイル一覧を変数に格納する
files=($(find "${dir}" -type f))
コミット前のファイルがあったらエラーを出す
if test -n "$(git status -s)"; then \
echo "Uncommitted files found"; \
git status -s; \
exit 1; \
fi
Goで型に応じて処理を分けるのってどうやるんだっけと思ったら結局こういう力技switchになるのか
タイムスタンプを文字列として生成するときにRFC3339Nano
フォーマットがちょうどいいんだけど、末尾の0が省略されてしまう件の解消法。
CI/CD: release-pleaseのYoshiって何?
release-pleaseのstrategiesには*-yoshiってのがあって謎だった。
どうやら開発元のGoogleのチームが使う用に調整されたやつらしい。
The Java release strategies that are currently implemented are tailored towards Google Cloud's Java client libraries team.
ドキュメント: ドキュメントページの自動生成
cobraにはコマンドのドキュメントをMarkdownで出力する機能がある。
doc.GenMarkdownTree
で全コマンドのドキュンメントを生成できる。
ただ、トップページ相当のものも欲しかったのでそちらは自分で自動生成するコードを作った。
GitHub Pagesでトップページになるようにindex.md
を生成している。
ページはGitHub Pagesで公開している。
手間をかけず、mainにPushするとデプロイされる。
完成系
OwnerがUserかOrgか見分ける方法
GraphQLでGitHub ProjectをProject numberから取得しようとした場合、以下の方法で取得する必要がある。
ProjectV2Owner -> projectV2(number)
ProjectV2Ownerが実装されているのはUserかOrganization(かIssueかPullRequest)
クエリから辿るとuser|viewer->Userまたはorganization->Organizationで取得する。
つまり、
- ProjectをProject numberから取得するにはOwnerの情報が必要
- CLIから受け取ったOwnerがUserかOrgかは実行してみないとAPIを叩いて調べる必要がある。
めんどー。
GitHub CLIでも同様の処理をしている。
ドキュメント: ローカルでプレビューできるようにする
JekyllがRubyだからこれだけのためにGemfile作るのだ。
index.md
から README.md
に変更
ドキュメント: トップページを GitHub Pagesはindex.html
, index.md
, README.md
を探索してトップページにする。
README.md
にしておくと、GitHub上でディレクトリを開いたときに表示されて良い。