Output first
リポジトリ同梱の loc-guard.yml
の 7〜14行目の設定を適切なものにして、対象リポジトリの .github/workflows/loc-guard.yml
として放り込んでください。1ヶ月もしないうちに、Velocity が 20% くらいは改善します(かも知れない)。
効果を集計したい場合は、GitHub CLI をセットアップし、リポジトリ同梱の pr_collector.sh
を実行した結果を、スプシサンプル を参考に管理用スプシを作成し、data シートに貼り付けてください。
それだけ。
はじめに
そんなものあったら苦労しない。と思うかも知れませんが、あります。もしまだ導入していなければという話ですが。最初に結論を言ってしまうと、それは LOC Guard
です。Pull Request / Merge Request に対して、LOC (Line of Changes) のキャップを設けるというもので、いわゆる小さな Pull Request を徹底するというものです。なんだ、もうやってるという方は、素晴らしいです。どんどん先に進んでください。
Agile, Lean… に限らず、計測や定量解析というのは、非常に大切です。開発チームは「より良いものを、より速く作りたい」と思っていて、「より良いもの」に対する KPI は、サービスやビジネスに直結するものや、バグ・障害などなので、比較的取りやすく、そしてしっかり監理していることが多いですが、「より速く」にたいする KPI の Velocity を構成するサブメトリクスを管理しているチームというのは、驚くほど少ないです(私の経験上)。
理由は簡単で、計測する・記録するのが面倒くさいからです。
日報で報告させる、チケットに細かく記録させる…記録する側からすれば正直、なにこれ?おいしいの?状態で、記録するモチベーションも上がりませんし、日々コピペで正確にならなかったりします。記録コストが高いと、確実に破綻します。
そんな経験を経て、定量解析しなさすぎ問題が発生し、Agile 開発しているのに、AI を導入したのに、有効に機能しない状態に陥ります。
基本的には、DORA 4keys
、RCT
さえ管理・監視しておけば良いと思いますが、いきなり DORA 4keys
を見せられても戸惑いますので、Quick Win として、デベロッパーの記録コスト0で始められる LOC Guard
を紹介したいと思います。ものすごく効きます。
なお、「生産性」の定義ですが、本記事では、Velocity とします。Velocity は、ビジネス全体を通して見ると、ビジネス的に評価したい生産性のサブメトリクスであることは言うまでもありません。
なんでレビュー?
LOC Guard
は PR/MR の Gate で、つまり Review プロセスの話になります。ソフトウェア開発データ白書のデータから計算すると、全体プロセスの1%でしかない Review プロセスを改善したところで Velocity は絶対に上がりません。が、Visual Stream Mapping
などでプロセスごとの Lead Time を簡易的に調べてみると、実際には Reivew プロセスがボトルネック(の上位)に入っていることが多々あります。
この違いは、
- データ白書のデータは、おそらく Touch Time
- Requirement Analysis のレベルが違う
- Review の仕組み化の有無・レベルが違う
などが主な原因ではないかと思っています。平均値と中央値などもあるかも知れません。まぁチームによって様々です。いずれにしても、Reivew プロセスが問題になりがちなので、決め打ちしました。調べた結果 Reivew プロセスは問題なかったという場合でも、調べるコストもほとんどないので、その時は良かったねで。
もっと詳しく説明も可能ですが、長くなるので割愛します。
なんで LOC?
LOC(変更量)が大きければ、Review や Test が大変になるであろうというのは、直感的ですが、LOC と Reivew 時間や Lead Time の相関を調べた研究はたくさんあります。興味があれば調べてみてください。
研究論文などのサマリー
論文 / 記事 | 説明 | LOC/PR サイズとの相関に関する知見 |
---|---|---|
Learning to Predict Code Review Completion Time in Modern Code Review (SpringerLink) | コードレビュー完了時間を予測するための ML モデルを構築。レビューリクエスト 280,000 件以上を対象。 | この論文で使われている多数の特徴量のうち、差分量・変更行数(churn / diff サイズ系)も重要な因子として扱われており、レビュー時間の予測に効いているという記述あり |
Pull Request Latency Explained: An Empirical Overview (SpringerLink) | PR のレイテンシ(レビュー遅延)に影響する要因を整理・実証的に分析。 | この論文では「PR の遅延時間 (latency)」に影響する因子を系統的に整理しており、差分の大きさ(変更量)がその要因の一つに挙げられている可能性が示唆されている |
Mining Code Review Data to Understand Waiting Times Between Acceptance and Merging (arXiv) | 提案から最初の応答、承認、マージまでの待ち時間を大規模データから調査。 | 小規模な変更(行数が少ない PR)は「即時承認(review iteration が少ない)」傾向がある、という傾向を観測しており、変更量が小さいことがレビュー迅速化に資する旨の示唆あり |
An Empirical Study on Code Review Activity Prediction and Its Impact in Practice (arXiv) | レビュー待機時間 (feedback 待ち時間) の予測とその影響を定量的に調査。 | ここでも「提出する変更 (change) の性質(大きさ・複雑さ)」がレビュー遅延に影響を与える要因として議論されており、変更規模の偏りと待機時間の中位数との関係を分析している旨記載あり |
Are We Speeding Up or Slowing Down? On Temporal Aspects of Code Velocity (arXiv) | 大型 OSS プロジェクト(Blender, FreeBSD, LLVM, Mozilla)におけるコードレビュー期間の時間的傾向を分析。 | この研究では「プロジェクト規模の成長」や「コード変更量(churn)」があってもコードレビュー速度 (time-to-merge 等) が短縮または維持されているという実証結果を示していて、単純に「LOC 増 → リードタイム増」の傾向が常に成り立つとは限らない旨を述べている点が興味深い |
Does size matter in Pull Requests: Analysis on 30k Developers (Adadot Insights) | 実際の PR を多数集めて、PR サイズとレビュー時間・コメント数・失敗率などを統計分析。 | 変更行数・PR サイズがレビュー時間・コメント数と有意な相関を持つという傾向を示すデータを報告している(ただし “silver bullet” にはならない旨の注意も) |
超簡単にまとめると、多くの研究では、PR の Reivew Time や Lead Time は複合的な要因があるが、LOC は有効な特徴量のひとつであると位置づけられています。
データで検証
LOC Guard
でいきなりプルリクを落とされても、現場は混乱するだけなので、まずはデータを取得しましょう。データは、すべて Git で取れます。GitHub、GitLab、BitBucket などどれでも取れます。Git 以外は試す機会がないので分かりませんが、きっと取れると思います。デベロッパーは追加で何もしなくて良いです。Reivew にプルリク使ってない場合は、取れません。使いましょう。
今回は GitHub で説明します。
データ検証の流れは、
- 欲しいデータを決める
- API でデータを取得してみる
- Script 化する
- 集計する
です。
欲しいデータを決める
色々なデータが取れるんですが、ミニマムで PR の Lead Time と LOC が分かれば良いので、それを取ることにしましょう。PR の状態は、merged
で。
LOC は、API では直接取れないので、計算が必要で、集計時にやれば良いのですが、本記事の最終目標は、LOC Guard
の導入なので、データ取得時に計算することにします。
number
, createdAt
, mergedAt
, loc
ひとまず、これが欲しい。
API でデータを取得してみる
GitHub API の使い方は、各自調べていただくとして、REST か GraphQL かという話では、リクエスト数を考慮して、GraphQL にします。
今回使った GitHub CLI のバージョンは、2.81.0
です。
$ gh --version
gh version 2.81.0 (2025-09-30)
https://github.com/cli/cli/releases/tag/v2.81.0
ひとまず、gh auth login
で認証したあと、以下を打ってみます。
gh api graphql -f query='
query {
repository(owner:"repo owner", name:"repo name") {
pullRequests(first:3, states:MERGED, orderBy:{field:UPDATED_AT, direction:DESC}) {
nodes {
number
createdAt
mergedAt
additions
deletions
baseRefName
}
}
}
}
'
owner
, name
は適当なものに書き換えてください。よく使う OSS とかでも良いです。迷惑かけないように。
例えば、octocat/Hello-World
の場合、以下のようなデータが JSON 形式で取れます。
{
"data": {
"repository": {
"pullRequests": {
"nodes": [
{
"number": 6,
"createdAt": "2011-09-14T04:43:08Z",
"mergedAt": "2012-03-06T23:06:50Z",
"additions": 1,
"deletions": 1,
"baseRefName": "master"
}
]
}
}
}
}
mergedAt
- createdAt
で、Lead Time、additions
+ deletions
で LOC が計算できます。
baseRefName
はターゲットブランチで、release ブランチとか backup ブランチとか後々必要のないブランチをフィルターするため。
Script 化する
GraphQL では1リクエストで最大100件なので、それより多くなっても大丈夫なように、100件ごとに取得してぐるぐる回します。
owner
, name
も自動で取得するようにします。
deploy**
, release**
, main
などへの PR は、必要ないので除外します。運用に併せてフィルターしてください。
#!/usr/bin/env zsh
set -euo pipefail
REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)"
OWNER="${REPO%%/*}"
NAME="${REPO##*/}"
echo "id,created_at,merged_at,loc"
AFTER="null"
while :; do
# GraphQL クエリ:MERGED の PR を新しい順に 100 件取得
RESP="$(
gh api graphql -F owner="$OWNER" -F name="$NAME" -F after="$AFTER" -f query='
query($owner:String!, $name:String!, $after:String) {
repository(owner:$owner, name:$name) {
pullRequests(states:MERGED, orderBy:{field:UPDATED_AT, direction:DESC}, first:100, after:$after) {
pageInfo { hasNextPage endCursor }
nodes {
number
createdAt
mergedAt
baseRefName
additions
deletions
}
}
}
}
' 2>/dev/null
)"
echo "$RESP" \
| jq -r '
.data.repository.pullRequests.nodes[]
| select((.baseRefName | test("^(deploy|release|main)"; "i")) | not)
| [.number, .createdAt, .mergedAt, (.additions + .deletions)]
| @csv
' \
| sed 's/^"//; s/"$//'
HAS_NEXT="$(echo "$RESP" | jq -r '.data.repository.pullRequests.pageInfo.hasNextPage')"
if [ "$HAS_NEXT" != "true" ]; then
break
fi
AFTER="$(echo "$RESP" | jq -r '.data.repository.pullRequests.pageInfo.endCursor' | jq -Rs .)"
done
リポジトリ配下で実行し、データが取れることを確認してみてください。データが出てこないなぁという場合、ブランチ・フィルターを見直すなどしてください。
PAT
を発行し、GH_TOKEN='ghp=xxxxxxx ./pr_collector.sh
などとすると良いかも知れません。export
しても良いです。PAT は read-only
権限で十分です。
集計する
はい。データは取れたので、集計します。集計は Sprint サイズや Deploy サイクルなどに合わせて決めれば良いですが、あんまり長くてもあんまり短くても良くないので、4 週間スライドウィンドウ・金曜アンカー・2週間ごと計測くらいが良いと思います。何を言ってるかと言うと、金曜日起点(レトロスペクティブやる日)で、直近 4 週間くらいを集計し、毎週だとあんまり変化がないかもなので、2週間おきに見るということです。
集計するのは、Lead Time と LOC の中央値、90パーセンタイル値、最大値で良いと思います。スプレッドシートの関数でちゃちゃっと計算できるので、やってみてください。
サンプルのスプレッドシートを置いときます。リポジトリ1つってことは、あんまりないのかと思うので、参考に適宜やってみてください。
自分でできるもん!という方向けに、面倒なポイントとして、スプレッドシートは、2011-09-14T04:43:08Z
このような書式の日付データはそのままでは計算できないので、変換する必要があるということくらいでしょうか。
取ったデータの活用
どこかのプロジェクトから取得したサンプルデータですが、割ときれいに LOC と LT の相関が見られます。
Retrospective でみんなで眺めます。LOC がデカいと Review の Lead Time が長くなる。というのがチーム全体に共有されます。なお、ここでやってはいけないのは、犯人探しです。誰がデカい PR 出したのか?なんてのは、どうでも良いことです。犯人はひとりでドキっとしてます。
ここで、まずは中央値(median)、次に Tail の 90 パーセンタイル (p90)・最大値 (max) の順で LT が 24 時間以内に収まるように LOC を調整していきましょう。このサンプルデータで言うと、LOC の目安は、まぁだいたい 90〜100 くらいでしょうか。Front end, Back end, API などリポジトリの内容によって、適切な LOC は変わるので、それぞれに合った目標を見つけましょう。
LT-LOC を Retrospective で Review していると、
「いや、でも PR は意味的にまとまっていた方が分かりやすいじゃん」
というヤツが必ず出てきますが、分かりやすく LT が増えるので「分かりやすくみんなに失敗をシェアしてくれてありがとう」と感謝しましょう。
なお、これ、5〜10分 x 月2回程度の作業ですが、手動です。本当は自動化してしまうのが望ましいですが、大した手間でもないですし、どうせガードしてしまうので、やらなくて良いです。どうしてもやりたい人は、スプレッドシート使わずに計算させて、Slack の Metrics チャネルに投げるとかしてみてください。
LOC Guard
はい。ということで、Small PR は正義!という意識がチームに芽生えたところで、いよいよ Gate を設置しましょう。
Gate もしくは Quality Gate は、約束が守られているかチェックする Pipeline のことで、GitHub では GitHub Actions
で PR をフックしてチェックします。
Gate は、いきなり落とすのではなく、Warn
から Block
、個別から全体と徐々に締めていくように運用します。
今回は、PR の LOC を計算し、しきい値を超えていたら警告を出すような Actions を作ります。見落とさないように PR の Conversation タブに結果コメントを残します。
name: LOC Guard (Conversation)
on:
pull_request:
types: [opened, synchronize, reopened]
# ignore branches
branches-ignore:
- main
- "release/**"
- "deploy/**"
env:
LOC_THRESHOLD: "100" # Number of lines changed to trigger the guard
GUARD_LEVEL: "0" # 0 = Warn (comment only), 1 = Block (CI fail)
jobs:
loc-guard:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: LOC Guard
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
BASE_SHA="$(jq -r '.pull_request.base.sha' "$GITHUB_EVENT_PATH")"
HEAD_SHA="$(jq -r '.pull_request.head.sha' "$GITHUB_EVENT_PATH")"
PR_NUMBER="${{ github.event.pull_request.number }}"
git fetch --no-tags --depth=200 origin "$BASE_SHA" "$HEAD_SHA" >/dev/null 2>&1 || true
MERGE_BASE="$(git merge-base "$BASE_SHA" "$HEAD_SHA")"
EXCLUDES=( ':!vendor/**' ':!node_modules/**' ':!public/build/**' ':!storage/**'
':!dist/**' ':!*.min.*' ':!composer.lock' ':!package-lock.json'
':!yarn.lock' ':!pnpm-lock.yaml' )
read ADD DEL <<<"$(git diff --numstat "$MERGE_BASE" "$HEAD_SHA" -- "${EXCLUDES[@]}" | awk '{a+=$1; d+=$2} END{print a+0, d+0}')"
CHANGED=$((ADD + DEL))
if [ "$CHANGED" -ge "$LOC_THRESHOLD" ]; then
gh pr comment "$PR_NUMBER" \
--body "⚠️ **LOC Guard**: $CHANGED lines changed ( +$ADD / -$DEL ) — threshold **${LOC_THRESHOLD}** or more.
Please consider splitting the PR or reducing diff size."
if [ "${GUARD_LEVEL}" = "1" ]; then
echo "::error::LOC Guard threshold exceeded"
exit 1
fi
else
gh pr comment "$PR_NUMBER" \
--body "✅ **LOC Guard**: $CHANGED lines changed ( +$ADD / -$DEL ) — under threshold **${LOC_THRESHOLD}**."
fi
Warn で実行すると…
警告コメントは出るけど、Check は通る。
Block で実行すると…
警告コメントが出て、Check が通らない(けど、merge はできる)。
閾値を下回ると…
Check が通ったコメントが出る。
対象外ブランチ (main) で実行すると…
何もチェックされない。
はい。ちゃんとできてますね。チェックは、3秒くらいで終わりました。
No Measure, No Control
説明のために細かく書きましたが、今回の趣旨の結論は、loc-guard.yml
を GitHub Actions に放り込むだけです。
チームの混乱がない導入手順は、
- 一般論としての、プロセスごとにかかる時間の統計データを知る(ソフトウェア開発データ白書)
- 一般論としての、PCE を知る
- 一般論としての、リトルの法則(WIP)を知る
- 自分たちの実情を VSM Workshop などで大雑把に把握する
- Review がボトルネックな結果がでれば万歳
- Review の LT と LOC のデータを集計する(
pr_collector.sh
+ スプシサンプル) - Retro でシェアし、定量解析大事じゃん!な空気を作る
- PR を小さくする具体的な手法も合わせてシェアする
- 手法はケースバイケースですが、ChatGPT にでも聞いて下さい
-
LOC Guard
を warn で導入する - 慣れてきたら block にする
こんな感じで、Small PR 成功体験を演出します。
そのうえで、仮説に基づく定量解析の重要さ・威力をしつこく説き、Retro の Try にそれを評価する Metrics がついていたら狂ったように褒めちぎります。
慣れてきたら、DORA 4keys の指標を Apache DevLake などで取得して、チームに展開します。
このようにして、"No Measure, No Control" カルチャを作ります。これが真の目的です。あとは、CI を developer や QA に開き、モチベーションが高ければ、チームの生産性は勝手に上がっていくことでしょう。
10月1日より第5期が始まった TAKT R&D では、苦しみもがく企業向けに、このようなお手伝いをしています。興味があればお声がけください。チームに入ってバリバリ進めるのもやぶさかではありませんが、どちらかと言えば Board 向けです。
ハノイでもホーチミンでもダナンでも出張します。日本は…美味しい食事があるならスポットで考えます。