週報を手書きするのが面倒だった
チームの週次活動報告を書くとき、GitHub Enterprise上のPRを手作業で集めていました。誰が何件マージしたか、どのissueに紐づいているか、テーマ別にグルーピングして…。毎週30分ほどかかる作業です。
この作業をbashスクリプト2本で自動化しました。GraphQL APIでPRデータを取得し、claude -p(Claude CLIのheadlessモード)でテーマ別の週報markdownに変換します。
./pr-report.sh -u "atani,alice,bob" -s "1 week ago"
これだけで、テーマ別にグルーピングされた週報markdownが標準出力に出てきます。
構成
ツールは2つのスクリプトで構成されています。
- pr-stats.sh — PRデータの取得・集計・可視化を行うメインスクリプト
- pr-report.sh — pr-stats.shの出力をclaude -pに渡して週報markdownを生成するラッパー
依存するのはbash、jq、gh CLI、claude CLIの4つだけです。
pr-stats.sh(データ取得・集計)
├─ GitHub GraphQL API → PRデータ取得
├─ jq → 集計・整形
└─ ~/.cache/pr-stats/ → キャッシュ(TTL 1日)
pr-report.sh(週報生成)
├─ pr-stats.sh -R → JSON出力
└─ claude -p → テーマ別markdown生成
データ取得:GraphQL APIで全PRを一括取得
REST APIでPRを1件ずつ取得すると、リポジトリの数だけリクエストが必要です。GraphQL APIなら組織横断で一括検索できます。
{
search(
query: "org:myorg is:pr is:merged merged:>=2026-03-04 author:atani author:alice"
type: ISSUE
first: 100
) {
issueCount
pageInfo { hasNextPage endCursor }
nodes {
... on PullRequest {
title
url
mergedAt
author { login }
repository { name }
additions
deletions
body
}
}
}
}
ポイントは search クエリの query パラメータです。org: で組織、author: で複数ユーザー、merged: で期間を指定できます。ページネーション対応で100件ずつ取得し、全件をJSONファイルにキャッシュします。
キャッシュの仕組み
ユーザーリスト・開始日・終了日からMD5ハッシュを生成し、キャッシュのキーにしています。
CACHE_KEY=$(echo "${USERS}-${SINCE}-${UNTIL}" | md5sum | cut -d' ' -f1)
CACHE_FILE="${HOME}/.cache/pr-stats/${CACHE_KEY}.json"
TTLは1日。同じパラメータで再実行するとキャッシュから読むため、APIリクエストなしで結果が返ります。-c オプションでキャッシュをクリアできます。
集計モード:日別・月別・四半期別
取得したデータはjqで集計します。-g オプションで集計単位を切り替えます。
# 月別統計
./pr-stats.sh -u "atani,alice,bob" -s 2026-01-01 -g monthly
# 日別統計(土日除外)
./pr-stats.sh -u atani -s "2週間前" -g daily -w
# 変更規模のグラフ表示
./pr-stats.sh -u atani -s "1ヶ月前" -g monthly -G
相対日付は日本語にも対応しています。「2週間前」「3ヶ月前」「昨日」をそのまま渡せます。macOSの date -v とLinuxの date -d の違いを内部で吸収しています。
normalize_date() {
case "$input" in
today|今日) num=0; unit="days" ;;
yesterday|昨日) num=1; unit="days" ;;
*) # "2週間前" → num=2, unit=weeks
if [[ "$input" =~ ^([0-9]+)週間前$ ]]; then
num="${BASH_REMATCH[1]}"; unit="weeks"
fi ;;
esac
# macOS / Linux で分岐
if [[ "$(uname)" == "Darwin" ]]; then
date -v -"${num}"w +%F
else
date -d "-${num} week" +%F
fi
}
匿名化モード:チーム外に見せる資料用
-a オプションで匿名化できます。自分(-m で指定)はそのまま表示し、PRマージ数が自分より多い人は「SENSEI」、それ以外は「Engineer_A」「Engineer_B」として表示します。
./pr-stats.sh -u "atani,alice,bob,charlie" -m atani -a -g monthly
事業部向けの報告で「チーム内の具体的な個人名は出さないけど、自分の位置は把握したい」というケースで使います。
週報生成:claude -p でテーマ別にグルーピング
pr-report.sh の核心は、PRデータのJSONを claude -p に渡す部分です。
REPORT_JSON=$("${SCRIPT_DIR}/pr-stats.sh" -R "$@")
cat <<PROMPT_EOF | claude -p
以下のPRデータ(JSON)をテーマ別にグルーピングし、
事業部向けの週次活動報告をmarkdown形式で作成してください。
## グルーピングルール
- 同じissue URLに紐づくPRは同じグループにまとめる
- 同じ目的で複数リポジトリにまたがるPRはまとめる
- Revert / Revert of Revert は元のPRと同じグループに
PRデータ(${PR_COUNT}件):
${REPORT_JSON}
PROMPT_EOF
-R オプションで出力されるJSONには、PRのタイトル・URL・リポジトリ名に加えて、PR本文から抽出したissue URLも含まれています。これにより、claude -pが「このPRとこのPRは同じissueに紐づいている」と判断できます。
なぜclaude -pなのか
jqだけでもPR一覧は作れます。テーマ別のグルーピングが難しいのです。
「DBスキーマ変更」「API層の修正」「管理画面の対応」が別リポジトリのPRとして存在していても、同じissueに紐づいていれば1つのテーマとしてまとめたい。タイトルの類似度判定やissueベースのグルーピングロジックをjqで実装するのも現実的ではありません。
claude -pに自然言語でグルーピングルールを渡すほうが、実装コスト・保守コストともに低いという判断です。
出力例
# 週次活動報告(2026/03/04 - 2026/03/10)
## 不要テーブル削除
myapp, myapp-api, myapp-web, myapp-admin, myapp-batch, myapp-schema
去年1年間アクセスがなかったテーブルに関連するコードとスキーマを削除。
6リポジトリにまたがる横断対応。
-

https://git.example.com/myorg/myapp/issues/7328(...)
## DB接続エラーの診断改善
myapp
DB接続エラーを検知して警告表示する診断機能を追加。
-

https://git.example.com/myorg/myapp/pull/1837(...)
## サマリ
3名で12件のPRをマージ。主な成果は技術的負債の解消と運用改善。
bashスクリプトで完結させた理由
PythonやNode.jsで書くこともできましたが、bashを選びました。
- 依存が少ない — gh CLIとjqはほぼどの開発マシンにもある
- パイプで繋げる —
pr-stats.sh | jq '.[]' | ...のように他のツールと組み合わせやすい - メンテナンスが楽 — 600行程度のスクリプト1本で完結している
claude -pの部分だけが外部依存ですが、ここをなくすとただのPR一覧になります。テーマ別グルーピングという「判断が必要な部分」だけをLLMに任せるのがポイントです。
まとめ
- GraphQL APIで組織横断のPRデータを一括取得
- jqで集計・整形し、キャッシュで高速化
- claude -pでテーマ別グルーピングと週報markdown生成
- 匿名化や相対日付(日本語対応)など、実用的なオプション
bashスクリプトで完結する軽量さと、LLMによるグルーピングの柔軟さを両立できた構成です。週報のような「データ収集は機械的、整理は判断が必要」なタスクには、この組み合わせが向いています。

コメント