Skip to content

Pathless I/O error: Is a directory (os error 21) from manual hashing fallback after transient .git/index.lock ENOENT (2.10 regression) #13732

Description

@poggersdave

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:

  1. 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.
  2. 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

  1. If the .git/index.lock race in 2.10's concurrent SCM hashing is fixable, that removes the trigger.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions