自分は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オプション- よしなに変換してくれる