CI integration

Recipes for wiring bca into a build pipeline. The bca check command already ships every output shape a modern CI needs (Checkstyle, SARIF, GitLab Code Climate JSON, clang/GCC warning lines, MSVC warning lines), plus bca report markdown for humans. This page is a consolidated map from the user's goal to the right combination of subcommand, flags, and platform glue.

Picking outputs

The matrix below maps each common goal to the bca invocation that feeds the corresponding CI surface. Linked sections below have the runnable example.

GoalCommand + flags
Hard gate on threshold regressionsbca check (thresholds from the auto-discovered bca.toml)
Ratchet thresholds on an existing codebasebca check --baseline .bca-baseline.toml (‡)
Inline PR annotations (GitHub)bca check … --report-format clang-warning --no-fail + GCC problem matcher
Code Scanning alerts (GitHub)bca check … --report-format sarif --no-fail + github/codeql-action/upload-sarif
Merge-request widget (GitLab Code Quality)bca check … --report-format code-climate --no-fail
Jenkins / SonarQube ingestionbca check … --report-format checkstyle
Human-readable PR/MR comment or downloadablebca report -O markdown --top 20 --strip-prefix "$PWD/"
Machine-readable artifact for dashboardsbca metrics --format json --output-dir ./out

(‡) Recommended adoption path when introducing thresholds on a codebase with existing offenders. See the Baselines recipe for the bootstrap-refresh-retire workflow.

The full reference for bca check's output formats, exit codes (0 clean, 2 violation, 1 tool error), and threshold config lives in the Check command page. For the Markdown report shape, see the Report command page and the Quality reports recipe.

GitHub Actions

Live worked example

big-code-analysis runs the recipes below against its own source on every push and PR. The workflow source — .github/workflows/pages.yml — exercises the threshold gate, the baseline ratchet, both report formats, and a SARIF upload to GitHub Code Scanning end-to-end against the workspace itself. (The SARIF upload runs on same-repo pushes and PRs only; fork PRs skip it because the upload needs a write-scoped token, exactly as the clippy SARIF job does.) The output sits on GitHub Pages alongside this book:

Copy snippets below straight into your own workflow; the bca version quoted is the latest published release at the time of writing.

The in-tree workflow installs bca by building it from the current checkout rather than downloading a pinned release — this avoids the CLI-artifact schema-skew failure mode described under Installing bca from a GitHub Release below for repos whose .bca-baseline.toml is always written by the same bca that gates it. Downstream adopters tracking a stable release line should stick with the pinned-tarball pattern; only switch to "build from checkout" if you, too, are mutating CLI artifact schemas in lockstep with the binary.

Threshold gate, SARIF, and clang-warning matcher

The three pre-existing recipes — hard threshold gate, SARIF upload to Code Scanning, and clang-warning + GCC problem matcher for inline PR annotations — live in the Check command page. Use the link rather than re-implementing them here.

The fastest, most reproducible install path is the prebuilt tarball from this repository's GitHub Releases. It is a single curl | sha256sum | tar, requires no Rust toolchain, and produces byte-identical binaries across runs. Pair it with actions/cache keyed by version so a green-path rerun skips the download entirely:

CLI-artifact schema compatibility. The BCA_VERSION you pin here must support the schema version of every CLI artifact your repo commits — most importantly .bca-baseline.toml (carries its own version field) and the bca.toml manifest. A baseline file written by a newer bca (carrying a newer schema version) is not loadable by an older bca and the gate will fail with baseline version N is not supported by this bca. When tracking main or regenerating baselines locally with a newer bca, either re-pin to a release that covers the new schema or switch to a cargo install --git build of bca pointed at the same commit your baseline was written from (see the cargo install alternative below). The compatibility contract is recorded in STABILITY.md.

env:
  BCA_VERSION: "2.0.0"
  BCA_TARGET:  "x86_64-unknown-linux-gnu"
  # sha256 of big-code-analysis-${BCA_VERSION}-${BCA_TARGET}.tar.gz from the
  # release's SHA256SUMS file. Bump together with BCA_VERSION.
  BCA_SHA256:  "a205fff13108d0f8c679a062e352ba8468109c4adfdd8c9e3567cf5fcc99c3d5"

steps:
  # Cache key MUST include BCA_SHA256 (and BCA_TARGET). Without the
  # sha256 in the key, rotating the published checksum without bumping
  # the version returns a stale binary on cache hit and silently
  # bypasses the `sha256sum --check` in the install step (which is
  # gated on cache miss). Including BCA_TARGET matters when the same
  # workflow runs against multiple `runs-on`.
  - name: Cache bca binary
    id: bca-cache
    uses: actions/cache@v5
    with:
      path: ~/.local/bin/bca
      key: bca-${{ runner.os }}-${{ env.BCA_TARGET }}-${{ env.BCA_VERSION }}-${{ env.BCA_SHA256 }}

  - name: Install bca from GitHub Releases
    if: steps.bca-cache.outputs.cache-hit != 'true'
    run: |
      set -euo pipefail
      stage="big-code-analysis-${BCA_VERSION}-${BCA_TARGET}"
      tarball="${stage}.tar.gz"
      url="https://github.com/dekobon/big-code-analysis/releases/download/v${BCA_VERSION}/${tarball}"
      mkdir -p "$HOME/.local/bin"
      curl -fsSL --proto '=https' --tlsv1.2 -o "/tmp/${tarball}" "$url"
      echo "${BCA_SHA256}  /tmp/${tarball}" | sha256sum --check --strict -
      tar -xzf "/tmp/${tarball}" -C /tmp
      install -m 0755 "/tmp/${stage}/bca" "$HOME/.local/bin/bca"
      rm -rf "/tmp/${tarball}" "/tmp/${stage}"

  - name: Prepend ~/.local/bin to PATH
    run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"

Available BCA_TARGET values (pick the one that matches runs-on): x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl, aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc. Windows assets use .zip instead of .tar.gz; the bca-web binary ships alongside bca in the same archive.

Alternative: cargo install via prebuilt-aware actions

When you cannot reach github.com from a runner (air-gapped, custom mirror) but can reach crates.io, the following two actions fall back transparently to cargo install when no prebuilt is published — at the cost of compile time on the cold path. Both pin to the same crates.io release as the GitHub Releases assets, so the CLI-artifact schema compatibility warning applies here unchanged.

If you specifically need a bca ahead of the latest crates.io release (e.g., your .bca-baseline.toml is committed at a newer schema than any published bca understands), swap the tool: big-code-analysis-cli@<version> or --version form for cargo install --git https://github.com/dekobon/big-code-analysis --rev <SHA> --locked big-code-analysis-cli against the exact commit the baseline was generated from. This is what the in-tree pages.yml workflow does (against the local checkout via --path) — it is a deliberate workaround for bca's own repo, not a recommended default for downstream adopters.

# Option 1: taiki-e/install-action
- name: Install bca
  uses: taiki-e/install-action@v2
  with:
    tool: big-code-analysis-cli@2.0.0
# Option 2: cargo-binstall
- name: Install cargo-binstall
  uses: cargo-bins/cargo-binstall@main
- name: Install bca
  run: cargo binstall --no-confirm big-code-analysis-cli --version 2.0.0

If either action falls back to compilation, cache the cargo registry + the installed binary so the second run is fast:

- name: Cache cargo registry and bca binary
  uses: actions/cache@v5
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      ~/.cargo/bin/bca
    # crates.io publishes immutable releases, so a `<version>` key is
    # sufficient here — there is no sha256 to rotate. (The GitHub
    # Releases install path above is different: republished release
    # assets share a version, so its cache key must include the sha256.)
    key: bca-${{ runner.os }}-2.0.0

Pin to a specific version (matching a published big-code-analysis-cli release on crates.io) so reports stay reproducible across runs. A floating install surfaces metric-counting changes as "mysterious CI flakes" on Mondays.

Posting the Markdown report as a PR comment

bca report markdown is purpose-built for PR/MR comments: a stable header structure, one row per hot spot, and short paths once you pass --strip-prefix. Pair it with marocchino/sticky-pull-request-comment so each push updates a single comment instead of stacking new ones:

name: bca-pr-report
on:
  pull_request:
    branches: [main]
jobs:
  report:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - name: Install bca
        uses: taiki-e/install-action@v2
        with:
          tool: big-code-analysis-cli@2.0.0
      - name: Generate report
        run: |
          bca \
            report -O markdown \
            --paths "$PWD" \
            --top 20 \
            --strip-prefix "$PWD/" \
            --output report.md
      - name: Post or update PR comment
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          path: report.md
          header: bca-quality-report

The same Markdown file is suitable for upload as a build artifact (actions/upload-artifact@v7) if you want it downloadable from the workflow run page in addition to the PR comment.

Baseline / ratchet pattern

bca check --baseline is the native ratchet: record today's offenders in a committed TOML file, fail only on regressions and new offenders, and shrink the file over time. Bootstrap once, commit, then point CI at it:

# Once, on a developer machine. Commit both files.
bca check --paths src/ \
    --write-baseline .bca-baseline.toml
git add bca.toml .bca-baseline.toml

This snippet bootstraps from src/ only — appropriate for a single-crate library. For a multi-crate workspace, see the live worked example: its .github/workflows/pages.yml scans the entire repo with --exclude-from .bcaignore, a checked-in deny-set covering vendored grammars, generated trees, and tests.

Share the exclude list across workflow, recipe, and bootstrap. Put the deny-set in a single file at the repo root (a .bcaignore by convention, mirroring .gitignore / .dockerignore) and point every bca invocation at it with --exclude-from .bcaignore. Patterns from --exclude-from are unioned with any inline --exclude <GLOB> flags into one deny-set — keep --exclude for one-off ad-hoc excludes. Blank lines and #-prefixed comment lines in the file are skipped. Patterns follow the same ./-prefix convention as --exclude arguments (the walker's emitted form). Pair edits to .bcaignore with a --write-baseline refresh — the baseline keys are sensitive to which files the walker visits.

- name: Threshold check with baseline
  run: |
    bca check --paths src/ \
        --baseline .bca-baseline.toml

A regressed function (current value > baseline value) still fails. A new offender not in the baseline still fails. An improved function passes silently and stays in the baseline until the next --write-baseline refresh.

Each surviving violation in the stderr stream is prefixed with a tag so a developer can tell at a glance whether they are looking at a brand-new offender or a known one that has worsened:

  • [new] — no baseline entry for this function / metric.
  • [regr +N%] — current value exceeds the recorded baseline by N percent. Special forms: [regr from 0] when the baseline value was zero, [regr +>9999%] when the regression exceeds 100× the baseline, [regr NaN] when the current value is NaN.

After the per-violation lines the stderr stream emits a per-file rollup footer with the format <path>: <count> violations (worst: <metric> = <value> vs limit <limit> at L<start>), sorted by violation count descending. This is intended to be the first thing a reader looks at: which file has the most problems, and which metric is the loudest in that file. Pass --no-summary to suppress the footer for downstream tooling that grep-pipes the stderr stream.

Actionable failure output

The four sub-sections below turn bca check's failure output from "a wall of offender lines" into a stack of CI-aware presentations: which files in this PR tripped a threshold (--since / --changed-only), inline file-diff annotations (--github-annotations), a rendered step-summary digest ($GITHUB_STEP_SUMMARY), and a copy-paste-safe remediation block. Each is independent; mix and match per CI surface. A combined worked example is at the end of this group.

Diff-aware mode (--since / --changed-only)

On a PR or push, the developer's first question is usually which of my files in this change tripped a threshold — not the whole-tree offender list. Two flags answer that:

  • --since <ref> partitions the per-file footer into a "Files in this range:" section (offenders in files touched between <ref> and HEAD) followed by "Other offenders:" (everything else). Per-violation lines are unchanged so existing grep-anchored tooling keeps working.
  • --changed-only drops violations from files outside the touched set entirely. Use it for PR gates that should be terse.
- uses: actions/checkout@v4
  with:
    # `--since origin/<base>` resolves a merge-base. The default
    # `fetch-depth: 1` checkout makes that ref unreachable; `0`
    # pulls the full history so the diff base resolves.
    fetch-depth: 0
- name: Threshold check with diff-aware footer
  run: |
    bca check --paths . --exclude-from .bcaignore \
        --baseline .bca-baseline.toml \
        --since "origin/${{ github.base_ref }}"

When --since is omitted, bca auto-detects the diff base from the environment in this precedence:

  1. BCA_DIFF_BASE — the explicit-override hatch. Use this from a local shell or non-GHA CI runner to mimic the auto-detection.
  2. GITHUB_BASE_REF — set by GitHub Actions on pull_request events. Expanded to origin/<value>; the runner is responsible for the corresponding git fetch.
  3. GITHUB_EVENT_BEFORE — set by GitHub Actions on push events to the SHA at HEAD before the push. The all-zeroes SHA (force push, brand-new branch) is treated as no signal.

Auto-detection failing — git missing, ref unresolvable, not a git checkout — is non-fatal without --changed-only: bca prints a warning and falls back to today's whole-tree footer. With --changed-only, the same failure is fatal so a misconfigured CI does not silently green-light by suppressing every violation.

The "Files in this range:" banner names the resolved base and the signal that produced it, so a CI-log reader can verify the gate latched onto the expected ref:

Files in this range (diff base: origin/main via GITHUB_BASE_REF):
./src/a.rs: 1 violation (worst: cyclomatic = 11 vs limit 2 at L1)

Other offenders:
./src/b.rs: 1 violation (worst: cyclomatic = 11 vs limit 2 at L1)

This is distinct from bca diff-baseline, which diffs baseline files between two on-disk paths and reports added / removed / worsened / improved entries. --since diffs source files between two git refs.

GitHub Actions inline annotations (--github-annotations)

The GHA UI renders ::error file=…,line=…,title=…::msg workflow commands as inline annotations on the file-diff view — much more discoverable than scrolling the raw job log. bca check emits one per violation per the tri-state --github-annotations <auto|always|never>: auto (the default) enables when $GITHUB_ACTIONS == "true" (set by every GHA workflow step), always forces them on, never suppresses them even inside a step (handy when a workflow runs bca check twice and wants annotations from only one run). A bare --github-annotations means always.

The annotations ride on top of the existing per-violation human stream — both are emitted. To avoid exhausting GitHub's 10-error-per-step UI quota, annotations are capped at 10 per metric; overflow rolls up to a single ::error::N more <metric> violations not shown line so the count stays visible.

- name: Threshold check with inline annotations
  run: |
    bca check --paths . --exclude-from .bcaignore \
        --baseline .bca-baseline.toml
  # No `--github-annotations` flag needed — auto-enabled in GHA.

Pair this with --since (above) so the annotations point at the files in the PR, not the entire offender list.

Step-summary markdown digest ($GITHUB_STEP_SUMMARY)

GitHub Actions exposes $GITHUB_STEP_SUMMARY — a path to a markdown file that, when populated, renders as the step's summary view in the job UI. bca check appends a digest containing the per-file rollup table, a per-metric count breakdown, and the top-10 offenders by value / limit ratio whenever that env var is set, or when --summary-file <path> is passed explicitly. --summary-file never suppresses the digest even when $GITHUB_STEP_SUMMARY is set.

The digest is bracketed by HTML-comment markers (<!-- bca-step-summary-begin --> / <!-- bca-step-summary-end -->) so a retried step replaces (not stacks) the previous block — three retries converge to exactly one up-to-date digest. Content outside the markers (e.g. summaries written by other tools earlier in the same step) is preserved.

- name: Threshold check with step-summary digest
  run: |
    bca check --paths . --exclude-from .bcaignore \
        --baseline .bca-baseline.toml
  # No flag needed — `$GITHUB_STEP_SUMMARY` is set automatically in GHA.

Local users can pipe the digest into any markdown file with --summary-file <path>. Empty input (clean run) still writes a "✓ No threshold violations." block so the step summary positively confirms the gate ran.

When the gate finds violations, bca check emits a trailing --- next steps --- block on stderr (and inside the step-summary digest from above):

--- next steps ---
* Detailed reports: bca-reports artifact at https://github.com/<owner>/<repo>/actions/runs/<run-id>
* To refresh baseline: bca check --paths . --exclude-from .bcaignore --write-baseline .bca-baseline.toml
* Adoption guide: https://dekobon.github.io/big-code-analysis/recipes/baselines.html

The refresh invocation mirrors the gate's resolved --paths, --exclude, --exclude-from, --config, and --baseline so a first-time reader of a failing CI log can copy-paste it verbatim. The artifact URL is derived from $GITHUB_REPOSITORY and $GITHUB_RUN_ID when both are present (always true in GHA); local runs — where there is no upload to point at — instead suggest running bca report to see the detailed view locally.

Suppress the block with --no-remediation for downstream tooling that grep-pipes stderr.

Refresh after focused refactors:

bca check --paths src/ \
    --write-baseline .bca-baseline.toml
git diff .bca-baseline.toml   # expect a shrinking file

Two --write-baseline runs over an unchanged tree produce byte-identical output, so spurious diffs only appear when offenders actually changed. See the Baselines recipe for the full adoption flow, PR-review heuristics, and the suppression composition rules.

Putting it all together

The four flags above compose. For a PR-gate workflow, the recommended invocation is:

- uses: actions/checkout@v4
  with:
    # `--since origin/<base>` resolves a merge-base. Default
    # `fetch-depth: 1` makes that ref unreachable; `0` pulls the
    # full history so the diff resolves.
    fetch-depth: 0

- name: Threshold gate (diff-aware + GHA UX)
  run: |
    bca check --paths . --exclude-from .bcaignore \
        --baseline .bca-baseline.toml \
        --since "origin/${{ github.base_ref }}"
  # No `--github-annotations` or `--summary-file` flag needed:
  # both auto-enable from `$GITHUB_ACTIONS == "true"` and
  # `$GITHUB_STEP_SUMMARY`. The trailing remediation block is also
  # auto-emitted.

What this gives you on a failing PR:

  1. Per-violation stderr lines — same shape as the legacy gate, so existing grep tooling keeps working.
  2. Per-file rollup footer with Files in this range: (touched in the PR) listed before Other offenders: — the developer sees their own contributions first.
  3. Inline GHA annotations on the file-diff view, capped at 10 per metric with an overflow rollup.
  4. Step-summary panel with a rendered markdown digest (per-file rollup, per-metric breakdown, top-10 offenders by ratio).
  5. Trailing remediation block naming the artifact, printing the exact --write-baseline refresh invocation, and linking to the Baselines recipe.

Knobs:

FlagEffectDefault
--since <ref>Partition footer; auto-detect from env if omittedOff, auto-detect via BCA_DIFF_BASE / GITHUB_BASE_REF / GITHUB_EVENT_BEFORE
--changed-onlyDrop violations outside the diff entirelyOff
--github-annotations <auto|always|never>Emit ::error file=…::msg workflow commands (bare flag = always)auto enables when $GITHUB_ACTIONS == "true"; never opts out
--summary-file <path|auto|never>Append markdown digest; never opts outauto detects $GITHUB_STEP_SUMMARY
--no-remediationSuppress the trailing --- next steps --- blockBlock emitted on failure

Local users running bca check outside GHA see no change in default behaviour: none of the four features auto-enable without an env signal. To preview the GHA experience locally:

GITHUB_ACTIONS=true GITHUB_STEP_SUMMARY=/tmp/bca-summary.md \
  BCA_DIFF_BASE=main \
  bca check --paths . --exclude-from .bcaignore \
      --baseline .bca-baseline.toml
cat /tmp/bca-summary.md

For a non-GHA CI (GitLab, Buildkite, Jenkins), set the env vars your runner exposes (or pass the flags explicitly) and the same output paths fire.

Offender-count delta against merge base (stopgap)

For teams who cannot commit a baseline file (e.g. policy reasons), a coarser approximation counts <error> elements in two Checkstyle documents — one on the merge base, one on the PR head — and fails when the count grows:

- name: Compute offender deltas vs. merge base
  run: |
    set -euo pipefail
    BASE="$(git merge-base origin/main HEAD)"
    git worktree add /tmp/base "$BASE"

    bca check --paths /tmp/base \
        --report-format checkstyle \
        --output /tmp/base.xml \
        --no-fail
    BASE_COUNT=$(grep -c "<error" /tmp/base.xml || true)

    bca check --paths "$PWD" \
        --report-format checkstyle \
        --output /tmp/head.xml \
        --no-fail
    HEAD_COUNT=$(grep -c "<error" /tmp/head.xml || true)

    echo "Offenders: base=$BASE_COUNT head=$HEAD_COUNT"
    if [ "$HEAD_COUNT" -gt "$BASE_COUNT" ]; then
      echo "::error::Offender count grew from $BASE_COUNT to $HEAD_COUNT"
      exit 1
    fi

This counts violations, not their identity: renaming an offender does not register as a regression, and improving one offender while regressing another nets to zero. The native baseline flow above is strictly more precise and is the recommended approach.

Self-scan threshold gate (local mirror of the CI gate)

CI's threshold gate fires only after push, which is too late if a refactor silently nudged a metric past its limit. The big-code-analysis repo's Makefile exposes four targets that mirror the CI gate (the Threshold gate step in .github/workflows/pages.yml) locally and add a second tier at 95% of every limit so encroachment is caught a commit or two before the hard gate trips:

make self-scan                            # hard gate, 100% of bca.toml thresholds
make self-scan-headroom                   # soft gate, default 95% (BCA_HEADROOM)
make self-scan-write-baseline             # refresh baseline at hard thresholds
make self-scan-write-baseline-headroom    # refresh baseline at soft thresholds

Path selection, the .bcaignore deny-set, the per-function thresholds, the cyclomatic ? policy, and the baseline file all live in the repo-root bca.toml manifest, which bca discovers automatically. The hard tier is exactly what CI runs; expanded, it is a bare check (no path / threshold / baseline flags — the manifest supplies them):

cargo run --quiet --release -p big-code-analysis-cli -- check

Both tiers consume the same bca.toml thresholds and the same .bca-baseline.toml; the soft tier just runs the hard recipe with every threshold value multiplied by BCA_HEADROOM. Both exit 0 clean, 2 on any threshold violation, 1 on tool error — the soft tier is a real gate, not advisory, so do not wrap make self-scan-headroom in || true. The two gate targets (self-scan, self-scan-headroom) are wired into make pre-commit, make ci, and .pre-commit-config.yaml; those chains run the hard tier before the soft tier, so a true regression always reports before near-limit headroom. The two write-baseline targets are side-effecting and deliberately not wired in.

BCA_HEADROOM=0.90 make self-scan-headroom widens the band; BCA_HEADROOM=0.99 tightens it to the last 1%. When the soft tier fires, absorb the offender into the baseline with make self-scan-write-baseline-headroom (which records every offender at the scaled thresholds — strictly a superset of the hard-tier offenders).

The pattern (hard tier mirroring CI + soft tier as early-warning band, both ratcheted by the same baseline) is project-agnostic — the Local threshold gates recipe documents the underlying principles, drop-in Makefile / just / package.json skeletons, and the helper script that scales thresholds, so you can adopt the same workflow in your own repo. The generic recipe uses the same BCA_* env-var names as the Makefile above, so overrides like BCA_HEADROOM=0.90 work identically across both.

GitLab CI

Full .gitlab-ci.yml example

The job below installs bca, runs the threshold check producing Code Climate JSON (for the MR Code Quality widget), Checkstyle XML, and a Markdown report, then uploads them as artifacts.

The same CLI-artifact schema-compatibility note from the GitHub Actions section applies here — the BCA_VERSION pin must cover the schema version of every CLI artifact you commit.

stages:
  - quality

variables:
  BCA_VERSION: "2.0.0"  # pin a published big-code-analysis-cli release
  BCA_TARGET:  "x86_64-unknown-linux-gnu"
  # sha256 of big-code-analysis-${BCA_VERSION}-${BCA_TARGET}.tar.gz from
  # the release's SHA256SUMS file. Bump together with BCA_VERSION.
  BCA_SHA256:  "a205fff13108d0f8c679a062e352ba8468109c4adfdd8c9e3567cf5fcc99c3d5"

bca-quality:
  stage: quality
  image: debian:stable-slim
  cache:
    # Same key shape as the GitHub Actions snippet — bumping
    # BCA_VERSION invalidates the cache automatically.
    key: "bca-$BCA_VERSION"
    paths:
      - .cache/bca/
  before_script:
    - apt-get update -qq && apt-get install -y --no-install-recommends ca-certificates curl tar
    - |
      set -euo pipefail
      install -d "$CI_PROJECT_DIR/.cache/bca" "$HOME/.local/bin"
      if [ ! -x "$CI_PROJECT_DIR/.cache/bca/bca" ]; then
        stage="big-code-analysis-${BCA_VERSION}-${BCA_TARGET}"
        tarball="${stage}.tar.gz"
        url="https://github.com/dekobon/big-code-analysis/releases/download/v${BCA_VERSION}/${tarball}"
        curl -fsSL --proto '=https' --tlsv1.2 -o "/tmp/${tarball}" "$url"
        echo "${BCA_SHA256}  /tmp/${tarball}" | sha256sum --check --strict -
        tar -xzf "/tmp/${tarball}" -C /tmp
        install -m 0755 "/tmp/${stage}/bca" "$CI_PROJECT_DIR/.cache/bca/bca"
        rm -rf "/tmp/${tarball}" "/tmp/${stage}"
      fi
      install -m 0755 "$CI_PROJECT_DIR/.cache/bca/bca" "$HOME/.local/bin/bca"
      export PATH="$HOME/.local/bin:$PATH"
  script:
    - bca
        check
        --paths "$PWD"
        --report-format code-climate
        --output gl-code-quality-report.json
        --no-fail
    - bca
        check
        --paths "$PWD"
        --report-format checkstyle
        --output bca-checkstyle.xml
        --no-fail
    - bca
        report -O markdown
        --paths "$PWD"
        --top 20
        --strip-prefix "$PWD/"
        --output bca-report.md
    # The threshold gate runs separately so the artifacts above still
    # publish on failure. Exit 2 = at least one threshold exceeded.
    - bca check --paths "$PWD"
  artifacts:
    when: always
    reports:
      codequality: gl-code-quality-report.json
    paths:
      - gl-code-quality-report.json
      - bca-checkstyle.xml
      - bca-report.md

A few notes about the example:

  • The first two bca check … --no-fail invocations collect offenders for the artifacts; the final bca check (no --no-fail) is the pass/fail gate. All three runs use the same threshold config so the artifacts always match the gate decision.
  • artifacts:when: always ensures every artifact is downloadable even on a red pipeline — which is exactly when you want them most.
  • artifacts:reports:codequality wires the Code Climate JSON directly into GitLab's MR Code Quality widget — see the Code Quality widget section below for the field-by-field semantics.

GitLab Code Quality widget

GitLab's first-class Code Quality experience (inline complaints on the MR diff, summary on the MR overview page) consumes Code Climate JSON. bca check emits this natively via --report-format code-climate, so the integration is a one-liner:

code_quality:
  stage: quality
  script:
    - bca check --paths "$CI_PROJECT_DIR"
          --report-format code-climate
          --output gl-code-quality-report.json
          --no-fail
  artifacts:
    when: always
    reports:
      codequality: gl-code-quality-report.json
    paths:
      - gl-code-quality-report.json

Severity bands are derived from how far each metric exceeds its configured threshold (value / limit ratio, inverted for the maintainability-index family where lower is worse): ≤ 1.5×minor, ≤ 2×major, ≤ 4×critical, > 4×blocker. The widget deduplicates findings by fingerprint; bca hashes path \0 function \0 metric (no line, no value) so a violation surviving an upstream line-drift edit still collapses into the same widget entry across pipeline runs.

Sanity-check a generated report locally:

jq 'all(.[]; has("description") and has("check_name")
     and has("fingerprint") and has("severity")
     and has("location"))' gl-code-quality-report.json
# → true
jq '[.[] | .severity] | unique' gl-code-quality-report.json
# → a subset of ["info","minor","major","critical","blocker"]

MR-only comment with the Markdown report

To attach the Markdown report as an MR note (the GitLab analogue of the GitHub PR comment recipe), use the project access token and the Notes API:

bca-mr-comment:
  stage: quality
  image: alpine:3
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  needs: ["bca-quality"]
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      BODY=$(jq -Rs '.' < bca-report.md)
      curl --fail --silent --show-error \
        --request POST \
        --header "PRIVATE-TOKEN: $CI_BCA_BOT_TOKEN" \
        --header "Content-Type: application/json" \
        --data "{\"body\": $BODY}" \
        "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"

CI_BCA_BOT_TOKEN is a project access token with api scope. The job depends on bca-quality so the Markdown artifact is in place before it runs.

Jenkins / SonarQube

Both Jenkins (via the Warnings Next Generation plugin) and SonarQube (via its Generic Issue importer) consume Checkstyle 4.3 XML directly. The same invocation feeds both:

bca check --paths src/ \
    --report-format checkstyle \
    --output report.checkstyle.xml

Wire report.checkstyle.xml into your existing Jenkins Record Issues / SonarQube External Issues step. The Checkstyle writer emits an empty (well-formed) document when there are no offenders, so neither tool needs special-casing for a clean run. See the Check command page for the writer's schema details.

Generic CI guidance

Applies regardless of provider:

  • Pin bca to a specific version. Both cargo install --version and cargo binstall --version accept the published crate version of big-code-analysis-cli. A floating install surfaces metric-counting changes as "mysterious CI flakes" on Mondays. Pin to a version whose CLI-artifact schemas (baseline, thresholds) match the files your repo commits — see the schema-compatibility note in the install section.
  • --jobs defaults to the effective CPU count. The flag honors available_parallelism() — cgroup-/cpuset-/quota-aware on Linux, OS CPU count on macOS/Windows — so CI runners no longer need to thread --jobs "$(nproc)" through every recipe. --jobs 1 remains a debugging knob, not a default.
  • Always pass --strip-prefix "$PWD/" to bca report markdown so the path column is identical across runners with different workspace paths. Without it the diff between two reports is dominated by /home/runner/work/... vs. /builds/group/project/... noise.
  • Store bca.toml at the repo root, alongside Cargo.toml / pyproject.toml / package.json. bca discovers it automatically, so a bare bca check reads the committed thresholds, paths, and baseline. Treat it as source: review threshold relaxations in code review.
  • Exit-code contract. bca check exits 0 clean, 2 on any threshold violation, 1 on tool error (bad config, unknown metric, unreadable path). Reserving 1 for tool errors lets CI distinguish "a function got too complex" from "the analyzer crashed". Pass --exit-codes=tiered (or set [check] exit_codes = "tiered" in bca.toml) to split the violation case by severity: 2 new offenders only, 3 regressions only, 4 both, 5 a --tier=soft violation that also breaches the hard limit. The tiered codes are opt-in; the default stays 0/1/2. Every fail-state remains non-zero, so exit != 0 → fail wrappers keep working — only tooling that tests $? -eq 2 explicitly needs to widen to 2-5.
  • Honor in-source suppression markers, audit with --no-suppress. The default bca check honors bca: suppress / bca: suppress-file markers; passing --no-suppress ignores them so auditors see the raw offender list.