skip to content

A teammate added includeFlat('common') and now the build fails on a fresh CI checkout with 'project directory does not exist'. What is going on and how do you fix or harden it?

level: seniorimportance: should knowfreq 20%

answer

  1. ../common missing on clean CI checkout
  2. settings evaluated early -> dir must exist
  3. fix: checkout sibling, or include, or includeBuild
  4. flat pushes topology outside the repo
  5. fail-fast require() on the directory

basics

~20 s

includeFlat expects ../common to exist next to the root. On CI only the root repo was cloned, so the sibling is missing. Fix by checking out the sibling beside the root, or restructure to include/composite build.

solid answer

~50 s

`includeFlat('common')` resolves the project directory to `${rootDir.parentFile}/common` (`../common`). On a developer machine the sibling checkout exists; on a clean CI runner only the root repository was cloned, so `../common` is absent and Gradle fails resolving the project directory. Fixes, in order of robustness: (1) make CI check out the sibling repo into the correct relative location before building (e.g. a multi-repo checkout step); (2) if `common` should really be part of this repo, move it under the root and switch to plain `include('common')`; (3) if `common` is a genuinely separate, independently-buildable component, replace the flat link with a **composite build** (`includeBuild('../common')`) and depend on it via dependency substitution — still requires the sibling to be present, but expresses the boundary better. The underlying lesson: flat layouts push checkout topology out of the repo, so it must be guaranteed by tooling/CI rather than assumed.

code

kotlin · 6 lines
kotlin
// settings.gradle.kts — guard the flat layout
val sibling = rootDir.parentFile.resolve("common")
require(sibling.isDirectory) {
    "Sibling checkout missing at $sibling. Clone 'common' next to this repo."
}
includeFlat("common")

go deeper

for a junior

Recognize that ../common must exist next to the root for includeFlat to work.

for a middle

Explain settings is evaluated early and the sibling is missing on clean CI; propose checking it out.

for a senior

Weigh checkout-step vs absorb-into-repo vs composite build, and add a fail-fast guard for clear errors.

for a principal

Set CI/checkout conventions so build topology is guaranteed by tooling, not implicit developer workspace layout.

## Why it fails Gradle evaluates `settings.gradle.kts` early, building the project tree. For `includeFlat('common')` it computes `projectDir = rootDir.parentFile/common`. If that directory doesn't exist, configuration fails (project directory does not exist / cannot resolve). Locally it works because your workspace already has the sibling checked out; CI typically clones only the named repo, so the sibling is missing. ## Diagnosis checklist 1. Confirm the resolved path: it's literally `../common` relative to the root project directory. 2. Check the CI checkout step — does it fetch the sibling repo into the parent directory? 3. Verify nobody assumed a developer-only folder layout. ## Remediation options **A. Provide the sibling on CI.** Add a checkout step that clones `common` into the parent dir so the flat layout is satisfied. Keeps the build unchanged but makes the external dependency explicit in CI config. **B. Absorb into the repo (preferred if it's really yours).** Move `common` under the root and use `include('common')`. Now a single clone is self-sufficient — the most reproducible outcome. **C. Composite build.** If `common` is a standalone, separately-versioned build, use `includeBuild('../common')` and depend on its coordinates; Gradle substitutes the local build. This expresses the boundary correctly, though the directory still must exist. ## Hardening tips - Don't rely on implicit developer workspace layout for CI-critical paths. - If you must keep flat, document and automate the checkout topology; consider failing fast with a clear message if `../common` is missing. - Prefer single-repo `include` or composite builds when you want a clone to 'just build'. ```kotlin // Fail fast with a clearer message than the default val common = rootDir.parentFile.resolve("common") require(common.isDirectory) { "Expected sibling checkout at $common — clone 'common' next to this repo (flat layout)." } includeFlat("common") ```

  • Why does it work locally but fail on CI?
    Locally the sibling repo is already checked out beside the root. CI usually clones only the root repo, so ../common does not exist when settings is evaluated.
  • Which fix gives the most reproducible single-clone build?
    Moving the project under the root and using plain include('common'), so one git clone yields a complete, buildable repo.
  • Does switching to includeBuild remove the need for the sibling directory?
    No — the included build's directory must still exist. It just models the boundary as an independent build with dependency substitution.

saying these in an interview costs you the question

  • Blaming Gradle rather than the missing sibling checkout.
  • Suggesting the build cache or daemon as the cause.
  • Recommending to keep flat without making CI provide the sibling.

context