Measuring trunk-based adoption from Git history Jump to heading
Teams describe themselves as trunk-based because they have one main branch and use pull requests. Whether they actually practise it shows in the numbers: how long branches live, how often each person integrates, how large changes are, how often trunk is broken. Those numbers are already recorded in Git history and pull request metadata. Measuring them turns βwe do trunk-based developmentβ from a label into something you can watch improve β and shows which practice to work on next, because the metrics usually reveal one bottleneck rather than a general shortfall. This page computes five indicators from data you already have, within trunk-based development setup.
When to use this approach Jump to heading
- Your team is adopting trunk-based development and wants evidence of progress.
- Opinions differ on whether branches are short or merges frequent enough.
- You want to find the main obstacle β reviews, CI, slicing β with data.
- You already measure CI duration, as in measuring pipeline duration trends.
Step 1 β Choose a small set of indicators Jump to heading
Five indicators cover the practice without turning it into a dashboard nobody reads.
Step 2 β Branch lifetime and change size from pull requests Jump to heading
Pull request metadata gives both: time from creation to merge, and lines changed.
gh pr list --state merged --limit 500 --search "merged:>=$(date -d '30 days ago' +%F)" \
--json createdAt,mergedAt,additions,deletions \
--jq '[.[] | {h: (((.mergedAt|fromdateiso8601) - (.createdAt|fromdateiso8601))/3600),
size: (.additions + .deletions)}] |
(map(.h) | sort) as $h | (map(.size) | sort) as $s |
"median lifetime \($h[length/2|floor]|floor) h, p90 \($h[(length*0.9)|floor]|floor) h; median size \($s[length/2|floor]) lines"' Creation-to-merge undercounts lifetime when people work locally for days before opening a pull request. For a closer measure, use the first commitβs author date on the branch.
Step 3 β Integration frequency from trunk history Jump to heading
Count merges to main per active author per working day. Trunk-based teams typically integrate at least daily.
since=$(date -d '30 days ago' +%F)
merges=$(git log --first-parent --since="$since" --format='%ae' origin/main | wc -l)
authors=$(git log --since="$since" --no-merges --format='%ae' origin/main | sort -u | wc -l)
days=$(( 30 * 5 / 7 ))
echo "integration frequency: $(echo "scale=2; $merges / $authors / $days" | bc) merges per author per working day" With squash merging, use git log --first-parent --format='%an' on main, where each commit is one pull request. With merge commits, the mergeβs second-parent author is the contributor.
Step 4 β Trunk green rate and revert rate Jump to heading
Green rate comes from CI runs on main; revert rate from commit messages on main.
gh run list --branch main --workflow ci.yml --limit 300 --json conclusion \
--jq '(map(select(.conclusion=="success"))|length) as $g | "trunk green: \(100*$g/length|floor)%"'
total=$(git log --first-parent --since="$since" --oneline origin/main | wc -l)
reverts=$(git log --first-parent --since="$since" --oneline --grep='^Revert ' origin/main | wc -l)
echo "revert rate: $(( 100 * reverts / total ))%" A low revert rate is not automatically good: a team that never reverts may be fixing forward slowly while trunk stays red. Read it together with green rate, as described in keeping trunk green.
Step 5 β Read the indicators together Jump to heading
Each indicator points to a different practice. Long branches with small changes suggest reviews are slow; long branches with large changes suggest work is not sliced; short branches with a low green rate suggest checks are not testing the merged result.
Review latency is covered in measuring review latency from Git history, and slicing in keeping branches short-lived in trunk-based development.
Step 6 β Track monthly and share the trend Jump to heading
Run the script monthly and keep the results in a small table in the repository or team wiki. The trend matters more than any single value, and sharing it keeps the conversation about practices rather than individuals.
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$(date +%Y-%m)" "$lifetime_h" "$freq" "$size" "$green" "$revert" >> docs/trunk-metrics.tsv Step 7 β Break the numbers down by area, not by person Jump to heading
In a large repository, one area can hold the averages back β a legacy service with long-lived branches, say. Break the indicators down by top-level directory to find where to focus, and keep them aggregated above the level of individuals.
# Median PR lifetime per top-level area, last 90 days
gh pr list --state merged --limit 1000 --search "merged:>=$(date -d '90 days ago' +%F)" \
--json createdAt,mergedAt,files \
--jq '.[] | "\(.files[0].path | split("/")[0])\t\(((.mergedAt|fromdateiso8601) - (.createdAt|fromdateiso8601))/3600|floor)"' |
sort | awk -F'\t' '{v[$1]=v[$1] " " $2} END {for (a in v) {n=split(v[a], x, " "); print a, "PRs:", n}}' Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Are these the DORA metrics? Jump to heading
They overlap: deployment frequency and change failure rate correspond to integration frequency and revert rate when deploys follow merges. The indicators here focus on the branching practice itself, which drives the DORA outcomes.
Can the numbers be gamed? Jump to heading
Any single one can β splitting pull requests artificially shortens lifetime. Looking at all five together, and talking about them as a team, makes gaming pointless.
What targets should we set? Jump to heading
Use your own trend rather than external targets. Commonly reported trunk-based teams merge at least daily per person with branches under a day, but the useful goal is steady improvement on whichever indicator is your bottleneck.
Related Jump to heading
- Trunk-Based Development Setup β the parent topic.
- Measuring Branch Age Against Conflict Risk β why lifetime matters.
- Limiting Feature Branch Lifetime β acting on the lifetime indicator.
- Iterating Refs with for-each-ref β reading branch data reliably.