API Versioning
How Stella Ops versions its OpenAPI contracts and SDKs, declares deprecations, and keeps specs deterministic and reproducible. This page is for API consumers planning upgrades and for maintainers evolving the contracts; for the wider contract/SDK programme see SDK & OpenAPI Program, and for shared request conventions see the API Overview.
Last updated: 2026-05-30 (deep doc↔code reconciliation against src/Api/StellaOps.Api.OpenApi)
Implementation status. Parts of this page describe a target design that is not yet wired up in the repository. Where a mechanism is aspirational it is marked (PROPOSED — not implemented) so consumers do not depend on it. The composer (
compose.mjs) is real and runs; the lint/compat/changelog scripts referenced frompackage.json(api:lint,api:examples,api:compat,api:compat:test,api:changelog) currently point atscripts/api-*.mjsfiles that are not present in the tree, and the driving CI workflows live under.gitea/workflows-archived/. Treat those steps as the intended pipeline rather than a guaranteed-green build until the scripts are restored.
Principles
- Semantic versioning: OpenAPI artifacts follow
MAJOR.MINOR.PATCH[-PRERELEASE]where breaking changes bump MAJOR, additive changes bump MINOR, fixes/docs bump PATCH, and freeze candidates carry a prerelease tag (e.g.0.1.0-alpha). The aggregate spec is currently0.0.1(unfrozen); per-service specs version independently (Authority0.1.1, Scanner1.0.0, Policy/Scheduler0.0.3, Graph/JobEngine/Export Center0.0.2). - Long-lived compatibility: At least N-1 major versions remain runnable for 12 months; minor versions remain compatible for 6 months after release unless a security fix requires earlier removal. (PROPOSED — not implemented): no supported-version window is enforced in code today.
- Explicit deprecations: Every breaking removal/change is preceded by a deprecation window. The enforced mechanism is the
x-deprecationOpenAPI extension on the operation (see Deprecations), checked by Spectral rules; standardDeprecation/SunsetHTTP response headers are the runtime signal. - Deterministic specs: The generated
src/Api/StellaOps.Api.OpenApi/stella.yamlaggregate is reproducible from per-service specs viacompose.mjs(stable ordering of paths, schemas, parameters, responses; no timestamps in the body). SDK freezes pin a committed SHA256 ofstella.yaml(see Release & lifecycle).
How versions are expressed
- Aggregate spec: a single composed document at
src/Api/StellaOps.Api.OpenApi/stella.yaml.info.versionis the aggregate API version (0.0.1). The composer also emits two extensions used to flag maturity:x-stellaops-contract-status:implementedorcontains-draft-stubs.x-stellaops-draft-services: list of service specs detected as scaffold/stub (currentlyexport-center,graph,jobengine,policy,scheduler). A spec is treated as a draft stub when itsinfo.titlecontains(stub)or itsinfo.descriptioncontainsscaffold.
- Per-service base URLs: there is no uniform
/api/v{major}/...convention across services. In practice the version is carried in different ways:- Server URL with no version path (e.g.
https://authority.stellaops.local). - Server URL embedding the version (e.g. Scanner:
https://scanner.stellaops.local/api/v1). - Versioned path prefixes on individual operations (e.g. Policy exceptions:
/api/v1/exceptions,/api/v1/exceptions/{exceptionId}).
- Server URL with no version path (e.g.
- Headers (PROPOSED — not implemented):
X-Stella-Api-Version: <major.minor.patch>(client-requested minor/patch negotiation) is not present in any spec or service insrc/. Minor/patch negotiation via request header is design intent only.X-Stella-Api-Deprecated-Fields: field1,field2is not emitted anywhere in code.Deprecation+Sunsetresponse headers are the standard runtime deprecation signal. They are described in theDeprecationSummaryschema (_shared/schemas/deprecation.yaml) but are not yet asserted in the per-service operation specs.
- Error codes (PROPOSED — not implemented):
API_VERSION_UNSUPPORTEDandAPI_VERSION_TOO_OLDdo not exist anywhere insrc/. There is no version-negotiation error contract today; errors use the standard envelope (error.code,error.message,trace_id) from the shared response components.
Deprecations
Deprecation is declared per-operation in the per-service OpenAPI files and enforced by Spectral rules (.spectral.yaml at the repo root):
- Mark the operation
deprecated: trueand attach anx-deprecationobject. The schema isDeprecationMetadatainsrc/Api/StellaOps.Api.OpenApi/_shared/schemas/deprecation.yaml. - Required
x-deprecationfields (rulestella-deprecated-has-metadata, severityerror):deprecatedAt(date-time),sunsetAt(date-time),successorPath,reason. - Optional fields:
successorOperationId,migrationGuide(uri; recommended by hint rulestella-deprecated-migration-guide),notificationChannels(slack|teams|email|webhook; recommended by hint rulestella-deprecated-notification-channels),affectedConsumerHints,breakingChanges[]. stella-deprecated-sunset-future(warn) flags sunset dates that have already passed.- Downstream consumers of this metadata: changelog generation, SDK publishing, and Notify-service deprecation events (
DeprecationNotificationEvent, event typeapi.deprecation.announced;DeprecationReportfor aggregated reporting).
Compatibility rules
- Additive changes (new fields/enums/paths) are allowed within the same major; fields default to
null/empty and must not change meaning. - Breaking changes require a new major: field removals/renames, required field additions, behavior changes that alter contracts.
- Enum extensions: existing values remain; new values must be tolerated by clients.
- Pagination/order: sort keys and determinism must be preserved across versions. Collection list/search endpoints expose
limit/cursorquery parameters (Spectral rulestella-pagination-params); state-changing201/202operations accept anIdempotency-Keyheader (rulestella-idempotency-header).
Release & lifecycle
- Edit the per-service spec under
src/Api/StellaOps.Api.OpenApi/<service>/openapi.yaml, then regenerate the aggregate:npm run api:compose(which runsnode src/Api/StellaOps.Api.OpenApi/compose.mjsand overwritessrc/Api/StellaOps.Api.OpenApi/stella.yaml). - Run lint + compatibility checks (all via
npm run, notpnpm):npm run api:lint— Spectral lint ofsrc/Api/StellaOps.Api.OpenApi/**/*.yaml(rules in root.spectral.yaml).npm run api:examples— example coverage check.npm run api:compat— runsnode scripts/api-compat-diff.mjs. The CI invocation is positional:node scripts/api-compat-diff.mjs <old.yaml> <new.yaml> --output text [--fail-on-breaking]. CI compares against the previous commit’sstella.yamland against the baseline atsrc/Api/StellaOps.Api.OpenApi/baselines/stella-baseline.yaml. (There is no--baseline <file>flag; the baseline is passed as the<old.yaml>positional argument.)npm run api:changelog— regeneratessrc/Api/StellaOps.Api.OpenApi/CHANGELOG.md(sections: Additive Operations, Breaking Operations).
Status: the
scripts/api-*.mjshelpers are not present in the repository (the wholescripts/directory is absent); onlycompose.mjsis committed. Because theapi:examples/api:compat/api:compat:test/api:changelognpm aliases could therefore only fail, they were removed frompackage.jsonin SPRINT_20260712_005 (DEAD-3);api:lintandapi:composeremain and work. Restoring this step means writing the helper scripts and re-adding the npm aliases. - Changelog lives in-tree at
src/Api/StellaOps.Api.OpenApi/CHANGELOG.md. (PROPOSED — not implemented): a per-releaseout/api/changelog/<yyyyMMdd>-<version>.mdwith embedded checksum/signature is design intent and does not exist; there is noout/api/directory. - Announce deprecations via release notes and the
x-deprecationmetadata above; standardDeprecation/Sunsetheaders should accompany affected endpoints for the configured deprecation window before removal. - SDK freeze: tag the commit
api/v{version}(e.g.api/v0.1.0-alpha), compute and commit the SHA256 ofstella.yaml(sha256sum stella.yaml > stella.yaml.sha256), and have SDK generators validate it viaSTELLA_OAS_EXPECTED_SHA256. See the API governance baseline. - Sunset execution: remove endpoints/fields only after the sunset date and after the major bump ships.
Client guidance
- Pin SDK versions to a frozen aggregate-spec SHA256 (
STELLA_OAS_EXPECTED_SHA256); the generated SDKs validate the hash before use. - Honor standard
Deprecation/Sunsetresponse headers by warning users and planning upgrades; consult the operation’sx-deprecation.successorPath/migrationGuidefor the replacement. - Prefer server-provided pagination tokens (
cursor); avoid relying on incidental field order. - (PROPOSED — not implemented): do not yet rely on an
X-Stella-Api-Versionrequest header for minor/patch negotiation — no service consumes it.
Testing
- The intended contract test is
npm run api:compat:test(node scripts/api-compat-diff.test.mjs), which exercises the compat differ for: added/removed operations and responses; parameter additions/removals/requiredness flips; request body additions/removals/requiredness and content-type changes; response content-type additions/removals. CI uses--fail-on-breakingto block removals/requiredness increases. - Status: as above,
scripts/api-compat-diff.test.mjsandscripts/api-compat-diff.mjsare not present in the tree; the test step is currently inert until restored. - (PROPOSED — not implemented): the fixture directory
tests/fixtures/api/versioning/does not exist. CI today derives its comparison inputs from git history (previous commit’sstella.yaml) and the committed baseline (src/Api/StellaOps.Api.OpenApi/baselines/stella-baseline.yaml) rather than from a versioning fixture set.
