ZenHubで個人タスクを管理していたがGitHubProjects(beta)が良さそうだったので移行することにした
移行に際して、一部過去の情報も含めて移行したいのでそこまでやったので作業ログ的に残しておく
ZenHubをフル活用できているわけでもなかったので割とすんなりいけたはず
移行を決めた理由
- Notionと割と近い使い心地
- カラムを自由に設定でき、自由度が高い
- ビューをカスタマイズして設定を保存できる
- カンバン
- リスト
- 複数リポジトリを横断したIssue/PR管理ができる
ZenHubでも同じような機能があって使っていた
- しかし、1つでも管理対象にしたいIssueがある場合対象リポジトリをZenHubで管理できるようにする必要があったため結構面倒になった記憶がある
結局よく使うメインのリポジトリにIssueを作成して他リポジトリのタスクを管理する、もしくは他リポジトリへのIssueリンクで済ますっていう感じになってしまった
- ここがどうしても使い心地としてあまり良くなくて決め手になったと言って良いかもしれない
ZenHubで使っていた機能
下記を主に使っていた
- Epic
- StoryPoint
- レポート
- burndown
- velocity tracking
Epic
プロジェクト(ベータ)の管理のベストプラクティス - GitHub Docs
上記をみるとZehHubでいうEpicは
- Issueでタスクリストとして扱う
- [ ] hogeでリスト化
- マイルストーンで管理する
- ラベルで管理する
が代替としてはありそう
マイルストーンはすでに使っているのでタスクリストで代替していくことにした
ひも付け時はEpic的に扱うIssueを編集しないといけないのでちょっと面倒だなーというくらい
今のところそんなに不便と感じていない
過去にEpicを使ってひも付けていたIssueはEpicを精査して必要であれば上記管理方法で管理できるようEpicのIssue内容に追記した
StoryPoint(estimate)
目安程度で使っていた、主に毎月どの位のポイント量を消化しているのかを把握するためにある程度感覚で付けていた
ProjectItemでPointというカラムを用意し、そちらを使うことにした
こんな感じ

後述するが今まで設定していたStoryPointも参考にしたいので書き捨てスクリプトを書いてZenHubのStoryPointからProjectsのPointへデータを移行した
レポート
以前はburndownとvelocity trackingのレポートを見ていた
この辺はGitHubのAPIを使ってBigQueryまで入れてしまえば後はなんとでもできる思っていたのでそんなに心配してなかった
もうInsightsが来るようなので必要ないかもしれないが…
実際にBigQueryへデータ入れ込んで表示させてみた(velocity trackingはサクッとできた)

Milestone
ZenHubの機能ではないが、GitHubのMilestoneを月ごとに作成して集計を行う単位としていた
メインのメモ用リポジトリだけの設定だったのでこの機会にProjects上で新たにカラムを作ってそちらで管理するように変更した(Month)
今のところメインのリポジトリでは今まで通りMilestoneも作成している
Iterationでも良いのでは?
と思ったがIterationは過去のものを作成できなかったので用途的に合わなかった
移行後の所感
- とりあえず的なものでもIssueを作成せずアイテムだけ追加できる
- IssueへのConvertもさっと行えるので楽

- Issue画面からProjectsへのひもづけ、独自に追加したカラムの値設定が行える
- これはだいぶ助かっている、わざわざProjectsの画面に戻らなくて良いので楽

- リストのソート順を
last updatedで行えないのは不便(2022-02-13現在、Issueは起票されていたのを見たので対応されるかも?)
移行作業
以下は移行にあたって調べたり試したりしたことのメモなので興味があれば読み進めていただければ…
方針が決まってしまえばあとはスクリプト書いて既存のZenHubのデータをGitHub Projects(beta)のデータに変換するだけ
大まかには下記の流れ
- 既存のZenHubIssue,Epicを取得
- ZenHubのIssueはGitHubのIssueにひもづけて追加情報を保存している感じなのでEpic,Issueともに取得する
- ProjectsのItemにひも付け
- StoryPointを取得→
Pointというカラムへ - Milestoneを取得→
Monthというカラムへ
- StoryPointを取得→
という感じ
調べていく中でGitHub CLIを使ってapiが使えるのを知った
これがめちゃくちゃ便利だった
これもうローカルでゴニョゴニョやるにはPERSONAL ACCESS TOKEN必要ないやつでは?というくらい良かった
主に下記のドキュメントをもとに進めていく
APIを使ったプロジェクト(ベータ)の管理 - GitHub Docs
project idの取得
❯ gh api graphql -f query='
query{
user(login: "swfz") {
projectsNext(first: 20) {
nodes {
id
title
}
}
}
}'
{
"data": {
"user": {
"projectsNext": {
"nodes": [
{
"id": "PN_xxxxxxxxxxxxxxxxx",
"title": "private project"
}
]
}
}
}
}
PN_xxxxxxxxxxxxxxxxxが後の工程で必要なのでメモしておく
アイテムの追加
CONTENT_IDを、追加したいIssueあるいはPull RequestのノードIDで置き換えてください。
適当なIssueのノードIDをGraphQLのAPIで取得して置き換え、projectsに追加されたか確認した
contentはprojectsのアイテムにひもづくIssueやPullRequestのこと
$ gh api graphql -f query='
mutation {
addProjectNextItem(input: {projectId: "PN_xxxxxxxxxxxxxxxxx" contentId: "I_xxxxxxxxxxxxxxx"}) {
projectNextItem {
id
}
}
}'
{
"data": {
"addProjectNextItem": {
"projectNextItem": {
"id": "PNI_xxxxxxxxxxxxxxxxxxxx"
}
}
}
}
更新に必要なFIELD_ID, ITEM_IDの取得
追加したカラムの情報を取得する
後のアイテム更新時に使用するのでMonthのIDとPointのIDをメモしておく
$ gh api graphql -f query='
query{
node(id: "PN_xxxxxxxxxxxxxxxxx") {
... on ProjectNext {
fields(first: 20) {
nodes {
id
name
settings
}
}
}
}
}'
{
"data": {
"node": {
"fields": {
"nodes": [
{
"id": "FIELD_ID_XXXXX1",
"name": "Title",
"settings": "{\"width\":319}"
},
{
"id": "FIELD_ID_XXXXX2",
"name": "Status",
"settings": "{\"width\":125,\"options\":[{\"id\":\"status_id_xxxxx1\",\"name\":\"Todo\",\"name_html\":\"Todo\"},{\"id\":\"status_id_xxxxx2\",\"name\":\"In Progress\",\"name_html\":\"In Progress\"},{\"id\":\"status_id_xxxxx3\",\"name\":\"Done\",\"name_html\":\"Done\"}]}"
},
{
"id": "FIELD_ID_XXXXX3",
"name": "Labels",
"settings": "null"
},
{
"id": "FIELD_ID_XXXXX4",
"name": "Repository",
"settings": "null"
},
{
"id": "FIELD_ID_XXXXX5",
"name": "Milestone",
"settings": "null"
},
{
"id": "FIELD_ID_XXXXX6",
"name": "Linked Pull Requests",
"settings": "null"
},
{
"id": "FIELD_ID_XXXXX7",
"name": "Point",
"settings": "{\"width\":69}"
},
{
"id": "FIELD_ID_XXXXX8",
"name": "Month",
"settings": "null"
}
]
}
}
}
}
全部のせると多すぎるので一部削った
アイテムの値の更新
projectId,itemId,fieldIdをそれぞれこれまで取得したIDで置き換え更新する
$ gh api graphql -f query='
mutation {
updateProjectNextItemField(
input: {
projectId: "PN_xxxxxxxxxxxxxxxxx"
itemId: "ITEM_ID"
fieldId: "FIELD_ID"
value: "2021-05-01"
}
) {
projectNextItem {
id
}
}
}'
過去のissueで移行対象Issueのリストを取得
本当はProjectにひも付いていないIssueのリストなどをスマートに取得したかったがプロジェクトにひも付いている/ついていない情報を取得する術を見つけられなかった
なのですでにcloseしたissue, milestoneごとにissueを取得していく(個人単位であればこの単位で上限超える事がないと判断したため)
- GitHub CLIで取得する
gh issue list --json number,title,url,milestone,closed,id --limit 50 --search "repo:swfz/hoge is:closed milestone:2021-11"
--jsonで指定したカラムが表示される
[ { "closed": true, "id": "I_xxxxxxxxxxx", "milestone": { "number": 49, "title": "2021-11", "description": "2021-11", "dueOn": "2021-11-30T00:00:00Z" }, "number": 1720, "title": "[11月] ブログ週1ペースで更新", "url": "https://github.com/swfz/hoge/issues/1720" }, ..... ..... ..... ]
CLIでこういう情報を取得できるのですごい楽だった
ZenHubでのデータを取得
ZenHubのAPIドキュメントは下記
ZenHubIO/API: Learn how to use ZenHub's API. github.com
ここではIssueのStoryPoint(Estimate)を取得したい
パスは/p1/repositories/:repo_id/issues/:issue_number
事前にZenHubのAPI Tokenは取得しておく
$ export ZENHUB_REPO_ID=xxxxxxxx
$ curl -XGET https://api.zenhub.io/p1/repositories/${ZENHUB_REPO_ID}/issues/1717 \
-H "X-Authentication-Token: ${ZENHUB_TOKEN}" \
-H "Content-Type: application/json"
{"plus_ones":[],"estimate":{"value":3},"is_epic":false,"pipelines":[{"name":"In Progress","pipeline_id":"xxxxxxxxx","workspace_id":"xxxxx"},{"name":"Product Backlog","pipeline_id":"yyyyyyyyyy","workspace_id":"yyyyy"}],"pipeline":{"name":"In Progress","pipeline_id":"xxxxxxxxxx","workspace_id":"xxxxx"}}
StoryPointは.estimete.valueで取得できそう
ProjectItemのstatus更新
Fieldリストで取得したjsonから該当カラムのsettingsの中身でDoneのIDをメモっておきこれをmutationで使用する
- GraphQLのクエリを一部抜粋
updateStatus: updateProjectNextItemField( input: { projectId: $project_id itemId: $item_id fieldId: "FIELD_ID_XXXXX2" value: "status_id_xxxxx3" } ) { projectNextItem { id } }
こんな感じ
projectId, itemId, fieldIdの3つを指定して対象を特定、valueで値を指定して更新する
セレクトボックスカラムの値はこのパターンで更新できる
- 参考
APIを使ったプロジェクト(ベータ)の管理 - GitHub Docs
移行時のスクリプト
直書きしている値がそれなりにあるので参考程度だが一応載せておく
- migrate_zenhub2projects.sh
#!/bin/bash
PROJECT_ID=PN_xxxxxxxxxxxxx
ZENHUB_REPO_ID=xxxxxxxx
FIELD_MONTH_ID=XXXXX
FIELD_POINT_ID=YYYYY
target_month=$1
target_month_value="$1-01T00:00:00"
target_issues=$(gh issue list --json number,title,url,milestone,closed,id --limit 50 --search "repo:swfz/hoge is:closed milestone:"${target_month}"")
echo ${target_issues} \
| tr -d '[:cntrl:]' \
| jq -cr '.[]' \
| while read -r line; do
sleep 2
echo '=========='
echo ${line}
echo '=========='
issue_number=$(echo ${line} | jq -cr '.number')
issue_node_id=$(echo ${line} | jq -cr '.id')
zenhub_issue=$(curl -XGET https://api.zenhub.io/p1/repositories/${ZENHUB_REPO_ID}/issues/${issue_number} -H "X-Authentication-Token: ${ZENHUB_TOKEN}" -H "Content-Type: application/json")
echo ${zenhub_issue}
estimate=$(echo ${zenhub_issue} | jq -cr '.estimate.value' | head -c -1)
echo ${estimate}
item_id=$(gh api graphql -f query='
mutation($project_id: String! $node_id: String!) {
addProjectNextItem(input: {projectId: $project_id contentId: $node_id}) {
projectNextItem {
id
}
}
}' -f project_id=${PROJECT_ID} -f node_id=${issue_node_id} -q '.data.addProjectNextItem.projectNextItem.id')
gh api graphql -f query='
mutation($project_id: String! $point_value: String! $point_id: String! $month_id: String! $item_id: String! $month_value: String!) {
updateMonth: updateProjectNextItemField(
input: {
projectId: $project_id
itemId: $item_id
fieldId: $month_id
value: $month_value
}
) {
projectNextItem {
id
}
}
updateStatus: updateProjectNextItemField(
input: {
projectId: $project_id
itemId: $item_id
fieldId: "FIELD_ID_XXXXX2"
value: "status_id_xxxxx3"
}
) {
projectNextItem {
id
}
}
updatePoint: updateProjectNextItemField(
input: {
projectId: $project_id
itemId: $item_id
fieldId: $point_id
value: $point_value
}
) {
projectNextItem {
id
}
}
}' -f project_id=${PROJECT_ID} -f point_value=${estimate} -f point_id=${FIELD_POINT_ID} -f month_id=${FIELD_MONTH_ID} -f item_id=${item_id} -f month_value=${target_month_value}
done
これをmilestoneごとに実行して4年分位のIssueに対して
- ポイントを移行した
- Projectsにひもづけた
- StatusはDoneとした
$ sh migrate_zenhub2projects.sh 2021-12 $ sh migrate_zenhub2projects.sh 2021-11 ..... ..... .....

上記は使用イメージ
まとめ
- EpicはIssueのタスクリストで代用
- ZenHubでのEstimateはPointというカラムを作成し移行
- MilestoneはMonthというカラムを作成し移行
- Milestoneの使い方がProjectsでいうiterationと同じだがiterationは過去の日付指定を行えなかった(2022-02-13現在)
- 移行に際して過去のデータも活用したいので新たにカラム(Month)を用意した
- レポートはAPIの結果をBigQueryへインポートしDataPortalで可視化
- まだ自分のところにはInsightsが来ていない、はやくInsights使えるようになってほしい
調べていく中で、GitHub CLIがかなり便利ということがわかった
APIたたくのと変わらない感じで色々データを出せるので楽
移行後、今のところ快適に使えている