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