Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. --granularity task or --granularity project;
  2. NX_LEGACY_AFFECTED: true selects projects and false tasks; as in Nx, other values are ignored;
  3. nx.json affected.legacy: true selects projects;
  4. 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 extends counts 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.json that 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 ^ and projects inputs 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. A project.json or project.local.json compared at both revisions counts by its inputs and the rule below instead;
  • the root tsconfig changed outside compilerOptions.paths, or, with selectivelyHashTsConfig, in path mappings into a project whose inputs it reads (see What changes the key);
  • the root lockfile changed and projectsAffectedByDependencyUpdates is "all", Nx’s default, and the task hashes installed packages: it declares no externalDependencies, or names some. Under a list of selectors, the tasks of the projects it names are affected;
  • the root package.json moved 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 and name:<pattern> by name only. A pattern without a label is tried as a name, then as a root when no name matched, so libs/* selects the projects directly under libs.
  • A plain pattern that is no project name and no glob selects the names containing it as a word, ignoring case: ui selects ui, @scope/ui and ui_kit, but not ui-e2e, as - and @ join words.
  • Patterns apply in order, each adding what it matches or, prefixed with !, removing it: '!tag:e2e,app-e2e' keeps app-e2e. When the first pattern excludes, selection starts with every project.
  • --exclude removes 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 project takes 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.