notebook

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

GitHubのProject(beta)のデータ(ProjectV2)をGraphQLで取得する

先日Project(beta)でGraphQLのAPIに更新があった

The new GitHub Issues - June 23rd update | GitHub Changelog

以前はProjectNextという名前でデータを取得できていた

すでにProjectNextでデータを取っていたが新たにできたProjectV2という型で取得するように変更したのでそのときのメモなどを残しておく

取得したデータを何に使っているかというと、そのままBigQueryに突っ込んでDataportalで次のようなダッシュボードを作って月の消化タスク量を計測している

本記事ではデータの取得(JSONの取得)までのメモを残している

その後のよしなにやる部分は別途書くかもしれない

ドキュメント

Projects(beta)

Using the API to manage projects (beta) - GitHub Docs

https://docs.github.com/en/issues/trying-out-the-new-projects-experience/using-the-api-to-manage-projectsdocs.github.com

基本操作はこのドキュメントを読みながら進めれば良さそう

GraphQLの型情報

Reference - GitHub Docs

https://docs.github.com/en/graphql/referencedocs.github.com

プロジェクトのデータ取得

number: 2はprojectのURLから取得する

例だとhttps://github.com/users/${user}/projects/2/views/8で2となる

loginは自分のユーザー名

  • sample.graphql
query{
  user(login: "swfz") {
    projectV2(number: 2) {
      title
      id
    }
  }
}
$ gh api graphql -f query="$(cat sample.graphql)"
{
  "data": {
    "user": {
      "projectV2": {
        "title": "@swfz's individual project",
        "id": "PVT_hogehoge"
      }
    }
  }
}

プロジェクト情報を取得できた

カスタムフィールドのID取得

Unions - GitHub Docs

        nodes{
          id
          updatedAt
          fieldValues(first: 50) {
            nodes{
              ... on ProjectV2ItemFieldTextValue {
                text
                field {
                  ... on ProjectV2FieldCommon {
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldDateValue {
                date
                field {
                  ... on ProjectV2FieldCommon {
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldSingleSelectValue {
                name
                field {
                  ... on ProjectV2FieldCommon {
                    name
                  }
                }
              }
            }
          }
        }

上記クエリは一部ドキュメントから引用した

サンプルだと3つの型を例示してこの型のフィールドの場合はこのフィールド名を指定するという感じ

Textならtext,Dateならdateといったように型によって取れる値のフィールド名が変わる

型の種類がどれくらいあるのかみたら11個もあった

一個ずつ確認しに行こうかと思ったがProjectV2のフィールドで共通の型ProjectV2FieldCommonというのを見つけたので出力してみる

  • fieldtype.graphql
query{
  node(id: "PVT_hogehoge") {
    ... on ProjectV2 {
      fields(first: 20) {
        nodes {
          ... on ProjectV2FieldCommon {
            id
            name
            dataType
          }
        }
      }
    }
  }
}
$ gh api graphql -f query="$(cat fieldtype.graphql)"
{
  "data": {
    "node": {
      "fields": {
        "nodes": [
          {
            "id": "PVTF_aaaaaaaaaa",
            "name": "Title",
            "dataType": "TITLE"
          },
          {
            "id": "PVTF_bbbbbbbbbb",
            "name": "Assignees",
            "dataType": "ASSIGNEES"
          },
          {
            "id": "PVTSSF_cccccccccc",
            "name": "Status",
            "dataType": "SINGLE_SELECT"
          },
          {
            "id": "PVTF_dddddddddd",
            "name": "Labels",
            "dataType": "LABELS"
          },
          {
            "id": "PVTF_eeeeeeeeee",
            "name": "Repository",
            "dataType": "REPOSITORY"
          },
          {
            "id": "PVTF_ffffffffff",
            "name": "Milestone",
            "dataType": "MILESTONE"
          },
          {
            "id": "PVTF_gggggggggg",
            "name": "Linked pull requests",
            "dataType": "LINKED_PULL_REQUESTS"
          },
          {
            "id": "PVTF_hhhhhhhhhh",
            "name": "Point",
            "dataType": "NUMBER"
          },
          {
            "id": "PVTF_iiiiiiiiii",
            "name": "Month",
            "dataType": "DATE"
          },
          {
            "id": "PVTF_jjjjjjjjjj",
            "name": "Reviewers",
            "dataType": "REVIEWERS"
          },
          {
            "id": "PVTIF_kkkkkkkkkk",
            "name": "Iteration",
            "dataType": "ITERATION"
          },
          {
            "id": "PVTF_llllllllll",
            "name": "Tracks",
            "dataType": "TRACKS"
          }
        ]
      }
    }
  }
}

これでいったん、Projectで使っているフィールド、カスタムフィールドのID、名前、タイプが取得できる

このdataTypeに対応するProjectV2ItemField{DataType}Valueを定義すればデータを取得できそう

Projectで扱っているIssue,PullRequestのデータ、カスタムフィールドのデータを取得するGraphQLクエリ

結局こんな感じのクエリになった

  • items_v2.graphql
query($projectId: ID! $cursor: String){
  node(id: $projectId) {
    ... on ProjectV2 {
      items(first: 50, after: $cursor) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes{
          id
          createdAt
          updatedAt
          isArchived
          type
          fieldValues(first: 20) {
            nodes{
              ... on ProjectV2ItemFieldTextValue {
                text
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldDateValue {
                date
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldSingleSelectValue {
                name
                nameHTML
                optionId
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldIterationValue {
                iterationId
                startDate
                title
                titleHTML
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldLabelValue {
                labels(first: 5) {
                  nodes {
                    color
                    description
                    isDefault
                    name
                    url
                  }
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldMilestoneValue {
                milestone {
                  id
                  title
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldNumberValue {
                number
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldPullRequestValue {
                pullRequests(first: 10) {
                  nodes {
                    title
                    id
                    number
                    url
                    closed
                    closedAt
                    createdAt
                    merged
                    mergedAt
                    repository {
                      name
                    }
                    assignees(first: 5) {
                      nodes{
                        name
                        login
                      }
                    }
                    reviewRequests(first: 5) {
                      nodes {
                        requestedReviewer {
                          ... on User {
                            name
                          }
                        }
                      }
                    }
                  }
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldRepositoryValue {
                repository {
                  name
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
              ... on ProjectV2ItemFieldUserValue {
                users(first: 3) {
                  nodes {
                    id
                    login
                    name
                  }
                }
                field {
                  ... on ProjectV2FieldCommon {
                    id
                    dataType
                    name
                  }
                }
              }
            }
          }
          content{
            ... on DraftIssue {
              title
              body
              createdAt
            }
            ...on Issue {
              title
              id
              number
              url
              closed
              closedAt
              createdAt
              repository {
                name
              }
              milestone {
                id
                title
              }
              assignees(first: 5) {
                nodes{
                  name
                  login
                }
              }
            }
            ...on PullRequest {
              title
              id
              number
              url
              closed
              closedAt
              createdAt
              merged
              mergedAt
              repository {
                name
              }
              assignees(first: 5) {
                nodes{
                  name
                  login
                }
              }
              reviewRequests(first: 5) {
                nodes {
                  requestedReviewer {
                    ... on User {
                      name
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

めちゃくちゃ長いw

ざっくりやっていることとしては

次のデータを取得している

  • ProjectV2Itemの各種フィールドのデータ
    • Point(ProjectV2ItemFieldNumberValue)
    • Month(ProjectV2ItemFieldDateValue)
    • Status(ProjectV2ItemFieldSingleSelectValue)
    • Iteration(ProjectV2ItemFieldIterationValue)
    • Milestone(ProjectV2ItemFieldMilestoneValue)
    • Repository(ProjectV2ItemFieldRepositoryValue)
    • Label(ProjectV2ItemFieldLabelValue)
    • etc…
  • ProjectV2Itemのcontentデータ
    • PullRequest
    • Issue
    • DraftIssue
      • ボード(Project)だけに存在するカード的なもの

カスタムフィールドの定義がProjectNextよりしっかりしたようでカスタムフィールドの型によって出力項目も変わる

IDが欲しいだけなら上記のクエリで書いている各フィールドの定義をProjectV2FieldCommonにまとめてしまうだけでOKだが今回は値も欲しいのでこのようなクエリになった

値が欲しいなら結局は型によってそれぞれ記述するしかなさそう

ProjectV2Itemの件数が50件にしているのは色々なフィールドを増やしまくった結果制限に達してしまったため下げた(最大だと100だったはず)

variables

渡す予定の引数について

cursorはページング用、レスポンスで返ってくるendCursorの値を指定して次のクエリをcursorに指定して投げる

projectIdは最初の段階でProjectV2のIDを取得しているので変数で指定する

実行(初回)

$ gh api graphql -f query="$(cat items_v2.graphql)" -f projectId=${GH_PROJECT_ID}
{
  "data": {
    "node": {
      "items": {
        "pageInfo": {
          "hasNextPage": true,
          "endCursor": "NTA"
        },
        "nodes": [
          {
            "id": "PVTI_aaaaaaaaaaaaaaaaaaaa",
            "createdAt": "2021-12-10T17:18:16Z",
            "updatedAt": "2021-12-10T17:20:33Z",
            "isArchived": false,
            "type": "ISSUE",
            "fieldValues": {
              "nodes": [
                {
                  "users": {
                    "nodes": [
                      {
                        "id": "bbbbbbbbbb",
                        "login": "swfz",
                        "name": "swfz"
                      }
                    ]
                  },
                  "field": {
                    "id": "PVTF_cccccccccccccccccc",
                    "dataType": "ASSIGNEES",
                    "name": "Assignees"
                  }
                },
                {
                  "repository": {
                    "name": "memo"
                  },
                  "field": {
                    "id": "PVTF_dddddddddddddddddd",
                    "dataType": "REPOSITORY",
                    "name": "Repository"
                  }
                },
                {
                  "labels": {
                    "nodes": [
                      {
                        "color": "128de5",
                        "description": "",
                        "isDefault": false,
                        "name": "blog",
                        "url": "https://github.com/swfz/sample/labels/blog"
                      }
                    ]
                  },
                  "field": {
                    "id": "PVTF_eeeeeeeeeeeeeeeeee",
                    "dataType": "LABELS",
                    "name": "Labels"
                  }
                },
                {
                  "text": "jqでファイルに書き出したリストを用いて比較する",
                  "field": {
                    "id": "PVTF_ffffffffffffffffff",
                    "dataType": "TITLE",
                    "name": "Title"
                  }
                },
                {
                  "name": "Done",
                  "nameHTML": "Done",
                  "optionId": "98236657",
                  "field": {
                    "id": "PVTSSF_gggggggggggggggggg",
                    "dataType": "SINGLE_SELECT",
                    "name": "Status"
                  }
                },
                {
                  "number": 1,
                  "field": {
                    "id": "PVTF_hhhhhhhhhhhhhhhhhh",
                    "dataType": "NUMBER",
                    "name": "Point"
                  }
                },
                {
                  "date": "2022-03-01",
                  "field": {
                    "id": "PVTF_iiiiiiiiiiiiiiiiii",
                    "dataType": "DATE",
                    "name": "Month"
                  }
                },
                {
                  "iterationId": "61051d6c",
                  "startDate": "2022-03-01",
                  "title": "2022-03",
                  "titleHTML": "2022-03",
                  "field": {
                    "id": "PVTIF_jjjjjjjjjjjjjjjjjj",
                    "dataType": "ITERATION",
                    "name": "Iteration"
                  }
                }
              ]
            },
            "content": {
              "title": "jqでファイルに書き出したリストを用いて比較する",
              "id": "kkkkkkkkkkkkkkkkkkkkkkkk",
              "number": 1466,
              "url": "https://github.com/swfz/sample/issues/1466",
              "closed": true,
              "closedAt": "2022-03-27T09:39:58Z",
              "createdAt": "2021-06-03T19:37:41Z",
              "repository": {
                "name": "memo"
              },
              "milestone": null,
              "assignees": {
                "nodes": [
                  {
                    "name": "swfz",
                    "login": "swfz"
                  }
                ]
              }
            }
          },

とりあえず最初の1件だけ抜き出した

こんな感じでデータが取れるのであとは用途によってよしなにすればよい

他余談

権限

アプリ - GitHub Docs

GitHub Appsで払い出したTOKENを用いてクエリをたたこうとするとNotFoundとなる

自分のプライベートタスク管理なのでuser projectを作成して活用していた

で、user projectへアクセスできないなーと思っていたがどうやらユーザー配下のデータにアクセスするにはOAuthAppもしくはPersonalAccessTokenじゃないと無理そう?

Forumにも同様の質問が投稿されていたが返信はついてなかった…

また、ProjectV2ではread:projectもしくはwrite:projectの権限が必要になったよう

Insight

Insightも使えるようになってグラフ化も簡単に行えるようになった

なのでカスタムフィールドにStoryPointなどを設定しベロシティトラッキング用のチャートを出力するということがコードなしに実現できる

ただ、実際に試してみたがバーンダウン系のチャートはうまく実現できなそうだったので自前でやるしかなさそう

まとめ

  • GitHubのGraphQL APIを用いてProject(beta)(ProjectV2)のデータを取得した
    • ProjectV2Item
  • ProjectV2ItemにひもづくPullRequestやIssueのデータも合わせて取得した
  • ProjectV2になってカスタムフィールドの型がしっかりしたのでカスタムフィールドの値をそれぞれ定義する必要があった
  • GitHubのCLIでGraphQLのAPIにクエリできるのが結構便利