notebook

都内でWEB系エンジニアやってます。

GitHub Projects(ProjectV2)にIssueやPullRequestを追加するGitHub CLIの拡張を作った

この記事は「Go Advent Calendar 2022 3」の12日の記事です!

Goのカレンダー | Advent Calendar 2022 - Qiita

背景・目的

自分は個人のタスク管理にGitHub Projects(ProjectV2)を使っていて、プライベート、個人で開発しているリポジトリのIssueやPullRequestをひもづけてタスク管理的な意味合いで使っている

そして、各リポジトリではだいたい次のような操作を行っている

  1. 開発
  2. PR作成
  3. Web面でPR表示
  4. Projectへひもづけ
    • この箇

5. 各種カスタムフィールドの値を設定 6. PullRequestの中身を確認してMerge

だいたいPullRequestを作るときはコマンドラインからghコマンドで作ることが多いので、ついでに上記のProjectへのひもづけ作業もCLIから行えたら楽できる

ただ、GitHubのCLI(コマンドライン)からProjectV2のプロジェクトへIssueやPullRequestを追加できる機能は現時点ではなさそうだった(2022-12-11)

ということで、GitHub CLIの拡張として自分で作ってみた

swfz/gh-ap: GitHub CLI Extension for ProjectV2. add project and update custom field vlaue in interactive

github.com

動作イメージ

GitHub CLIの対話的インターフェースで操作できるようにしている

  • ひもづけるプロジェクト
  • ひもづける対象
    • Issue
    • PullRequest
    • 今のブランチで出しているPullRequest(これが個人的には一番欲しかったので入れた!)
  • Status、Iteration、などの各種フィールドの値

という順番にそれぞれ選択もしくは入力する

インストール

gh extension install swfz/gh-ap

これはGitHub CLI Extensionだからって話だが上記コマンドだけで使えるようになる

権限のスコープ

拡張の機能上projectへの権限が必要となる

次のコマンドでprojectへのスコープを追加する

gh auth login --scopes 'project'

操作方法

gh ap

で今いるディレクトリのリポジトリのIssueやPullRequestを扱う

IssueやPullRequestの番号をpecoなど慣れている方法で入れたいパターンもあるかなということで

コマンドラインオプションからでも番号指定できるようにした

それぞれ次のようなオプションで指定可能

  • Issue
gh ap -issue ${issueNumber}
  • PullRequest
gh ap -pr ${pullRequestNumber}

その他の項目は対話的に決定していく

選択、入力のスキップ

カスタムフィールドの内容をまだ決めたくない場合などもあるかもしれない

その際に選択しなくても良いよう「Skip This Question.」という選択肢を用意してある

テキストの場合は空文字入力でスキップ可能

開発時の話

GitHub CLI 拡張機能の作成 - GitHub Docs

https://docs.github.com/ja/github-cli/github-cli/creating-github-cli-extensionsdocs.github.com

拡張の作成方法に関しては公式の参考にして作業していった

数コマンドですぐ開発に入れたのでとてもやりやすかった

言語は勉強したいなーと思っていたGo言語を選択した

結構時間がかかったがこれのおかげでちょっとは書けるようになった気がする

GitHub APIへのリクエストやghコマンドの実行

CLIで生成したテンプレートのコードにすでに記述されているがgo-ghというライブラリを使いAPIへのリクエストやCLIのコマンド実行を行っている

cli/go-gh: A Go module for interacting with gh and the GitHub API from the command line.

github.com

コードを書くうえで認証情報など設定をせずにREST, GraphQLのクライアントの生成などを行ってくれるので便利(GitHub CLIで使っている認証情報を使っているよう)

また、CLIのコマンドの実行もできるようにgh.Execという関数が用意されている

  • GitHub CLIで現在のブランチから生成されているPullRequestのデータを取得する関数の例
type Content struct {
    Id     string `json:"id"`
    Number int    `json:"number"`
    Title  string `json:"title"`
}

func ghCurrentPullRequest() Content {
    args := []string{"pr", "view", "--json", "id,number,title"}
    stdOut, _, err := gh.Exec(args...)
    if err != nil {
        log.Fatal(err)
    }

    var currentPR Content
    if err := json.Unmarshal(stdOut.Bytes(), &currentPR); err != nil {
        panic(err)
    }

    return currentPR
}

pr viewでブランチから生成されているPullRequestを取得できるのでそれを--jsonでJSONの出力にしてGoの構造体に変換している

対話的インターフェースでの操作

go-survey/survey: A golang library for building interactive and accessible prompts with full support for windows and posix terminals.

github.com

GitHub CLIが内部的にsurveyを使っていたのでそれを使うことにした

ソースコード読んでなるほどこうやるのかっていうのを学びながら進めた

  • Issue,PullRequestのリスト(Content[])を渡しsurveyで選択された選択肢の番号を返す関数の例
type Content struct {
    Id     string `json:"id"`
    Number int    `json:"number"`
    Title  string `json:"title"`
}

// contentTypeは`Issue` or `PullRequest`
func askContentNumber(contentType string, contents []Content) string {
    var numbers = make([]string, len(contents))
    for i, c := range contents {
        numbers[i] = strconv.Itoa(c.Number)
    }

    name := contentType + " Number"
    qs := []*survey.Question{
        {
            Name: "number",
            Prompt: &survey.Select{
                Message: name,
                Options: numbers,
                Description: func(value string, index int) string {
                    return contents[index].Title
                },
                Filter: func(filterValue string, optValue string, optIndex int) bool {
                    return strings.Contains(contents[optIndex].Title, filterValue)
                },
                PageSize: 50,
            },
        },
    }
    answers := map[string]interface{}{}
    err := survey.Ask(qs, &answers)
    if err != nil {
        log.Fatal(err.Error())
    }
    optionAnswer := answers["number"].(survey.OptionAnswer)

    return optionAnswer.Value
}

選択肢の値と、その説明を表示でき、さらに説明に対してインタラクティブにフィルタリングを掛けられる

選択したいのは番号だがIssueやPullRequestのタイトルも表示したい+フィルタリングしたいという使い方もオプションだけで可能だったりして便利だった

おわり

とりあえず動かせそうなところまで作った状態なのでまだ色々改善できる部分はありそうなので引き続き改善していきつつ使っていきたいと思っています

良ければ使ってみてください!

swfz/gh-ap: GitHub CLI Extension for ProjectV2. add project and update custom field vlaue in interactive