Skip to content
Login Start Free

Lint and Static Analysis Findings (Beta)

A per-commit annotation tells you what a linter found on this diff, then disappears with the PR. Gaffer keeps every upload. It tells you which findings are new on a branch, which have sat on main for weeks, and which files collect the most of them.

Gaffer parses SARIF 2.1.0. Any tool that emits it works. These are the common ones.

ToolCommand
ruffruff check --output-format sarif > results.sarif
ESLinteslint -f @microsoft/eslint-formatter-sarif -o results.sarif .
golangci-lint (v2)golangci-lint run --output.sarif.path results.sarif
clippycargo clippy --message-format=json | clippy-sarif > results.sarif

ESLint needs npm install --save-dev @microsoft/eslint-formatter-sarif. Clippy needs cargo install clippy-sarif.

Gaffer also reads SARIF from Semgrep, CodeQL, Trivy and Checkov. For example, semgrep scan --sarif --sarif-output=results.sarif, or trivy fs --format sarif -o results.sarif .. Checkov has -o sarif.

Upload a SARIF file the same way you upload a test report. Both .sarif and .json extensions work.

Terminal window
gaffer upload results.sarif

You can also upload from GitHub Actions or with the Upload API.

Linters exit non-zero when they find something. Keep the upload step running with if: always().

.github/workflows/lint.yml
name: Lint
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ruff
run: pipx run ruff check --output-format sarif > results.sarif
- name: Install Gaffer CLI
if: always()
run: curl -fsSL https://app.gaffer.sh/install.sh | sh
- name: Upload to Gaffer
if: always()
run: >
gaffer upload results.sarif
--commit-sha ${{ github.event.pull_request.head.sha || github.sha }}
--branch ${{ github.head_ref || github.ref_name }}
env:
GAFFER_PROJECT_TOKEN: ${{ secrets.GAFFER_PROJECT_TOKEN }}
curl
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: YOUR_PROJECT_TOKEN" \
-F 'tags={"commitSha":"abc123","branch":"main"}'

Pass --branch and --commit-sha to the CLI, or the matching tags to the API. The branch comparison below needs them.

A SARIF 2.1.0 log with at least one run. Each result should carry a ruleId and a physical location: a file and a start line. A finding without a location is stored and counted. It cannot appear in hotspots, and Gaffer cannot match it across uploads. A finding without partialFingerprints is keyed by rule, file and position.

Gaffer gives every finding a stable identity so it can recognise the same finding in the next upload. If the tool writes SARIF partialFingerprints, Gaffer uses them. Otherwise it hashes the rule, the file and the position. A position hash shifts when lines above the finding move. An edit near the top of a file can make an existing finding show up as new while its previous copy is marked resolved. Prefer a tool that emits partialFingerprints when you can.

Open the findings views from the project page.

  • New on this branch: a PR branch’s findings diffed against the main branch. Pre-existing findings stay separate.
  • Chronic: findings that survive a configurable number of consecutive main runs (default 20), grouped by rule and file.
  • Hotspots: files ranked by finding count, beside line coverage from the latest coverage report and failing or flaky tests.

Start with the hotspots that are poorly covered and also flaky. Those files hurt twice.