What happened
Since upgrading turbo 2.9.18 → 2.10.7 (and still on 2.10.8), our CI intermittently fails before any task starts with exactly:
x I/O error: Is a directory (os error 21)
`-> Is a directory (os error 21)
Package discovery completes, Remote caching enabled prints, then ~3 seconds of silence and the error above — no task started, no run summary. 49 failing CI attempts between 2026-08-06 and 2026-08-12, and zero occurrences ever observed on 2.9.18 (same repo, same workflows, scanned the four days before the upgrade plus the whole prior tenure's failure history).
Environment: GitHub-hosted ubuntu-latest runners, pnpm monorepo with ~50 workspace packages, fresh checkout per run, futureFlags.affectedUsingTaskInputs enabled, remote caching via a self-hosted cache sidecar (the failures show zero cache requests — the error precedes any cache traffic).
What the SCM trace shows
We set TURBO_LOG_VERBOSITY=turborepo_scm=trace on the gate steps and captured every subsequent occurrence (32 of them). Every single one shows the same two-step sequence:
[DEBUG] turborepo_scm::package_deps: git hashing failed for AnchoredSystemPath("<pkg>"): Git error: <repo>/.git/index.lock: No such file or directory (os error 2). Falling back to manual
…followed ~1 second later by the pathless EISDIR and the run dying.
Two things stand out:
- The git error is ENOENT on
.git/index.lock — a lock file vanishing mid-operation, not a held lock. Nothing else in the job touches git at that point, which suggests turbo 2.10's concurrent SCM work (concurrent repo-index construction / parallel blob hashing / deferred task-input hashing) racing with itself.
- It is always the same package — the only one in the repo whose tasks declare repo-spanning
$TURBO_ROOT$ glob inputs (including a $TURBO_ROOT$/**/*), i.e. by far the heaviest SCM hashing work. The race loser is deterministic even though the trigger is transient.
We also captured one fallback that survived: same package, but the git error was a vanished untracked temp file rather than index.lock, and manual hashing completed fine. So the fallback isn't inherently fatal — the EISDIR appears specific to the repo-wide-glob input set.
Where the pathless error seems to come from
In turborepo-scm at v2.10.7, hash_object.rs wraps read failures with the offending path and skips non-regular files, so the pathless error must be a bare ? on an io::Error elsewhere in the crate (the Io(#[from] std::io::Error) variant in lib.rs). The manual fallback's list-driven hash_files and the external-inputs globwalk block propagate raw io::Errors without attaching the path, which is why we can name the failing phase but not the failing read.
Repro status
We could not repro locally: a warm index leaves every package with to_hash_count=0, so the fallback never fires — it seems to need CI's fresh-checkout hashing load. On CI it reproduced 49 times in seven days across two patch versions (2.10.7, 2.10.8), and re-running the identical commit usually passes. We've pinned back to 2.9.18 as containment.
Asks
- If the
.git/index.lock race in 2.10's concurrent SCM hashing is fixable, that removes the trigger.
- Independently: attaching the path to the manual fallback's read errors (like
hash_object.rs already does) would make this class of failure diagnosable from the error alone.
Happy to share full turborepo_scm=trace logs from the captured occurrences.
What happened
Since upgrading turbo 2.9.18 → 2.10.7 (and still on 2.10.8), our CI intermittently fails before any task starts with exactly:
Package discovery completes,
Remote caching enabledprints, then ~3 seconds of silence and the error above — no task started, no run summary. 49 failing CI attempts between 2026-08-06 and 2026-08-12, and zero occurrences ever observed on 2.9.18 (same repo, same workflows, scanned the four days before the upgrade plus the whole prior tenure's failure history).Environment: GitHub-hosted
ubuntu-latestrunners, pnpm monorepo with ~50 workspace packages, fresh checkout per run,futureFlags.affectedUsingTaskInputsenabled, remote caching via a self-hosted cache sidecar (the failures show zero cache requests — the error precedes any cache traffic).What the SCM trace shows
We set
TURBO_LOG_VERBOSITY=turborepo_scm=traceon the gate steps and captured every subsequent occurrence (32 of them). Every single one shows the same two-step sequence:…followed ~1 second later by the pathless EISDIR and the run dying.
Two things stand out:
.git/index.lock— a lock file vanishing mid-operation, not a held lock. Nothing else in the job touches git at that point, which suggests turbo 2.10's concurrent SCM work (concurrent repo-index construction / parallel blob hashing / deferred task-input hashing) racing with itself.$TURBO_ROOT$glob inputs (including a$TURBO_ROOT$/**/*), i.e. by far the heaviest SCM hashing work. The race loser is deterministic even though the trigger is transient.We also captured one fallback that survived: same package, but the git error was a vanished untracked temp file rather than
index.lock, and manual hashing completed fine. So the fallback isn't inherently fatal — the EISDIR appears specific to the repo-wide-glob input set.Where the pathless error seems to come from
In
turborepo-scmat v2.10.7,hash_object.rswraps read failures with the offending path and skips non-regular files, so the pathless error must be a bare?on anio::Errorelsewhere in the crate (theIo(#[from] std::io::Error)variant inlib.rs). The manual fallback's list-drivenhash_filesand the external-inputs globwalk block propagate rawio::Errors without attaching the path, which is why we can name the failing phase but not the failing read.Repro status
We could not repro locally: a warm index leaves every package with
to_hash_count=0, so the fallback never fires — it seems to need CI's fresh-checkout hashing load. On CI it reproduced 49 times in seven days across two patch versions (2.10.7, 2.10.8), and re-running the identical commit usually passes. We've pinned back to 2.9.18 as containment.Asks
.git/index.lockrace in 2.10's concurrent SCM hashing is fixable, that removes the trigger.hash_object.rsalready does) would make this class of failure diagnosable from the error alone.Happy to share full
turborepo_scm=tracelogs from the captured occurrences.