Skip to content

build(api): commit the openapi spec as a yaml - #960

Merged
ExtraToast merged 1 commit into
mainfrom
build/openapi-yaml
Sep 2, 2026
Merged

build(api): commit the openapi spec as a yaml#960
ExtraToast merged 1 commit into
mainfrom
build/openapi-yaml

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Why

services/api/openapi.json was one line. jq -S -c minified it, so every branch that
touched a controller regenerated that line, and two such branches conflicted on it every
time — whatever each had actually changed. Git cannot merge a single line two ways.

Resolving it never meant reading a diff either, because there is nothing to read: the fix is
always to take either side and regenerate. #921 hit this three times in one branch.

The comment in scripts/openapi-common.sh said the extension had to stay .json:

The spec is minified JSON produced by jq -c below. We keep the extension as .json so
downstream tools that pick their parser by extension (notably @hey-api/openapi-ts, which
otherwise parses .yaml as YAML and errors on single-line flow-style documents) read it
correctly.

That is a consequence of keeping minified content in a .yaml file, not a reason to keep
minifying. A flow-style document in a .yaml file breaks the parser; a block-style one does
not.

What this achieves

services/api/openapi.yaml, block style, keys sorted: 22,495 lines where there was one.
Two branches that each add an endpoint now conflict only where they genuinely disagree, and
a conflict that does happen is legible.

The generated TypeScript client is byte-identical to the one the JSON produced, which is
the evidence that the parser concern above was about flow style rather than about YAML.

How

The generator test already wrote the spec, so it writes the YAML too — sorted keys, block
style, width unbounded so a long description stays on its line and a reworded sentence is a
one-line diff. It uses snakeyaml, which services/api already bundles.

dumpOpenApiSpec is therefore a copy, and jq leaves that code path entirely.
normalize_api_spec has nothing left to normalise; it is kept as a presence check so the
call sites still read the same for every spec.

Ten places named the old path and all move: .gitattributes, .github/diff-stats.yml, the
sync assertion in validate.yml, openapi-ts.blueshell.config.ts, the frontend compose
mount, both generation scripts, and the Gradle task with its comments.

Worth a reviewer's attention

The diff is 22.5k lines and all but 55 of them are one generated file. .gitattributes
already marks it linguist-generated, so GitHub collapses it.

The first PR after this that changes an endpoint will show a large spec diff, because it
is the first content diff against a format that has no history. After that they are small,
which is the point.

Sorting is now done in Kotlin rather than by jq -S. sortKeys sorts every object's
keys and leaves arrays in the order the api gave them — springdoc's ordering is meaningful
for parameters and required.

Verification

:services:api:test exit 0
Frontend unit 986 passed
yarn typecheck · yarn lint exit 0
generate-openapi-local.sh run twice, then diffed no change — generation is idempotent
Generated client vs. the JSON-generated one byte-identical

Split out of #921, which needed it and will rebase onto it.


Diff breakdown added removed, scaled to the largest row.

api                                             +22501    -27    3
  unit tests         █░                            +40    -10    1
  build & config     █████████████████████████░ +22461    -17    2
  generated          ░                              +0     -1    1  ~

frontend                                            +2     -2    2
  build & config     █░                             +2     -2    2

ci                                                 +12    -24    4
  build & config     █░                            +12    -24    4

repo                                                +1     -1    1
  build & config     █░                             +1     -1    1

──────────────────────────────────────────────────────────────────
production                                          +0     -0
tests                                              +40    -10
total (hand-written)                            +22516    -54  10 files
~ generated (excluded)                              +0     -1  1 file

Every branch that touches a controller regenerates the spec, and the spec
was one line of minified JSON. Two such branches conflicted on that line
every time, whatever they had each changed, and resolving it meant
regenerating rather than reading a diff. This branch hit it three times.

It is `openapi.yaml` now, block style, keys sorted: 22,495 lines where
there was one. Two branches adding an endpoint each conflict only where
they actually disagree.

The old comment in openapi-common.sh said the extension had to stay .json
because @hey-api/openapi-ts parses .yaml as YAML "and errors on
single-line flow-style documents". That was a consequence of keeping
minified content in a .yaml file, not a reason to keep minifying. Block
YAML reads fine, which the generated client proves: it is byte-identical
to the one the JSON produced.

The generator test writes the YAML with snakeyaml, which the api already
bundles, so `dumpOpenApiSpec` is a copy and the jq dependency is gone.
`normalize_api_spec` has nothing left to normalise and is kept as a
presence check so the call sites still read the same for every spec.
@ExtraToast ExtraToast self-assigned this Sep 2, 2026
@ExtraToast ExtraToast changed the title build(api): the committed spec is block yaml, a line per value build(api): commit the openapi spec as a yaml Sep 2, 2026
@ExtraToast
ExtraToast merged commit 2515fa0 into main Sep 2, 2026
27 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant