[api-contracts] Check the whole stable and tech_preview API surface for breaking changes#280633
[api-contracts] Check the whole stable and tech_preview API surface for breaking changes#280633TinaHeiligers wants to merge 12 commits into
Conversation
|
Pinging @elastic/kibana-core (Team:Core) |
jloleysens
left a comment
There was a problem hiding this comment.
Nice work @TinaHeiligers ! Not fully done reviewing, left a few comments I'd like to align on first. Specifically the use of @ mentions and when PR comments are surfaced (currently it seems we are gating the comment based on the tier?)
|
|
||
| return `## API Contract Breaking Changes — Stable & Technical Preview | ||
|
|
||
| cc ${ownerMentions} |
There was a problem hiding this comment.
I understand the goal is to not let this slip by unnoticed, but I think surfacing a comment in the PR already achieves that. I'm concerned including an @ mention is going to be too noisey, especially if we work is in progress (PR authors may have unintentionally made a change that we are notifying owning teams of).
Pointing PR authors to the team to talk to is a cool idea though!
There was a problem hiding this comment.
Um, I had a look into using CODEOWNERS for all endpoints and it's a bigger lift than I think is worth adding in this PR (details below for future enhandcements)
Details
@kbn/code-owners gives us getOwningTeamsForPath(filePath), resoling teams from a repo file path. But a breaking change only gives us an API endpoint (e.g. /api/fleet/agents, method POST) and a schema JSON-pointer in source (e.g. /components/schemas/...). The checker's only input is the bundled oas_docs/output/kibana*.yaml, and the spec carries nothing that bridges endpoint → source file:
-
operations only have
x-metaTags: product_name: Kibana, x-state, x-codeSamples— no team, no plugin, no path; -
source is a JSON pointer into the schema tree, not a file;
-
there isn't an API-path→plugin/owner mapping anywhere in oas_docs/.
So to resolve CODEOWNERS for the whole surface involves having to create an endpoint to file bridge, and every option is a real project: -
Generation-time enrichment (cleanest): stamp an x-owner/source hint onto each operation in router-to-openapispec so the checker can read it. Cross-cutting — touches the OAS pipeline and every producer, and handwritten specs still wouldn't have it (same class of gap we've been designing around).
-
Route-registry introspection: enumerate registered routes → registering plugin dir → CODEOWNERS. Requires booting/inspecting Kibana; not available to a script that just diffs two YAML files.
-
URL-prefix → plugin heuristic: brittle and incomplete; CODEOWNERS is keyed by file path, not URL, and lots of endpoints don't map cleanly.
The Terraform path avoids all this because it uses a hand-maintained terraform_provider_apis.yaml mapping endpoint → resource → owners, which I admit is outdated already. Replicating that by hand for the whole surface would be a maintenance sink and not worth it.
| log.error(report); | ||
| const experimentalCount = breakingChanges.length - entries.length; | ||
| if (experimentalCount > 0) { | ||
| log.info(`${experimentalCount} experimental-tier breaking change(s) excluded`); |
There was a problem hiding this comment.
May not be following quite right: but I think it would still be nice to surface that a breaking change is being introduced due to the impact even for experimental APIs. That at least makes sure that authors are informed that what they are about to ship is gonna break clients.
There was a problem hiding this comment.
My concern with including experimental APIs is that it's going to be too noisy and folks will ignore the comments, knowing that the checks also applying to APIs that are allowed to introduce breaking changes.
That being said, the original issue specifically scopes the work as
detect breaking changes across the whole public API surface, enforce policy by API stability tier (stable / GA, tech_preview, experimental)
so yes, I'll add it.
| /** | ||
| * Parse an OpenAPI `x-state` string into a stability tier (and optional `since`). | ||
| * Inverse of `getXState` in @kbn/router-to-openapispec (src/util.ts), which writes | ||
| * these strings. Unrecognized, empty, or missing input is treated as `stable` | ||
| * (most conservative), so an unknown state is never under-classified. | ||
| */ |
There was a problem hiding this comment.
Bc these utilities are coupled in this way, I wonder if we should import that utility from router-to-openapispec and assert that this utility can "decode" it's output from a test.
Note: not all spec for Kibana APIs comes from router-to-openapispec... Hand written APIs may define x-state differently. Worth consider for this classification to work.
There was a problem hiding this comment.
Hand written APIs may define x-state differently
The only stability signal in the OAS is x-state and the handwritten variants are case/spacing deviations of the same nomenclature (Technical preview, Generally available; Added in X, added in X, ''/absent.)
There was a problem hiding this comment.
Good call on both points.
I've added a round-trip test that asserts parseXState decodes its output for every stability tier (stack + serverless env), so if the generator's wording ever changes the test fails loudly.
On hand-written specs, I deliberately didn't couple classification to getXState. parseXState's tolerant to case etc with a conservative stable default, and I've added the data-driven test over the what we have as x-state values in oas_docs/output/*.yaml.
The tests cover different things — the round-trip guards the generated contract, the data-driven test guards hand-written reality.
Also LMK if the comment/essay for parseXState's too verbose. I lean towards over-documenting rather than under.
There was a problem hiding this comment.
example reports after the changes: https://gist.github.com/TinaHeiligers/0b118410bb6929163b77496419e4e5ac
TinaHeiligers
left a comment
There was a problem hiding this comment.
address review comments
| * (in check_contracts) and never reach the report, so a reported entry is always | ||
| * stable or tech_preview. | ||
| */ | ||
| export type CaughtTier = 'stable' | 'tech_preview'; |
There was a problem hiding this comment.
naming's hard, open to changing CaughtTier to DetectedTier or something else.
| log.error(report); | ||
| const experimentalCount = breakingChanges.length - entries.length; | ||
| if (experimentalCount > 0) { | ||
| log.info(`${experimentalCount} experimental-tier breaking change(s) excluded`); |
There was a problem hiding this comment.
My concern with including experimental APIs is that it's going to be too noisy and folks will ignore the comments, knowing that the checks also applying to APIs that are allowed to introduce breaking changes.
That being said, the original issue specifically scopes the work as
detect breaking changes across the whole public API surface, enforce policy by API stability tier (stable / GA, tech_preview, experimental)
so yes, I'll add it.
Co-authored-by: Jean-Louis Leysens <jloleysens@gmail.com>
| */ | ||
| export const parseXState = (xState: string | undefined): ParseXStateResult => { | ||
| if (!xState) { | ||
| return { tier: 'stable' }; |
There was a problem hiding this comment.
Choosing stable as the fall back tier so that it mirrors the platform contract:
/src/core/packages/http/server/src/router/route.ts
availability?: {
/** @default stable */
stability?: 'experimental' | 'stable' | 'tech_preview';
most operations don't carry x-state (~ 70% stack, ~69% serverless) so catching BC's in unknowns carries the benefit of strongly encouraging API owners to declare one.
What I'm concerned about is that ~70% don't declare x-state. It's ok while we run the contracts check as a soft fail but might catch contributors off guard.
| } | ||
|
|
||
| const experimentalCount = breakingChanges.length - entries.length; | ||
| const gatingEntries = entries.filter((entry) => isGatingTier(entry.tier)); |
There was a problem hiding this comment.
gating tier and reporting tier are intentionally separate. Reporting tier is what comments surface in the notification, the gating tier determines the check's exit code.
💛 Build succeeded, but was flaky
Failed CI StepsMetrics [docs]
History
|
Fix #269134.
Summary
The API contracts check used to only look at APIs the Terraform provider consumes, so it missed breaking changes in every other stable or Technical Preview API.
This PR extends the check to the whole Kibana OpenAPI surface. Each breaking change is classified by its stability tier (read from
x-statein the base spec). Stable and Technical Preview breaks fail the check; experimental breaks are reported for visibility but never gate, since experimental APIs are allowed to break.Terraform-provider tracking is removed entirely. The check no longer keys off Terraform-consumed APIs, so the
src/terraform/*matching,terraform_provider_apis.yaml, and the owner@mentionsin the PR comment are all gone.The hard gate and the
tech_previewapproval signal are follow-up in #261072.Detaileds
main(unchanged).x-statetier from the base spec. Parsing is tolerant of the casing and spacing variance in hand-written specs, and a round-trip test asserts the parser still decodes whatgetXStatein@kbn/router-to-openapispecproduces, so generator drift fails loudly.x-statedefaults tostable, the most conservative tier. An unknown state can only resolve up tostable, never into experimental, so a break is never under-classified.soft_failkeeps merge unblocked. The flip to a hard gate is removing one line.@mentions.Implementation flow
flowchart LR A[diff whole OAS surface] --> B[apply allowlist] B --> C{classify by tier} C -->|stable / tech_preview| D[report + non-zero exit] C -->|experimental| E[report as informational, exit 0] D --> F{soft_fail on?} F -->|yes| G[warn: comment, merge unblocked] F -->|no| H[hard gate: merge blocked] E --> I[comment posts, no gate]soft_failis the only warn-vs-block control, so the rollout flips one line rather than shipping new code.How to test this
The behavior to verify is what the checker gates on, reports as informational, and suppresses. Run it against
mainafter editing the generated spec to simulate each case, then revert the spec edit.Gate on a stable break: remove a path or a required response property from a stable endpoint in
oas_docs/output/kibana.yaml(e.g./api/alerting/rule/{id}/snooze_schedule), then run the command. Expect a non-zero exit,Detected 1 breaking change(s) in stable/tech_preview APIs: 1 stable, 0 tech_preview, and a report entry with"tier": "stable".Gate on a Technical Preview break: repeat on an endpoint whose
x-stateisTechnical Preview(e.g./api/features). Expect a non-zero exit,0 stable, 1 tech_preview, and"tier": "tech_preview".Report an experimental break without gating: repeat on an endpoint whose
x-stateisExperimental(e.g./api/agent_builder/a2a/{agentId}). Expect exit 0,1 experimental-tier breaking change(s) reported (informational, not blocking),No breaking changes detected in stable or tech_preview APIs, and a report entry with"tier": "experimental"still present so the notifier can surface it.Suppress via the allowlist: add an entry for the stable break from step 1 to
packages/kbn-api-contracts/allowlist.json(use"method": "all"for a path-level change) and re-run. Expect exit 0 andAll breaking changes are allowlisted.Confirm the CI posture: the check exits non-zero on a stable/tech_preview break, the Buildkite step is
soft_fail, merge isn't blocked, and the notifier posts (or updates) the PR comment regardless of exit code.Checklist
Identify risks
This is scoped to the contract-checker tooling and its CI wiring. It changes what CI reports, not any runtime API behavior.
soft_failkeeps merge unblocked; the allowlist suppresses approved BCs; experimental BCs are informational, not gatingx-state, so their BCs classify asstablex-statefor a precise tiermain's current OAS, so merged changes are in the baseline and don't re-triggertech_previewBC merges while the check is softsoft_failwhen ready[updated after review feedback]