---
title: CI Integration
description: Run Specdiff in CI pipelines to block breaking API changes, post diff summaries to pull requests, and adopt gradually with rule and path ignores.
url: https://pr-1-b9e16090e83d.thally.app/specdiff/ci-integration
---

# CI Integration

Run Specdiff in CI pipelines to block breaking API changes, post diff summaries to pull requests, and adopt gradually with rule and path ignores.

Specdiff fits into any CI system that can run Node.js 22 or later. The typical pattern is: extract the base-branch spec, run `specdiff` against the PR branch spec, and fail the build when breaking changes are found.

## GitHub Actions workflow

This workflow compares the OpenAPI spec on the PR branch against the base branch and posts a Markdown summary to the pull request.

```yaml
name: API compatibility
on: pull_request
jobs:
  specdiff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - name: Extract base branch spec
        run: git show origin/${{ github.base_ref }}:openapi.yaml > /tmp/openapi-base.yaml
      - name: Fail on breaking changes
        run: npx -y @specdiff/cli /tmp/openapi-base.yaml openapi.yaml --fail-on breaking --format markdown | tee -a "$GITHUB_STEP_SUMMARY"
```

The key steps in this pattern:

1. **Fetch full history** with `fetch-depth: 0` so `git show` can access the base branch.
2. **Extract the base-branch spec** using `git show` to write the old spec to a temporary file.
3. **Run specdiff** with `--fail-on breaking` so the step exits with code 1 when breaking changes are detected.
4. **Pipe Markdown output** to `$GITHUB_STEP_SUMMARY` so the diff report appears directly in the PR checks summary.

## Exit codes

Specdiff uses specific exit codes that CI systems interpret correctly:

| Exit Code | Name | CI Meaning |
|---|---|---|
| 0 | ok | No changes at or above the threshold were found. Build passes. |
| 1 | thresholdExceeded | Changes at or above the `--fail-on` threshold exist. Build fails. |
| 2 | usage | Invalid flags or unknown rule code. Fix the CI script. |
| 3 | inputError | A spec file could not be read or parsed. Check file paths and format. |

The `--fail-on` flag controls the threshold. Setting `--fail-on warning` fails the build on both breaking changes and warnings. Setting `--fail-on none` always passes, which is useful when you only want the report without gating.

## Output formats for CI

Choose the format that fits your workflow:

- `--format markdown` produces a GitHub-flavoured Markdown report suitable for `$GITHUB_STEP_SUMMARY` or PR comments.
- `--format json` outputs the full `DiffResult` as JSON, useful for downstream scripts that parse the result programmatically.
- `--format text` (default) outputs a human-readable summary, appropriate for plain log output.

Use `--output <file>` or `-o <file>` to write the report to a file instead of stdout.

## Gradual adoption

When adding Specdiff to an existing project, you may need to suppress known issues temporarily. Two flags help with gradual rollout:

### Ignoring specific rules

Use `--ignore-rule` to suppress rules that produce noise during initial adoption. The flag is repeatable.

```yaml
- name: Diff with relaxed rules
  run: |
    npx -y @specdiff/cli /tmp/openapi-base.yaml openapi.yaml \
      --fail-on breaking \
      --ignore-rule description-changed \
      --ignore-rule deprecated-added \
      --format markdown | tee -a "$GITHUB_STEP_SUMMARY"
```

### Ignoring specific paths

Use `--ignore-path` to exclude entire sections of the spec from comparison. The value is a JSON pointer prefix; all changes at or beneath that path are dropped. The leading `#` is optional.

```yaml
- name: Diff ignoring internal endpoints
  run: |
    npx -y @specdiff/cli /tmp/openapi-base.yaml openapi.yaml \
      --fail-on breaking \
      --ignore-path "/paths/~1internal" \
      --ignore-path "/paths/~1admin" \
      --format markdown | tee -a "$GITHUB_STEP_SUMMARY"
```

Note that `/` characters within path segments must be escaped as `~1` in JSON pointers (for example, `/pets` becomes `~1pets`).

## Library approach for custom CI scripts

For more control, use `@specdiff/core` directly in a Node.js script. This lets you apply custom overrides, inspect individual changes, and build conditional logic beyond what the CLI flags offer.

```ts
import { readFileSync } from "node:fs";
import { diffDocuments, exceedsThreshold, formatMarkdown } from "@specdiff/core";

const before = JSON.parse(readFileSync("openapi-base.json", "utf8"));
const after = JSON.parse(readFileSync("openapi.json", "utf8"));

const result = diffDocuments(before, after, {
  ignoreRules: ["description-changed"],
  overrides: { "default-changed": "breaking" },
});

// Write a Markdown report
const report = formatMarkdown(result);
console.log(report);

// Exit with code 1 if breaking changes found
if (exceedsThreshold(result, "breaking")) {
  process.exit(1);
}
```

The `exceedsThreshold` function checks whether the result contains any change at or above a given severity. It accepts `"breaking"`, `"warning"`, `"info"`, or `"none"` (which always returns `false`).

This pattern works in any CI runner that supports Node.js. Save the script as a file in your repository and invoke it in your CI step:

```yaml
- name: Custom specdiff check
  run: node scripts/check-api-compat.mjs
```

### Parsing YAML specs in scripts

The `@specdiff/core` package has no runtime dependencies and works with plain JavaScript objects. If your specs are YAML, add a YAML parser such as `yaml`:

```ts
import { readFileSync } from "node:fs";
import { parse } from "yaml";
import { diffDocuments, exceedsThreshold } from "@specdiff/core";

const before = parse(readFileSync("openapi-base.yaml", "utf8"));
const after = parse(readFileSync("openapi.yaml", "utf8"));

const result = diffDocuments(before, after);

if (exceedsThreshold(result, "breaking")) {
  console.error("Breaking API changes detected");
  process.exit(1);
}
```

Alternatively, use `@specdiff/cli` as a library. Its `loadDocument` function handles both JSON and YAML files:

```ts
import { loadDocument } from "@specdiff/cli";
import { diffDocuments, exceedsThreshold } from "@specdiff/core";

const before = await loadDocument("openapi-base.yaml", process.cwd());
const after = await loadDocument("openapi.yaml", process.cwd());

const result = diffDocuments(before, after);

if (exceedsThreshold(result, "breaking")) {
  process.exit(1);
}
```