Running affected tasks
For an opt-in consumer-specific comparison of shared sources, see Project change projections, and for leaving out projects whose entry points do not import a change, see Import reachability. Ordinary selection and task cache keys retain the behavior described here.
qk show projects --affected --base main
qk show affected web --base main
qk affected -t build,test --base main
qk affected -t build,test --granularity project --base main
As in Nx, changed files are those between the merge base of --base and
--head, or with no head, between the merge base and the working tree,
including uncommitted and untracked files. The base defaults to NX_BASE,
then nx.json defaultBase, then main; the head to NX_HEAD. --files,
--uncommitted and --untracked replace the comparison. A file named with
--files counts as changed whole: its revisions are not compared, so a JSON
or lockfile change is not narrowed to what changed inside it. Files matching
the root .gitignore or .nxignore are left out. Untracked files are those
Git lists as untracked and not ignored, so an .nxignore negation does not
bring back one .gitignore excludes, though it does for a file named with
--files. As in Nx, a base or head starting with - is refused, since git
would read it as an option.
Unlike Nx, when the base is a branch with an upstream, such as main
following origin/main, and the head left the upstream later than the local
branch, the base is that later point. Otherwise a local main that has not
been updated would count everything that landed on origin/main since as
changes. When the compared commits still include some already on the default
branch’s upstream, as with an explicit --base older than where the head
branched, affected and show affected warn. On the default branch itself
the range is its own history, and nothing is flagged.
Tasks or projects
As in Nx 24, qk affected selects the tasks a change reaches, by the rules
in Selecting affected tasks by inputs,
and runs them with the tasks they depend on. Project selection, Nx’s
behavior before 24, runs the targets of every affected project instead, by
the rules in How changes reach projects. The
first of these that is set decides:
--granularity taskor--granularity project;NX_LEGACY_AFFECTED:trueselects projects andfalsetasks; as in Nx, other values are ignored;- nx.json
affected.legacy:trueselects projects; - otherwise, tasks are selected.
To keep selecting projects, as Nx’s migration to 24 does for an existing
workspace, set affected.legacy in nx.json:
{
"affected": { "legacy": true }
}
show projects --affected -t <targets> decides the same way, as in Nx 24:
under task selection it lists the projects owning an affected task of the
targets, and under project selection the affected projects with one of the
targets. Without -t it lists the affected projects, and with -p, unless
given --granularity task, it selects projects, as Nx does.
Change projections apply to project selection only (see
Project change projections). Under an affected
profile with projections, qk affected and show projects --affected -t
select projects where they would select tasks, and say so on stderr; with
--granularity task they fail instead.
How changes reach projects
A changed file touches the project whose root most specifically contains it.
A file named by a {workspaceRoot} input touches the projects declaring it;
an input without glob syntax, such as {workspaceRoot}/generated, names that
file or everything under it.
A change to nx.json, the file it extends or nx.local.json touches
projects by what it changes. qk reads the configuration at both revisions,
merged as qk loads it, and compares the values, so a change to comments or
formatting touches nothing. A change to a setting every task’s cache key
holds, such as plugins, pluginsConfig or workspaceLayout, touches every
project (see What changes the key). A change
to targetDefaults or namedInputs touches the projects whose targets or
named inputs it resolves differently: qk resolves every project with the base
revision’s configuration and the head’s manifests, and compares. A change
only to settings that change no task’s work, such as defaultBase,
parallel or release, touches nothing. Where a revision cannot be read or
parsed, or the files were named with --files, every project is touched.
A changed project.json or project.local.json that a project has at both
revisions touches its project as any file does when an input of the
project’s targets selects it, as default does unless the workspace leaves
the files out of it (see What changes the key).
It also touches projects by what it changes. qk reads the changed manifests
at the base revision, resolves the workspace with them and the head’s other
files, and compares every project’s configuration, since a tag or a name
decides what other projects’ inputs and dependencies select. A project is
touched where its targets, named inputs or other keys differ, where its
dependencies do, or where an input of it naming projects, such as
tag:shared, selects others. Where no input selects the file, a change to
comments or formatting touches nothing. A manifest the change added, or one
that cannot be read at the base revision or was named with --files,
touches its project as any other file does.
A deleted project.json or package.json that removes a project touches the
projects that depended on it. Nx touches every project instead, having no way
of knowing what depended on it; qk reads the removed project’s manifests at
the base revision and looks for what still declares it. qk’s graph is made of
manifests alone, and a project whose files the change leaves alone has the
same manifests at both revisions, so what depended on the removed project is
what the change touches already and what still names it: a package.json
dependency that linked to it, or an implicitDependencies entry or pattern
selecting it. Their dependents follow as for any other touched project. A
deletion can remove a package and leave its project, when a project.json
still makes it, or move a project under the same name. Either way a project
whose dependency on the package the head graph no longer has is touched, as
when a file: path names where the package was; one whose link survives,
such as a workspace: range to the moved package, depends on it as before,
and one whose implicitDependencies exclude it was never linked to it. Every project
is still touched when qk cannot tell: for a deleted root lockfile, for files
named with --files, which are not compared with a base revision, or when the
removed project’s manifests cannot be read there.
Path mappings changed in the root tsconfig touch the projects they point into, and any other change to it every project.
A change to the root lockfile (pnpm-lock.yaml, pnpm-lock.yml,
package-lock.json, yarn.lock, bun.lock or bun.lockb) touches projects
by nx.json’s pluginsConfig["@nx/js"].projectsAffectedByDependencyUpdates:
every project under "all", Nx’s default, or the projects a list of
selectors names. Under "auto", a pnpm-lock.yaml change touches the
projects whose importer installs something different, by the same installed
sets the cache keys use; another lockfile, or one qk cannot compare, touches
every project.
In the root package.json, a changed dependency touches the projects
installing that package, or the workspace project it names, and an
@types/ package also the package it types. A changed override, under
overrides, resolutions or pnpm.overrides, touches the projects
installing the package its selector names: the last key, past a pnpm
parent> and without a @range. A removed dependency, a change to a global
package (nx and the plugins nx.json loads), an override naming no installed
package, or a file that cannot be compared touches every project. Installed
packages are read from pnpm-lock.yaml; without it, a changed dependency
touches every project. Every project depending on a touched project is
affected too.
Some rules deliberately differ from Nx:
- Under
"auto", the installed sets leave out projects that install exactly what they did before, which Nx reports, and catch transitive changes such as a dependency’s dependency moving version, which Nx can miss. - A list of selectors touches its projects only when a lockfile changed. Nx touches them on every change, lockfile or not, which reads as a bug; its task selection checks for a lockfile change, as qk does.
- A change to nx.json touches only what it changes, by the rule above. Nx hashes nx.json whole into every task, so any change to it, even a comment, touches every project.
- A change to the file nx.json
extendscounts as a change to nx.json does. Nx does not look at that file, although it configures the workspace as much as nx.json does. - A change to a project’s
project.jsonthat no input of the project selects touches what it changes, by the rule above. Nx touches the project whatever changed.
show projects --affected prints projects in graph order; Nx prints its
traversal order, so compare the two as sets.
Explaining the result
qk show affected takes the same change options and explains the result.
Without a project it lists every affected project with the first reason it
is touched, or the dependency it is affected through. With a project it
prints a shortest dependency path to a touched project, the project’s other
affected dependencies, and every reason the touched project is touched. For
a lockfile reason it lists what the importer installs differently, by
package: versions that moved first, then packages added or removed, then
those that kept their version but resolved different peers or patch, then
those whose dependencies changed as a consequence. After what was compared,
it says which granularity qk affected selects by and what decided it (see
Tasks or projects).
$ qk show affected web --base HEAD^ --head HEAD
2 changed files between 3f1c0a9e2b7d and HEAD.
The base is where HEAD left HEAD^, 1 commit back.
Granularity: task (the default).
web is affected because it depends on ui:
web -> ui (static)
ui is touched:
what packages/ui installs changed in pnpm-lock.yaml (3 packages)
react (direct): 19.0.0 -> 19.1.0
react-dom (direct): 19.0.0: peers or patch changed
scheduler: dependencies or integrity changed
Selecting affected tasks by inputs
Under task selection, a task is affected when Nx 24 would select it, or when
its cache key changed, except that qk narrows a lockfile change, as below,
and a deleted manifest, an nx.json change and a project.json change, as in
How changes reach projects. As in Nx, that is
when:
- a changed file is one of its resolved inputs, a declared output included: a tracked output is a file like any other;
- a file projects are created from changed in a project whose inputs it
reads, its own or one its
^andprojectsinputs reach, or such a file was deleted and removed a project that one of those depended on, by the rule above, which falls back on every task where it does. Aproject.jsonorproject.local.jsoncompared at both revisions counts by its inputs and the rule below instead; - the root tsconfig changed outside
compilerOptions.paths, or, withselectivelyHashTsConfig, in path mappings into a project whose inputs it reads (see What changes the key); - the root lockfile changed and
projectsAffectedByDependencyUpdatesis"all", Nx’s default, and the task hashes installed packages: it declares noexternalDependencies, or names some. Under a list of selectors, the tasks of the projects it names are affected; - the root
package.jsonmoved an installed package the task hashes, by the rules above, or named the task’s project as a dependency.
Where Nx selects every task on any nx.json change, qk selects a task when the
change moves its key: when it changed a setting every key holds, or when,
resolved with the base revision’s targetDefaults and namedInputs, the
task’s definition differs, or what its inputs select does, its own or a
dependency’s through ^ inputs. A named input that selects the same files
leaves the task alone. A task declaring {workspaceRoot}/nx.json as an input
is affected by any change to the file, as its key holds it whole.
A changed project.json or project.local.json selects every task whose
inputs select it, as any input does, its dependents’ through ^ inputs
included; with default holding {projectRoot}/**/*, that is every task
reading the project’s files, as in Nx. For a task whose inputs leave it out,
it counts as nx.json does: the task is selected when, resolved with the
manifests the base revision had, its definition differs, or what its key
holds of its project’s configuration, such as its root and output paths,
or what its inputs select, or the dependency manifests and importers qk adds
for it. Editing one target then selects that target’s tasks, and a tag the
tasks whose inputs select projects by it.
Beyond those, qk’s key also reads what its lockfile importers or
externalDependencies install, pnpm-workspace.yaml outside its resolution
keys, and each dependency project’s package.json in the fields its key
reads, so a task is affected when any of these changed, or a dependency’s
version moved into or out of the range the task’s project declares for it.
Under "auto", these installed sets are what a lockfile change is judged by.
Nx instead selects, on any lockfile change, every task it keys by the whole
lockfile, such as those of a project that depends on no package, and every
task that hashes a package whose lockfile entry moved, even where what the
task’s importers install stayed the same. qk keys neither, so it selects
neither.
A task is also affected when it reads the declared outputs of an affected
task, through dependentTasksOutputFiles or a fileset that selects them. As
in Nx, depending on a task without reading its outputs carries nothing, so
a task ordered after a build but reading only sources is not affected by the
build’s changes. A file the task’s inputs select counts by any change to it,
as in its key. An unaffected task would therefore be a cache hit, except that
env and runtime inputs count as unchanged, since the base revision’s
environment is unknowable. An affected one can still be a hit where Nx
selects more than qk keys, such as for a changed declared output or a
lockfile change under "all". A test-only change then reaches the test tasks
and not the builds whose production inputs exclude tests.
A continuous task a task depends on, such as the dev server an e2e task runs
against, counts its inputs as the task’s own instead, and so do those of
the continuous tasks it depends on in turn. A dependsOn entry with
inputs: false leaves them out, unless another entry names the same task
without it.
qk show tasks -t <targets> --affected lists the affected tasks with their
first reason, or the whole analysis with --json, whatever the granularity.
--json gives the whole analysis, or for one project its path with the
typed reasons, including the snapshot keys a lockfile change added,
removed or changed. Both carry granularity, task or project, and
granularityFrom: --granularity, NX_LEGACY_AFFECTED, affected.legacy,
projections or default.
Project selectors
Project selectors are read as Nx’s findMatchingProjects reads them,
wherever Nx reads them: --projects and --exclude, implicitDependencies,
dependsOn and inputs projects, targetDefaults filter.projects and a
projectsAffectedByDependencyUpdates array.
- A project name selects that project. A glob selects the names it matches;
globs are minimatch globs, so
*and?do not cross a/(@scope/*, not@scope*, selects@scope/ui) and**does. tag:<pattern>selects by tag,directory:<glob>by project root andname:<pattern>by name only. A pattern without a label is tried as a name, then as a root when no name matched, solibs/*selects the projects directly underlibs.- A plain pattern that is no project name and no glob selects the names
containing it as a word, ignoring case:
uiselectsui,@scope/uiandui_kit, but notui-e2e, as-and@join words. - Patterns apply in order, each adding what it matches or, prefixed with
!, removing it:'!tag:e2e,app-e2e'keepsapp-e2e. When the first pattern excludes, selection starts with every project. --excluderemoves what its patterns match among the projects already selected.- Empty patterns and
nx-cloud:references to other workspaces are skipped. A selection matching nothing is empty. As in Nx,show projecttakes a name that is no project as a pattern, which must match exactly one.
The projects of an input are read by Nx’s native hasher, which differs:
a first ! pattern starts from nothing, and a plain name matches only that
name.
Quote globs so the shell does not expand them. Commas delimit CLI selectors, so brace globs containing commas are not supported on the command line.