platform-api-docs

CI/CD

Validation

OpenAPI specs are linted with Spectral 6 in Docker (node:24-alpine). Same image locally and in CI.

./scripts/openapi-lint.sh          # lint; exit 1 on error (CI)
./scripts/openapi-lint.sh --fix    # lint, patch missing example fields, re-lint

CI builds --target lint only, prints the stylish report in the job log, writes a table to the GitHub job summary, and uploads .openapi-lint-report/ as an artifact. Warnings do not fail the gate (--fail-severity error). --fix only auto-patches oas3-valid-media-example missing windowStart/windowEnd.

What the ruleset checks

.spectral.yaml extends spectral:oas — the OpenAPI specification rules — and adds the conventions this estate has agreed with itself, which spectral:oas has no opinion about.

spectral:oas grades one document in isolation: valid schema, resolvable refs, examples that match their schemas, no unused components. That is worth having and it is not the same question as do these ten contracts agree with each other. On 2026-09-18 the estate published two different error shapes — four APIs on RFC 7807 application/problem+json, three on an Errors array — and this check reported zero findings at every severity.

The gradera- rules close that gap:

Rule What it stops
gradera-error-response-is-problem-json A 4xx/5xx body declared as application/json
gradera-shared-error-response-is-problem-json The same, for reusable components.responses
gradera-no-error-array-schema An Error or Errors schema — the pre-D-37 envelope
gradera-problem-schema-has-estate-members A Problem that does not require code and errorId
gradera-problem-type-uri A type that is not https://gradera.dev/problems/{slug}
gradera-error-example-carries-a-code An error example with no code for a client to branch on

All of them are error severity, and so are operation-operationId, operation-tags, info-description, oas3-unused-component and operation-must-have-summary, which were already enabled but at warn. The lint runs with --fail-severity error, so a rule at warn cannot fail the build: it is documentation with a linter’s accent.

Exemptions

overrides at the bottom of .spectral.yaml names every contract that is allowed to break one of these rules, and why. Today that is three — v1-alerts, v1-audit, v1-notifications — whose services D-37 did not convert, plus one endpoint (POST /flows/{flowSlug}/publish, 422) that answers with a deliberate domain body rather than the general error envelope. They are listed so the remaining divergence can be counted; converting a service is what removes an entry, because editing its contract alone would only make it wrong.

Note when adding one: { and } must be percent-encoded in an override’s JSON pointer. Spelled literally, the override silently matches nothing and the rule still fires.

If the linter cannot run

Spectral is given --ruleset explicitly rather than left to find the file. Without it, a ruleset missing from the checkout or the bind mount makes Spectral print No ruleset has been found, lint nothing, and the report then says “0 errors” — a clean bill of health over a run that checked nothing. Named, the same situation reads as ENOENT, and the summary says OpenAPI Lint could not run instead of passed.

What is linted

sos-admin/, nexus-flow/, tenant-management/ and microapps/. The BFF contracts under microapps/ were outside the glob until 2026-09-18 and were never linted at all.