notebook

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

ZenHubでの個人タスク管理をGitHubProjects(beta)へ移行した

ZenHubで個人タスクを管理していたがGitHubProjects(beta)が良さそうだったので移行することにした

移行に際して、一部過去の情報も含めて移行したいのでそこまでやったので作業ログ的に残しておく

ZenHubをフル活用できているわけでもなかったので割とすんなりいけたはず

移行を決めた理由

  • Notionと割と近い使い心地
    • カラムを自由に設定でき、自由度が高い
    • ビューをカスタマイズして設定を保存できる
      • カンバン
      • リスト
  • 複数リポジトリを横断したIssue/PR管理ができる
    • ZenHubでも同じような機能があって使っていた

      • しかし、1つでも管理対象にしたいIssueがある場合対象リポジトリをZenHubで管理できるようにする必要があったため結構面倒になった記憶がある
    • 結局よく使うメインのリポジトリにIssueを作成して他リポジトリのタスクを管理する、もしくは他リポジトリへのIssueリンクで済ますっていう感じになってしまった

      • ここがどうしても使い心地としてあまり良くなくて決め手になったと言って良いかもしれない

ZenHubで使っていた機能

下記を主に使っていた

  • Epic
  • StoryPoint
  • レポート
    • burndown
    • velocity tracking

Epic

プロジェクト(ベータ)の管理のベストプラクティス - GitHub Docs

docs.github.com

上記をみるとZehHubでいうEpicは

  • Issueでタスクリストとして扱う
    • - [ ] hogeでリスト化
  • マイルストーンで管理する
  • ラベルで管理する

が代替としてはありそう

マイルストーンはすでに使っているのでタスクリストで代替していくことにした

ひも付け時はEpic的に扱うIssueを編集しないといけないのでちょっと面倒だなーというくらい

今のところそんなに不便と感じていない

過去にEpicを使ってひも付けていたIssueはEpicを精査して必要であれば上記管理方法で管理できるようEpicのIssue内容に追記した

StoryPoint(estimate)

目安程度で使っていた、主に毎月どの位のポイント量を消化しているのかを把握するためにある程度感覚で付けていた

ProjectItemでPointというカラムを用意し、そちらを使うことにした

こんな感じ

f:id:swfz:20220217194945p:plain

後述するが今まで設定していたStoryPointも参考にしたいので書き捨てスクリプトを書いてZenHubのStoryPointからProjectsのPointへデータを移行した

レポート

以前はburndownとvelocity trackingのレポートを見ていた

この辺はGitHubのAPIを使ってBigQueryまで入れてしまえば後はなんとでもできる思っていたのでそんなに心配してなかった

もうInsightsが来るようなので必要ないかもしれないが…

実際にBigQueryへデータ入れ込んで表示させてみた(velocity trackingはサクッとできた)

f:id:swfz:20220217194950p:plain

Milestone

ZenHubの機能ではないが、GitHubのMilestoneを月ごとに作成して集計を行う単位としていた

メインのメモ用リポジトリだけの設定だったのでこの機会にProjects上で新たにカラムを作ってそちらで管理するように変更した(Month)

今のところメインのリポジトリでは今まで通りMilestoneも作成している

Iterationでも良いのでは?

と思ったがIterationは過去のものを作成できなかったので用途的に合わなかった

移行後の所感

  • とりあえず的なものでもIssueを作成せずアイテムだけ追加できる
    • IssueへのConvertもさっと行えるので楽

f:id:swfz:20220217194957p:plain

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

f:id:swfz:20220217195002p:plain

  • リストのソート順をlast updatedで行えないのは不便(2022-02-13現在、Issueは起票されていたのを見たので対応されるかも?)

移行作業

以下は移行にあたって調べたり試したりしたことのメモなので興味があれば読み進めていただければ…

方針が決まってしまえばあとはスクリプト書いて既存のZenHubのデータをGitHub Projects(beta)のデータに変換するだけ

大まかには下記の流れ

  • 既存のZenHubIssue,Epicを取得
    • ZenHubのIssueはGitHubのIssueにひもづけて追加情報を保存している感じなのでEpic,Issueともに取得する
  • ProjectsのItemにひも付け
    • StoryPointを取得→Pointというカラムへ
    • Milestoneを取得→Monthというカラムへ

という感じ

調べていく中でGitHub CLIを使ってapiが使えるのを知った

これがめちゃくちゃ便利だった

これもうローカルでゴニョゴニョやるにはPERSONAL ACCESS TOKEN必要ないやつでは?というくらい良かった

主に下記のドキュメントをもとに進めていく

APIを使ったプロジェクト(ベータ)の管理 - GitHub Docs

docs.github.com

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
.....
.....
.....

f:id:swfz:20220217195007p:plain

上記は使用イメージ

まとめ

  • EpicはIssueのタスクリストで代用
  • ZenHubでのEstimateはPointというカラムを作成し移行
  • MilestoneはMonthというカラムを作成し移行
    • Milestoneの使い方がProjectsでいうiterationと同じだがiterationは過去の日付指定を行えなかった(2022-02-13現在)
    • 移行に際して過去のデータも活用したいので新たにカラム(Month)を用意した
  • レポートはAPIの結果をBigQueryへインポートしDataPortalで可視化
    • まだ自分のところにはInsightsが来ていない、はやくInsights使えるようになってほしい

調べていく中で、GitHub CLIがかなり便利ということがわかった

APIたたくのと変わらない感じで色々データを出せるので楽

移行後、今のところ快適に使えている