notebook

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

既存のPullRequestやIssueをGitHub Projects Beta(ProjectV2)にひもづける

自分はGitHubのProjects(Beta)を個人のタスク管理に使っている

それまではZenHubを使っていたがProjects(beta)に移行した

移行の際、せっかくならそれまで対応してきたIssueやPullRequestなどをProjcets(beta)にひもづけた状態で使い始めたい

各リポジトリのIssueやPullRequestをひもづける際に、ぽちぽちひもづける場合、量が多いとたいへんになってくる

そこで今回はGitHub CLIとGraphQL APIを用いてProjects(Beta)にIssueやPullRequestをひもづけてみる

今回は対象がPullRequestだけだったので例としてPullRequestのみを扱っている

やったこと

  • ひもづけ対象のPullRequestを取得する
  • 対象PullRequestをProjects(Beta)へひもづける(ProjectV2Item)
  • カスタムフィールドのIDを取得する
  • ひもづけたProjectV2Itemの特定フィールドの値を更新する
  • 処理をまとめてShellScriptにする

ひもづけ対象のPullRequestを取得する

GitHubのCLIでAPI行う

$ gh pr list --json number,title,url,id --limit 50 --search "repo:swfz/til is:closed article in:title merged:2022-05-01..2022-06-01"
[
  {
    "id": "PR_kwDOEEpzH844r9Av",
    "number": 811,
    "title": "article: netlify to cloudflare",
    "url": "https://github.com/swfz/til/pull/811"
  },
  {
    "id": "PR_kwDOEEpzH844gcxE",
    "number": 806,
    "title": "article: svg icon color",
    "url": "https://github.com/swfz/til/pull/806"
  },
  {
    "id": "PR_kwDOEEpzH844Y6UZ",
    "number": 805,
    "title": "article: gist embed",
    "url": "https://github.com/swfz/til/pull/805"
  },
  {
    "id": "PR_kwDOEEpzH843UaSc",
    "number": 777,
    "title": "article: upgrade to jest28",
    "url": "https://github.com/swfz/til/pull/777"
  }
]

--searchのクエリ部分はほしいPullRequestやIssueに合わせて変えれば良い、下記ドキュメントに詳細が載っている

Issue およびプルリクエストを検索する - GitHub Docs

https://docs.github.com/ja/search-github/searching-on-github/searching-issues-and-pull-requestsdocs.github.com

--jsonで対象どのカラムを出力するかを指定できる(カンマ区切り)

後の工程でjqを使って処理するのでJSONで出力している

取得は簡単にできた

Projects(Beta)へひもづける

これもGitHub CLIから行う

まず必要なIDなどを取得していく

ProjectのID取得

まず対象ProjectのIDを取得する

  • project.graphql
query{
  user(login: "user1") {
    projectV2(number: 2) {
      title
      id
    }
  }
}

loginは自身のユーザー名

number: 2はprojectのURLから

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

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

data.user.projectV2.idをメモしておく

Projectへのひもづけ

対象のProjectを特定したので実際に1件PRをひもづけてみる

GH_PROJECT_IDは前の節で取得したID

pr_node_idはPullRequestのID(PR_xxxxx)

# 適切な値を代入しておく
GH_PROJECT_ID=""
pr_node_id=""

gh api graphql -f query='
  mutation($project_id: ID! $node_id: ID!) {
    addProjectV2ItemById(input: {projectId: $project_id contentId: $node_id}) {
      item {
        id
      }
    }
  }' -f project_id=${GH_PROJECT_ID} -f node_id=${pr_node_id} -q '.data.addProjectV2ItemById.item.id'

-qで出力する値を指定している

ProjectV2のItemのIDが後の工程で必要なためIDだけ抜き出している

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

PullRequestをひも付けたあと、カスタムフィールドの値も自動で更新したいので準備する

  • fields.graphql

PVT_xxxxxはProjectV2のID

query{
  node(id: "PVT_xxxxx") {
    ... on ProjectV2 {
      fields(first: 20) {
        nodes {
          ... on ProjectV2Field {
            id
            name
          }
          ... on ProjectV2IterationField {
            id
            name
            configuration {
              iterations {
                startDate
                id
              }
            }
          }
          ... on ProjectV2SingleSelectField {
            id
            name
            options {
              id
              name
            }
          }
        }
      }
    }
  }
}
$ gh api graphql -f query="$(cat fields.graphql)"
{
  "data": {
    "node": {
      "fields": {
        "nodes": [
          {
            "id": "PVTF_titleid",
            "name": "Title"
          },
          {
            "id": "PVTF_assigneeid",
            "name": "Assignees"
          },
          {
            "id": "PVTSSF_statusid",
            "name": "Status",
            "options": [
              {
                "id": "aaaaaaaaa",
                "name": "New"
              },
              {
                "id": "bbbbbbbb",
                "name": "Todo"
              },
              {
                "id": "cccccccc",
                "name": "In Progress"
              },
              {
                "id": "dddddddd",
                "name": "Done"
              }
            ]
          },
          {
            "id": "PVTF_labelid",
            "name": "Labels"
          },
          {
            "id": "PVTF_repoid",
            "name": "Repository"
          },
          {
            "id": "PVTF_milestoneid",
            "name": "Milestone"
          },
          {
            "id": "PVTF_linkedpullrequestid",
            "name": "Linked pull requests"
          },
          {
            "id": "PVTF_pointid",
            "name": "Point"
          },
          {
            "id": "PVTF_monthid",
            "name": "Month"
          },
          {
            "id": "PVTIF_iterationid",
            "name": "Iteration",
            "configuration": {
              "iterations": [
                {
                  "startDate": "2022-07-01",
                  "id": "eeeeeeee"
                },
                {
                  "startDate": "2022-08-01",
                  "id": "ffffffff"
                },
                {
                  "startDate": "2022-09-01",
                  "id": "gggggggg"
                }
              ]
            }
          },
          {
            "id": "PVTF_tracksid",
            "name": "Tracks"
          }
        ]
      }
    }
  }
}

値の更新対象のフィールドIDをメモしておく

Projectのデータの更新

必要なIDと値を事前に定義しておき次のようなmutation queryを投げる

更新している値はmonth,point,status

gh api graphql -f query='
  mutation($project_id: ID! $point_value: Float! $point_id: ID! $month_id: ID! $item_id: ID! $month_value: Date! $status_id: ID! $status_value: String!) {
    updateMonth: updateProjectV2ItemFieldValue(
      input: {
        projectId: $project_id
        itemId: $item_id
        fieldId: $month_id
        value: {
          date: $month_value
        }
      }
    ) {
      projectV2Item {
        id
      }
    }
    updateStatus: updateProjectV2ItemFieldValue(
      input: {
        projectId: $project_id
        itemId: $item_id
        fieldId: $status_id
        value: {
          singleSelectOptionId: $status_value
        }
      }
    ) {
      projectV2Item {
        id
      }
    }
    updatePoint: updateProjectV2ItemFieldValue(
      input: {
        projectId: $project_id
        itemId: $item_id
        fieldId: $point_id
        value: {
          number: $point_value
        }
      }
    ) {
      projectV2Item {
        id
      }
    }
}' -f project_id=${GH_PROJECT_ID} -F point_value=1 -f point_id=${FIELD_POINT_ID} -f month_id=${FIELD_MONTH_ID} -f item_id=${item_id} -f month_value=${FIELD_MONTH_VALUE} -f status_id=${FIELD_STATUS_ID} -f status_value="${FIELD_STATUS_VALUE}"

変数が多くなってしまったが次のように指定する

  • GH_PROJECT_ID
    • ProjectV2のID
  • item_id
    • ひもづけの節で取得したID
    • ProjectV2ItemのID
  • FIELD_POINT_ID
    • カスタムフィールドPointのID
    • number(Float)なので-Fでよしなに変換してくれるようなオプション指定をしている
    • そうしないと次のような怒られが発生した
      • "Could not coerce value \"1\" to Float"
    • StoryPoint的な値
  • FIELD_MONTH_ID
    • カスタムフィールドMonthのID
  • FIELD_MONTH_VALUE
    • カスタムフィールドMonthのValue(Date)
    • 2022-06-01など
    • どの月のItemか
  • FIELD_STATUS_ID
    • ステータスフィールドのID
  • FIELD_STATUS_VALUE
    • ステータスフィールドのValue(ID)
    • 前節で取得したStatusのoptionsの中のid
    • どのステータスにするか

これで既存PullRequestの取得からProjects(Beta)へのひもづけ、カスタムフィールドの値更新を行った

各処理をまとめてShellScriptにする

雑だがこれらをまとめると次のようなスクリプトになる

前提として大文字のIDや固定値でよい変数は事前に調べて代入しておく

(もし参考にする場合はPullRequestを取得する箇所のクエリでrepoを替えてあげてください)

  • relation_project_beta.sh
#!/bin/bash

TARGET_DATE=2020-06-01
FIELD_STATUS_ID=""
FIELD_STATUS_VALUE=
FIELD_POINT_ID=""
FIELD_MONTH_ID=""
FIELD_MONTH_VALUE=${TARGET_DATE}

PR_SEARCH_END_DATE=$(date -d "${TARGET_DATE} 1 month" +"%Y-%m-01")

if [ -z $GH_PROJECT_ID ]; then
  echo "please set GH_PROJECT_ID environment variable and try again."
  exit 1
fi

target_pull_requests=$(gh pr list --json number,title,url,id --limit 50 --search "repo:swfz/til is:closed article in:title merged:${TARGET_DATE}..${PR_SEARCH_END_DATE}")

echo ${target_pull_requests} \
  | tr -d '[:cntrl:]' \
  | jq -cr '.[]' \
  | while read -r line; do

  sleep 1

  echo '=========='
  echo ${line}
  echo '=========='

  pr_node_id=$(echo ${line} | jq -cr '.id')

  echo ${pr_node_id}
  echo ${GH_PROJECT_ID}

  item_id=$(gh api graphql -f query='
    mutation($project_id: ID! $node_id: ID!) {
      addProjectV2ItemById(input: {projectId: $project_id contentId: $node_id}) {
        item {
          id
        }
      }
    }' -f project_id=${GH_PROJECT_ID} -f node_id=${pr_node_id} -q '.data.addProjectV2ItemById.item.id')

  gh api graphql -f query='
    mutation($project_id: ID! $point_value: Float! $point_id: ID! $month_id: ID! $item_id: ID! $month_value: Date! $status_id: ID! $status_value: String!) {
      updateMonth: updateProjectV2ItemFieldValue(
        input: {
          projectId: $project_id
          itemId: $item_id
          fieldId: $month_id
          value: {
            date: $month_value
          }
        }
      ) {
        projectV2Item {
          id
        }
      }
      updateStatus: updateProjectV2ItemFieldValue(
        input: {
          projectId: $project_id
          itemId: $item_id
          fieldId: $status_id
          value: {
            singleSelectOptionId: $status_value
          }
        }
      ) {
        projectV2Item {
          id
        }
      }
      updatePoint: updateProjectV2ItemFieldValue(
        input: {
          projectId: $project_id
          itemId: $item_id
          fieldId: $point_id
          value: {
            number: $point_value
          }
        }
      ) {
        projectV2Item {
          id
        }
      }
  }' -f project_id=${GH_PROJECT_ID} -F point_value=1 -f point_id=${FIELD_POINT_ID} -f month_id=${FIELD_MONTH_ID} -f item_id=${item_id} -f month_value=${FIELD_MONTH_VALUE} -f status_id=${FIELD_STATUS_ID} -f status_value="${FIELD_STATUS_VALUE}"
done
  • GH_PROJECT_IDは事前に環境変数を設定しておく
  • 月ごとにPRを取得してProjectV2にひも付け
  • 各種カスタムフィールドの更新
    • Pointを更新(今回のケースではすべて1で固定)
    • Month(対象月)の更新(今回のケースでは1回の実行ですべて同じ値)
    • ステータスの更新(今回のケースでは過去のPullRequestが対象だったのですべてDoneのIDを値として設定した)

まとめ

  • GitHubのCLIを使ってPullRequestの検索をした
  • GitHubのGraphQL APIを用いてProjects(beta)に既存のPullRequestをひもづけた
  • ひも付けたItemに対する自身で定義したカスタムフィールドの値の更新をGraphQLのmutationで行った
  • GitHub CLIが便利、特に-Fオプション
    • よしなに変換してくれる