Docs · Glidepath
The api-compat release check
Every release PR gets an api-compat check that compares the API the release ships with the API running in the
environment it is going to. It reports breaking changes, says which ones touch operations that are actually in use,
and lets an app owner accept a breaking change on the record. It is advisory today: it reports and never blocks.
What it compares
- The candidate: the API spec signed onto the promoted image at build time (
cicd.yamlapi.spec, defaultopenapi.yaml; see the cicd.yaml reference, “Theapi:block”). - The baseline: the spec signed onto the image the target environment runs now, which is the image the PR’s release file names on the PR’s base commit.
Both are read from the images’ https://hangar.io/attestations/api-spec/v1 attestations and verified first: a
sigstore bundle signed by this app’s own pipeline-runner (app-<app>-cicd or infra-<app>-cicd) under the
platform’s Fulcio, with a Rekor entry. An attestation that fails verification is ignored and the comment says so; it
is never read. The diff is oasdiff (in the toolbox image): its error and
warning findings are breaking, its info findings are counted as non-breaking.
oasdiff follows the spec’s own declarations. Removing a response property the spec marks optional is not breaking to
it; mark the fields clients rely on required if their removal should count.
Verdicts
| Verdict | Meaning |
|---|---|
pass |
No breaking change. |
warn |
Breaking changes, reported only. |
block |
Breaking changes, at least one to an operation in use, no owner override, and blocking switched on (below). The only verdict that fails the check. |
overridden |
Breaking changes accepted by an app owner. |
no-spec |
The promoted image carries no (verified) spec. A spec is optional; nothing is wrong. |
no-baseline |
Nothing to compare with: the first release to this environment, or its current image has no spec. |
unsupported |
An AsyncAPI spec. Diffing AsyncAPI is not supported yet. |
When the check itself cannot run (no PR, GitHub or the registry unreachable, oasdiff failing) its outcome is error
and the check run fails like any gate’s; while the gate is advisory that failure does not block either.
The PR comment shows a table of breaking changes (operation, change, level, in use) and ends with one hidden line,
<!-- glidepath:api-compat {json} -->, which Tower reads (and release-log-emit copies the verdict and counts into
the release log as api_compat_*). Its fields: version, verdict, env, candidate and baseline
(image, digest, specSha256, title, version, or null), breaking[] (id, level, text,
operation {method, path}, inUse, rate per minute), breakingTotal, nonBreakingCount,
usage {source, window, callers}, override {by, at, reason} or null, and notes. A very long list is cut to keep
the comment under GitHub’s size limit; breakingTotal keeps the real count.
“In use”
An operation is in use when the target environment’s service (<app>-<env>) served it in the last
apiCompat.usageWindow (default 7d), from the OTel collector’s span metrics
(traces_span_metrics_calls_total, server spans). Callers come from the trace service graph. Limits:
- Operations, not fields. Traces show that
GET /api/flights/{flightNumber}was called, not which response fields the caller read. Field-level impact needs consumer contract tests. - Routes only where the agent knows them. A span named only
GET, or a wildcard route likeGET /**, makes that operation’s usage unknown rather than “not in use”. - Sparse traffic. No calls in a quiet week is weak evidence.
- Reachability. The check queries
apiCompat.usage.<cluster>.urlfrom the control-plane cluster’s pipeline pods, with no credentials. A cluster without an entry, or a failed query, is usageunknown, and unknown never blocks. In the lab, hangar-prod’s Prometheus is ClusterIP-only and not reachable from hangar-dev, so releases to prod report usageunknownuntil a reachable endpoint exists (a query frontend exposed to the control plane).
Blocking (off by default)
Two switches, both needed:
- The gate is required on the cluster:
releaseGuardrailOverrides: { api-compat: { required: true } }in that cluster’s glidepath-catalog values (release-guardrails.md); re-run the ruleset script. apiCompat.blockWhenInUse: truein the same values.
Then a breaking change to an operation observed in use with no override is block. Breaking changes to
operations not in use, or with unknown usage, stay warn. An eligible rollback is always advisory.
The owner override
A PR comment starting /api-break-ok <reason> accepts the breaking changes, when written:
- after the promoted commit (an older one does not carry over to a new image), and
- by a GitHub user whose role on the app’s source repository (
github.com/<owner>/<app>) is admin or maintain, checked live through the GitHub App on every run.
Anyone else’s command is ignored and listed in the comment. Then comment /retest api-compat- to re-run the check.
The newest valid override wins; who, when and the reason (the rest of the comment’s first line) are in the comment,
and the hidden JSON line Tower reads.