API & Backend

Validate OpenAPI Specifications in GitHub Actions

A three-step guide: Lint the spec, Check changes, Review the results.

An OpenAPI CI pipeline should reject invalid or noncompliant descriptions before merge, resolve multi-file specifications into a reproducible bundle, show annotations on the affected pull-request lines, and publish only artifacts that passed validation. Syntax checking is the minimum. A useful pipeline also enforces organization rules, reviews compatibility, tests examples, and makes ownership visible.

This tutorial uses Redocly CLI 2.x and GitHub Actions to validate an OpenAPI 3.x description. The design is tool-friendly but not tool-dependent: each stage has a clear input and output, so you can replace the linter or documentation renderer without changing the lifecycle.

OpenAPI versions and tooling evolve. Use the official OpenAPI Specification as the source of truth and verify command options in the current Redocly CLI lint documentation.

Define what CI is protecting

OpenAPI describes an interface consumed by servers, SDK generators, documentation renderers, contract tests, gateways, and developers. A small mistake can propagate to every downstream tool. CI should protect at least four layers:

  1. Structure: the document conforms to the selected OpenAPI version.
  2. Style: operations follow organization conventions for IDs, errors, security, descriptions, and examples.
  3. Compatibility: a change does not accidentally break existing consumers.
  4. Artifact integrity: the exact bundle used downstream is generated from reviewed sources.

Do not treat a green linter as proof that the API works. A valid description can document the wrong response code, contain an impossible example, or drift from production. Add runtime contract tests and implementation comparison where the API risk justifies them.

Keep the pipeline close to the spec source. Our docs-as-code guide explains review and ownership patterns, while our API documentation guide covers the content an integration contract must communicate.

Organize the OpenAPI source for review

A small API can remain in one file. Larger APIs benefit from a root document with referenced paths and schemas:

openapi/
  openapi.yaml
  paths/
    projects.yaml
    project-by-id.yaml
  components/
    errors.yaml
    project.yaml
redocly.yaml
.github/
  workflows/
    openapi.yml

The root defines metadata, servers, security, and references:

openapi: 3.1.0
info:
  title: Projects API
  version: 1.0.0
  description: Create and manage developer projects.
servers:
  - url: https://api.example.com
security:
  - bearerAuth: []
paths:
  /v1/projects:
    $ref: ./paths/projects.yaml
  /v1/projects/{projectId}:
    $ref: ./paths/project-by-id.yaml
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer

Split along concepts that teams review, not at every possible object. Hundreds of tiny files make navigation harder and increase reference mistakes. Keep related request, success, and error details close enough to evaluate together.

Use relative references that resolve inside the repository. Avoid remote references in the build unless they are pinned, authenticated appropriately, and available during CI. A network dependency can make contract validation nondeterministic.

Install and pin the linter

Install the CLI as a development dependency rather than invoking latest in CI:

npm install --save-dev @redocly/cli

Add scripts:

{
  "scripts": {
    "api:lint": "redocly lint --format=stylish",
    "api:lint:ci": "redocly lint --format=github-actions",
    "api:bundle": "redocly bundle projects@v1 --output dist/openapi.yaml",
    "api:check": "npm run api:lint && npm run api:bundle"
  }
}

Commit the package lock and use npm ci. This ensures a pull request and a later release validate with the same linter version. Review dependency updates deliberately because new rules or parser behavior can change results.

Developers should run the same commands locally. Do not maintain a “fast local” configuration that permits errors rejected only after push. Editor integration can improve feedback, but the versioned CLI configuration remains the shared contract.

The CLI supports GitHub Actions output, which turns problems into file annotations. That is more actionable than a long unstructured job log.

Configure baseline API rules

Create redocly.yaml:

apis:
  projects@v1:
    root: ./openapi/openapi.yaml

extends:
  - recommended-strict

rules:
  operation-operationId: error
  operation-summary: error
  operation-description: warn
  operation-2xx-response: error
  operation-4xx-response: error
  security-defined: error
  no-unused-components: error
  no-server-example.com: error
  tag-description: warn

Start with a documented ruleset and adjust intentionally. Turning every warning into an error on an existing large specification can produce an ignore file instead of improvement. Establish a baseline, prevent new debt, and schedule cleanup with owners.

Every exception needs a reason, owner, and review date. A permanent ignore entry with no context is invisible policy drift. Prefer a narrow inline or path-specific exception when supported rather than disabling a rule globally.

Rules should express consumer value. Requiring operationId helps SDK stability. Requiring error responses prompts teams to describe failures. Cosmetic preferences are less important than authentication, pagination, idempotency, and response semantics.

Create the GitHub Actions workflow

Add .github/workflows/openapi.yml:

name: OpenAPI

on:
  pull_request:
    paths:
      - 'openapi/**'
      - 'redocly.yaml'
      - 'package.json'
      - 'package-lock.json'
      - '.github/workflows/openapi.yml'
  push:
    branches: [main]
    paths:
      - 'openapi/**'
      - 'redocly.yaml'

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npm run api:lint:ci
      - run: npm run api:bundle

      - uses: actions/upload-artifact@v7
        with:
          name: projects-openapi
          path: dist/openapi.yaml
          if-no-files-found: error
          retention-days: 14

Pin third-party actions to full commit SHAs in higher-assurance environments and use a dependency updater to propose safe revisions. Major tags are readable here, but they can move.

The workflow grants only read access to repository content. Validation does not need write permissions, cloud credentials, or deployment secrets. Keep untrusted pull-request code away from privileged tokens.

Path filters reduce unnecessary runs, but remember that a required check skipped by paths can interact with branch protection. Test the repository’s merge rules rather than assuming skipped behavior.

GitHub documents workflow syntax and permissions in its official Actions reference. Review it when changing events or token authority.

Bundle only after linting succeeds

Bundling resolves references into a downstream artifact. It catches missing targets and gives documentation, SDK, and gateway jobs one file to consume.

npm run api:lint
npm run api:bundle

In a shell script, run the second command only if the first succeeds. The Redocly guide on linting and bundling uses this same fail-fast sequence.

Do not commit generated bundles unless a consumer requires them in source control. Committed source plus committed output often drift. Prefer generating once in CI, attaching the artifact, and promoting that exact artifact through documentation and release jobs.

Record the source commit and tool version with the artifact. If a generated SDK behaves incorrectly, operators should be able to identify the specification and generator that produced it.

Inspect whether bundling preserves external references, examples, or extensions as downstream tools expect. Different bundlers can legitimately produce structurally different but equivalent documents.

Add organization-specific lint rules

Generic rules cannot enforce your error envelope, pagination fields, tenant header, or idempotency policy. Add custom checks for stable product conventions.

For example, every mutating operation might require an Idempotency-Key header and a 409 response. Every collection response might require data and page. Every operation should include at least one realistic example.

Represent the policy first in prose with good and bad examples. Then encode it in a custom plugin or a separate validation script. A clear policy prevents the linter from becoming an unexplained source of reviewer commands.

Create plugins/rules/require-idempotency-key.js with a Redocly visitor. context.key is the operation’s HTTP method because the operation is a child of the Path Item object:

export default function requireIdempotencyKey() {
  return {
    Operation: {
      enter(operation, context) {
        if (!['post', 'put', 'patch', 'delete'].includes(context.key)) return;

        const parameters = operation.parameters ?? [];
        const present = parameters.some((parameter) =>
          !('$ref' in parameter) &&
          parameter.in === 'header' &&
          parameter.name.toLowerCase() === 'idempotency-key'
        );

        if (!present) {
          context.report({
            message: 'Mutating operations must define the Idempotency-Key header.',
            location: context.location.child('parameters')
          });
        }
      }
    }
  };
}

Register the rule in a plugin object, enable it in redocly.yaml, and test it with minimal passing and failing fixtures. Redocly’s custom-rule documentation defines the visitor and reporting APIs used here. If path-level parameters can satisfy your policy, resolve and merge those explicitly rather than checking only operation.parameters.

Do not lint prose into sameness. Descriptions need technical review, not just minimum character counts. Automated policy should catch repeatable structural mistakes and leave judgment to reviewers.

Detect breaking API changes

A structural diff can flag removed operations, required parameters, narrowed schemas, deleted enum values, and incompatible response changes. Compare the proposed bundle with the bundle from the target branch, not with an arbitrary release downloaded from an untrusted URL.

Classify changes rather than declaring every diff breaking. Adding an optional response field may be compatible for tolerant clients but problematic for strict generated models. Widening a request enum and widening a response enum have different consumer effects.

Protect the comparison baseline. Generate the base artifact from the checked-out base commit with the same locked toolchain, then compare. Avoid running privileged workflows on attacker-controlled pull-request code.

Require an explicit review label or architecture approval for accepted breaking changes. The approval should link to a migration plan, versioning decision, consumer inventory, and rollout timeline. A checkbox saying “breaking change intended” is not sufficient.

Contract diffs do not detect semantic breaks such as changing sorting, rate limits, or the meaning of a field. Those need review and tests. Include changelog fragments in the same pull request so outward communication is not reconstructed after release.

Validate examples and runtime behavior

Examples are code. Parse JSON and YAML examples, validate them against their schemas, and exercise shell commands where possible. A syntactically valid example with a nonexistent enum value damages trust quickly.

Add contract tests against a disposable environment or recorded fixtures. Verify status codes, required headers, content types, and response schemas. Keep tests deterministic and remove sensitive production data from fixtures.

For implementation-first systems, compare captured traffic or framework routes with the description. For design-first systems, generate server interfaces or tests from the approved artifact. Neither approach removes the need for human review.

Test authentication and error paths, not only 200 responses. Validate malformed identifiers, missing scopes, conflicts, pagination terminals, rate limits, and idempotent retries. These are the areas where documentation most often drifts from reality.

If examples call a live sandbox, give CI a dedicated low-privilege identity and bounded cleanup. Do not expose secrets to pull requests from forks.

Generate previews without creating a security hole

A documentation preview helps reviewers evaluate navigation, rendering, code samples, and schema descriptions. Build it from the validated bundle and attach a static artifact or deploy it to an isolated preview environment.

Treat rendered descriptions as untrusted content. Sanitize HTML according to the renderer’s model. Do not give a pull-request preview access to production cookies, internal networks, or deployment credentials.

Post one stable preview link through a trusted workflow after validation. Avoid a bot comment on every push; update the existing comment if the platform supports it. The preview status should not replace the required validation check.

Preserve preview artifacts long enough for review but apply automatic expiry. They may reveal unreleased endpoint names or examples, so keep private repositories and access controls aligned.

After merge, publish documentation from the exact validated artifact. Rebuilding with a different dependency set introduces a gap between review and production.

Make failures useful to contributors

A CI error should include file, line, rule ID, explanation, and a link to the rule or internal standard. GitHub annotation output supplies the location; your rule message should explain the remediation.

Provide local commands in CONTRIBUTING.md:

npm ci
npm run api:lint
npm run api:bundle

Explain common reference failures, how to test a custom rule, how exceptions are approved, and who owns the API standard. Keep a minimal failing example for new contributors.

Set a job timeout and cancel superseded runs to reduce feedback delay. A long queue teaches developers to bypass the process. Cache dependencies safely, but do not cache generated validation results across different specs.

Measure time to feedback, recurring rule violations, exceptions, and escaped production drift. Do not use “number of lint errors” as a performance target that encourages suppressions.

Assign ownership in repositories and monorepos

Every API description needs a team that can approve semantic changes, answer consumer questions, and repair drift. Use CODEOWNERS for relevant source paths, but pair it with catalog or service metadata that identifies the operational owner. File review ownership and production responsibility may not be the same team.

In a monorepo, define each API root explicitly in redocly.yaml and run validation only for affected descriptions without weakening shared rules. Reusable workflows can standardize Node setup, dependency installation, lint format, artifact naming, and retention. Keep product-specific compatibility policy near the API rather than hiding every decision in a central pipeline repository.

Prevent one API from overwriting another bundle by using stable artifact names that include API identity and version. Fail when two roots claim the same identifier. Publish an inventory page that links source, owner, latest artifact, production documentation, and changelog.

Schedule a full validation run even when path-filtered pull requests provide fast feedback. Shared rule updates, dependency upgrades, and referenced components can affect descriptions that did not change directly. The scheduled result should create an owned issue or alert, not silently age in an Actions history page.

Frequently asked questions

Is OpenAPI validation the same as contract testing?

No. Linting validates the description’s structure and chosen rules. Contract testing compares implementation behavior with the contract. Mature pipelines use both because either side can be internally valid and still disagree.

Should generated OpenAPI bundles be committed?

Usually no. Generate the bundle from reviewed source in CI and promote that artifact. Commit it only when a consumer truly requires repository access, and add a check that fails when source and bundle differ.

How strict should an OpenAPI ruleset be?

Strict enough to protect consumer-facing conventions without making exceptions routine. For an existing API, establish a baseline and prevent new violations, then retire old debt with owners and deadlines.

Can GitHub Actions safely preview API documentation?

Yes, if untrusted pull-request content receives no privileged secrets, the renderer sanitizes content, and previews are isolated and access-controlled. Separate validation from any privileged deployment step.

Turn the specification into a governed artifact

The strongest OpenAPI pipeline validates structure, encodes meaningful standards, reviews compatibility, tests examples, and promotes one traceable bundle. It gives contributors fast local feedback and gives reviewers evidence beyond a green checkbox.

If your team needs an OpenAPI reference, governance workflow, or developer portal that stays aligned with releases, explore our API documentation service. A specification becomes valuable when developers can trust both its content and its lifecycle.

Start a conversation

Ready to improve your developer documentation?

Tell us what developers need to accomplish and where the current experience falls short.

Expect a reply within one business day.