Skip to main content

Catch OpenAPI breaking changes in pull requests with the oasdiff GitHub Action

Updated 8 min readMarkdown
Catch OpenAPI breaking changes in pull requests with the oasdiff GitHub Action

If your API is described by an OpenAPI spec, you can catch most breaking changes before they merge, for free, with one workflow file. oasdiff compares the spec in a pull request with the spec on the base branch and tells you what would break existing clients.

This post walks through the setup line by line, shows what the output looks like on a real change, how to tune it, and, the part most guides skip, what to actually do with each finding.

What you need

  • A GitHub repository with an OpenAPI 3 spec committed to it. The examples use openapi.yaml in the repo root; change the path to yours.
  • If your spec is generated from code, a build step that writes it to a file first. The action compares files, not code.
  • That's it. The free actions run the oasdiff CLI inside your runner, with no account and no token.

The oasdiff/oasdiff-action repository holds several actions, one per folder:

Action What it does Fails the job
oasdiff/oasdiff-action/breaking@v0 Breaking changes (errors and warnings) as PR annotations Only with fail-on
oasdiff/oasdiff-action/changelog@v0 Every change that can affect clients, breaking or not No
oasdiff/oasdiff-action/diff@v0 The full diff, including description-only edits Only with fail-on-diff: true
oasdiff/oasdiff-action/validate@v0 Checks one spec against the OpenAPI rules On errors

This post uses breaking. Note that @v0 is a moving tag that follows every stable v0.x.y release; pin a release tag if you want to choose when to upgrade.

Step 1: add the workflow

Create .github/workflows/oasdiff.yaml:

name: oasdiff
on:
  pull_request:
    branches: [ "main" ]
permissions:
  contents: read
  pull-requests: write
jobs:
  breaking-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: git fetch --depth=1 origin ${{ github.base_ref }}
      - uses: oasdiff/oasdiff-action/breaking@v0
        with:
          base: 'origin/${{ github.base_ref }}:openapi.yaml'
          revision: 'HEAD:openapi.yaml'
          fail-on: WARN
          github-token: ${{ github.token }}

What each part does:

  • on: pull_request runs on every PR into main. To skip PRs that don't touch the spec, add paths: [ "openapi.yaml" ] under pull_request.
  • permissions: contents: read to check out the code; pull-requests: write lets the action post the review link as a PR comment. Without it, the link goes to the job summary instead.
  • git fetch --depth=1 fetches only the base branch so the action can read the old spec from git. Full history isn't needed.
  • base and revision use oasdiff's <git ref>:<path> syntax: the spec on the base branch and the spec in the commit being tested. Each also accepts a plain file path or an http(s) URL.
  • fail-on decides whether the job fails (step 2). Leave it out and the action only reports.
  • github-token is used to post the review link. On PRs from forks the token is read-only, so the link falls back to the job summary.

Step 2: decide what fails the job

oasdiff sorts each breaking change into a level:

  • error: a client that followed the old contract can stop working
  • warning: it might; a human should look
  • info: shown by changelog, not by breaking

fail-on picks the lowest level that fails the job:

fail-on Job fails when Use it when
not set (default) never because of a change you're trying it out, or the check should inform and not stop anyone
ERR at least one error you want a hard stop on changes oasdiff is sure about
WARN at least one error or warning every possible break needs a decision before merge

One failure fail-on doesn't control: if oasdiff can't run at all (a spec that doesn't parse, a file missing on one side, an external $ref it isn't allowed to load) the step fails regardless. If the check must never fail the job, also add continue-on-error: true to the step.

Step 3: open a PR and read the output

Here's a sample change: a PR that makes a new query parameter region required on GET /orders, removes the old limit parameter, and drops the required status field from the GET /orders/{id} response.

Annotations on the PR

The action emits GitHub annotations, titled with the oasdiff rule ID. They show up on the Files changed tab and in the run summary:

::error title=new-required-request-parameter,file=openapi.yaml,line=24,col=12::in API GET /orders added the new required `query` request parameter `region`
::error title=response-required-property-removed,file=openapi.yaml::in API GET /orders/{id} removed the required property `status` from the response with the `200` status
::warning title=request-parameter-removed,file=openapi.yaml::in API GET /orders deleted the `query` request parameter `limit`

One gotcha: an added parameter has a line in the new file, so its annotation sits on that line. A removed field has no line in the new file, so GitHub shows it at the top of the file. Read the run summary, not just the diff view.

The full report

The step output breaking only holds the first line (the count). For the whole report, set output-to-file: oasdiff-breaking.txt and upload it as an artifact, or run the CLI locally:

3 changes: 2 error, 1 warning, 0 info
error	[new-required-request-parameter] at openapi.yaml
	in API GET /orders
		added the new required `query` request parameter `region`

error	[response-required-property-removed] at openapi.yaml
	in API GET /orders/{id}
		removed the required property `status` from the response with the `200` status

warning	[request-parameter-removed] at openapi.yaml
	in API GET /orders
		deleted the `query` request parameter `limit`

When there are breaking changes, the action uploads both specs to oasdiff.com (encrypted in CI first; the key stays in the link) and posts a side-by-side review link that expires after 7 days.

If your specs must not leave your runners, set review: false. Nothing is uploaded, and detection and annotations work the same.

Step 4: tune it

Keep settings in a .oasdiff.yaml file in the repo root instead of in every workflow. Inputs in the workflow's with: win over the file.

# .oasdiff.yaml (repository root)
fail-on: ERR
err-ignore: ./oasdiff-err-ignore.txt

When a breaking change is intended and agreed, an ignore file stops it from failing every later run. Each line is the method and path, then the change text as oasdiff printed it:

# oasdiff-err-ignore.txt: one accepted breaking change per line, with the PR that accepted it
# PR #1290: region is required from v2; the two known callers already send it
GET /orders added the new required `query` request parameter `region`

Two more inputs worth knowing:

  • allow-external-refs is false in the action (the CLI default is true), so an untrusted PR can't make the runner fetch URLs. Set it to true if your spec is split across files or references external URLs.
  • composed: true compares many spec files at once. The side-by-side review isn't available in that mode.

Step 5: what to do with the output

A red job tells the author something broke. It doesn't tell anyone what to do next. For each finding:

  • Error: look for an additive change first. Many errors have a non-breaking version: a new optional parameter instead of a required one, a new field next to the old one, a new endpoint or version. Ask for that first.
  • Error you keep: name who calls the endpoint. If it has to break, list the teams that call the endpoint and tell them before the merge, with what changes and when. Then add the line to the ignore file in the same PR, so the reason lives in the history.
  • Warning: decide and write it down. request-parameter-removed is breaking only if the server rejects input it doesn't know. Check, and put the answer in the PR description.
  • No breaking changes: still read the changelog. New endpoints and fields don't break anyone, but consumers may want them. The changelog action lists them.

The step the action can't do

The action answers "what changed?". The hardest line in step 5, "list the teams that call the endpoint", isn't in the spec. If every caller lives in the same repo, the PR reviewers are the right people and you're done. If other teams call your API from other repos, someone has to know who they are.

That's the gap we built Graphwake for: consumer teams subscribe to the endpoints they call, and when a PR changes one in a breaking way, only those teams get an alert in Slack, Microsoft Teams or email. It runs as its own CI step, next to the oasdiff Action, which keeps its own fail-on.


Originally published on graphwake.com. Graphwake is not affiliated with the oasdiff project; oasdiff and its name belong to its authors.

Do you gate merges on breaking changes, or only report them? Curious where teams draw that line.

No comments yet. Be the first to comment.

More from this blog