GitHub CLIでIssueのCloseされるまでの日数を取得する(ちょっとShell有り)

8 min読了の目安(約7700字TECH技術記事

はじめに

最近ようやくGitHub CLIを使い始めました。
マニュアルを呼んでいると、CLI経由でGraphQLが投げられるとわかったので、GraphQLの勉强がてら、GitHubのIssueのリストを取得して、Open→Closeまでのリードタイムを測るようなスクリプトを組んでみました。

なお、動作環境はMacOS 10.15.6(19G2021) となっています。

Issueを取得するスクリプト

はじめに、Issueのデータと、OpenからCloseするまでの日数(リードタイム)を取得するスクリプトを記載します。

#!/bin/bash 

# $@をダブルクォーテーションで囲うのが必要。それぞれの引数を""で囲って展開するため。
# 囲わないと、日付の部分にシングルクォーテーションが入ってしまい、挙動がおかしくなる
gh api graphql -F queryString="$@" --paginate -f query='
query($endCursor: String, $queryString: String!) {
  search(query: $queryString, type: ISSUE, first: 100, after: $endCursor) {
    edges {
      node {
        ... on Issue {
          number
          title
          url
          createdAt
          closedAt
          comments(first: 100) {
            totalCount
          }
          state
          labels(first: 100) {
            nodes {
              name
            }
          }
          assignees(first: 100) {
            nodes {
              name
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
' |
jq -r  '.data[].edges[].node | [.createdAt, .closedAt, .number, .url, .comments.totalCount, .state, .title, .assignees.nodes[].name, .labels.nodes[].name] | @tsv' |
# titleにはスペースが入っていることがあるので、一番最後にしてreadで読む
while IFS='	' read createdAt closedAt number url commentsCount state title others; do
  # null回避
  [ $closedAt = "null" ] && closedAt=$(date -u +%Y-%m-%dT%H:%M:%SZ)

  closedAt=$(echo -n $closedAt | tr -d "'")
  createdAt=$(echo -n $createdAt | tr -d "'")
  leadDays=$(( $(( $(date -u -jf %FT%TZ  ${closedAt} +%s)  -  $(date -u -jf %FT%TZ  ${createdAt} +%s) )) / (60 * 60 * 24) ))
  createdAtJST=$(date -jf %FT%TZ  ${createdAt} +%FT%T%z)
  closedAtJST=$(date -jf %FT%TZ  ${closedAt} +%FT%T%z)
  
  # Issue番号 ステータス 作成日時 Close日時 リードタイム コメント数 URL タイトル、Author
  echo $number $state $createdAtJST $closedAtJST $leadDays $commentsCount $title $url $others
done 


使用例

./ggq_issue.sh  "repo:ModelingKai/Reservation2 is:issue is:closed "
21 CLOSED 2020-12-29T15:23:59+0900 2021-01-03T11:08:11+0900 4 0 予約Idファクトリを導入したADR作成する https://github.com/ModelingKai/Reservation2/issues/21 Jun Nakajima
17 CLOSED 2020-12-06T15:51:28+0900 2020-12-29T12:45:46+0900 22 0 ADRの導入の検討をする https://github.com/ModelingKai/Reservation2/issues/17
15 CLOSED 2020-11-15T13:04:24+0900 2020-11-22T11:45:57+0900 6 0 Azure App Service に ASP.NET Core アプリケーションをデプロイする GitHub Actions を追加 https://github.com/ModelingKai/Reservation2/issues/15
13 CLOSED 2020-11-15T09:09:26+0900 2020-11-22T11:35:38+0900 7 0 ASP.NET MVCをherokuにデプロイする https://github.com/ModelingKai/Reservation2/issues/13
6 CLOSED 2020-11-01T13:26:44+0900 2020-11-08T11:54:26+0900 6 1 予約ステータスの実装をする https://github.com/ModelingKai/Reservation2/issues/6
5 CLOSED 2020-11-01T13:24:00+0900 2020-11-08T11:54:12+0900 6 0 予約の状態遷移をRDRAに戻って考える https://github.com/ModelingKai/Reservation2/issues/5

順を追ってスクリプトの中身を説明していきます。

スクリプトの説明

GraphQLの実行

gh api graphql -F queryString="$@" --paginate -f query='

gh api graphql でCLI経由でgraphqlを投げることができます。
query= の後に、実際投げるクエリ。
-F の後はパラメータを指定ができます。
--paginate を設定すると、 要素の最後までを取得することができます。

https://cli.github.com/manual/gh_api

In '--paginate' mode, all pages of results will sequentially be requested until there are no more pages of results. For GraphQL requests, this requires that the original query accepts an '$endCursor: String' variable and that it fetches the 'pageInfo{ hasNextPage, endCursor }' set of fields from a collection.

GraphQLの中身

query($endCursor: String, $queryString: String!) {
  search(query: $queryString, type: ISSUE, first: 100, after: $endCursor) {

$endCursor: String--paginate オプションを付けた場合は必要です。
search(query: $queryString にスクリプトでもらった引数を渡します。

参考:searchコネクション

    edges {
      node {
        ... on Issue {
          number
          title
          url
          createdAt
          closedAt
          comments(first: 100) {
            totalCount
          }
          state
          labels(first: 100) {
            nodes {
              name
            }
          }
          assignees(first: 100) {
            nodes {
              name
            }
          }
        }
      }
    }

searchから取得できるのが、SearchResultItemである為、... on Issue でIssueとして扱っています。
参考:SearchResultItem

以下のpageInfoは、ghコマンドに--paginate オプションを付けている場合は必要です。

    pageInfo {
      hasNextPage
      endCursor
    }

GraphQLで取得した結果例

gh api graphql で取得するデータは以下の例のようにJSON形式となります。

{
  "data": {
    "search": {
      "edges": [
        {
          "node": {
            "number": 21,
            "title": "予約Idファクトリを導入したADR作成する",
            "url": "https://github.com/ModelingKai/Reservation2/issues/21",
            "createdAt": "2020-12-29T15:23:59Z",
            "closedAt": "2021-01-03T11:08:11Z",
            "comments": {
              "totalCount": 0
            },
            "state": "CLOSED",
            "labels": {
              "nodes": []
            },
            "assignees": {
              "nodes": [
                {
                  "name": "Jun Nakajima"
                }
              ]
            }
          }
        },
        {
          "node": {
	  ...

この状態だと集計などに扱いにくい為、1Issue、1レコードに纏めたいと思います。

JSONのパース

JSONのパースに jqコマンドを使用します。
Issueのタイトルにスペースやカンマが入っていることを考慮し、@tsv でtsv形式に変換しています。

jq -r  '.data[].edges[].node | [.createdAt, .closedAt, .number, .url, .comments.totalCount, .state, .title, .assignees.nodes[].name, .labels.nodes[].name] | @tsv' |

jqコマンドでパース後のデータ(tsv)

2020-12-29T15:23:59Z	2021-01-03T11:08:11Z	21	https://github.com/ModelingKai/Reservation2/issues/21	0	CLOSED	予約Idファクトリを導入したADR作成する	Jun Nakajima
2020-12-06T15:51:28Z	2020-12-29T12:45:46Z	17	https://github.com/ModelingKai/Reservation2/issues/17	0	CLOSED	ADRの導入の検討をする
2020-11-15T13:04:24Z	2020-11-22T11:45:57Z	15	https://github.com/ModelingKai/Reservation2/issues/15	0	CLOSED	Azure App Service に ASP.NET Core アプリケーションをデプロイする GitHub Actions を追加
2020-11-15T09:09:26Z	2020-11-22T11:35:38Z	13	https://github.com/ModelingKai/Reservation2/issues/13	0	CLOSED	ASP.NET MVCをherokuにデプロイする
2020-11-01T13:26:44Z	2020-11-08T11:54:26Z	6	https://github.com/ModelingKai/Reservation2/issues/6	1	CLOSED	予約ステータスの実装をする
2020-11-01T13:24:00Z	2020-11-08T11:54:12Z	5	https://github.com/ModelingKai/Reservation2/issues/5	0	CLOSED	予約の状態遷移をRDRAに戻って考える

Closeまでの時間を計算する

while read でデータを読み込みます。
このとき、タブ区切りで読み込みをしたいので、while IFS=' ' read で指定しています。
(IFS='\t' を指定しましたが、うまくいきませんでした)

createdAtとclosedAtは、UTCでISO8601形式(yyyy-MM-ddTHH:mm:ss)となっているので、UNIX時間にした後、差分を計算したあとに、日に直しています。

while IFS='	' read createdAt closedAt number url commentsCount state title others; do
  # null回避
  [ $closedAt = "null" ] && closedAt=$(date -u +%Y-%m-%dT%H:%M:%SZ)

  closedAt=$(echo -n $closedAt | tr -d "'")
  createdAt=$(echo -n $createdAt | tr -d "'")
  leadDays=$(( $(( $(date -u -jf %FT%TZ  ${closedAt} +%s)  -  $(date -u -jf %FT%TZ  ${createdAt} +%s) )) / (60 * 60 * 24) ))
  createdAtJST=$(date -jf %FT%TZ  ${createdAt} +%FT%T%z)
  closedAtJST=$(date -jf %FT%TZ  ${closedAt} +%FT%T%z)
  
  echo $number $state $createdAtJST $closedAtJST $leadDays $commentsCount $title $url $others
done 

おわりに

単純ではありますが、Issueのリストを取得して整形するところまでをやってみました。
スプレッドシートなどに貼ってグラフ化とかするのであれば、おそらくjqコマンドでパースした状態をそのまま貼れば良さそうなので、わざわざwhile readで頑張らなくても良さそうです。

今回欲しいデータもShellで整形しなくても良いように、GraphQL側でやり方ってなんかあるのかな。

あと、Label検索やAuthor検索する時、OR条件でやりたいけど、そういうやり方は無いんですかね…?