Skip to content
ClientAffected

Affected Dependencies

In a monorepo, a change to one workspace rarely stays there. The affected set is the workspace you touched plus everything that depends on it — the packages whose tests, builds and publishes could break because of your change.

vlt has no affected command. Affected is inherited state, so it is expressed with Dependency Selector Syntax instead: :diff() finds what changed, and :has() walks back up to what depends on it.

What changed

:diff() matches packages whose files changed against a git commitish, defaulting to HEAD:

Terminal
# uncommitted changes
$ vlt query ':workspace:diff()'
Terminal
# everything this branch changed
$ vlt query ':workspace:diff(main...HEAD)'

Use the three-dot form. :diff(<ref>) runs git diff --name-only <ref>, which compares that ref to your working tree in both directions — as soon as main moves ahead, everything changed on main is reported as changed here too. main...HEAD diffs against the merge base instead, which is what “changed on this branch” means.

Quote any ref containing a /: :diff("origin/main...HEAD"). Bare, the selector parser rejects the slash and the query exits non-zero.

git diff also ignores untracked files, so a brand new file only counts once it is staged or committed.

Why the :workspace prefix

:diff() matches the project root whenever anything in the repo changed, so a bare :diff(main...HEAD) almost always includes . as well. :workspace drops it.

The trade-off: a change to root-level files only — tsconfig.json, a CI workflow, the lockfile — maps to the root package alone, so :workspace:diff() returns nothing at all for it. A gate built on :workspace skips the job on those changes.

What depends on what changed

:has() matches packages with a direct dependency on a match:

Terminal
$ vlt query ':workspace:has(:diff(main...HEAD))'

Nest it to reach further up the graph. Each level of :has() is one more hop away from the change:

Terminal
# dependents of the dependents
$ vlt query ':workspace:has(:has(:diff(main...HEAD)))'

The affected set

Combine both with a comma. Selector lists are unioned, so this is “changed, or depending on something changed”:

Terminal
$ vlt query ':workspace:diff(main...HEAD),
:workspace:has(:diff(main...HEAD))'

Given a monorepo where packages/utils changed on your branch:

my-monorepo
├── packages/utils/ ← files changed
├── packages/core/ depends on utils
├── packages/cli/ depends on core
└── apps/docs/ depends on nothing

that union selects:

my-monorepo
├── packages/utils/ ✅ changed
├── packages/core/ ✅ depends on utils
├── packages/cli/ depends on core, two hops away
└── apps/docs/

packages/cli is two hops from the change, so it needs another level of :has(). Add one selector per hop your graph is deep:

Terminal
$ vlt query ':workspace:diff(main...HEAD),
:workspace:has(:diff(main...HEAD)),
:workspace:has(:has(:diff(main...HEAD)))'

Running scripts on the affected set

vlt run takes the same selector via --scope, so a CI job can test only what your branch could have broken:

Terminal
$ vlt run test --scope=':workspace:diff(main...HEAD),
:workspace:has(:diff(main...HEAD)),
:workspace:has(:has(:diff(main...HEAD)))'

Workspaces without the script are skipped rather than failing: --scope implies --if-present, as do --workspace, --workspace-group and --recursive.

--workspace and --workspace-group narrow by path glob or named group instead of by query, but they are not combinable with --scope — when --scope is given it wins and both are ignored.

Gating a CI job

--expect-results turns a query into a check: vlt query exits non-zero when the result count does not match, so a job can bail out early when nothing relevant changed.

Terminal
$ vlt query ':workspace:diff("origin/main...HEAD")' --expect-results='>0' \
|| echo 'nothing affected, skipping'

Keep the ref quoted here. A parse error also exits non-zero, so an unquoted origin/main would be swallowed by the || and report “nothing affected” forever.

Results are counted as graph edges, not as packages: one workspace depended on from several places counts more than once. --view=json emits those same edges — each entry is an edge with the package under to — so workspace paths need extracting and deduping:

Terminal
$ vlt query ':workspace:diff("origin/main...HEAD")' --view=json \
| jq -r '.[].to.location' | sort -u

See also

  • :diff() — match packages by git changes
  • :has() — match packages by their dependencies
  • Workspaces — defining workspaces and groups
  • vlt run — running scripts across workspaces