Claude Media
Claude CodeでGitHub Projectsの進捗を集計し残作業を報告

Claude CodeでGitHub Projectsの進捗を集計し残作業を報告

GitHub ProjectsのステータスとイテレーションをGraphQL APIで全件取得し、マイルストーンごとの残作業を集計して、顧客に送れる文面にする手順です。

GitHub Projectsの進捗は、画面を見れば分かります。ただ、顧客や上長に「v1.0まであと何が残っているか」を週ごとに報告するとなると、画面のスクリーンショットでは足りません。内部のIssue名や担当者名が写り込み、数字も毎回手で数え直すことになります。

この記事では、GraphQL APIでProjectsのアイテムを全件取り、マイルストーンごとの残作業をスクリプトで数えたうえで、Claude Codeに顧客向けの文面を書かせる流れを組みます。数える作業は決まった手順のスクリプトに、言い換えと抜け漏れの指摘はClaude Codeに分けるのが要点です。

全体像: 取得と集計はスクリプト、文面だけをClaudeに任せる

件数の集計までモデルに任せると、数え間違いの余地が残ります。そこで、数字が決まるところまでをghとjqで固定します。

手順

週次報告までの5段

  1. 1

    トークンを用意する

    読み取り専用のスコープだけを持つトークンで認証します。

  2. 2

    プロジェクトのIDを調べる

    組織名とプロジェクト番号から、GraphQLのノードIDを引きます。

  3. 3

    アイテムを全件取る

    100件ずつページを送り、ステータス・イテレーション・マイルストーンの値を付けて受け取ります。

  4. 4

    1行1アイテムの表に整える

    jqでフィールド名をキーにした形にそろえます。

  5. 5

    集計してからClaudeに渡す

    マイルストーンごとの残件数を出し、集計結果だけをclaude -pに渡します。

取得用のトークンはread:projectに絞る

GitHubの手順では、クエリにはread:projectスコープ、ミューテーションにはprojectスコープが要ります。トークンは個人アクセストークン(classic)でも、GitHub Appのインストールアクセストークンでもかまいません。ghを使うなら、読み取りだけの場合は次のように認証します。

gh auth login --scopes "read:project"

スコープが足りないままProjectsのフィールドを引くと、gh 2.90.0では次の形のエラーが返ります。メッセージは'number' field requires one of the following scopes: ['read:project']のように、どのフィールドがどのスコープを要求したかまで教えてくれます。repoやread:orgだけのトークンでは通らない点が、最初のつまずきどころです。

読み取り専用のトークンを使う利点は、もう一つあります。ここから先でClaude Codeにコマンドの実行を任せる場面があっても、アイテムの追加や削除といった書き込みのミューテーションはスコープの時点で通りません。

プロジェクトのIDとフィールドを調べる

更新系の操作ではフィールドのIDとオプションのIDが要りますが、今回のような読み取りでは、必要なのはプロジェクトのノードIDだけです。値の側にフィールド名が付いてくるので、名前で絞り込めます。

組織のプロジェクトは、組織名と番号で引きます。

gh api graphql -f query='
  query($org: String!, $number: Int!) {
    organization(login: $org) {
      projectV2(number: $number) { id title }
    }
  }' -f org=octo-org -F number=5 --jq '.data.organization.projectV2'

ユーザーのプロジェクトならorganization(login: ...)をuser(login: ...)に替えます。番号はプロジェクトのURL末尾の数字です。

プロジェクトにどんなフィールドがあるかは、fields(first: 20)で一覧できます。ステータスは単一選択、イテレーションはイテレーション型で返り、選択肢の名前やイテレーションの開始日も一緒に取れます。報告の文面に使う「Done」などの名前が、プロジェクトごとに違うことが多いので、ここで実際の名前を控えておきます。

アイテムを100件ずつ全件取る

次のクエリをquery.graphqlとして保存します。公式の例ではアイテムを20件、フィールド値を8件までしか取っていません。Projectsは組み込みを含めて最大50フィールドを持てるので、フィールド値は50件まで取る設定にします。

query($project: ID!, $endCursor: String) {
  node(id: $project) {
    ... on ProjectV2 {
      items(first: 100, after: $endCursor) {
        pageInfo { hasNextPage endCursor }
        nodes {
          isArchived
          fieldValues(first: 50) {
            nodes {
              ... on ProjectV2ItemFieldSingleSelectValue {
                name field { ... on ProjectV2FieldCommon { name } }
              }
              ... on ProjectV2ItemFieldIterationValue {
                title startDate duration
                field { ... on ProjectV2FieldCommon { name } }
              }
              ... on ProjectV2ItemFieldNumberValue {
                number field { ... on ProjectV2FieldCommon { name } }
              }
              ... on ProjectV2ItemFieldMilestoneValue {
                milestone { title dueOn }
                field { ... on ProjectV2FieldCommon { name } }
              }
            }
          }
          content {
            ... on Issue { title url state }
            ... on PullRequest { title url state }
            ... on DraftIssue { title }
          }
        }
      }
    }
  }
}

フィールドの種類ごとに、取り出す断片(フラグメント)が違います。

フィールドの種類型名値の取り方
単一選択(Status)型名ProjectV2ItemFieldSingleSelectValue値の取り方name
イテレーション型名ProjectV2ItemFieldIterationValue値の取り方title、startDate、duration
数値(見積もり)型名ProjectV2ItemFieldNumberValue値の取り方number
マイルストーン型名ProjectV2ItemFieldMilestoneValue値の取り方milestone { title dueOn }

公式の例に載っているのはテキスト・日付・単一選択の断片です。イテレーション・数値・マイルストーンの型は、スキーマ照会の__type(name: "...")で持っているフィールドを引いて決めました。gh api graphql -f query='{__type(name:"ProjectV2ItemFieldIterationValue"){fields{name}}}'のように試せるので、手元の環境で型名を確かめたいときにも使えます。

全ページを一度に取るには、--paginateと--slurpを併用します。ghは、クエリが$endCursor: String変数を受け取り、pageInfo { hasNextPage endCursor }を取っていれば、次のページを自動でたどります。--slurpを付けると、全ページが1つの配列にまとまります。

PROJECT_ID=$(gh api graphql -f query='query { organization(login: "octo-org") {
  projectV2(number: 5) { id } } }' --jq '.data.organization.projectV2.id')
 
gh api graphql --paginate --slurp \
  -f query="$(cat query.graphql)" \
  -f project="$PROJECT_ID" > out/pages.json

GraphQLには、取得量の決まりがあります。firstとlastは1〜100の範囲で必ず指定し、1回の呼び出しで要求できるノードは合計50万までです。1ページ100件、フィールド値50件なら、1回に要求するノード数は5,000件ほどで、上限には遠く届きません。一方、レート制限はポイント制で、ユーザーのトークンなら1時間あたり5,000ポイントです。週1回の集計なら問題になりませんが、数百のプロジェクトを回すスクリプトは別です。

ステータス・イテレーション・マイルストーンを1行にそろえる

取得した結果は、フィールド値が配列で入れ子になっています。集計しやすいよう、フィールド名をキーにした1行1アイテムの形に変えます。rows.jqとして保存します。

[ .[].data.node.items.nodes[]
  | select(.isArchived | not)
  | { title: .content.title,
      url: .content.url,
      fields: ( [ .fieldValues.nodes[] | select(.field.name)
                  | { key: .field.name,
                      value: (.milestone.title // .title // .number // .name) } ]
                | from_entries ) } ]
jq -f rows.jq out/pages.json > out/rows.json

この変換を、2ページにまたがる擬似データ(進行中・完了・未着手・アーカイブ済みの4件)で動かすと、アーカイブ済みが落ち、各行が{"Status": "In Progress", "Iteration": "Sprint 4", "Estimate": 3, "Milestone": "v1.0"}のような形にそろいました。テキスト型のフィールドは断片を用意していないので、その値は行に入りません。報告に使いたいなら、ProjectV2ItemFieldTextValueの断片を足します。

フィールド名を変えたとき、たとえばStatusを「ステータス」に直したときは、rows.jqの後ろで使う名前も合わせます。

マイルストーンごとの残作業を数える

次のagg.jqは、マイルストーンごとの合計・完了・残り・残りの見積もり合計を出します。「完了」はStatusがDoneのものとし、それ以外とStatus未設定を残りに数えます。

group_by(.fields.Milestone // "(未設定)")
| map({ milestone: (.[0].fields.Milestone // "(未設定)"),
        total: length,
        done: (map(select(.fields.Status == "Done")) | length),
        remaining: (map(select(.fields.Status != "Done")) | length),
        estimate_left: (map(select(.fields.Status != "Done")
                            | .fields.Estimate // 0) | add) })
jq -f agg.jq out/rows.json > out/summary.json
jq -r '.[] | select(.fields.Status != "Done")
  | "- \(.title) [\(.fields.Status // "未設定")]"' out/rows.json > out/open.txt

先ほどの擬似データでは、v1.0が合計1件・完了0件・残り1件(見積もり3)、マイルストーン未設定が1件と出ました。数字が決まるのはここまでです。summary.jsonとopen.txtが、Claudeに渡す材料になります。

Claude Codeで顧客向けの文面にする

材料をパイプで渡せば、Claudeに実行権限を与えなくても文面にできます。公式のヘッドレス実行の説明にも、差分をパイプで渡す例があり、「パイプで渡せばClaudeにBashの権限は要らない」と書かれています。GitHubへのアクセスはスクリプト側に閉じ、モデルにはトークンが渡りません。

顧客向けの変換ルールは、毎回プロンプトに書かずにファイルへ置きます。

# rules.md
- 読み手はプロジェクトの発注側の担当者。社内のIssue番号、担当者名、ブランチ名は書かない
- 件数は summary.json の値をそのまま使う。自分で数え直さず、合計が合わなければ指摘する
- 残作業は「何ができていないか」を顧客の言葉で1行にする。Statusの名前(In Progressなど)は使わない
- マイルストーン未設定の項目は、別枠で「期日未定の作業」として件数だけ書く
- 見積もりの合計は「目安」と明記する。工数の約束に見えないようにする
cat rules.md out/summary.json out/open.txt | claude -p \
  "上のルールに従い、マイルストーンごとの残作業の報告文を書いてください。\
ルールに反する材料があれば、本文の前に列挙してください"

出力をそのまま顧客へ送らない運用にしておくと安全です。--output-format jsonを付けると、resultに文面、total_cost_usdに概算費用が入るので、スクリプトから拾えます。--json-schemaで{"report": ..., "problems": [...]}の形を指定すれば、問題点の一覧を機械的に取り出せます。その場合、検証に通らなかった実行はstructured_outputが付かないため、使う前に有無を調べます。

claude -pを週次で回すと使用量の面でも気になる点があるので、5時間枠への影響はclaude -pが5時間枠を対話の約1.8倍消費する報告で計測方法を整理しています。リリース時の顧客向け文面については、Claudeで顧客向けリリースノートを書くと共通する考え方が多く、社内用語の除去ルールはそちらのほうが詳しい内容です。

対話の中で頼む場合は、スクリプトだけを許可する

Claude Codeのセッションで「進捗を取ってきて」と頼む使い方もあります。このときBash(gh api graphql *)を許可すると、同じコマンドでミューテーションも通ってしまいます。許可規則は*より前の文字列で照合されるので、読み取り専用のクエリだけを許したことにはなりません。

そこで、取得部分をscripts/fetch-project.shにまとめ、そのスクリプトだけを許可します。

{
  "permissions": {
    "allow": ["Bash(./scripts/fetch-project.sh *)"],
    "deny": ["Bash(gh api graphql *)"]
  }
}

公式の説明では、規則は拒否・確認・許可の順に評価され、広い拒否規則は狭い許可規則より優先されます。Claudeが直接打つgh api graphqlはこの拒否規則で止まり、許可されたスクリプトの実行だけが残ります。トークンのスコープをread:projectにしておけば、書き込みにはそもそもprojectスコープが要るので、規則をすり抜けた場合の備えにもなります。

数が合わないときの切り分け

集計結果が画面の数字と食い違ったら、次の順に疑います。

落とし穴

アーカイブしたアイテムを数えている、または落としている

rows.jqはselect(.isArchived | not)でアーカイブ済みを除いています。画面側のビューがアーカイブ済みを含む設定だと、数字が食い違います。顧客に見せる画面と同じ基準にそろえてから比べます。

落とし穴

アイテムの権限が足りず、中身が取れていない

プロジェクトには、閲覧権限のないアイテムが含まれることがあります。その場合のアイテム種別はREDACTEDになります。contentが空の行が目立つときは、トークンの持ち主が対象のリポジトリを読めるか確かめます。

落とし穴

リポジトリ側のマイルストーンの進捗と比べている

リポジトリのマイルストーンは、openIssueCountやprogressPercentageといった件数・進捗のフィールドを自前で持っています。Projectは複数のリポジトリや下書きIssueを含められるので、同じ名前のマイルストーンでも件数が一致しないことがあります。顧客に示す数字はどちらの基準かを先に決めます。

落とし穴

Statusの名前が「Done」ではない

Doneと書いたところを別名にしているプロジェクトでは、完了が全件残りに入ります。フィールド一覧で選択肢の名前を確かめ、agg.jqの比較を直します。

まとめ

進捗の集計で決めるのは、「完了」の基準と、顧客に見せる単位の2つです。この2つをスクリプトに固定すれば、数字は毎週同じ手順で出ます。Claude Codeに任せる範囲は、文面への言い換えと、ルールに反する材料の指摘までに絞ると、取りこぼしも誤送信も起きにくくなります。

Projectsの値を取る処理が組めたら、同じ型をほかの棚卸しにも使えます。GitHub組織のメンバー側の整理はClaude CodeでGitHub組織のメンバーを棚卸しするにあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.github.com/en/issues/planning-and-tracking-with-projects/automating-y…
  2. docs.github.com/en/graphql/overview/rate-limits-and-node-limits-for-the-gr…
  3. docs.github.com/en/graphql/guides/using-pagination-in-the-graphql-api
  4. docs.github.com/en/issues/planning-and-tracking-with-projects/learning-abo…
  5. cli.github.com/manual/gh_api
  6. code.claude.com/docs/en/headless
  7. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn