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.
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_REFwith the right@variant. A check that diffs@stagingwhile 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
WARNINGas 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.