# Lint in CI/CD

> **Scope of this page**
>
> This page documents the Zuplo **gateway** runtime (API, AI, and MCP gateways), not the Developer Portal.
>
> It applies to every Zuplo hosting option: Managed Edge, Managed Dedicated, and Self-Hosted. See [Hosting options](https://zuplo.com/docs/articles/hosting-options).

`zuplo lint` exits with a non-zero code when it finds problems, so any CI system
can use it as a gate. This page shows a GitHub Actions workflow for each common
setup, then the pieces you need to adapt them to another CI system.

The examples assume your Zuplo project is at the root of the repository and
lists `zuplo` in `package.json`, as projects created with
[`create-zuplo-api`](../cli/create-zuplo-api.mdx) do. `zuplo lint` doesn't need
an API key; it reads only the files in your repository.

## Fail pull requests on lint errors

The simplest setup runs the linter and lets its exit code pass or fail the job:

```yaml title=".github/workflows/lint.yaml"
name: Lint

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npx zuplo lint
```

The job fails when the linter finds an error. To fail on warnings too, change
the step to `npx zuplo lint --fail-on warn`, or set
[`failOn`](./api-lint-configuration.mdx#failon) in `zuplo.lint.jsonc` so local
runs and CI agree.

To lint before deploying, add the same step before `zuplo deploy` in your
deployment workflow. See [Custom CI/CD](./custom-ci-cd.mdx) for deployment
workflows.

## Show results in GitHub code scanning

With `--format sarif`, the linter writes results in the SARIF format that GitHub
code scanning reads. Code scanning shows each result as an annotation on the
line it applies to in the pull request diff, and tracks open results in the
repository's **Security** tab.

```yaml title=".github/workflows/lint.yaml"
name: Lint

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read
  security-events: write

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npx zuplo lint --format sarif > zuplo-lint.sarif

      - name: Upload results
        if: ${{ !cancelled() }}
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: zuplo-lint.sarif
          category: zuplo-lint
```

The `if: ${{ !cancelled() }}` condition uploads the results even when the lint
step fails, so the annotations appear on the pull requests that need them. The
lint step still fails the job.

:::note

Code scanning is available on public repositories, and on private repositories
with GitHub Advanced Security. Without it, use the
[pass/fail workflow](#fail-pull-requests-on-lint-errors) — the results appear in
the job log.

:::

## Report breaking changes on pull requests

[Breaking change detection](./api-lint-breaking-changes.mdx) compares your
OpenAPI files with the version on another branch. In CI, point the baseline at
the remote branch and make sure Git has fetched it.

```jsonc title="zuplo.lint.jsonc"
{
  "version": 2,
  "breakingChanges": {
    "enabled": true,
    "baseline": "origin/main",
    "severity": "error",
  },
}
```

`actions/checkout` fetches a single commit by default, and the local `main`
branch doesn't exist in that checkout. Set `fetch-depth: 0` to fetch the full
history, including `origin/main`:

```yaml title=".github/workflows/lint.yaml"
steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0

  - uses: actions/setup-node@v4
    with:
      node-version: 24

  - name: Install dependencies
    run: npm ci

  - name: Lint
    run: npx zuplo lint
```

If the baseline isn't in the checkout, the run fails with
`baseline ref "origin/main" not found`. Fetch the branch before linting.

`origin/main` also works on your machine after a `git fetch`, so the same
configuration file serves local runs and CI.

To report breaking changes without blocking the merge, set `"severity": "warn"`.
The changes still appear in the output and in code scanning, and the job passes
unless `failOn` is `"warn"`.

## Use the JSON output in scripts

`--format json` prints a single JSON document with a summary, every result, and
any breaking changes or fixes. Use it to build your own report, such as a pull
request comment or a job summary:

```yaml title=".github/workflows/lint.yaml"
- name: Lint
  id: lint
  run: npx zuplo lint --format json > zuplo-lint.json || true

- name: Summarize
  run: |
    jq -r '"### Zuplo lint\n\n\(.summary.errors) errors, \(.summary.warnings) warnings, \(.summary.breakingChanges) breaking changes"' \
      zuplo-lint.json >> "$GITHUB_STEP_SUMMARY"
    jq -e '.passed' zuplo-lint.json > /dev/null
```

`--fail-on none` keeps the lint step from failing before the summary is written;
the final `jq -e '.passed'` fails the job when the run didn't pass. The `passed`
field already takes `failOn` from your configuration file into account. See
[Output formats](./api-lint.mdx#output-formats) for the full JSON shape.

## Other CI systems

Any CI system that runs Node.js can run the linter. Each workflow above uses
three things you need to carry over:

1. **Install the project's dependencies** with `npm ci` (or your package
   manager's equivalent), so `npx zuplo` uses the version in your lockfile.
2. **Run `npx zuplo lint`** from the project root, or pass `--dir`. With
   `--dir`, file paths in the output are relative to that directory, so SARIF
   results won't line up with files in code scanning unless the project is at
   the repository root.
3. **Read the exit code.** `0` passes, `1` means the linter found problems, and
   `2` means it couldn't run. Treat `2` as a broken pipeline rather than a lint
   result — check the error message in the log.

For breaking change detection, also fetch the baseline branch before linting,
for example with `git fetch origin main`.

## Troubleshooting

### The linter binary for your platform isn't installed

The linter ships as a separate, platform-specific package that npm installs
alongside the Zuplo CLI as an optional dependency. If optional dependencies are
skipped — for example with `npm install --omit=optional` — `zuplo lint` stops
with:

```txt
The zuplo-lint binary for your platform (linux-x64) is not installed.
```

Install the dependencies again without skipping optional dependencies. The error
message names the exact package for your platform, such as
`@zuplo/lint-linux-x64`, if you need to install it directly.

### Invalid directory

```txt
Invalid directory: The project directory is not the root of a Zuplo project.
```

The command needs a `config/` directory containing at least one file that ends
in `.oas.json`. Run it from the project root, or pass `--dir` to point at the
project. This error exits with `2`.

## Next steps

- [Lint configuration](./api-lint-configuration.mdx) — presets, severities, and
  breaking change settings.
- [Testing your API](./testing.mdx) — run tests against a live gateway in the
  same pipeline.
- [Custom CI/CD](./custom-ci-cd.mdx) — deploy from your own CI system.
