---
title: 'Branching Strategy and Pipeline Relationship'
source: 'https://academia.sh/en/courses/ci-cd/branching-strategy-and-pipeline-relationship'
course: 'Continuous Integration and Delivery'
language: en
updated: '2026-09-13T05:08:18+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares two branching schemes on the same defect set: what the integration gate catches, and how many clean changes sit blocked beside it'
    - 'Scans the merge interval to measure the trade-off among gate runs, wastefully blocked changes, and total gate minutes'
    - 'Shows, as a number, the extra record trunk-based development needs to conceal unfinished work'
---

# Branching Strategy and Pipeline Relationship

Long-lived branching and trunk-based development run on the same ninety-change stream: branch lifetime, conflicting files, gate runs per integration, and when the integration gate runs under each scheme are counted. Trunk-based development's own cost — the concealment record — is measured separately.

The pipeline is built: its stages are ordered, each stage's gate is defined, and what each gate catches,
misses, and holds up is counted. This does not say **how** a change will enter production. The pipeline's
existence is not a flow scheme; the flow scheme is set by when a change joins the shared trunk and by what
steps it advances from there to production. This topic measures those decisions, starting with the first one.

The first decision is this: when does a developer's written change enter the shared trunk. There are two
extreme schemes. In long-lived branching, work accumulates on its own branch and merges all at once, weeks
later. In trunk-based development, every change joins the trunk on the day it is written. The two schemes
carry the same work, with the same people, through the same defect set — but the **integration gate** over
them runs at different moments, on pieces of different size. The gate here is an acceptance decision: it
either lets the change into the shared trunk or it does not. It is the first promotion decision in the chain.

- **RS1.** The fictional regional measurement network's verification service is kept in a single repository.
  Six developers, sixty workdays, ninety changes, forty source files. Both the network and the figures are
  fictional.
- **RS2.** A feature belongs to a single author and is written in one to five pieces over consecutive days.
  Handed-off work, jointly written features, and pair programming are outside the model.
- **RS3.** The defect set has three classes: an **isolated defect** (also visible on the branch), an
  **interaction defect** (visible only once both changes have entered the trunk), and a
  **production-data-dependent defect** (invisible at every integration gate).
- **RS4.** The local gate running on the branch catches every isolated defect. The integration gate's
  marginal contribution is therefore the interaction defects.
- **RS5.** One gate run takes fourteen minutes; a red batch is fixed and runs once more. The human time
  spent fixing it and queue waiting are outside the model.
- **RS6.** Conflicts are counted at the file level. Line-level overlap, renaming, and file moves are not
  modeled. The probability of a flaky red is six percent, and it is only distinguishable when no real
  defect is present.

## Measurement Setup

What is measured is not a real repository but a model set up with `node`. The model produces a change
stream, consumes that stream under two different merge schemes, and counts when and on how many changes
the gate runs in each scheme. The random generator is written by hand, and its seed is visible.

```js
// branch.mjs — model of two branching schemes on the same change stream; not a real repository, a model
export const SEED = 20260901;
export const DAYS = 60, CHANGES = 90, DEVELOPERS = 6, FILES = 40;
export const GATE_MIN = 14, LOCAL = 0.10, PRODUCTION = 0.08, PAIR = 0.06, FLAKY = 0.06;

export function generator(seed) {                  // linear congruential generator, seed visible
  let x = seed >>> 0;
  return () => { x = (Math.imul(x, 1664525) + 1013904223) >>> 0; return x / 4294967296; };
}

// The stream is produced once: both schemes consume the same changes and the same defect set.
// A feature belongs to a single author and is written in a few pieces over consecutive days.
export function stream() {
  const rnd = generator(SEED), l = [];
  while (l.length < CHANGES) {
    const author = Math.floor(rnd() * DEVELOPERS), start = 1 + Math.floor(rnd() * (DAYS - 5));
    const pieces = 1 + Math.floor(rnd() * 5), feat = l.length && l[l.length - 1].feature + 1 || 0;
    for (let j = 0; j < pieces && l.length < CHANGES; j += 1) {
      const d = new Set();                         // hot-file bias: squaring skews toward low indices
      for (let k = 0, n = 1 + Math.floor(rnd() * 3); k < n; k += 1) d.add(Math.floor(rnd() ** 2 * FILES));
      l.push({ no: l.length, author, day: start + j, file: [...d], feature: feat,
        local: rnd() < LOCAL, production: rnd() < PRODUCTION });
    }
  }
  return l.sort((a, b) => a.day - b.day || a.no - b.no);
}

// An interaction defect belongs to a pair: two changes from different authors touching the same file.
// Invisible on an isolated branch; it only surfaces once both have entered the trunk.
export function pairs(l) {
  const c = [];
  for (let i = 0; i < l.length; i += 1) for (let j = i + 1; j < l.length; j += 1) {
    if (l[i].author === l[j].author || !l[i].file.some((f) => l[j].file.includes(f))) continue;
    let h = Math.imul(l[i].no + 1, 2654435761) ^ Math.imul(l[j].no + 1, 40503);
    if ((Math.imul(h ^ (h >>> 13), 1274126177) >>> 0) / 4294967296 < PAIR) c.push([l[i].no, l[j].no]);
  }
  return c;
}

// interval 0: trunk-based, every change merges on its own. interval k: an author's branch merges every k days.
export function run(l, pair, interval) {
  const rnd = generator(SEED + 7), merge = new Map(), batch = new Map();
  for (const c of l) {
    const day = interval === 0 ? c.day : Math.ceil(c.day / interval) * interval;
    const key = interval === 0 ? `t${c.no}` : `${c.author}:${day}`;
    merge.set(c.no, day);
    if (!batch.has(key)) batch.set(key, { day, author: c.author, member: [] });
    batch.get(key).member.push(c);
  }
  const s = { batch: batch.size, runs: 0, red: 0, cleanBlocked: 0, flakyBlocked: 0, conflicts: 0,
    local: 0, interaction: 0, escaped: 0, age: 0 };
  const trunk = new Map();
  for (const p of [...batch.values()].sort((a, b) => a.day - b.day)) {
    const base = p.day - (interval || 1), guilty = new Set();
    for (const f of new Set(p.member.flatMap((c) => c.file))) {
      const seen = trunk.get(f);
      if (seen && seen.author !== p.author && seen.day > base) s.conflicts += 1;
      trunk.set(f, { author: p.author, day: p.day });
    }
    for (const c of p.member) { s.age += p.day - c.day; if (c.production) s.escaped += 1;
      if (c.local) { guilty.add(c.no); s.local += 1; } }
    for (const [a, b] of pair) {                   // shows up in the batch of whichever member merges later
      if (Math.max(merge.get(a), merge.get(b)) !== p.day) continue;
      const later = p.member.find((c) => c.no === b) || p.member.find((c) => c.no === a);
      if (!later || merge.get(a) === merge.get(b) && !p.member.some((c) => c.no === a)) continue;
      guilty.add(later.no); s.interaction += 1;
    }
    s.runs += 1;
    const flaky = guilty.size === 0 && rnd() < FLAKY;
    if (guilty.size || flaky) {                    // a red batch is fixed and reruns
      s.red += 1; s.runs += 1;
      s.cleanBlocked += p.member.length - guilty.size;
      if (flaky) s.flakyBlocked += p.member.length;
    }
  }
  return { ...s, age: s.age / l.length, size: l.length / batch.size, min: s.runs * GATE_MIN };
}
```

The model has a single knob: `interval`. At zero, every change integrates on its own; this is trunk-based
development. At ten, everything an author writes in ten days merges into a single batch; this is long-lived
branching. The defect set, on the other hand, is the same no matter what `interval` is — an interaction
defect belongs to a pair, and both members of that pair enter the trunk sooner or later.

## Two Schemes, One Stream

```js
// run1.mjs — the same stream is run through two branching schemes
import { stream, pairs, run, SEED, CHANGES, DEVELOPERS, GATE_MIN } from './branch.mjs';

const l = stream(), c = pairs(l);
console.log(`seed ${SEED} | ${CHANGES} changes, ${DEVELOPERS} developers, 60 workdays`);
console.log(`defect set: ${l.filter((x) => x.local).length} isolated defects, ${c.length} interaction defects, ` +
  `${l.filter((x) => x.production).length} production-dependent defects`);
console.log(`integration gate run ${GATE_MIN} min\n`);
console.log('scheme                integrations  batch   branch life  conflicting  gate      gate-only  wasted    gate');
console.log('                                    size    (days)       files        runs      catches    blocked   minutes');
for (const [ad, interval] of [['trunk-based', 0], ['long-lived branch', 10]]) {
  const s = run(l, c, interval);
  console.log(`${ad.padEnd(22)} ${String(s.batch).padEnd(13)} ${s.size.toFixed(2).padEnd(7)} ` +
    `${s.age.toFixed(2).padEnd(12)} ${String(s.conflicts).padEnd(12)} ${String(s.runs).padEnd(9)} ` +
    `${String(s.interaction).padEnd(10)} ${String(s.cleanBlocked).padEnd(9)} ${s.min}`);
}
```

```
seed 20260901 | 90 changes, 6 developers, 60 workdays
defect set: 10 isolated defects, 24 interaction defects, 8 production-dependent defects
integration gate run 14 min

scheme                integrations  batch   branch life  conflicting  gate      gate-only  wasted    gate
                                    size    (days)       files        runs      catches    blocked   minutes
trunk-based            90            1.00    0.00         8            117       24         2         1638
long-lived branch      20            4.50    4.47         44           35        24         54        490
```

Both schemes caught the same twenty-four interaction defects. What the gate **catches** does not change;
what changes is when it catches it and how much clean work it stops to do so.

The branch-life column gives the timing. Under trunk-based development, a change enters the gate on the
day it is written; the average age is zero. Under branching, it waits four and a half days on average,
because the gate runs only at merge time. The local gate on the branch runs during that whole wait, but it
can see nothing beyond the ten isolated defects: an interaction defect, by definition, waits for two
changes to meet. So under the branching scheme, **half the gates run early, the discriminating one runs
late**.

The cost shows up in the wasted-blocked column. Under trunk-based development a batch is a single change;
when the gate is red, the only thing that stops is the guilty change, and only two changes are wastefully
blocked — both from a flaky red. Under branching a batch is four and a half changes on average; when the
gate is red, the whole batch stops, and fifty-four clean changes wait for something that is not their own
fault. Catching the same twenty-four defects costs two unnecessary stops under one scheme and fifty-four
under the other.

The time column reverses direction. Trunk-based development spent 1,638 gate-minutes, branching spent 490.
Machine time per gate run is the same; the difference is in the run count. Branching pays about
twenty-four and a half minutes of gate time per integration, trunk-based pays eighteen — but trunk-based
pays it ninety times over. Running the gate rarely cuts machine time to a third, and also delays the moment
it discovers the defect it is there to find.

## Merge Interval Scan

To see what lies between the two extremes, the interval is scanned. The same scan also produces
trunk-based development's own bill: unfinished work sits in the trunk and needs a separate scheme so it
does not leak into production.

```js
// run2.mjs — the merge interval is scanned; the cost of trunk-based — half-finished work — is counted separately
import { stream, pairs, run } from './branch.mjs';

// A feature left half-done in the trunk: its first piece has entered, its last piece has not.
function unfinished(l, interval) {
  const dayOf = (c) => (interval === 0 ? c.day : Math.ceil(c.day / interval) * interval);
  const feat = new Map();
  for (const c of l) {
    if (!feat.has(c.feature)) feat.set(c.feature, []);
    feat.get(c.feature).push({ d: dayOf(c), author: c.author });
  }
  let records = 0, featCount = 0;
  for (const p of feat.values()) {
    const last = Math.max(...p.map((x) => x.d));    // every piece entering before the last piece must be concealed
    const early = p.filter((x) => x.d < last).length;
    records += early; featCount += early > 0 ? 1 : 0;
  }
  return { records, featCount, features: feat.size };
}

const l = stream(), c = pairs(l);
console.log(`${l.length} changes belong to ${unfinished(l, 0).features} features; piece count 1-5\n`);
console.log('interval  batch  branch life  conflicting  gate    red      wasted   flaky     gate       hidden   features');
console.log('(days)    size   (days)       files        runs    batches  blocked  blocked   minutes    records  hidden');
for (const a of [0, 1, 2, 3, 5, 10, 20]) {
  const s = run(l, c, a), y = unfinished(l, a);
  console.log(`${String(a || 'trunk').padEnd(9)} ${s.size.toFixed(2).padEnd(6)} ${s.age.toFixed(2).padEnd(12)} ` +
    `${String(s.conflicts).padEnd(12)} ${String(s.runs).padEnd(7)} ${String(s.red).padEnd(8)} ` +
    `${String(s.cleanBlocked).padEnd(8)} ${String(s.flakyBlocked).padEnd(9)} ${String(s.min).padEnd(10)} ` +
    `${String(y.records).padEnd(8)} ${y.featCount}`);
}
```

```
90 changes belong to 30 features; piece count 1-5

interval  batch  branch life  conflicting  gate    red      wasted   flaky     gate       hidden   features
(days)    size   (days)       files        runs    batches  blocked  blocked   minutes    records  hidden
trunk     1.00   0.00         8            117     27       2        2         1638       60       23
1         1.30   0.00         8            95      26       9        2         1330       60       23
2         2.09   0.47         18           67      24       31       2         938        46       20
3         2.73   0.98         24           55      22       43       1         770        38       16
5         3.33   2.08         37           46      19       48       0         644        25       12
10        4.50   4.47         44           35      15       54       0         490        13       6
20        6.43   9.80         59           25      11       56       0         350        1        1
```

The scan sets two curves against each other. As the interval grows, gate minutes fall from 1,638
to 350, conflicting files rise from eight to fifty-nine, and wastefully blocked changes rise from
two to fifty-six. A three-day interval is an interesting middle point: gate minutes are roughly
halved, but wastefully blocked changes have already gone from two to forty-three, more than
twentyfold. The trade-off is not linear — every minute saved from machine time turns, past a
certain point, into wait time for work that carries no fault of its own.

The flaky column carries a quiet finding. Once the interval reaches five days, the changes a flaky
run wastefully blocks drop to zero. Flakiness has not gone down; close to three quarters of the
batches already carry a real defect, so a flaky run can no longer be told apart from a real one.
Counting what a gate wrongly holds requires the gate to be green most of the time. In a scheme
where red is the normal state, wrongly holding a change becomes unmeasurable — and unmeasurable
wrong-holding turns into a team habit of skipping the gate.

The last two columns are trunk-based development's own bill. Ninety changes belong to thirty features; in
the trunk-based scheme, sixty of them enter the trunk before the feature they belong to is complete.
Twenty-three features sit half-done in the trunk. For these changes not to leak into production, every one
of them needs to carry a concealment record: the code will be in the trunk, the behavior will stay off. At
a ten-day interval this number drops to thirteen, at a twenty-day interval to one — because unfinished work
stays hidden on the branch instead.

Trunk-based development's cost is exactly here, and it is as concrete as gate minutes: sixty concealment
records, a switch to open and close for each of twenty-three features, and twenty-three separate cleanup
jobs to remove those switches once each feature is done. This is not a side effect of trunk-based
development; it is an entry requirement. A team that moves to trunk-based development without first setting
up a concealment scheme has built a scheme that leaks unfinished work into production.

## The Gate's Three Numbers

The integration gate's account closes with this measurement. **What it catches:** across the ninety-change
stream, ten isolated and twenty-four interaction defects, thirty-four in total. **What only it catches:**
twenty-four — the local gate on the branch would also catch the isolated defects, but only the integration
gate can see an interaction defect. The gate's marginal value is this twenty-four, and it is the same under
both schemes.

**What it misses:** eight production-data-dependent defects, under both schemes. This number does not
change with branching strategy; merge frequency does not make visible what the gate cannot see in the
first place. **What it wrongly holds:** two clean changes under trunk-based development, fifty-four under
the ten-day branching scheme. **The time it adds:** between eighteen and twenty-four and a half minutes
per integration; between 490 and 1,638 minutes in total.

Looked at through these numbers, the decision is not "which scheme is better." The gate's value is fixed;
what gets chosen is which currency that fixed value gets paid in — machine minutes, or the wait time and
conflict resolution of clean work.

## Summary

- The same stream was run through two schemes: the integration gate caught the same twenty-four interaction
  defects under both, and missed the same eight production-dependent defects under both.
- The split is in when the gate runs: at zero days old under trunk-based development, at an average of four
  and a half days under the ten-day branching scheme. The local gate on the branch runs early but cannot
  see interaction defects.
- As batches grow, the cost of wrongly holding a change grows with them: wastefully blocked clean changes
  go from two under trunk-based development to fifty-four at a ten-day interval. Conflicting files go from
  eight to forty-four.
- Gate minutes run in the opposite direction: 1,638 for trunk-based development, 490 for the ten-day
  interval. A gate that runs rarely is cheap, and it speaks late.
- Past a five-day interval, a flaky run becomes indistinguishable, because most batches are already red;
  unmeasurable wrong-holding turns into a habit of skipping the gate.
- Trunk-based development's cost was counted: sixty concealment records, twenty-three features, and
  twenty-three cleanup jobs afterward. Moving to trunk-based development without this scheme in place opens
  production to unfinished work.

## Next Step

This measurement ends the moment a change enters the trunk. What enters the trunk is not yet in
production: at least one build artifact and a few environments stand between. The next question is how
that artifact moves between environments. Does the same artifact get promoted, or does every environment
compile its own build? The next lesson runs both schemes on the same candidate sequence: the number of
environment-specific defects, what the promotion gate catches, whether the artifact's identity is preserved
across environments, and how long an artifact waits at the promotion gate.
