GitHub CLIでGraphQL APIを叩き、milestoneにひもづくissueとPRを抽出する。ついでにスプレッドシートに貼る


# 概要

GitHub上の特定の[milestone](https://docs.github.com/en/github/managing-your-work-on-github/about-milestones)にひもづくPRとissueのタイトル、URL、作成日時をスプレットシートにシュッと貼りたいときに便利なクエリです。

# 結論

```shell
$ gh api graphql -F owner='toshi0607' -F name='jctl' -F number=2 -F first=10 -f query='
  query($name: String!, $owner: String!, $number: Int!, $first: Int) {
    repository(owner: $owner, name: $name) {
      milestone(number: $number) {
        issues(first: $first) {
          nodes {
            title
            url
            createdAt
          }
        }
        pullRequests(first: $first) {
          nodes {
            title
            url
            createdAt
          }
        }
      }
    }
  }
'| jq -r '.data.repository.milestone | .issues.nodes[], .pullRequests.nodes[] | [.url, .title, .createdAt] | @tsv'
https://github.com/toshi0607/jctl/issues/5	lint	2019-10-02T07:44:16Z
https://github.com/toshi0607/jctl/issues/6	unit test	2019-10-02T07:44:25Z
https://github.com/toshi0607/jctl/issues/7	CMD option	2019-10-02T07:44:51Z
https://github.com/toshi0607/jctl/pull/9	add readme	2019-10-04T02:01:35Z
https://github.com/toshi0607/jctl/pull/10	add sample .envrc	2019-10-04T15:35:29Z
https://github.com/toshi0607/jctl/pull/11	add test case	2019-10-06T14:39:32Z
https://github.com/toshi0607/jctl/pull/12	use os.UserHomeDir	2019-10-11T14:31:11Z
https://github.com/toshi0607/jctl/pull/13	set TTL	2019-10-11T15:12:06Z
```

# GitHub CLI

GitHubにクエリするにあたり[GitHub CLI](https://github.com/cli/cli)を利用しました。2020年6月12日にリリースされた[v0.10.0](https://github.com/cli/cli/releases/tag/v0.10.0)では、サブコマンドでGitHubのGraphQL APIとREST APIが叩けるようになったようです。

こんな構文です。
```shell
$ gh api graphql -F owner=':owner' -F name=':repo' -f query='
      query($name: String!, $owner: String!) {
              repository(owner: $owner, name: $name) {
                      releases(last: 3) {
                              nodes { tagName }
                      }
              }
      }
'
```

macOSの場合は`Homebrew`でつぎのように気軽にインストールできます。

```shell
$ brew install github/gh/gh

# version出すときにリリースタグのURL出すのいいかも
$ gh --version
gh version 0.11.1 (2020-07-28)
https://github.com/cli/cli/releases/tag/v0.11.1
```

他のOSの場合は[この辺り](https://github.com/cli/cli#installation)を参照してインストールし、触ってみてください。

# Repositoryの取得

[このRepository](https://github.com/toshi0607/jctl)を例に、結論のクエリを組み立てていきます。

実際のGraphQLのクエリは、オブジェクト毎にページが用意されています。引数、取得できるフィールド、関連づけて取得できるデータ（Connections）、関連して取得できるデータの引数が確認できます。

Repositoryのページは[これ](https://developer.github.com/v4/object/repository/)です。

特定のRepository自体の情報はつぎのように取得できます。引数にはownerフィールドとnameフィールドを指定しています。

```shell
$ gh api graphql -F owner='toshi0607' -F name='jctl' -f query='
  query($name: String!, $owner: String!) {
    repository(owner: $owner, name: $name) {
      milestone(number: 2) {
        url
        createdAt
      }
    }
  }
'
{
  "data": {
    "repository": {
      "milestone": {
        "url": "https://github.com/toshi0607/jctl/milestone/2",
        "createdAt": "2020-08-19T12:21:27Z"
      }
    }
  }
}
```

# Milestoneの取得

Repositoryを起点に取得できるデータは、collaborators、forks、issues、pullRequests、milestonesなどです。今回取得したいのはmilestonesです。

関連情報を取得するときは、repositoryを取得するときに指定したような記法でrepositoryの一フィールドに指定します。

引数にはmilestoneのnumberフィールドを指定しています。実物は[このmilestone](https://github.com/toshi0607/jctl/milestone/2)です。

```shell
$ gh api graphql -F owner='toshi0607' -F name='jctl' -F number=2 -f query='
  query($name: String!, $owner: String!, $number: Int!) {
    repository(owner: $owner, name: $name) {
      milestone(number: $number) {
        title
        url
        createdAt
      }
    }
  }
'
{
  "data": {
    "repository": {
      "milestone": {
        "title": "test2",
        "url": "https://github.com/toshi0607/jctl/milestone/2",
        "createdAt": "2020-08-19T12:21:27Z"
      }
    }
  }
}
```

Milestoneのページは[これ](https://developer.github.com/v4/object/milestone/)です。

# milestoneにひもづくデータの取得

milestoneにひもづくissueを取得してみましょう。先ほどと同様に、入れ子方式でissueを指定します。

ただし、今度はissueを複数取得するつもりなのでissue`s`を指定します。引数はissue自体のフィールドではなく、関連するissueをフィルタリングするもの（orderBy、statesなど）のひとつであるfirstを指定しています。このようにして取得できるissueの集まりは、issueオブジェクトではなく[IssueConnection](https://developer.github.com/v4/object/issueconnection/)です。issueオブジェクトはnodesの中に入っています。

```shell
$ gh api graphql -F owner='toshi0607' -F name='jctl' -F number=2 -F first=10 -f query='
  query($name: String!, $owner: String!, $number: Int!, $first: Int) {
    repository(owner: $owner, name: $name) {
      milestone(number: $number) {
        issues(first: $first) {
          nodes {
            title
            url
            createdAt
          }
        }
      }
    }
  }
'
{
  "data": {
    "repository": {
      "milestone": {
        "issues": {
          "nodes": [
            {
              "title": "lint",
              "url": "https://github.com/toshi0607/jctl/issues/5",
              "createdAt": "2019-10-02T07:44:16Z"
            },
            {
              "title": "unit test",
              "url": "https://github.com/toshi0607/jctl/issues/6",
              "createdAt": "2019-10-02T07:44:25Z"
            },
            {
              "title": "CMD option",
              "url": "https://github.com/toshi0607/jctl/issues/7",
              "createdAt": "2019-10-02T07:44:51Z"
            }
          ]
        }
      }
    }
  }
}
```

PRも同じように取得できます。

```shell
$ gh api graphql -F owner='toshi0607' -F name='jctl' -F number=2 -F first=10 -f query='
  query($name: String!, $owner: String!, $number: Int!, $first: Int) {
    repository(owner: $owner, name: $name) {
      milestone(number: $number) {
        issues(first: $first) {
          nodes {
            title
            url
            createdAt
          }
        }
        pullRequests(first: $first) {
          nodes {
            title
            url
            createdAt
          }
        }
      }
    }
  }
'
{
  "data": {
    "repository": {
      "milestone": {
        "issues": {
          "nodes": [
            {
              "title": "lint",
              "url": "https://github.com/toshi0607/jctl/issues/5",
              "createdAt": "2019-10-02T07:44:16Z"
            },
            {
              "title": "unit test",
              "url": "https://github.com/toshi0607/jctl/issues/6",
              "createdAt": "2019-10-02T07:44:25Z"
            },
            {
              "title": "CMD option",
              "url": "https://github.com/toshi0607/jctl/issues/7",
              "createdAt": "2019-10-02T07:44:51Z"
            }
          ]
        },
        "pullRequests": {
          "nodes": [
            {
              "title": "add readme",
              "url": "https://github.com/toshi0607/jctl/pull/9",
              "createdAt": "2019-10-04T02:01:35Z"
            },
            {
              "title": "add sample .envrc",
              "url": "https://github.com/toshi0607/jctl/pull/10",
              "createdAt": "2019-10-04T15:35:29Z"
            },
            {
              "title": "add test case",
              "url": "https://github.com/toshi0607/jctl/pull/11",
              "createdAt": "2019-10-06T14:39:32Z"
            },
            {
              "title": "use os.UserHomeDir",
              "url": "https://github.com/toshi0607/jctl/pull/12",
              "createdAt": "2019-10-11T14:31:11Z"
            },
            {
              "title": "set TTL",
              "url": "https://github.com/toshi0607/jctl/pull/13",
              "createdAt": "2019-10-11T15:12:06Z"
            }
          ]
        }
      }
    }
  }
}
```

# タブ区切りに変換する

タブ区切りにしておくと、スプレッドシートに貼りやすいです。graphqlから帰ってきたJSONを[jq](https://stedolan.github.io/jq/)で加工します。

```shell
$ gh api graphql -F owner='toshi0607' -F name='jctl' -F number=2 -F first=10 -f query='
  query($name: String!, $owner: String!, $number: Int!, $first: Int) {
    repository(owner: $owner, name: $name) {
      milestone(number: $number) {
        issues(first: $first) {
          nodes {
            title
            url
            createdAt
          }
        }
        pullRequests(first: $first) {
          nodes {
            title
            url
            createdAt
          }
        }
      }
    }
  }
'| jq -r '.data.repository.milestone | .issues.nodes[], .pullRequests.nodes[] | [.url, .title, .createdAt] | @tsv'
https://github.com/toshi0607/jctl/issues/5	lint	2019-10-02T07:44:16Z
https://github.com/toshi0607/jctl/issues/6	unit test	2019-10-02T07:44:25Z
https://github.com/toshi0607/jctl/issues/7	CMD option	2019-10-02T07:44:51Z
https://github.com/toshi0607/jctl/pull/9	add readme	2019-10-04T02:01:35Z
https://github.com/toshi0607/jctl/pull/10	add sample .envrc	2019-10-04T15:35:29Z
https://github.com/toshi0607/jctl/pull/11	add test case	2019-10-06T14:39:32Z
https://github.com/toshi0607/jctl/pull/12	use os.UserHomeDir	2019-10-11T14:31:11Z
https://github.com/toshi0607/jctl/pull/13	set TTL	2019-10-11T15:12:06Z
```

スプレッドシートの1Aに貼り付けるとこんな感じです。

![スクリーンショット 2020-08-19 22.31.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46547/50d4be6a-bd29-b39d-08fb-423fb5ce0e41.png)


「特定のRepositoryのMilestoneにひもづくPRやissueを取得してスプレッドシートに貼りやすいように加工する」というユースケース自体は頻繁にないかもしれません。それでも、GitHub CLIでgraphqlを簡単に扱え、いろいろな使い道がありそうだったので具体的な例として残してみることにしました。

# 別の方法

GitHub CLIにはissueサブコマンドとprサブコマンドもあります。

prのlistコマンドではデフォルトでopenなPRが取得できます。Milestoneベースでフィルタリングする方法はなさそうでした。

```shell
$ gh pr list --help
List and filter pull requests in this repository

USAGE
  gh pr list [flags]

FLAGS
  -a, --assignee string   Filter by assignee
  -B, --base string       Filter by base branch
  -l, --label strings     Filter by labels
  -L, --limit int         Maximum number of items to fetch (default 30)
  -s, --state string      Filter by state: {open|closed|merged|all} (default "open")
  -w, --web               Open the browser to list the pull request(s)

INHERITED FLAGS
      --help              Show help for command
  -R, --repo OWNER/REPO   Select another repository using the OWNER/REPO format

EXAMPLES
  $ gh pr list --limit 999
  $ gh pr list --state closed
  $ gh pr list --label "priority 1" --label "bug"
  $ gh pr list --web

LEARN MORE
  Use "gh <command> <subcommand> --help" for more information about a command.
  Read the manual at https://cli.github.com/manual

$ gh pr list --state closed -R toshi0607/jctl

Showing 8 of 8 pull requests in toshi0607/jctl that match your search

#13  set TTL             feature/set-ttl
#12  use os.UserHomeDir  feature/use-UserHomeDir
#11  add test case       feature/add-test-case
#10  add sample .envrc   feature/add-sample.envrc
#9   add readme          feature/add-readme
#3   fix CI              feature/fix-CI
#2   add logs            feature/add-logs
#1   add gha test        feature/add-gha-test
```

issueのlistコマンドでは、milestone名でフィルタリングできそうです。

```
$ gh issue list --help
List and filter issues in this repository

USAGE
  gh issue list [flags]

FLAGS
  -a, --assignee string   Filter by assignee
  -A, --author string     Filter by author
  -l, --label strings     Filter by labels
  -L, --limit int         Maximum number of issues to fetch (default 30)
      --mention string    Filter by mention
  -m, --milestone name    Filter by milestone name
  -s, --state string      Filter by state: {open|closed|all} (default "open")
  -w, --web               Open the browser to list the issue(s)

INHERITED FLAGS
      --help              Show help for command
  -R, --repo OWNER/REPO   Select another repository using the OWNER/REPO format

EXAMPLES
  $ gh issue list -l "help wanted"
  $ gh issue list -A monalisa
  $ gh issue list --web

LEARN MORE
  Use "gh <command> <subcommand> --help" for more information about a command.
  Read the manual at https://cli.github.com/manual
```

ただし、表示される時間は最新更新時間（今回はmilestoneを紐付け他時間）で、milestoneのフィルタリングはよくわかりませんでした。

```shell
$ gh issue list -R toshi0607/jctl

Showing 3 of 3 issues in toshi0607/jctl

#7  CMD option    about 1 hour ago
#6  unit test     about 1 hour ago
#5  lint          about 1 hour ago

$ gh issue list -R toshi0607/jctl -m test2

No issues match your search in toshi0607/jctl

$ gh issue list -R toshi0607/jctl --milestone test2

No issues match your search in toshi0607/jctl
```

それを差し置いても、他にも便利な使い方ができそうです。

```shell
$ gh issue
Work with GitHub issues

USAGE
  gh issue <command> [flags]

CORE COMMANDS
  close:      Close issue
  create:     Create a new issue
  list:       List and filter issues in this repository
  reopen:     Reopen issue
  status:     Show status of relevant issues
  view:       View an issue

FLAGS
  -R, --repo OWNER/REPO   Select another repository using the OWNER/REPO format

INHERITED FLAGS
  --help   Show help for command

ARGUMENTS
  An issue can be supplied as argument in any of the following formats:
  - by number, e.g. "123"; or
  - by URL, e.g. "https://github.com/OWNER/REPO/issues/123".

EXAMPLES
  $ gh issue list
  $ gh issue create --label bug
  $ gh issue view --web

LEARN MORE
  Use "gh <command> <subcommand> --help" for more information about a command.
  Read the manual at https://cli.github.com/manual

$ gh pr
Work with GitHub pull requests

USAGE
  gh pr <command> [flags]

CORE COMMANDS
  checkout:   Check out a pull request in Git
  close:      Close a pull request
  create:     Create a pull request
  diff:       View a pull request's changes.
  list:       List and filter pull requests in this repository
  merge:      Merge a pull request
  ready:      Mark a pull request as ready for review
  reopen:     Reopen a pull request
  review:     Add a review to a pull request
  status:     Show status of relevant pull requests
  view:       View a pull request

FLAGS
  -R, --repo OWNER/REPO   Select another repository using the OWNER/REPO format

INHERITED FLAGS
  --help   Show help for command

ARGUMENTS
  A pull request can be supplied as argument in any of the following formats:
  - by number, e.g. "123";
  - by URL, e.g. "https://github.com/OWNER/REPO/pull/123"; or
  - by the name of its head branch, e.g. "patch-1" or "OWNER:patch-1".

EXAMPLES
  $ gh pr checkout 353
  $ gh pr create --fill
  $ gh pr view --web

LEARN MORE
  Use "gh <command> <subcommand> --help" for more information about a command.
  Read the manual at https://cli.github.com/manual
```