Federated Schema Validation in CI/CD Pipelines

This page shows exactly how to wire a federated composition check into CI so a breaking subgraph change is caught on the pull request, with the precise error payloads you will see and how to act on them. It is the focused companion to schema validation in CI/CD pipelines, itself part of GraphQL Federation Architecture & Design; read the parent for the broader checkpoint architecture.

When to use this pattern

  • You run Apollo Federation v2 with two or more independently deployed subgraphs and need a per-PR gate.
  • A managed supergraph variant exists in the registry that the check can diff a proposed SDL against.
  • You want machine-readable results (FAILURE / WARNING / INFO) you can parse to block or annotate a merge.

Prerequisites

How federated composition checks work

Federated composition is not isolated SDL validation. The composition engine computes a unified supergraph by resolving entity keys, merging type definitions across subgraphs, and validating directive compatibility — so a subgraph that is internally valid can still break the merge. A check therefore runs five stages: fetch the current supergraph baseline from the registry, inject the proposed subgraph SDL, diff the resulting supergraph against the baseline, classify each change by severity, and enforce the gate by blocking on FAILURE while allowing WARNING with a PR annotation.

The five stages of a federated composition check Rover fetches the baseline supergraph, injects the proposed subgraph SDL, diffs the two supergraphs, classifies each change as failure, warning, or info, then enforces the merge gate. 1. fetch baseline 2. inject proposed SDL 3. diff supergraphs 4. classify severity 5. gate block / allow

Implementation walkthrough

The workflow below runs a composition check on every PR that touches subgraph SDL, then hands the JSON output to a small Node script that decides the build’s exit status.

name: Federated Schema Validation
on:
  pull_request:
    paths:
      - 'subgraph/**/*.graphql'

jobs:
  schema-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Cache Rover Binary
        uses: actions/cache@v4
        with:
          path: ~/.rover
          key: ${{ runner.os }}-rover-latest

      - name: Install Rover
        run: |
          curl -sSL https://rover.apollo.dev/nix/latest | sh
          echo "$HOME/.rover/bin" >> $GITHUB_PATH

      - name: Run Composition Check
        env:
          APOLLO_KEY: ${{ secrets.APOLLO_KEY }}        # graph:read + graph:write token
          APOLLO_GRAPH_REF: ${{ vars.APOLLO_GRAPH_REF }} # graph-id@variant, e.g. platform-api@staging
        run: |
          rover subgraph check "$APOLLO_GRAPH_REF" \
            --schema ./subgraph/schema.graphql \
            --name subgraph-service \
            --format json > check_output.json

      - name: Evaluate Results
        run: node ./scripts/evaluate-check.js check_output.json

The evaluator parses Rover’s structured output, prints each breaking change with its code and path, and exits non-zero to fail the pipeline.

const { readFileSync } = require('fs');

const checkData = JSON.parse(readFileSync(process.argv[2], 'utf8'));
// Rover JSON wraps results under data.changes
const changes = checkData.data?.changes ?? checkData.changes ?? [];
const failures = changes.filter((c) => c.severity === 'FAILURE');

if (failures.length > 0) {
  console.error('Composition failed. Breaking changes detected:');
  failures.forEach((f) => {
    const path = Array.isArray(f.path) ? f.path.join('.') : (f.path ?? 'unknown');
    console.error(`  [${f.code}] ${f.description} (Path: ${path})`);
  });
  process.exit(1);
}
console.log('Schema composition passed. No breaking changes.');
process.exit(0);

Severity maps to action like this: FAILURE covers removed fields, changed argument nullability, modified @key directives, and type-ownership conflicts; WARNING covers deprecated fields, added optional arguments, and new entity references; INFO covers additive types and directive additions. Block only on FAILURE, annotate on WARNING.

Verification steps

Reproduce the check locally before trusting CI, then confirm the exact behaviour.

# Reproduce the merge locally
rover supergraph compose \
  --config ./supergraph-config.yaml \
  --output ./supergraph.graphql

# Extract only the failing paths from the CI JSON
jq '.data.changes[] | select(.severity == "FAILURE") | {code, description, path}' check_output.json

A clean run composes to supergraph.graphql and the jq filter returns nothing. The most common hard failures and their fixes:

Error Code Example Message Root Cause Resolution
INVALID_FIELD_SHARING Field "User.email" is defined in multiple subgraphs but is not marked as @shareable Uncoordinated type ownership Apply @shareable in each subgraph that defines the field, or consolidate to one owner
KEY_FIELDS_MISSING_EXTERNAL @key field "id" must be declared @external in extending subgraph Extending subgraph not marking key fields @external Add @external to the @key fields in the extending subgraph
FIELD_TYPE_MISMATCH Field "User.email" type mismatch: expected "String!", found "String" Nullability drift between subgraphs Align SDL nullability; shared fields require exact type signatures

To confirm the gate itself works, push a deliberately breaking change (drop a non-deprecated field) to a throwaway branch and verify the workflow exits non-zero with the field named in the log.

Common mistakes & gotchas

  • Checking against the wrong variant. Always pass an explicit APOLLO_GRAPH_REF with the right @variant. A check that diffs @staging while production runs a different schema produces false greens — enforce the variant per environment, as covered in schema validation in CI/CD pipelines.
  • Hardcoding the supergraph SDL in CI. Fetch it dynamically with the check or rover subgraph fetch; a stale committed copy drifts from the registry and masks real breaks.
  • Treating every WARNING as a blocker. Deprecations and additive changes are warnings by design. Blocking on them stalls planned migrations; annotate the PR and track the deprecation window instead.

Frequently Asked Questions

What permissions does the APOLLO_KEY need for a composition check?

A service token with graph:read and graph:write for the target graph. Read alone is insufficient because the check registers a transient composition against the variant.

Can I run the check without registry access?

Partly. rover supergraph compose --config validates a local merge offline, which catches structural conflicts like INVALID_FIELD_SHARING, but it cannot diff against production traffic or the registered baseline — so it will not detect client-impacting breaking changes. Use it as a fast local pre-check, not a replacement for the registry-backed gate.

How do I scale this across many subgraphs?

Use a CI matrix that runs one check per subgraph in parallel, each passing its own --name and --schema, against the shared APOLLO_GRAPH_REF. That keeps wall-clock time flat as the number of services grows.