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

Development

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
cargo build --release --locked

The Cargo workspace contains twelve crates:

CrateResponsibility
qk-configWorkspace discovery, JSONC/YAML parsing, typed project configuration and normalization
qk-graphWorkspace edges, project selection, reverse reachability and graph export
qk-taskgraphDependency expansion, configuration selection and task DAG validation
qk-executorCommand preparation, environment, argument interpolation, process groups and output capture
qk-runnerBounded task scheduling, dependency failure propagation and cancellation
qk-lockfilepnpm v9 lockfile parsing and what each importer installs
qk-cacheInput hashing, cache entry storage, output restoration and log replay
qk-affectedGit change analysis and project/task affected selection
qk-historySQLite run history, task records and cache-key explanations
qk-input-analysisFile-access report schema, conservative suggestions and compatible observation unions
qk-traceThe eBPF recorder: its kernel program, privileged helper and strace-format traces
qk-cliArgument parsing and output; produces the qk binary

Fixture and CLI tests use self-contained workspaces.

Parity with Nx

tools/parity/parity.mjs measures qk against Nx. By default it uses the synthetic workspace in tools/parity/fixture and the goldens committed in tools/parity/goldens, captured with the Nx release pinned in tools/parity/nx. It needs Node, and pnpm for the cases that run package scripts:

node tools/parity/parity.mjs compare --qk target/release/qk

Each run builds a scratch git repository from the fixture. The cases in tools/parity/cases.json compare task graphs, show projects --affected for a change applied on a branch (the files in tools/parity/changes/<case>, plus any deletions the case lists), with the affected tasks of affectedTargets and the projects show projects --affected -t lists for them, by default and with nx.json affected.legacy, and runs, which execute tasks without cache and compare whether the run failed and which tasks ran. The comparison requires byte-identical show projects --json, a graph --file equal after normalisation, and --graph task graphs with the same tasks, with the same target, root, outputs and cache, continuous and parallelism flags, and the same dependencies and continuous dependencies. The differences it normalises away are listed at the top of the script. An affected or run case that differs fails unless it records the exact difference it accepts and why.

After changing the fixture or the pinned Nx, recapture and commit the goldens:

(cd tools/parity/nx && pnpm install)
node tools/parity/parity.mjs capture

capture --check captures into a scratch directory and fails when the committed goldens differ. CI runs both compare and capture --check.

The harness can also point at any workspace with its own Nx installation and git history, reading cases from <goldens>/parity.json, with affected cases given as base and head revisions:

node tools/parity/parity.mjs capture --workspace <dir> --goldens <dir>
node tools/parity/parity.mjs compare --workspace <dir> --goldens <dir> --qk target/release/qk

For an external workspace, capture also records Nx’s median wall times, and compare prints them beside qk’s.

Releases

Releases are cut with cargo-release (cargo install cargo-release --locked), configured under [workspace.metadata.release] in the root Cargo.toml. From an up-to-date main with a clean working tree:

cargo release <patch|minor|major>             # dry run: shows the new version
cargo release <patch|minor|major> --execute

This bumps the version shared by every crate, commits it as chore: Release, creates the single tag v<version> and pushes the commit and the tag. The tag then triggers the release workflow below, which is the only thing that publishes: the GitHub release, the npm packages and the crates.io crates. Never run cargo publish by hand, and do not push the tag before the version bump is on it, because the workflow rejects a tag that does not match the workspace version.

Pushing a tag v<version> that matches the workspace version runs .github/workflows/release.yml: it builds qk for Linux x64 and arm64, macOS arm64 and Windows x64 and attaches the binaries to a GitHub release. When the repository variable NPM_PUBLISH is true, the workflow publishes npm packages generated by cargo-npm 0.1.4 using trusted publishing (OIDC). Each package must trust the GitHub repository Pajn/qk and workflow release.yml, with permission to publish directly and no environment constraint. The npm job grants id-token: write; no npm token secret is needed. The main package is @runqk/cli, exposing the qk command. Its platform dependencies are:

  • @runqk/cli-linux-x64
  • @runqk/cli-linux-arm64
  • @runqk/cli-darwin-arm64
  • @runqk/cli-win32-x64

The names and targets are configured in crates/qk-cli/Cargo.toml under package.metadata.npm. Platform packages publish before the main package. The workflow checks that all four are present before publishing. With NPM_PUBLISH unset, GitHub binary releases still run and npm publishing is skipped.

The workflow also publishes every crate to crates.io through trusted publishing, with tools/release/publish-crates.sh. It publishes only the crates whose version crates.io does not hold yet, in dependency order. crates.io can be slow to list a crate it has just accepted, which fails the publishing of the crates depending on it, so the script tries again, publishing what is still missing. If publishing still stops partway, run the workflow by hand with the release’s tag (gh workflow run release.yml -f tag=v<version>): that run publishes the release’s missing crates and nothing else.

To inspect packages locally after compiling the configured release targets:

cargo install cargo-npm --version 0.1.4 --locked
cargo npm generate -p qk-cli

npm bootstrap and trusted publishing

The five npm packages start at version 0.0.0 under the bootstrap tag. These placeholders contain licensing and metadata but no native executable. They allow configuring trust before the first functional binary release. Their generator is separate from cargo-npm’s release packaging:

node tools/npm/bootstrap.mjs --repository Pajn/qk --out /tmp/qk-bootstrap

For initial setup, log in to npm locally with an account that owns the packages and has two-factor authentication enabled. Publish the four platform packages from that directory, then @runqk/cli, using npm publish <directory> --access public --tag bootstrap. Configure and verify trust for each package:

npm trust github <package> --repository Pajn/qk --file release.yml --allow-publish --yes
npm trust list <package>

These commands require npm 11.15.0 or newer and may request browser verification. If the default user configuration is read-only, select a writable login configuration with NPM_CONFIG_USERCONFIG=/path/to/.npmrc. After verifying all five trust entries, set the GitHub repository variable NPM_PUBLISH to true. Push the release workflow before creating a matching version tag. See npm’s trusted publishing documentation.

crates.io releases

The release workflow publishes all ten workspace crates to crates.io through OIDC, with qk-cli published after its internal dependencies. Install the CLI with cargo install qk-cli --locked. Internal crate dependencies use exact workspace versions so a release consistently uses the matching source. CI verifies the packaged distribution with cargo publish --workspace --locked --dry-run, including compiling the extracted crates without workspace paths.

CI and release builds also run installation smoke tests on all four release platforms, including Linux arm64. They pack the host packages generated by cargo-npm and install their tarballs offline into a fresh npm prefix. The installed command shim runs from an independent workspace whose paths contain spaces. The same checks run against an executable extracted from a binary archive: version, project discovery, strict doctor, task execution, cache restoration after deleting outputs, and invalidation after changing inputs. No packages are published by these tests. To run them locally:

cargo npm generate -p qk-cli --target <host-triple>
node tools/npm/smoke.mjs --packages npm --binary target/<host-triple>/release/qk

Use qk.exe on Windows. The release build must already exist for the host triple.

Trusted publishing must be configured for every workspace crate, since the short-lived token can only publish crates that trust this workflow. Each crate must trust GitHub owner Pajn, repository qk, and workflow release.yml, with no environment constraint. Configure these under each crate’s Settings → Trusted Publishing on crates.io. See crates.io’s setup guide.

crates.io requires an initial local publication before trust can be configured. qk-cli already has a metadata-only 0.0.0 reservation, whose isolated source is retained in tools/crates-io/qk-cli. The nine dependency crates also need an initial publication and trust configuration before the first workflow release. Generate their metadata-only 0.0.0 reservations with python3 tools/crates-io/bootstrap.py --out /tmp/qk-cargo-bootstrap, then publish each generated manifest locally with cargo publish --manifest-path <manifest>. These placeholders have no public API; functional releases use the workspace source and version. Subsequent releases need no long-lived Cargo token in GitHub secrets. The workflow verifies the packages before requesting its short-lived token.

Benchmarks

tools/bench/bench.mjs generates a workspace of 300 projects, 3,600 files and a 3,000-package pnpm lockfile, populates the cache, and measures with hyperfine: show projects, graph, show projects --affected with no changes, and a fully cached run-many, once with every output in place and once with every output removed first, so all 9,300 are restored. Both cached runs also run with .nxignore enabled to exercise filesystem walking and ignored generated files. A separate restoration matrix uses the same graph with both repeated and distinct blob contents, absent and stale complete output directories, and partial restoration with an excluded sentinel. Every measured restoration is followed by an untimed check of all output contents, stale-file removal, exclusion preservation and build counters, so cache misses cannot pass as restoration measurements. Preparation and verification are outside timing. Watch samples measure each source edit to the callback’s timestamp, with Git ignore rules and with .nxignore, including native event delivery, debounce, workspace reload and Node startup. Two initial samples are discarded; results are observations, with no timing thresholds in CI. It needs hyperfine, git and Node:

node tools/bench/bench.mjs --qk target/release/qk [--projects 300] [--runs 10] [--json out.json] [--markdown out.md]

CI runs it on every push, writing the table to the job summary and the numbers to an artifact.

GitHub Actions is configured for Linux, macOS and Windows. The lockfile is checked in for reproducible dependency resolution. On Windows, C dependencies are compiled with clang-cl (CC_x86_64_pc_windows_msvc=clang-cl), in CI and for releases alike: tree-sitter-scss, which fallout parses stylesheets with, passes its C compiler a GCC-style warning flag that cl.exe rejects.

See Maintaining the documentation for book builds and Pages setup.

The parity harness also captures isolated behavioral cases: direct/transitive selective dependency artifacts, broad output globs, .nxignore negations, stdin lists (including spaces and empty input), and watch callbacks queued while a command runs. Capture uses the pinned Nx daemon only for the watch oracle; qk remains daemon-free. Callback records live outside task inputs. Distinct edit timestamps avoid coarse metadata-cache timing artifacts. The Rust watch integration test runs on all three CI platforms; Windows uses a private console so cancellation cannot signal parallel tests.