Skip to content

Continuous integration

The whole CI surface is the exit code: 0 if nothing is over the threshold, 1 if something is. No reporter to configure, and on GitHub Actions the annotations are built in.

.github/workflows/complexity.yml
name: Complexity
on: [push, pull_request]
jobs:
bonsai-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx bonsai-lint --format github .

No toolchain setup, no caching worth bothering with, no matrix. npx fetches a binary under 2 MB and the scan itself is fast enough that it is not the thing you wait for. A monorepo of more than a million lines, in four languages, scans in about a second.

--format github prints each breach as a workflow command, so it shows as an annotation on the pull request, and fails exactly when the text report does. A marker without a reason warns on its unit, a loose baseline entry is filed on its baseline file, and config warnings and read errors on the run. Paths are made relative to GITHUB_WORKSPACE, so the step can run from any directory; a file outside it is annotated on the run. A repository checked out into a subdirectory with path: is annotated under that subdirectory, which GitHub cannot place. GitHub annotates at most 10 errors per step; the log lists every one.

A pull request’s changed TypeScript file with two annotations on one line: a bonsai-lint breach as a check failure, and a warning that its suppression marker needs a reason

.gitlab-ci.yml
complexity:
image: node:22-alpine
script:
- npx bonsai-lint .

Most existing codebases have findings on day one. Gating on the absolute number means either a threshold so high it catches nothing, or a red build nobody can fix this sprint.

Record what exists, commit it, and the gate then only fires on things that get worse:

Terminal window
npx bonsai-lint --write-baseline .
git add .bonsai-lint-baseline.json

CI runs the same command it always did. A unit already in the baseline passes at its recorded score or below; anything new, or anything that climbed, fails. With --format github, a unit that climbed is annotated with both scores:

A pull request’s new PHP file with a check failure on Checkout::applyDiscounts, which scores 28, above its baselined 21

See baselines for how entries are keyed and what happens when code moves.

To make the baseline tighten as the code improves, add --strict-baseline, or set strict-baseline = true in bonsai-lint.toml. An entry that accepts more than the code now needs then fails the run until the baseline is rewritten over the whole workspace.

Exit 1 does not only mean “too complex”, or a strict baseline accepting more than the code needs. It also means the scan could not be trusted: a path that could not be read, or no supported files found under the paths given.

That is deliberate. A quality gate that silently checks nothing when it is misconfigured is worse than no gate, because it reports success. If you point the command at a directory that does not exist, you want a red build, not a green one.

For anything that needs to consume results rather than just pass or fail:

Terminal window
npx bonsai-lint --format json . > complexity.json

The shape is documented in JSON output. Findings are sorted worst-first and the ordering is deterministic, because the report is assembled after the scan rather than streamed as results arrive. Two runs of the same commit are byte-for-byte identical regardless of how many workers ran. A diff between runs is always a real change.