PR Lead Time
Measures how long PRs take to move through delivery, split into three stages, all in business hours (JST, Sat/Sun and Japanese public holidays excluded):
- PR open -> merged into the base branch (e.g.
develop) - base branch merge -> landed on the deploy branch (e.g.
main) - total: PR open -> landed on the deploy branch
Results are aggregated weekly or monthly (median, mean, N per stage) so trends can be tracked over time (e.g. for an OKR like "reduce PR lead time median by 30%").
Prerequisites
- Run from inside a git checkout of the target repo (or pass
--repo-dir). ghCLI authenticated (gh auth status) with access to the repo.- Ruby (stdlib only, no gems/Bundler needed).
Usage
ruby ~/.claude/skills/pr-lead-time/scripts/pr_lead_time.rb [options]
| Option | Default | Description |
|---|---|---|
| --base-branch NAME | develop | Branch PRs merge into (stage 1 -> stage 2 boundary) |
| --deploy-branch NAME | main | Branch that represents "deployed" |
| --since DATE | 3 months back (monthly) / 4 weeks back (weekly) | Start date, YYYY-MM-DD, inclusive |
| --until DATE | today | End date, YYYY-MM-DD, inclusive |
| --weekly / --monthly | monthly | Aggregation granularity |
| --format markdown\|json | markdown | Output format |
| --repo-dir PATH | cwd | Path to the git checkout |
Example:
cd ~/path/to/some-repo
ruby ~/.claude/skills/pr-lead-time/scripts/pr_lead_time.rb --monthly
ruby ~/.claude/skills/pr-lead-time/scripts/pr_lead_time.rb --since 2026-06-01 --until 2026-06-30 --weekly --format json
How it works
- PR list:
gh pr list --search "is:merged base:<base-branch> merged:<since>..<until>". Because stacked-PR tools (e.g.gh stack) retarget each branch's base to the trunk branch before it merges, this correctly captures stacked PRs too —createdAtstill reflects when the PR was first opened, so the full stack wait time is included. - Deploy time: for each PR's merge commit, walk the deploy branch's
first-parent history (chronological) and binary-search (via
git merge-base --is-ancestor) for the earliest commit that contains it as an ancestor. That commit's date is the "deployed at" time. This assumes the base branch is merged into the deploy branch via merge commits (not squash) — if squash-merged, ancestry can't be determined and the PR's stage 2/3 are skipped (counted under "ancestry判定不能" in the output). - Business hours: elapsed wall-clock time with any time falling on a
Saturday, Sunday, or Japanese public holiday excluded. Holiday data comes
from holiday-jp/holiday_jp-ruby's
holidays.yml, fetched once and cached undercache/holidays.yml(refetched if older than 30 days). Company-specific holidays (year-end/New Year, summer break, etc.) are not accounted for. - Bucketing: stage 1 is grouped by the base-branch merge date; stages 2 and 3 are grouped by the deploy date (not the merge date) — a PR merged near a week/month boundary can end up in a different bucket for stage 1 vs stage 2/3. This is intentional: each stage's trend should reflect when that stage's event actually happened.
- Excluded from all stats: PRs authored by
dependabot[bot]orrenovate[bot](maintenance bots). PRs authored by dev-agent bots (e.g.devin-ai-integration[bot], Claude Code) are included — they represent real development flow, not automated maintenance. - Excluded from stage 2/3 only: PRs merged into the base branch but not
yet found on the deploy branch as of the query's
--until(still in flight). Stage 1 for these PRs is still counted.
Development
Pure logic (lib/business_hours.rb, lib/holiday_calendar.rb, lib/stats.rb,
lib/bucketing.rb, lib/aggregator.rb) is unit-tested with minitest:
cd ~/.claude/skills/pr-lead-time
for f in test/*_test.rb; do ruby -Itest -Ilib "$f"; done
lib/github_client.rb (all gh/git I/O) is intentionally not unit-tested —
it's a thin wrapper exercised via real runs against an actual repo.