api-linting
How OpenAPI specs are linted in this repo. Use when diagnosing a `bundleSpec` failure or understanding what rules the toolchain enforces.
SKILL.md.claude/skills/api-linting/SKILL.md
GitHubAPI linting
OpenAPI linting runs as part of gradle's :assembleSpec / :bundleSpec / :generate chain. There is no separate "lint api" step a developer invokes by hand. If those gradle tasks succeed, the spec is good for codegen and publishing.
What the toolchain actually checks
| Layer | Where it runs | What it catches |
|---|---|---|
Redocly (@redocly/cli) |
assembleSpec / bundleSpec |
Structural errors, unresolved $refs, malformed YAML, invalid OpenAPI 3.0.x. |
bundleSpec ($ref inlining) |
bundleSpec |
Whether every reference can be inlined into a self-contained spec. |
| Codegen rules | generate* |
Operation IDs, schema names, response shapes the generator can't translate. |
| Module-specific spectral rules | Generator pins @redocly/cli but does not ship a .spectral.yml. Existing modules with custom spectral configs run via lintExec. |
Style rules for naming, descriptions, etc. |
For a fresh module, none of the above require a .spectral.yml in the module directory — the defaults are sufficient. Only add one if you need to override a redocly rule for that specific module (rare, document why).
Running it
zbb bundleSpec
That's the cheapest "is my spec valid?" check. Failures are usually one of:
Can't resolve $ref './node_modules/@…/…'— the dependency isn't installed, or the path is wrong, or you're referencing a rawapi.ymlfrom another module instead of itsdist/module-….yml.Required property '…' is missing— schema violates OpenAPI 3.0.3.Operation 'getXxx' must have …— codegen-enforced shape (operationId pattern, required success response, etc.).
Read the redocly output verbatim; it points at the offending line and column. Don't paper over it with redocly bundle … --force — --force ignores unresolved refs, and the generator will then choke trying to consume the broken bundle.
Rules the redocly+codegen pipeline enforces de-facto
Without any custom .spectral.yml, the spec is rejected if it:
- Uses inline schemas in response bodies (codegen emits
InlineResponse200types — surface asGate 2: Type Generationfailures). Every response body must$refa named component. - Omits
operationIdon any operation. Codegen wraps operations byoperationId. - Uses
operationIdcontaining characters outside[a-zA-Z0-9]or not starting with a letter. - Uses
nullable: true(OpenAPI 3.0.x specific; the codegen rejects this — useoneOf: […, { type: 'null' }]or rethink the type). - Defines explicit 4xx / 5xx responses (the codegen owns error mapping; declare only the success response).
- Names a parameter or schema starting with
describe(collides with mocha's BDD globals in test files that import generated types).
The agent audit grep for these is in @.claude/skills/tool-requirements/SKILL.md ("Forbidden tokens in api.yml").
Adding a .spectral.yml (only if you need to)
Most modules don't need one. Add it only when:
- You want to enforce a style rule beyond redocly's defaults (e.g. require examples on all 2xx responses for one specific module).
- A redocly rule legitimately doesn't fit the API in question and needs to be silenced for that module only.
Template:
extends:
- "./node_modules/@zerobias-org/util-spectral-config/rules/.oas3.strict.spectral.yml"
rules:
# Override or add module-specific rules here, with a comment explaining why
# operation-description: warn
Wire it into the gradle build by referencing it from the module's build.gradle.kts config block (if the zb.typescript-connector plugin doesn't pick it up automatically — current versions do).
What the build does NOT enforce automatically
The toolchain catches structural and codegen problems. It does not catch:
- Tag consistency across operations (e.g. mismatched
tags:field across endpoints that conceptually belong together) - Whether
x-product-operationscovers every operation - Domain naming (e.g. is
listUsersreally the right operationId for that endpoint?) - Pagination style choice (page-number vs token vs cursor)
Those are review-level concerns. @api-reviewer covers them; the codegen doesn't.
Common failure shapes
Can't resolve $ref ./node_modules/@zerobias-org/product-…/catalog.yml#/Product—npm viewfirst. If the version exists, ensure the dep is independenciesof the module'spackage.json. The generator template pins it to"latest"; pin to an exact version once you know which one you're using.InlineResponse200types appearing ingenerated/— spec has an inline schema in a response body. Hoist it tocomponents/schemas/and$refit.operationId 'list-users' is invalid— must match^[a-zA-Z][a-zA-Z0-9]*$. UselistUsers.Tag 'Users' used but not defined globally— add it to the spec's top-leveltags:array.
CI behavior
CI runs :gateCheck, not the build. The committed gate-stamp.json carries proof that bundleSpec and generate succeeded locally. So a spec that lints clean on your machine is also clean in CI; CI doesn't independently re-lint.
Related
- @.claude/skills/api-spec-core/SKILL.md — what makes a valid OpenAPI 3.0.3 spec
- @.claude/skills/api-critical-12/SKILL.md — the 12 rules a spec must satisfy
- @.claude/skills/api-regeneration/SKILL.md — what
:generateactually does - @.claude/skills/build-quality/SKILL.md — lifecycle context