The Merge That Broke a Shipped App
The Merge That Broke a Shipped App
A developer renames Post.body to Post.content for consistency. Unit tests pass, the server boots, the staging smoke test passes — staging queries were written by the same person, so of course they use the new name. The pull request merges on Friday.
The mobile build from six months ago does not. Its operations still select body, and validation rejects them the moment they arrive. Nobody can hotfix an app sitting in review queues and update cycles; the only options are to roll the rename back or to accept that every old client errors until it updates.
Nothing in the test suite was wrong. The suite simply had no access to the one input that mattered: the operations real clients have already shipped. A schema check is the pipeline stage that has it.
The check runs on the schema in the pull request, before merge, and answers three questions with the artifacts you already produce: what changed relative to the schema currently published for this graph, which registered operations stop validating against the proposal, and — when usage data is available — how many active clients each change touches. Fail the build on the answer, and Friday becomes uneventful.
The classification rules themselves are covered in the schema evolution rules topic; this topic is about the machinery that applies them on every pull request, and the release step that keeps the registry honest afterward.
What a Schema Check Actually Compares
What a Schema Check Actually Compares
Three inputs, three check types. Teams that understand the difference interpret failures correctly instead of just hitting "re-run."
1. Breaking-change check — proposed schema versus the published one. A structural diff classifies each edit with the rules you already know: removals, renames, required arguments, output nullability loosening, and input tightening fail; additions and deprecations pass. This check needs no client data at all, which makes it fast and also its limitation: it cannot tell you whether anyone would actually break.
2. Operation check — the proposed schema versus every registered client operation. Each checked-in operation document, persisted query, or submitted artifact is revalidated against the proposal, and the failure report names the operation, the field it lost, and the repository it came from:
FAIL breaking-change: field "Post.body" removed
op: GetPostSummary (mobile-app/repo, persisted id 3f9a...)
op: nightly-digest (reports/repo, query #47)
That output changes the conversation. "The field is used by two operations in two repos" is actionable; "this is a breaking change" invites debate.
3. Usage-weighted severity — the diff joined with field-level telemetry. A removed field with zero requesting operations in 90 days reports as informational; the same removal with 40 daily requests reports as blocking. This is where the numbers from the versionless API strategy become enforcement instead of dashboards.
For federated graphs the check adds a fourth dimension: composition. The proposed subgraph SDL must still compose with its siblings into a valid supergraph, which catches errors like removing a key field an entity resolves by — a break invisible to a single-subgraph diff.
Tooling is convergent rather than exotic. Apollo Rover's graph check (and subgraph check for federation) runs all three against a graph ref; GraphQL Inspector offers diff and usage checks with a GitHub action; several gateways offer registry-hosted checks. The command surface differs, the inputs do not: proposed schema, published baseline, operation set.
The Pipeline: Check, Merge, Publish
The Pipeline: Check, Merge, Publish
The check only means something if it gates merges, and the registry only means something if it reflects what shipped. Both are a few lines of pipeline:
# .github/workflows/schema.yml
name: schema
on:
pull_request:
paths: ['schema/**']
push:
branches: [main]
paths: ['schema/**']
jobs:
check:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rover
run: curl -sSL https://rover.apollo.dev/posix/latest | sh
- name: Breaking-change + operation check
run: ./bin/rover graph check storefront@current --schema ./schema/storefront.graphql
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
publish:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rover
run: curl -sSL https://rover.apollo.dev/posix/latest | sh
- name: Publish the released schema
run: ./bin/rover graph publish storefront@current --schema ./schema/storefront.graphql
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
Design decisions hiding in that file:
- Check on pull requests, publish only on the default branch. The published schema is the baseline every future check compares against; publishing a proposal before it merges poisons the baseline with unreleased fields.
- Path filters so a schema check does not run on documentation-only pull requests, and so a release note does not trigger a publish.
- Secrets never reach forked pull requests. A fork cannot read
APOLLO_KEY, so the job must degrade gracefully — report "check skipped, requires maintainer approval" rather than failing every community contribution, or run a schema-only diff that needs no credentials. - The check is the only required status. Make it required in branch protection; an optional check is a suggestion with extra steps.
Triage when it fails. The failure names the edit, the operations, and the clients. Three legitimate resolutions:
- Change the proposal — usually the right answer: the edit was breaking and an additive alternative exists.
- Migrate usage first — if the removal is correct, move the listed operations to the replacement, regenerate their artifacts, and reopen the PR when the check reports zero affected operations.
- Approve with evidence — for a genuinely intentional break (dead field with telemetry to prove it, or a coordinated release), record the evidence in the PR and let a schema owner merge. If this needs to happen often, the usage pipeline is broken, not the check.
After merge, graph publish updates the registry; gateways and other consumers fetch the new baseline from there, and the next pull request diffs against the schema that is actually live. Skipping the publish step is the quiet failure mode: checks keep comparing against an old baseline until the first false failure teaches someone the pipeline was half-wired.
Interview framing: walk the loop — propose, check against published schema and registered operations, gate, merge, publish — then explain one failure triage with a concrete example. Mentioning that deprecated-but-used fields still fail checks is the detail that shows you have operated this, not only read about it.
Practice Problems
The repo keeps the schema at schema/storefront.graphql and publishes it as storefront@current. Write the GitHub Actions workflow that blocks pull requests with breaking changes (including ones that affect registered operations) and publishes the schema only after merge. State how the job behaves on forked pull requests.
Example:
Input: PR removes Post.body; two registered operations still select it
Output: check job fails with the breaking-change report naming both operations; merge is blocked until the proposal changes or usage migrates
The check compares the branch schema against the published baseline and revalidates registered operations, which is where the impacted-operation list comes from.
Optimal Solution — O(a + b) time, O(a + b) space
One workflow, two jobs split by event: a check job on pull requests that installs the registry CLI and runs a breaking-change plus operation check against the published graph ref, and a publish job on pushes to main that publishes the same file. Fork runs get an explicit skipped step because secrets are unavailable.
name: schema
on:
pull_request:
paths: ['schema/**']
push:
branches: [main]
paths: ['schema/**']
jobs:
check:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rover
run: curl -sSL https://rover.apollo.dev/posix/latest | sh
- name: Breaking-change + operation check
if: github.event.pull_request.head.repo.full_name == github.repository
run: ./bin/rover graph check storefront@current --schema ./schema/storefront.graphql
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
- name: External fork - report skip
if: github.event.pull_request.head.repo.full_name != github.repository
run: echo "schema check skipped on forks; maintainer must run it on a branch"
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rover
run: curl -sSL https://rover.apollo.dev/posix/latest | sh
- name: Publish released schema
run: ./bin/rover graph publish storefront@current --schema ./schema/storefront.graphql
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }} Edge Cases:
- First-ever run with no published schema (bootstrap by publishing once before requiring the check)
- Forked pull requests without secrets (skip with an explicit message, never a silent pass)
- Renamed schema file breaking path filters and the --schema argument together
Quiz
1. How is an operation check stronger than a pure schema diff?
2. A check fails because a @deprecated field was removed and an operation still selects it. What is the right response?
3. When should the schema be published to the registry?
4. Why gate schema changes in CI instead of detecting breakage after deploy?
Flashcards
Question
The three inputs of a schema check
Click to reveal answer
Answer
Proposed schema from the branch, published schema as baseline, registered client operations as impact set.
Question
The three check types
Click to reveal answer
Answer
Breaking-change diff, operation revalidation, usage-weighted severity; federation adds a composition check.
Question
Publish timing
Click to reveal answer
Answer
Check on pull requests, publish to the registry only after merge to the default branch — the published schema is the baseline.
Question
Deprecated but used
Click to reveal answer
Answer
Removal still fails checks while any registered operation selects the field; deprecation is a notice, not a license.
Question
Check failure triage
Click to reveal answer
Answer
Change the proposal, migrate usage until zero affected operations, or merge with recorded evidence and a schema owner.
Question
Forked PRs and secrets
Click to reveal answer
Answer
No APOLLO_KEY on forks: report a skipped check or run a credential-free schema diff instead of failing the job.
Revision Notes
Key Takeaways
- 1. A diff says what changed; an operation check says who breaks — you need both
- 2. Usage data turns breaking-change detection into proportionate severity
- 3. The published schema is the baseline: check before merge, publish after
- 4. Triage has three exits: change the schema, migrate usage, or merge with evidence
Interview Tips
- • Walk the full loop — propose, check, gate, merge, publish — and name what each comparison uses
- • Use a concrete failure report in your answer: which operation, which field, which repo
- • Explain why deprecated-but-used fields still block removal; it shows you understand deprecation as a lifecycle, not a label
Cheat Sheet
Schema Checks in CI Cheat Sheet
Inputs: proposed schema (branch) · published schema (baseline) · registered operations (impact)
Checks: breaking-change diff · operation revalidation · usage-weighted severity · composition (federation)
on:
pull_request: { paths: ['schema/**'] }
push: { branches: [main], paths: ['schema/**'] }
jobs:
check: # PRs only
steps:
- run: curl -sSL https://rover.apollo.dev/posix/latest | sh
- run: ./bin/rover graph check storefront@current --schema ./schema/storefront.graphql
env: { APOLLO_KEY: ${{ secrets.APOLLO_KEY }} }
publish: # default branch only
steps:
- run: ./bin/rover graph publish storefront@current --schema ./schema/storefront.graphql
Rules
- check on PR, publish only after merge — published schema is the next baseline
- make the check a required status; path-filter to schema changes
- forked PRs: skip with a message (no secrets) or run a credential-free diff
- failure triage: change proposal · migrate usage to zero · merge with recorded evidence