Wire into CI

OpenSIP CLI is a CLI that exits with a code. Wiring it into CI is two lines for the basic case and ~15 lines for the full setup with PR annotations and baselines.

The minimal setup

# .github/workflows/fitness.yml
name: Fitness
on: [pull_request, push]

jobs:
  fit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: curl -fsSL https://opensip.ai/cli/install.sh | bash
      - run: opensip fit

opensip fit exits 0 if every check passed, non-zero otherwise. The build fails on red. No further setup required.

That's the floor. The rest of this page is the polish: how to surface findings as PR comments, how to adopt incrementally without blocking PRs on legacy violations, and how to keep CI fast.

PR annotations via SARIF

opensip-cli exports the SARIF format that GitHub understands natively via the fit export --format baseline subcommand. The flow is two steps: run fit --gate-save (which records findings into the project SQLite store, then exits according to the failOnErrors/failOnWarnings thresholds — ADR-0020: the step itself is the gate, not a free pass), then fit export --format baseline --out fit.sarif to write the SARIF document. Uploaded findings appear inline in the PR's "Files changed" view.

- run: opensip fit --gate-save        # record findings, then exit per fail thresholds
- run: opensip fit export --format baseline --out fit.sarif
  if: always()      # the save happened before the exit — export even when the gate failed
- uses: github/codeql-action/upload-sarif@v3
  if: always()      # upload even when a previous step failed
  with:
    sarif_file: fit.sarif
    category: opensip-fit

The if: always() is important — fit --gate-save hard-fails the step when error-level findings breach the configured thresholds (set failOnErrors: 0 in the fitness: block for a ratchet-only adoption where only net-new Code Scanning alerts block PRs), and GitHub skips subsequent steps after a failure by default. The baseline is saved before the exit code is set, so the SARIF export + upload still have everything they need — they just have to actually run.

For GitLab, convert the exported SARIF to the Code Quality widget format with GitLab's converter, renaming the output to gl-code-quality-report.json. (There is no native GitLab code-quality emitter today — go through SARIF.)

Baseline-gate flow

If the codebase already has violations, gating on "all violations" blocks every PR until cleanup is done. Almost no team accepts that. The baseline-gate flow is the alternative: capture today's violations, gate only on new ones.

# Run once locally, on a clean main branch
opensip fit --gate-save
# This writes the baseline into opensip-cli/.runtime/datastore.sqlite

Then in CI:

- run: opensip fit --gate-compare

--gate-compare exits 0 if no new violations appeared since the baseline. Existing ones are tolerated. The baseline lives in SQLite (opensip-cli/.runtime/datastore.sqlite); since .runtime/ is gitignored, you'll want to publish + restore the baseline store as a CI artifact.

The artifact pattern: the gate baseline is a SQLite store, not a committed file. The standard flow is to run fit --gate-save on main-branch builds and upload opensip-cli/.runtime/datastore.sqlite as a workflow artifact; PR builds download that artifact into opensip-cli/.runtime/ before running fit --gate-compare. (For a human-readable export — e.g. to inspect the baseline or feed GitHub Code Scanning — use fit export --format baseline --out baseline.sarif, which reads the same store.) See output, gate, SARIF and the architecture-gate CI patterns for the full workflow.

Recommended full setup

name: Fitness
on:
  pull_request:
  push:
    branches: [main]

jobs:
  fit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: curl -fsSL https://opensip.ai/cli/install.sh | bash

      # Restore the baseline store produced by the last main-branch build.
      - uses: actions/download-artifact@v4
        if: github.event_name == 'pull_request'
        continue-on-error: true
        with:
          name: fit-baseline
          path: opensip-cli/.runtime/

      # On PRs: gate against new violations only.
      # On main: refresh the baseline (so the next PR sees current state).
      - name: Run fit
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            opensip fit --gate-compare
          else
            opensip fit --gate-save
          fi

      # Export the SARIF for PR annotations (reads the SQLite store).
      - name: Export SARIF
        if: always()
        run: opensip fit export --format baseline --out fit.sarif

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: fit.sarif
          category: opensip-fit

      # On main: publish the refreshed baseline store for the next PR.
      - uses: actions/upload-artifact@v4
        if: github.event_name != 'pull_request'
        with:
          name: fit-baseline
          path: opensip-cli/.runtime/datastore.sqlite

This is the shape we recommend: PRs see "is this getting worse?", main updates the bar. Developers fix legacy violations at their own pace; CI is fast (no full-codebase pass on every PR).

Speed

Typical timings on the opensip-cli self-graph (~300 files, ~145 checks):

If fit is slow on a large repo, the usual culprits:

What opensip fit actually does in CI

For a deeper understanding of the gate flow itself — what the baseline contains, how new-vs-old violations are matched, what the exit codes mean — see output, gate, SARIF.

Where to go next

| You want to … | Go to … |

|---|---|

| Adopt incrementally on a large existing codebase | Adopt in a monorepo |

| Coexist with ESLint / migrate gradually | Migrate from ESLint |

| Understand the baseline format and diff logic | Output, gate, SARIF |

| See all CI-relevant CLI flags | CLI commands |