DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on

Clone Depth Is a Queue-Drift Budget

A depth-one clone is often justified with a simple argument: “Our build scripts do not read Git history.”

That argument can be perfectly true and still miss the failure boundary.

In hosted CI, the system that needs history may be the checkout engine itself. If a build is requested by branch, the provider can capture one commit when the job is created and perform the actual clone later, after the job has waited for a runner. If the branch moves in between, a depth-one fetch may contain only the newer tip. The provider then cannot reset to the commit it already promised to build.

The build fails before restore, compilation, or tests begin. Nothing in the application changed. The queue changed the repository state visible to checkout.

Two times, two identities

The bug becomes easier to reason about when we separate two moments.

At queue time:

branch: main
captured commit: A
Enter fullscreen mode Exit fullscreen mode

At execution time:

branch tip: C
depth-one fetch: [C]
requested reset: A
result: A is unreachable
Enter fullscreen mode Exit fullscreen mode

The important detail is not the letters. It is that the request named a mutable branch while the provider later expected an immutable commit to remain reachable.

A little branch movement is enough:

queue time       A  <- captured
                  \
branch advances   B -- C  <- fetched tip

depth 1 contains: [C]
required commit:   A
Enter fullscreen mode Exit fullscreen mode

If the runner had received the immutable commit and fetched that object directly, branch movement would not matter. If the shallow history had retained enough ancestors, the reset could also succeed. With neither property, ordinary queue delay becomes a race.

History is not only for your scripts

Clone depth is usually discussed as a performance control. Less history means less network transfer and faster checkout, especially for large repositories.

That is useful, but it is incomplete. The checkout contract can include operations your build file never mentions:

  • resolving a branch when a build is submitted;
  • retaining the selected commit while the job waits;
  • cloning the branch when a runner becomes available;
  • resetting the worktree to the earlier selection;
  • comparing or annotating revisions for the CI interface.

The correct question is therefore not only, “How much history do our scripts read?” It is also, “How much history must the platform retain to honour the identity it captured?”

Prefer immutable dispatch

The cleanest design is to identify the build with an immutable commit from end to end.

Conceptually:

{
  "workflow": "mobile-release",
  "commit": "<immutable-sha>"
}
Enter fullscreen mode Exit fullscreen mode

The runner should then fetch that commit explicitly or use a provider feature that guarantees its availability. This removes the branch-movement race rather than budgeting for it.

However, some hosted systems or integrations accept only a workflow and branch. In that case, clone depth becomes a practical queue-drift budget.

If the branch can advance by d commits between build creation and runner checkout, the fetched history must preserve at least the captured commit. A useful starting model is:

required depth > expected branch movement during worst queue wait
Enter fullscreen mode Exit fullscreen mode

Add headroom, because averages are not safety boundaries. Use observed high-percentile queue waits and merge cadence rather than a comforting round number. Alert when a real job approaches or exceeds the assumption.

The trade-off is real

Increasing depth is not free.

More history increases transfer and object negotiation. On a large repository or an unreliable link, an overly generous fetch can introduce a different failure mode: slow or disconnected checkout. A fixed depth also remains probabilistic. A longer queue or unusually busy branch can exceed it.

That gives us three choices:

  1. Immutable commit dispatch: strongest correctness; depends on provider support and integration work.
  2. Bounded shallow history: pragmatic and often cheap; still relies on a measured drift assumption.
  3. Full history: simplest reachability model; potentially expensive and unnecessary.

The right choice depends on repository size, queue behaviour, branch velocity, and provider capability. The important leadership move is to make that choice explicit.

Test the timing contract

Checkout configuration deserves failure-oriented tests and observability just like application code.

I would verify:

  • a build created at commit A still starts after the branch advances;
  • checkout logs distinguish fetch failures from compilation failures;
  • queue duration and intervening commit count are captured;
  • a depth-budget breach is visible rather than labelled a generic flake;
  • changing shared CI anchors or templates updates every intended workflow;
  • release targets remain unchanged when checkout configuration changes.

The regression test is not “the YAML parses.” Parsing is necessary, but the original failure was temporal. At minimum, document the provider’s create-versus-start behaviour and verify the configuration reaches every workflow that shares that checkout policy.

A better review question

Shallow clones are not inherently unsafe. Unexamined timing assumptions are.

The next time a pipeline proposes depth one, ask two questions:

  1. What Git operations do our scripts perform?
  2. What Git operations does the CI provider perform between dispatch and execution?

That second question turns clone depth from a magic performance number into an explicit reliability budget—and often reveals that an immutable commit is the better contract.

Top comments (0)