diff --git a/README.md b/README.md index c03e2e9..ad22849 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,62 @@ Import-scoped BOMs so child projects can declare these dependencies **without a - **Spring Boot** — `spring-boot-dependencies` (`3.5.14`) - **Testcontainers** — `testcontainers-bom` (`2.0.5`) +- **OpenAPI** — `springdoc-openapi-bom` (`2.8.17`) and `swagger-bom` (`2.2.47`), + plus `org.webjars:swagger-ui` (`5.32.2`) — see + [The OpenAPI stack](#the-openapi-stack) + +### The OpenAPI stack + +springdoc, Swagger and the Swagger UI webjar are **one coupled set**, and the parent +manages all three so a child gets a version-uniform stack without configuring +anything. + +This is not tidiness. springdoc declares its Swagger dependency without a version and +inherits it from its own aggregator POM, so Swagger floats in every consuming project +and is decided by nearest-wins mediation. A library that uses only the Swagger +annotations declares only that artifact — the normal thing to do — and its +declaration sits closer to the application than the `swagger-core-jakarta` springdoc +contributes two levels down. The stack then splits along exactly that boundary, and +Swagger's own modules call each other across incompatible releases: + +``` +io.swagger.v3.core.jackson.ModelResolver (swagger-core-jakarta 2.2.47) + → io.swagger.v3.oas.annotations.media.Schema.$dynamicRef() + absent in annotations 2.2.29 +``` + +The result is a `NoSuchMethodError` at schema resolution, not a missing feature. +Managing the versions in the parent removes the mediation entirely, because +`dependencyManagement` is consulted before nearest-wins and applies at any depth. + +**What this does not cover.** Management overrides *transitive* resolution only. A +project that declares a Swagger artifact directly with an explicit `` still +wins over the managed version and keeps whatever it had. Dropping that `` +activates the managed one. + +**Retargeting the stack in a child.** Setting `` in a child moves all +14 Swagger coordinates at once. The floor is `2.2.47`: `swagger-bom` was first +published at that version, so a lower value fails the build with a non-resolvable +import. Use a direct declaration with a `` to pull an older Swagger. + +#### Maintenance: bump the three together + +`springdoc.version`, `swagger.version` and `swagger-ui.version` **must** move +together. Bumping springdoc alone would publish a split stack to every child project +at once, and this parent's own build would not notice — it has no Java sources and +never exercises the stack. + +The matching values are not exposed by `springdoc-openapi-bom`, which manages +springdoc artifacts only. Read them from springdoc's aggregator POM: + +``` +org/springdoc/springdoc-openapi//springdoc-openapi-.pom + → swagger.version + → swagger-ui.version +``` + +For reference: springdoc `2.8.6` pairs with Swagger `2.2.29` and Swagger UI `5.20.1`; +springdoc `2.8.17` pairs with `2.2.47` and `5.32.2`. ### Pinned plugin versions diff --git a/docs/TODO.md b/docs/TODO.md index 0372611..b1206b9 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -133,9 +133,14 @@ JAX-RS artifacts are pinned for every child as a side effect — at a version se for springdoc compatibility, not for that child. It is silent: the build succeeds either way. +Measured during the implementation of spec 003, so this is confirmed rather than +suspected: a probe child declaring `swagger-annotations` (the javax artifact) and +`swagger-jaxrs2-jakarta` without versions resolves both to 2.2.47. + Worth deciding whether that is wanted. The alternatives are pinning only the three jakarta artifacts by hand (an artifact list that goes stale when springdoc adds a -Swagger module) or leaving it as is and documenting the reach. +Swagger module, and which would also sidestep the 2.2.47 floor that `swagger-bom` +imposes) or leaving it as is and documenting the reach. **Context:** Identified while choosing between `swagger-bom` and hand-written pins during spec 003; the wider reach was accepted to avoid maintaining an artifact list. diff --git a/docs/specs/003-openapi-stack-versions/behaviors.md b/docs/specs/003-openapi-stack-versions/behaviors.md index 2284208..5f7f1a9 100644 --- a/docs/specs/003-openapi-stack-versions/behaviors.md +++ b/docs/specs/003-openapi-stack-versions/behaviors.md @@ -88,11 +88,22 @@ is managed. "Consumer" means a project whose parent is `java-parent`. ### A consumer may still override deliberately -- **Given** a consumer that declares its own `swagger.version` property or its own - `dependencyManagement` entry for a Swagger artifact +- **Given** a consumer that declares its own `swagger.version` property - **When** the project resolves its dependencies -- **Then** the consumer's value wins, because a child's `dependencyManagement` takes - precedence over the inherited one +- **Then** the whole Swagger stack moves to that version, because property + interpolation happens in the child's effective POM and therefore reaches the + parent's BOM import +- **And** one property in the child retargets all 14 coordinates at once + +### Overriding the Swagger version below 2.2.47 fails the build + +- **Given** a consumer that sets `swagger.version` to a release older than 2.2.47 +- **When** the project is built +- **Then** the build fails at model-building time with a non-resolvable import POM, + because `io.swagger.core.v3:swagger-bom` was first published at 2.2.47 +- **And** the failure is explicit rather than silent +- **And** declaring the artifact directly with a `` is unaffected and remains + the way to use an older Swagger ## Side effects @@ -115,8 +126,9 @@ is managed. "Consumer" means a project whose parent is `java-parent`. - **Given** the parent's existing `spring-boot-dependencies` 3.5.14 import - **When** the effective POM is computed -- **Then** no coordinate is managed by both, because Spring Boot manages no - `io.swagger`, `org.springdoc` or `org.webjars:swagger-ui` entry +- **Then** no coordinate is managed by both: `spring-boot-dependencies` 3.5.14 + contains no `io.swagger` and no `org.springdoc` entry at all, and its only + `org.webjars` entries are `webjars-locator-core` and `webjars-locator-lite` - **And** the relative order of the imports has no effect on the resolved versions ## Maintenance diff --git a/docs/specs/003-openapi-stack-versions/design.md b/docs/specs/003-openapi-stack-versions/design.md index ff57699..97e5fc2 100644 --- a/docs/specs/003-openapi-stack-versions/design.md +++ b/docs/specs/003-openapi-stack-versions/design.md @@ -192,6 +192,15 @@ silently fails to cover it and the split-stack failure returns in a new place. T BOM is published by the Swagger project, covers all 14 artifacts, and has no parent POM of its own, so importing it drags nothing else in. +**Constraint discovered during implementation:** `swagger-bom` was first published at +**2.2.47** — every earlier version returns 404 on Maven Central, and the published +range is 2.2.47 through 2.2.55. The version this change needs is therefore the very +first one that has a BOM at all. Two consequences follow. A future springdoc release +pinned to a Swagger older than 2.2.47 could not use the import and would need the +three jakarta artifacts pinned individually. And a child overriding `swagger.version` +below 2.2.47 gets a hard model-building failure rather than an override — declaring +the artifact directly with a version remains available and unaffected. + **Rationale — importing `springdoc-openapi-bom` even though it does not fix the bug.** It manages only springdoc's own artifacts and says nothing about Swagger, so it contributes nothing to the failure at hand. It is imported because the parent @@ -257,6 +266,12 @@ The rule is written where someone would otherwise break it, in two places: version chosen for springdoc compatibility. Tracked in `docs/TODO.md`. - **Children that hard-pin Swagger themselves are unaffected**, silently. Their direct declaration still wins, so they keep whatever they had and see no error and no fix. + Measured on a probe child: a direct `swagger-annotations-jakarta` 2.2.29 declaration + keeps the split stack even with the fix in place. +- **`swagger.version` has a floor of 2.2.47.** The BOM does not exist below that, so + overriding the property downwards fails the build at model-building time with a + non-resolvable import. The error is explicit rather than silent, but it is a new + way for a child to break that did not exist before. ## Acceptance diff --git a/docs/specs/003-openapi-stack-versions/steps.md b/docs/specs/003-openapi-stack-versions/steps.md new file mode 100644 index 0000000..0685335 --- /dev/null +++ b/docs/specs/003-openapi-stack-versions/steps.md @@ -0,0 +1,128 @@ +# Implementation Steps: OpenAPI stack versions + +Spec: [`design.md`](design.md) · [`behaviors.md`](behaviors.md) · Issue #6 + +`java-parent` has `packaging=pom` and no Java sources, so the fix cannot be exercised +by building the parent alone. The executable form of these scenarios is Maven +dependency resolution against a throwaway probe child, per +[Acceptance](design.md#acceptance). + +--- + +## Step 1: Declare the coupled version properties + +- [x] Add `swagger.version` and `swagger-ui.version` next to the existing + `springdoc.version` +- [x] Write the lockstep comment above them: the three are coupled, mixing Swagger + versions produces `NoSuchMethodError` inside Swagger itself, and the matching + values are read from `` / `` in + `org/springdoc/springdoc-openapi//springdoc-openapi-.pom` + +**Acceptance criteria:** +- [x] `./mvnw help:evaluate -Dexpression=swagger.version -q -DforceStdout` prints `2.2.47` +- [x] `./mvnw help:evaluate -Dexpression=swagger-ui.version -q -DforceStdout` prints `5.32.2` +- [x] The values match springdoc 2.8.17's aggregator POM + +**Related behaviors:** The lockstep values are discoverable + +--- + +## Step 2: Import the BOMs and manage the Swagger UI webjar + +- [x] Import `org.springdoc:springdoc-openapi-bom` at `${springdoc.version}` +- [x] Import `io.swagger.core.v3:swagger-bom` at `${swagger.version}` +- [x] Manage `org.webjars:swagger-ui` at `${swagger-ui.version}`, with a comment + noting no BOM covers it +- [x] Remove the explicit `springdoc-openapi-starter-webmvc-ui` entry the BOM replaces + +**Acceptance criteria:** +- [x] `./mvnw -Pfull-build clean verify` passes, including pomchecker +- [x] The effective POM manages every springdoc starter and all 14 Swagger coordinates +- [x] No coordinate is managed twice + +**Related behaviors:** springdoc resolves without a version · Other springdoc starters +are now usable without a version · No overlap with the Spring Boot BOM · An +unresolvable BOM fails fast + +--- + +## Step 3: Prove the fix against a probe child + +- [x] Install the modified parent locally with `./mvnw -N install` +- [x] Create a throwaway child inheriting it that declares + `springdoc-openapi-starter-webmvc-ui` without a version, plus a dependency + that contributes `swagger-annotations-jakarta` 2.2.29 +- [x] Resolve the tree and record the Swagger and Swagger UI versions +- [x] Repeat against the **unmodified** parent to confirm the probe reproduces the + split — otherwise the check passes vacuously and proves nothing +- [x] Record both trees for the pull request description + +**Acceptance criteria:** +- [x] Against the modified parent: `swagger-annotations-jakarta`, + `swagger-models-jakarta` and `swagger-core-jakarta` all resolve to 2.2.47, and + `swagger-ui` to 5.32.2 +- [x] Against the unmodified parent: `swagger-annotations-jakarta` resolves to 2.2.29 + while `swagger-core-jakarta` resolves to 2.2.47 + +**Related behaviors:** The Swagger stack is uniform · The Swagger UI webjar is pinned · +A library contributing an older annotations artifact no longer splits the stack · The +same graph fails on the previous parent version · A direct pin in the consumer still +wins · Dropping the direct pin activates the fix + +--- + +## Step 4: Document the managed stack and the maintenance rule + +- [x] Add the OpenAPI stack to the README's managed-versions section +- [x] Add a maintenance subsection describing the coupling, the failure it prevents, + and the concrete lookup path for a future springdoc bump + +**Acceptance criteria:** +- [x] The lookup path names the exact artifact and the exact properties +- [x] The consequence of getting it wrong is stated: the parent would publish a split + stack to every child at once + +**Related behaviors:** The lockstep values are discoverable · Bumping springdoc alone +reintroduces the failure + +--- + +## Step 5: Final verification + +- [x] `./mvnw -Pfull-build clean verify` +- [x] `docs/specs/INDEX.md` status updated + +**Acceptance criteria:** +- [x] Build passes +- [x] The probe project is removed and leaves no trace in the repository + +**Related behaviors:** The parent still builds and validates + +--- + +## Behavior Coverage + +18 scenarios. Layer is dependency resolution throughout. All rows marked *Measured* +were confirmed against throwaway probe projects during implementation; the probes +were removed afterwards and left no artifacts behind. + +| Scenario | Verification | Step | +|---|---|---| +| springdoc resolves without a version | Measured — probe child resolves 2.8.17 | 2, 3 | +| Other springdoc starters are now usable without a version | Measured — `webflux-ui` resolves to 2.8.17 without a version | 3 | +| The Swagger stack is uniform | Measured — annotations, models and core all 2.2.47 | 3 | +| The Swagger UI webjar is pinned | Measured — 5.32.2; note the probe did not create a competing webjar version, so the pin is preventive | 3 | +| A library contributing an older annotations artifact no longer splits the stack | Measured — 2.2.29 contributor is overridden to 2.2.47 | 3 | +| The same graph fails on the previous parent version | Measured — pre-change parent yields annotations 2.2.29 next to core 2.2.47 | 3 | +| Depth of the conflicting declaration does not matter | Follows from management semantics; the probe covers depth 2 | 3 | +| A direct pin in the consumer still wins | Measured — direct 2.2.29 declaration survives the fix | 3 | +| Dropping the direct pin activates the fix | Measured — removing `` yields 2.2.47 | 3 | +| The javax Swagger line becomes managed | Measured — `swagger-annotations` and `swagger-jaxrs2-jakarta` resolve to 2.2.47 | 3 | +| A consumer using the JAX-RS artifacts is affected the same way | Measured together with the row above | 3 | +| No overlap with the Spring Boot BOM | Verified against `spring-boot-dependencies` 3.5.14 | 2 | +| Bumping springdoc alone reintroduces the failure | Not executable — documented risk | 4 | +| The lockstep values are discoverable | Lookup path followed once during implementation | 1, 4 | +| The parent still builds and validates | `./mvnw -Pfull-build clean verify` | 2, 5 | +| An unresolvable BOM fails fast | Measured — a child setting `swagger.version=2.2.38` fails at model building | 3 | +| A consumer may still override deliberately | Measured — `swagger.version=2.2.50` moves all Swagger coordinates | 3 | +| Overriding the Swagger version below 2.2.47 fails the build | Measured — `swagger-bom` does not exist below 2.2.47 | 3 | diff --git a/docs/specs/INDEX.md b/docs/specs/INDEX.md index 68523d2..4f4c4bd 100644 --- a/docs/specs/INDEX.md +++ b/docs/specs/INDEX.md @@ -4,4 +4,4 @@ |-----|-------------|------|-------|-------------|--------------|--------| | 001 | 001-reproducible-build-timestamp | Reproducible build timestamp | build, infrastructure, documentation | Fixed `project.build.outputTimestamp` literal in the parent POM, inherited by all child projects, maintained by `release.sh` — so a third party can rebuild any Open Elements Java artifact byte-identically | #3 | done | | 002 | 002-pinned-line-endings | Pinned line endings | build, infrastructure, documentation | `.gitattributes` forcing LF (CRLF for `*.bat`/`*.cmd`), a matching `.editorconfig` aligned with Google Java Format, and an inherited Spotless `lineEndings=UNIX` — so a checkout produces the same text bytes on every platform | #4 | done | -| 003 | 003-openapi-stack-versions | OpenAPI stack versions | build, api, documentation | Import `springdoc-openapi-bom` and `swagger-bom` and manage `org.webjars:swagger-ui`, so the coupled OpenAPI stack resolves uniformly in every consumer instead of splitting into a `NoSuchMethodError` | — | open | +| 003 | 003-openapi-stack-versions | OpenAPI stack versions | build, api, documentation | Import `springdoc-openapi-bom` and `swagger-bom` and manage `org.webjars:swagger-ui`, so the coupled OpenAPI stack resolves uniformly in every consumer instead of splitting into a `NoSuchMethodError` | #6 | done | diff --git a/pom.xml b/pom.xml index b72e230..2ffe378 100644 --- a/pom.xml +++ b/pom.xml @@ -88,8 +88,25 @@ 3.5.14 2.0.5 - 2.8.17 1.0.0 + + + 2.8.17 + 2.2.47 + 5.32.2 @@ -116,10 +133,32 @@ pom import + org.springdoc - springdoc-openapi-starter-webmvc-ui + springdoc-openapi-bom ${springdoc.version} + pom + import + + + io.swagger.core.v3 + swagger-bom + ${swagger.version} + pom + import + + + + org.webjars + swagger-ui + ${swagger-ui.version} org.jspecify