Continuous integration
Running fabr in CI needs little setup: install Node.js and fabr, run the same commands you run locally, and keep fabr’s cache between runs. There is no dependency install step, because fabr fetches packages, and the Node.js release your tools and tests run on, as part of the build.
# GitHub Actionssteps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm install -g @fabr-build/cli@0.4.0 - uses: actions/cache@v4 with: path: ~/.cache/fabr key: fabr-${{ runner.os }}-${{ runner.arch }}-${{ github.sha }} restore-keys: fabr-${{ runner.os }}-${{ runner.arch }}- - run: fabr test --report reports/tests.xml --format junit @acme/core @acme/app - run: fabr build releaseInstall
Section titled “Install”Fabr needs Node.js 22.19 or later, and node on the PATH, to run itself. That is the only use
of the runner’s Node.js: your tools and tests run on the release the
NODE setting names, which fabr downloads and
keeps in its cache, so the version actions/setup-node installs doesn’t affect what is built.
Install a specific version of @fabr-build/cli, so that CI and your team use the same one; a
build file has no way to require a fabr version. fabr --version prints what is installed.
Choose what to run
Section titled “Choose what to run”Run the same commands you would locally, naming what the repository produces and what you want tested; fabr builds everything those depend on. There is no “everything” form, and there doesn’t need to be: a package nothing you name depends on isn’t needed for this commit’s result.
fabr testthe targets whose tests should pass. It builds what the tests need, so these targets need no separatefabr buildstep. It runs only the named targets’ tests, not those of the packages they depend on, so name each package whose tests you want run.fabr buildwhat you ship that has no tests: the application you deploy, or thesynctarget that publishes your packages. Building asynctarget produces exactly what would be uploaded, without uploading it.fabr buildyour checks: a lint or format check is a target like any other that fails the build when the check fails; see A step that checks.
When the list of tested targets is long, or shared with your team, declare it once as a property and name that:
TESTED = @acme/core @acme/app @acme/cli;fabr test TESTEDThe list is kept by hand, so add each new package to it. See Projects with several packages.
A command given no targets fails with The 'build' command requires a target, so a step whose
target list comes out empty, for example from an unset variable, fails and doesn’t pass by doing
nothing.
Exit status
Section titled “Exit status”Fabr exits with status 0 when everything named was built and every test passed, and with 1 for
anything else, a failing test and a build error alike; the output says which. fabr audit exits
with 1 when it finds an advisory. A run interrupted by SIGINT, SIGTERM or SIGHUP stops its
running steps first and exits with 130, 143 or 129. The
command-line reference lists every status.
fabr test runs every named target’s tests to the end, writes its reports and only then exits
with 1, so a step after it that collects results gets them all; see Test results.
Output
Section titled “Output”Without a terminal, fabr writes a plain log to stderr, one line when each step starts and one when it ends:
info:Compiling @acme/core (required by @acme/app)info:✓ Compiling @acme/core (734ms)info:Testing @acme/coreinfo:✓ Testing @acme/core (413ms)info:@acme/core: 12 tests passedinfo:Built @acme/coreOutput from the tools that steps run is included as it arrives, each line marked with the target
it belongs to (@acme/core out| …). Errors are printed as blocks pointing at the line of the
build file or source responsible, and the log ends with error:Build failed.
- Colour is off when stderr isn’t a terminal, and can’t be forced on.
NO_COLORturns it off on a terminal. -qleaves out the start lines and the tools’ output, keeping completion lines and errors. A step that fails still shows its output.- There is no machine-readable log, and no annotations for GitHub or other CI systems.
Data that a command prints, such as a fabr ls listing or fabr cat contents, goes to stdout,
separate from the log.
Fabr’s cache holds downloaded packages and the result of every build step and test run. Keeping it between CI runs is what makes them fast: an unchanged project downloads and rebuilds nothing, and a change rebuilds only what depends on it.
The cache is a directory on the machine: ~/.cache/fabr on Linux and ~/Library/Caches/fabr on
macOS. To put it somewhere else, set FABR_CACHE_DIR, and keep it outside the project directory
so that no source pattern can match its contents. Keep it on a local disk: like most build
caches, it relies on renames being atomic, which network filesystems such as NFS don’t reliably
guarantee. There is no shared or remote cache for several machines to use at once, so in CI you
save the directory at the end of a run and restore it at the start of the next.
Saving it
Section titled “Saving it”- Save it on every run, with a key that changes each time and a prefix to restore from, as in the example at the top of this page. A cache saved once and never updated stops matching as the project changes.
- Key it by operating system and architecture. Platform-specific packages, the Node.js release, and the results built with them differ between platforms, so a cache shared across them mostly grows. The runner’s own Node.js version doesn’t need to be in the key.
- Save it after
fabrhas finished, not while it is running. A finished run leaves nothing half-written behind. - Use an archive format that keeps hardlinks and modification times. The cache stores each
unpacked package file once and hardlinks it into place, and it judges how recently an entry
was used by its modification time.
tarkeeps both, soactions/cacheand other tar-based caches need nothing special.- A zip-based cache, or a copy made without
rsync -Horcp -a, turns each hardlink into a second copy. The restored cache still works, but it is larger, and stays larger. - An archive that resets modification times stops the automatic trimming described below from removing anything.
Restoring it
Section titled “Restoring it”A restored cache can’t make a build produce the wrong result by being out of date. A cached result is used only when the inputs that produced it are unchanged, so a cache from an older commit, another branch or another machine is used where it still applies and ignored where it doesn’t. A missing or damaged entry is rebuilt, and deleting the whole cache is always safe.
Two things qualify that:
- Fabr trusts the contents of its cache. It doesn’t check a restored entry against anything, so whoever can write the cache can decide what a build produces. Don’t restore, into a job that publishes or deploys, a cache that an untrusted job could have saved. On GitHub, a pull request from a fork can’t write to the base branch’s cache; with other systems and self-hosted runners, check how caches are scoped, or publish from a job that starts with an empty cache.
shisn’t tracked by version. Ascripttarget runs on the runner’ssh, and a cache restored into a job with a different shell, or different programs for a script to start, serves results built with the old ones. See Known limitations.
Fabr trims the cache itself. About once a day, at the end of a run, it removes entries that haven’t been used for 30 days. There is no command or setting for this. In CI the trimmed cache only replaces the stored one if that run’s cache is saved, which is another reason to save on every run.
fabr run also keeps the installed files of each program it runs in the cache, to reuse on the
next run. In CI they add to the size of the saved cache, and are trimmed like anything else once
they go unused for 30 days.
Test results
Section titled “Test results”fabr test prints a summary for each name you give and the details of each failure, and fails
when any test failed. To give your CI system the results too, write them to a file with
--report: as JUnit XML, which CircleCI’s store_test_results, GitLab, Jenkins and most others
read, or as a
CTRF report:
fabr test --report reports/tests.xml --format junit TESTEDThe file holds the results of every target tested, one test suite per test file, with each failed test’s message. It is written whether or not the tests pass, before the command fails for any that didn’t, so collect it in a step that runs after a failure too. A build error that stops the run before the tests finish writes no file.
# GitHub Actions: keep the report even when the tests fail - run: fabr test --report reports/tests.xml --format junit TESTED - uses: actions/upload-artifact@v4 if: always() with: name: test-results path: reports/A failed test run is cached like a passing one, so running the same commit again, as a CI retry
does, reports the same failures without running the tests. To run them again, for a test you
suspect is flaky, add --retry-failed.
Each target’s own report is also kept as ctrf-report.json, which fabr cp copies out, failures
and all:
fabr cp '@acme/core<BUILD_OPERATION=test>:ctrf-report.json' reports/coreCoverage
Section titled “Coverage”fabr test --coverage also measures code coverage. It prints a summary line and writes an lcov
report, which Codecov, Coveralls, SonarQube and most CI coverage tools read:
fabr test --coverage=reports/coverage TESTEDThis writes reports/coverage/lcov.info, with each source named by its path in your project;
--coverage alone writes coverage/lcov.info. The directory is relative to the current
directory. Coverage is reported whether or not the tests pass. A run with
--coverage is cached separately from one without it, so a job that always passes --coverage
reuses its own earlier results. Testing describes what is
measured.
Security advisories
Section titled “Security advisories”fabr audit checks the packages your targets depend on against the OSV
database and exits with status 1 when any advisory affects one of them; see
Auditing dependencies for what it checks and how to act on a finding. In CI,
write the report as SARIF and upload it, so that each advisory appears as a code-scanning alert
at the line of your build file that brings the package in:
# GitHub Actions: show advisories as code-scanning alerts - run: fabr audit --format sarif @acme/app > audit.sarif continue-on-error: true - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: audit.sarifDrop continue-on-error to fail the job instead. An audit that couldn’t run, for instance because
the advisory database couldn’t be reached, also exits with 1 and leaves audit.sarif empty, so
continue-on-error hides that failure from the audit step; the error is in its log. Advisories are read fresh on each run and never cached, so a commit that
passed yesterday can fail today when a new advisory is published; run the audit in a job of its
own, or on a schedule, if that shouldn’t block a merge.
Software bill of materials
Section titled “Software bill of materials”fabr sbom writes a CycloneDX bill of materials of a target’s
dependencies to stdout. Keep it with each release, or hand it to a dependency-tracking service:
# GitHub Actions: keep the SBOM with the build - run: fabr sbom -q @acme/app > app.cdx.json - uses: actions/upload-artifact@v4 with: name: sbom path: app.cdx.jsonIt downloads no packages, and the same sources give the same file. See Writing a bill of materials.
Finding out where a run’s time goes
Section titled “Finding out where a run’s time goes”--trace on fabr build, fabr test or fabr run writes a timeline of the work the run did,
which Perfetto opens. Keep it as an artifact to see why a CI run was
slow: which steps ran, what ran in parallel, and where the run waited.
fabr test --trace=reports/trace.json TESTEDWork served from the cache doesn’t appear in the trace. See Tracing a run.
Network access
Section titled “Network access”Fabr contacts the npm registry (or the registry NPM_REPOSITORY_URL names), the hosts that
registry’s packages are downloaded from, nodejs.org and unofficial-builds.nodejs.org for the
Node.js release, and the URLs of any fetch targets. A Java build also contacts Maven Central (or
the repository MAVEN_REPOSITORY_URL names) and api.adoptium.net for the JDK, and fabr audit
contacts api.osv.dev.
- A build whose packages and Node.js release are all in the cache makes no requests.
Published versions are never re-fetched. The exception is a list that can change, such as
nodejs.org’s list of releases when
NODEnames a version range, or a Maven package’s versions when a requirement names a range: fabr checks it again at most once a day, or on every run given--refresh. Write exact versions to avoid it. - Proxies are taken from
HTTP_PROXY,HTTPS_PROXYandNO_PROXY. - Private registries take credentials from
.npmrc, where a value can come from the environment://npm.example.com/:_authToken=${NPM_TOKEN}. See Private registries. - A download that fails transiently is retried twice, after one second and then four: a
connection that is refused, drops or times out, or a
408,429or5xxresponse. Any other failure, or a third failure, fails the build; run it again, and what was downloaded before the failure is kept.
Build steps and the environment
Section titled “Build steps and the environment”Build steps and tests don’t see the CI environment. They run with no environment variables other
than the ones a target declares (test_env, or env on a generate), so a secret or a CI
variable has to be passed deliberately. fabr run is the exception: the program it runs gets the
whole environment.
Fabr runs as many steps at once as the machine has processors, and each test file is one of those
steps. There is no option to lower that number. On a runner with many processors and little
memory, limit each test process instead, with NODE_OPTIONS in test_env.
Publishing from CI
Section titled “Publishing from CI”fabr sync publishes packages; see Publishing packages. In CI:
- Make sure each
syncentry requires a release build (= @acme/core<BUILD_TYPE=release>;), so that the published packages don’t depend on how the step is invoked. - Provide a token through
.npmrcthat can publish without a one-time password. With no terminal fabr can’t ask for one. - Running
fabr syncagain after a partial failure is safe: versions already published are skipped.
Not supported
Section titled “Not supported”- A shared or remote cache.
- A limit on parallel steps.
- A machine-readable log, or CI annotations.
--reportand--tracewrite the test results and the timeline as files, but the log itself is plain text. - A report of outdated dependencies.