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.

Five trunk-based indicatorsBranch lifetime measures how long work stays off trunk. Integration frequency measures how often each person merges. Change size measures how much each merge carries. Trunk green rate measures whether trunk stays usable. Revert rate measures how often merged changes have to be undone.Branch lifetimehours openIntegration freq.merges/person/dayChange sizelines per PRTrunk green% green runsRevert rate% merges revertedlook at all five together β€” any one can be gamed alone

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.

What the indicators point toIf branches are long but changes are small, review latency is the bottleneck. If branches are long and changes are large, work needs slicing behind flags. If branches are short but trunk is often red, merge-result testing and a merge queue are missing.Which pattern do the numbers show?long branches, small PRsFix review latencytargets, rotationlong branches, large PRsSlice the workflags, abstractionshort branches, red trunkTest merge resultsmerge queueone bottleneck usually dominates β€” fix it before the others

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
Median branch lifetime over a year of adoptionAn illustrative team adopting trunk-based development. Median pull request lifetime starts at several days, drops sharply after review targets are introduced, and falls further once features are routinely sliced behind flags.median hours from PR open to merge (illustrative)Q171 hQ2, review targets30 hQ3, slicing + flags14 hQ49 heach drop lines up with one practice change β€” that is the value of measuring

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.