Lesson 3.9: Schema watch — what moved while you were away
Difficulty: Intermediate | Duration: 15 min | Prerequisites: Lesson 3.1
Overview
Lesson 3.8 gated on the API's design. This one gates on its change: bowire diff compares two snapshots of a surface and reports what was added, removed, or changed shape. It is the schema half of the PR bot, and it needs no access to the provider's source — only two discoveries of it.
Take a snapshot
A snapshot is the discovered service list written to a file:
bowire diff snapshot https://api.example.com --protocol rest -o base.json
Commit that file, or keep it as a CI artefact from the last green build. It is the "before" side.
Diff two sides
--base and --head each take a snapshot file or a live URL, so you can compare a file against production, two files against each other, or two environments directly:
# what changed since the committed baseline
bowire diff --base base.json --head https://api.example.com --protocol rest
# staging against production, no files at all
bowire diff --base https://staging.example.com --head https://api.example.com --protocol rest --format markdown
A real run, with one method removed on the head side:
$ bowire diff --base base.json --head head.json --format markdown
**API schema:** -1 method.
**Removed methods**
- `Users` `GET /api/users/{id}`
--format markdown is the one to pipe into a pull-request comment; json (the default) is the one to parse.
Gate on it
bowire diff --base base.json --head https://api.example.com --fail-on breaking
--fail-on |
Exit non-zero when |
|---|---|
none (default) |
never |
breaking |
a service or method was removed, or a signature changed |
any |
any change at all to the callable surface |
breaking is the useful default for a pipeline: adding an endpoint should not fail a build, removing one should. The removal above exits 1 under breaking.
Wire it into a pull request
# .github/workflows/api-diff.yml
- name: Snapshot the base branch's API
run: bowire diff snapshot ${{ env.BASE_URL }} --protocol rest -o base.json
- name: Diff the PR's API against it
run: bowire diff --base base.json --head ${{ env.PR_URL }} --protocol rest
--format markdown --output diff.md --fail-on breaking
- name: Comment the diff
if: always()
run: gh pr comment ${{ github.event.number }} --body-file diff.md
Note if: always() on the comment step: the diff step exits non-zero on a breaking change, and the comment explaining why is exactly what you want posted in that case.
A gRPC server without reflection
Discovery needs a schema. A gRPC server with Server Reflection switched off — the state Bowire's own scanner recommends — has none to offer over the wire, so hand it a compiled descriptor set:
protoc --descriptor_set_out=api.protoset --include_imports api.proto
bowire diff snapshot https://api.example.com --protocol grpc --grpc-descriptor-set api.protoset -o base.json
The flag is ignored by every other protocol, so it is harmless in a shared script.
Key Takeaways
bowire diff snapshot <url> -o <file>writes the "before" side; commit it or keep it as a CI artefact.bowire diff --base … --head …accepts a snapshot file or a live URL on either side, so file-vs-live and env-vs-env both work without a temporary file.--fail-on breakingfails on removals and signature changes but not on additions, which is the shape a pull-request gate wants.--format markdownproduces the comment body;jsonis for a parser.- A reflection-less gRPC server is diffable with
--grpc-descriptor-set.
What's Next
Continue: → Lesson 3.8: CI gates runs alongside this one — design rules, contracts and the rollup.