From 85e091fbc088b8daef52132f94c957e9b4ff0bfc Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 13 Jul 2026 10:52:49 +0100 Subject: [PATCH 1/3] build(japicmp): enforce binary compatibility against the 2.0.0 baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The japicmp gate ran report-only through the 2.0 major transition and compared against a JitPack-hosted v1.7.0. With 2.0.0 published to Maven Central, point the baseline at graph-compose-core:2.0.0 (resolved from Central, no extra repository) and fail the build on any binary-incompatible change to the public surface, so a regression can no longer ship in a 2.x update. Source-incompatible changes stay report-only. The baseline is the major's floor and holds for the whole 2.x line, advancing only at the next major. The gate short-circuits when the working version equals the baseline, so it only bites once development moves off the release version: bump every module to 2.0.1-SNAPSHOT. The install snippets stay pinned to the published 2.0.0, so VersionConsistencyGuard now also accepts the latest published release (the topmost dated CHANGELOG entry) alongside the pom and Planned versions. Drop the two stale 1.x removal excludes (TextMeasurementSystem, ConfigLoader) — the 2.0.0 baseline predates them. Tests: VersionConsistencyGuardTest 12/12 (poms 2.0.1-SNAPSHOT, snippets 2.0.0); reactor-wide `mvn validate` green; `-P japicmp verify` on core is BUILD SUCCESS against 2.0.0, and a controlled public->package-private change fails with METHOD_LESS_ACCESSIBLE. --- .github/workflows/ci.yml | 7 +- CHANGELOG.md | 11 +++ benchmarks/pom.xml | 2 +- bundle/pom.xml | 2 +- core/pom.xml | 91 ++++++++----------- .../VersionConsistencyGuardTest.java | 45 ++++++--- coverage/pom.xml | 2 +- docs/api-stability.md | 26 ++++++ examples/pom.xml | 2 +- pom.xml | 2 +- qa/pom.xml | 2 +- render-docx/pom.xml | 2 +- render-pdf/pom.xml | 2 +- render-pptx/pom.xml | 2 +- templates/pom.xml | 2 +- testing/pom.xml | 2 +- wrapper/pom.xml | 2 +- 17 files changed, 121 insertions(+), 83 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 448f01be..1673a1d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,12 +291,11 @@ jobs: - name: Compare public API against baseline # The `japicmp` profile resolves the baseline release pinned - # by the `japicmp.baseline` property in core/pom.xml (via the - # profile-local JitPack repository) and diffs it against the - # freshly-built artifact. Fails the job on any binary- + # by the `japicmp.baseline` property in core/pom.xml (the + # published graph-compose-core on Maven Central) and diffs it + # against the freshly-built artifact. Fails the job on any binary- # incompatible modification to the public surface. Source- # incompatible changes are reported only (phased policy). - # See CHANGELOG v1.6.6 "Build" notes for the policy rationale. run: ./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core - name: Upload japicmp report diff --git a/CHANGELOG.md b/CHANGELOG.md index e1bcf89e..c5403c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to GraphCompose are documented here. Versions follow semantic versioning; release dates are ISO 8601. +## v2.0.1 — Planned + +### Build + +- The binary-compatibility gate (japicmp) now **enforces** backward compatibility + against the published `2.0.0` baseline: an accidental binary-incompatible change to + the public API fails the build instead of only being reported. Development proceeds + on the `2.0.1-SNAPSHOT` line, so the gate compares the working tree against that + baseline and catches a regression before it can ship in a 2.x update. Source-level + incompatibilities remain report-only for now. + ## v2.0.0 — 2026-07-13 The 2.0 development line. Binary-breaking by design — japicmp runs report-only diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 199409a5..6e5eb31a 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -7,7 +7,7 @@ io.github.demchaav graph-compose-build - 2.0.0 + 2.0.1-SNAPSHOT ../pom.xml diff --git a/bundle/pom.xml b/bundle/pom.xml index cfa92991..5ad64177 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -32,7 +32,7 @@ graph-compose and graph-compose-templates dependencies below use ${project.version}, so they follow automatically. --> - 2.0.0 + 2.0.1-SNAPSHOT jar GraphCompose Bundle diff --git a/core/pom.xml b/core/pom.xml index 73643181..7f16d512 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ io.github.demchaav graph-compose-core - 2.0.0 + 2.0.1-SNAPSHOT GraphCompose Core A declarative layout engine for programmatic document generation, implemented primarily in Java. This is the lean engine coordinate; depend on the `graph-compose` artifact for the drop-in, PDF-capable install. @@ -91,7 +91,14 @@ 0.26.1 - v1.7.0 + + 2.0.0 japicmp - - - jitpack.io - https://jitpack.io - - @@ -673,8 +675,8 @@ - com.github.DemchaAV - GraphCompose + io.github.demchaav + graph-compose-core ${japicmp.baseline} @@ -686,51 +688,34 @@ true - false + true false false true true com.demcha.compose.document.layout.payloads - - com.demcha.compose.engine.measurement.TextMeasurementSystem - - com.demcha.compose.ConfigLoader diff --git a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java index 9db9e0de..1b3c91d5 100644 --- a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java +++ b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java @@ -34,15 +34,17 @@ * which is the drift class that previously let the benchmarks module run * against the previous release. * - *

The snippet checks accept the version as matching either - * the current {@code pom.xml} version or the version named in - * the top {@code CHANGELOG.md} {@code Planned} entry. This makes the - * forward-looking Maven Central snippet (which has to advertise the - * about-to-ship version so users copy a coord that will resolve once the tag - * lands) compatible with the pre-cut window where {@code pom.xml} still carries - * the previous release version. {@code cut-release.ps1} bumps both pom and - * snippet to the same target in the release commit, after which the two paths - * converge and the test continues to pass. + *

The snippet checks accept the version as matching any of: + * the current {@code pom.xml} version, the version named in the top + * {@code CHANGELOG.md} {@code Planned} entry, or the latest published + * release (the topmost dated {@code CHANGELOG.md} entry). The Planned target + * covers the pre-cut window where {@code pom.xml} still carries the previous + * release while the README + showcase already advertise the about-to-ship + * version; the published-release target covers the mirror-image post-release + * window where {@code pom.xml} has moved to the next {@code -SNAPSHOT} while the + * snippets keep advertising the version actually on Maven Central. + * {@code cut-release.ps1} bumps both pom and snippet to the same target in the + * release commit, after which the paths converge and the test continues to pass. */ class VersionConsistencyGuardTest { @@ -344,11 +346,14 @@ void companionReadmeInstallSnippetsMatchTheirPomVersions() throws Exception { /** * Returns the set of versions that any install snippet may legitimately - * advertise: the current {@code pom.xml} version always, plus the version - * named in the top {@code CHANGELOG.md} {@code Planned} entry if one - * exists. The Planned entry covers the pre-cut window where {@code pom.xml} - * still carries the previous release version while the README + showcase - * already advertise the about-to-ship version. + * advertise: the current {@code pom.xml} version always, plus (if present) + * the version named in the top {@code CHANGELOG.md} {@code Planned} entry + * and the latest published release (the topmost dated CHANGELOG entry). The + * Planned entry covers the pre-cut window where {@code pom.xml} still carries + * the previous release while the snippets advertise the about-to-ship version; + * the published-release entry covers the post-release {@code -SNAPSHOT} window + * where {@code pom.xml} has moved on while the snippets keep advertising the + * version actually on Maven Central. */ private Set acceptableTargets() throws Exception { Set targets = new LinkedHashSet<>(); @@ -359,6 +364,18 @@ private Set acceptableTargets() throws Exception { if (planned.find()) { targets.add(planned.group(1)); } + // The latest PUBLISHED release: the topmost dated CHANGELOG entry + // (## vX.Y.Z — YYYY-MM-DD). During the post-release -SNAPSHOT dev window the + // pom carries the next dev version (e.g. 2.0.1-SNAPSHOT) while the install + // snippets correctly keep advertising the version that is actually on Maven + // Central (2.0.0) — so a user copies a coordinate that resolves today, not one + // that only lands with the next tag. find() returns the first (newest) dated + // entry; a "— Planned" entry is skipped because it has no date. + Matcher released = Pattern.compile("^## v([0-9][^ \\n]*)\\s*[\\u2014\\-]\\s*\\d{4}-\\d{2}-\\d{2}", Pattern.MULTILINE) + .matcher(changelog); + if (released.find()) { + targets.add(released.group(1)); + } return targets; } diff --git a/coverage/pom.xml b/coverage/pom.xml index eebd7c9d..fb9b6ae2 100644 --- a/coverage/pom.xml +++ b/coverage/pom.xml @@ -26,7 +26,7 @@ io.github.demchaav graph-compose-build - 2.0.0 + 2.0.1-SNAPSHOT ../pom.xml diff --git a/docs/api-stability.md b/docs/api-stability.md index 65d207f4..be0bf5cc 100644 --- a/docs/api-stability.md +++ b/docs/api-stability.md @@ -183,6 +183,32 @@ window starts, and its `Status` flips to `deprecated 1.x`. | `DocumentSession.pageMargins(List)` / `PageMarginRule` | Stable | planned | Per-page margins resolve a block's content width by the page it *begins* on (the engine measures each block once, before pagination). A margin that changes the content width therefore does not re-wrap a block mid-flow across a page boundary. | Revisit a page-aware per-line/per-fragment width model so a block can re-wrap when it crosses a margin boundary, if demand warrants. | — | — | | `io.github.demchaav:graph-compose` single-jar packaging | Stable | **landed 2.0** | The one published jar bundled the engine, the PDFBox render backend, the POI semantic backend, zxing, and the template families, so an engine-only or bring-your-own-backend consumer still pulled all of them. | **Done in 2.0.** Split into per-concern lockstep modules, render backends discovered via a `ServiceLoader` SPI. The root coordinate is renamed `graph-compose-core` (the lean engine); `graph-compose` is kept as a back-compat wrapper over `graph-compose-core` + `graph-compose-render-pdf`, so it still renders PDF out of the box. Templates are opt-in (`graph-compose-templates`); DOCX / PPTX ship in `graph-compose-render-docx` / `-render-pptx`. Migration: [modules guide](migration/v2.0.0-modules.md). | [ADR 0016](adr/0016-multi-module-packaging.md) | — | +### Binary-compatibility enforcement + +The Stable-tier promise (§ 1 — no binary breaks outside a major release) is enforced +mechanically by [japicmp](https://siom79.github.io/japicmp/), run in a `japicmp` Maven +profile on the engine module during `verify`. + +- **Baseline:** the published `graph-compose-core` on Maven Central, pinned by the + `japicmp.baseline` property in `core/pom.xml`. It is the current major's **floor** — + `2.0.0` for the whole 2.x line — and advances only at the next major. Holding it at + the floor (rather than the previous release) is what enforces the Stable promise: + every 2.x build must stay binary-compatible with the `2.0.0` public surface, not + merely with the last minor. +- **What fails the build:** any binary-incompatible change to the public surface + against the baseline — a removed or less-accessible public method/field/type, a + changed signature, and so on. `@Internal` packages (`com.demcha.compose.engine.*`, + `com.demcha.compose.document.layout.*` and its render-handoff payload records) are + excluded; they carry no compatibility promise (§ 1). Source-only incompatibilities + (e.g. adding a default method to an interface) are reported but do not fail, pending + a finalized 2.x source-compatibility policy. +- **Activity window:** the gate compares the working version against the baseline, so + it is a no-op only when the two are equal — the `2.0.0` release commit itself — and + active for every `-SNAPSHOT` development cycle across the 2.x line that follows. + +During the 2.0 major transition the gate ran report-only (the major intentionally +broke 1.x binary compatibility); it enforces from the `2.0.0` baseline forward. + --- ## 4. Tier mapping per package diff --git a/examples/pom.xml b/examples/pom.xml index 3e65182a..3dea9e92 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -7,7 +7,7 @@ io.github.demchaav graph-compose-build - 2.0.0 + 2.0.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index b70a9141..9b29c825 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.demchaav graph-compose-build - 2.0.0 + 2.0.1-SNAPSHOT pom GraphCompose Build Aggregator diff --git a/qa/pom.xml b/qa/pom.xml index 3b09b0cb..48c54c24 100644 --- a/qa/pom.xml +++ b/qa/pom.xml @@ -22,7 +22,7 @@ io.github.demchaav graph-compose-build - 2.0.0 + 2.0.1-SNAPSHOT ../pom.xml diff --git a/render-docx/pom.xml b/render-docx/pom.xml index 8440791b..5f01e93e 100644 --- a/render-docx/pom.xml +++ b/render-docx/pom.xml @@ -19,7 +19,7 @@ --> io.github.demchaav graph-compose-render-docx - 2.0.0 + 2.0.1-SNAPSHOT GraphCompose Render — DOCX Semantic DOCX export backend for GraphCompose, backed by Apache POI. diff --git a/render-pdf/pom.xml b/render-pdf/pom.xml index 9fd99459..4947d6b7 100644 --- a/render-pdf/pom.xml +++ b/render-pdf/pom.xml @@ -25,7 +25,7 @@ --> io.github.demchaav graph-compose-render-pdf - 2.0.0 + 2.0.1-SNAPSHOT GraphCompose Render — PDF The PDFBox-backed PDF render backend for GraphCompose. diff --git a/render-pptx/pom.xml b/render-pptx/pom.xml index 89eb144c..1e27a770 100644 --- a/render-pptx/pom.xml +++ b/render-pptx/pom.xml @@ -19,7 +19,7 @@ --> io.github.demchaav graph-compose-render-pptx - 2.0.0 + 2.0.1-SNAPSHOT GraphCompose Render — PPTX Semantic PPTX export backend for GraphCompose (slide-safe semantic node validation). diff --git a/templates/pom.xml b/templates/pom.xml index e5341e90..34028cd3 100644 --- a/templates/pom.xml +++ b/templates/pom.xml @@ -17,7 +17,7 @@ --> io.github.demchaav graph-compose-templates - 2.0.0 + 2.0.1-SNAPSHOT GraphCompose Templates Built-in CV, cover-letter, invoice, and proposal document templates for GraphCompose. diff --git a/testing/pom.xml b/testing/pom.xml index 6c43ab49..3fa18625 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -19,7 +19,7 @@ --> io.github.demchaav graph-compose-testing - 2.0.0 + 2.0.1-SNAPSHOT GraphCompose Testing Consumer testing support for GraphCompose: layout-snapshot assertions and PDF visual regression. diff --git a/wrapper/pom.xml b/wrapper/pom.xml index 24b75897..2e9978d9 100644 --- a/wrapper/pom.xml +++ b/wrapper/pom.xml @@ -22,7 +22,7 @@ --> io.github.demchaav graph-compose - 2.0.0 + 2.0.1-SNAPSHOT GraphCompose The graph-compose coordinate: a drop-in aggregator over graph-compose-core for a PDF-capable install. From 26ba5bb0d4df85441b00027deaf8876b72968aed Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 13 Jul 2026 12:13:47 +0100 Subject: [PATCH 2/3] build(japicmp): exclude the full Internal surface, add a publish preflight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align the binary-compatibility gate with docs/api-stability.md. The Internal tier — com.demcha.compose.engine.** and com.demcha.compose.document.layout.**, plus any element marked @Internal — carries no compatibility promise, so exclude all of it; the gate previously excluded only document.layout.payloads. Only the Stable public surface is now enforced. Run the gate in the publish workflow before any deploy, so a binary-incompatible change to the public surface aborts the release even if the tag reached the publish job by bypassing branch protection (the CI japicmp job only gates pull requests). Tighten VersionConsistencyGuardTest: during a -SNAPSHOT development cycle the install snippets must advertise the latest published release (the version resolvable on Maven Central), not the in-development SNAPSHOT or a Planned version. The release commit bumps the pom off SNAPSHOT and rewrites the snippets to the release version, at which point they must equal it. Verified on a 2.0.1-SNAPSHOT working tree: the gate fails a Stable break (DocumentColor.rgba -> package-private, METHOD_LESS_ACCESSIBLE) and passes an Internal break (LayoutCompiler.compile -> package-private, excluded); the guard rejects a snippet pinned to an unpublished 2.0.1. --- .github/workflows/publish.yml | 12 +++ core/pom.xml | 26 ++++-- .../VersionConsistencyGuardTest.java | 90 +++++++++---------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ba076dd8..d7ce9320 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -99,6 +99,18 @@ jobs: # unpublished tests-jar no longer fails the deploy. run: ./mvnw -B -ntp clean install + - name: Verify binary compatibility against the published baseline + # Defence in depth: run the japicmp gate on the tagged commit before any + # deploy, so an accidental binary-incompatible change to the public + # surface aborts the publish even when the tag reached here by bypassing + # branch protection (the CI japicmp job only gates pull requests). Compares + # the freshly built graph-compose-core against the japicmp.baseline release + # on Maven Central and fails the job on any Stable-surface break; the + # Internal packages (engine.**, document.layout.**) are excluded per + # docs/api-stability.md. Test build is skipped — the install step above + # already ran the full suite on this commit. + run: ./mvnw -B -ntp -f core/pom.xml -P japicmp -Dmaven.test.skip=true verify + - name: Publish engine to Maven Central # Activates the release profile (sources + javadoc + gpg sign + # central-publishing) and flips gpg.skip=false. The deploy diff --git a/core/pom.xml b/core/pom.xml index 7f16d512..1cd3357d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -705,17 +705,25 @@ true true - com.demcha.compose.document.layout.payloads + com.demcha.compose.engine.** + com.demcha.compose.document.layout.** + @com.demcha.compose.document.api.Internal diff --git a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java index 1b3c91d5..870ed0d8 100644 --- a/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java +++ b/core/src/test/java/com/demcha/documentation/VersionConsistencyGuardTest.java @@ -12,7 +12,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -34,17 +33,14 @@ * which is the drift class that previously let the benchmarks module run * against the previous release. * - *

The snippet checks accept the version as matching any of: - * the current {@code pom.xml} version, the version named in the top - * {@code CHANGELOG.md} {@code Planned} entry, or the latest published - * release (the topmost dated {@code CHANGELOG.md} entry). The Planned target - * covers the pre-cut window where {@code pom.xml} still carries the previous - * release while the README + showcase already advertise the about-to-ship - * version; the published-release target covers the mirror-image post-release - * window where {@code pom.xml} has moved to the next {@code -SNAPSHOT} while the - * snippets keep advertising the version actually on Maven Central. - * {@code cut-release.ps1} bumps both pom and snippet to the same target in the - * release commit, after which the paths converge and the test continues to pass. + *

The install-snippet checks require the snippets to advertise a version that + * a user can actually resolve. During a normal {@code -SNAPSHOT} development cycle + * that is the latest published release (the topmost dated + * {@code CHANGELOG.md} entry) — not the in-development {@code -SNAPSHOT} and not a + * forward-looking {@code Planned} version. In the release commit itself + * {@code cut-release.ps1} bumps the pom off {@code -SNAPSHOT} and rewrites the + * snippets to the new release version, so once the pom carries a concrete release + * version the snippets must equal it. See {@link #acceptableTargets()}. */ class VersionConsistencyGuardTest { @@ -229,10 +225,10 @@ void readmeInstallSnippetsMatchTheProjectVersion() throws Exception { String gradleSnippetVersion = firstMatchingGroup(readme, INSTALL_SNIPPET_PATTERNS_README_GRADLE); assertThat(mavenSnippetVersion) - .describedAs("README Maven install snippet must reference the current pom or CHANGELOG Planned version (one of %s)", targets) + .describedAs("README Maven install snippet must reference the latest published release, or the release version in a release commit (one of %s)", targets) .isIn(targets); assertThat(gradleSnippetVersion) - .describedAs("README Gradle install snippet must reference the current pom or CHANGELOG Planned version (one of %s)", targets) + .describedAs("README Gradle install snippet must reference the latest published release, or the release version in a release commit (one of %s)", targets) .isIn(targets); } @@ -249,19 +245,19 @@ void showcaseSiteVersionMatchesTheProjectVersion() throws Exception { String site = Files.readString(PROJECT_ROOT.resolve("web/index.html")); assertThat(firstGroup(site, "\"softwareVersion\":\\s*\"v?([0-9][^\"]*)\"")) - .describedAs("web/index.html JSON-LD softwareVersion must equal the current pom or planned version (one of %s)", targets) + .describedAs("web/index.html JSON-LD softwareVersion must equal the latest published release, or the release version in a release commit (one of %s)", targets) .isIn(targets); // Match up to the delimiter (space before `·`) rather than // digits-and-dots only, so a pre-release version like 2.0.0-rc.1 is // captured too — consistent with the JSON-LD and snippet patterns. assertThat(firstGroup(site, "v([0-9][^\\s&]*)\\s*·\\s*MIT")) - .describedAs("web/index.html hero version badge must equal the current pom or planned version (one of %s)", targets) + .describedAs("web/index.html hero version badge must equal the latest published release, or the release version in a release commit (one of %s)", targets) .isIn(targets); assertThat(firstMatchingGroup(site, INSTALL_SNIPPET_PATTERNS_SHOWCASE_MAVEN)) - .describedAs("web/index.html Maven install snippet must equal the current pom or planned version (one of %s)", targets) + .describedAs("web/index.html Maven install snippet must equal the latest published release, or the release version in a release commit (one of %s)", targets) .isIn(targets); assertThat(firstMatchingGroup(site, INSTALL_SNIPPET_PATTERNS_SHOWCASE_GRADLE)) - .describedAs("web/index.html Gradle install snippet must equal the current pom or planned version (one of %s)", targets) + .describedAs("web/index.html Gradle install snippet must equal the latest published release, or the release version in a release commit (one of %s)", targets) .isIn(targets); } @@ -289,10 +285,10 @@ void moduleReadmeInstallSnippetsMatchTheProjectVersion() throws Exception { String readme = Files.readString(PROJECT_ROOT.resolve(module.getKey())); String artifact = Pattern.quote(module.getValue()); assertThat(firstGroup(readme, "" + artifact + "\\s*v?([0-9][^<]*)")) - .describedAs("%s Maven install snippet must equal the current pom or planned version (one of %s)", module.getKey(), targets) + .describedAs("%s Maven install snippet must equal the latest published release, or the release version in a release commit (one of %s)", module.getKey(), targets) .isIn(targets); assertThat(firstGroup(readme, "io\\.github\\.demchaav:" + artifact + ":v?([0-9][\\w.\\-]*)")) - .describedAs("%s Gradle install snippet must equal the current pom or planned version (one of %s)", module.getKey(), targets) + .describedAs("%s Gradle install snippet must equal the latest published release, or the release version in a release commit (one of %s)", module.getKey(), targets) .isIn(targets); } } @@ -345,38 +341,38 @@ void companionReadmeInstallSnippetsMatchTheirPomVersions() throws Exception { }; /** - * Returns the set of versions that any install snippet may legitimately - * advertise: the current {@code pom.xml} version always, plus (if present) - * the version named in the top {@code CHANGELOG.md} {@code Planned} entry - * and the latest published release (the topmost dated CHANGELOG entry). The - * Planned entry covers the pre-cut window where {@code pom.xml} still carries - * the previous release while the snippets advertise the about-to-ship version; - * the published-release entry covers the post-release {@code -SNAPSHOT} window - * where {@code pom.xml} has moved on while the snippets keep advertising the - * version actually on Maven Central. + * Returns the set of versions an install snippet may legitimately advertise. + * + *

During a normal {@code -SNAPSHOT} development cycle this is only + * the latest published release — the version actually resolvable on + * Maven Central — so a user who copies a snippet always gets a coordinate that + * resolves today, never the in-development {@code -SNAPSHOT} nor a + * forward-looking {@code Planned} version. In the release commit itself + * {@code cut-release.ps1} bumps the pom off {@code -SNAPSHOT} to the new + * release version and rewrites the snippets to match, so once the pom is a + * concrete release version the snippets must equal it.

*/ private Set acceptableTargets() throws Exception { - Set targets = new LinkedHashSet<>(); - targets.add(effectiveVersion(PROJECT_ROOT.resolve("core/pom.xml"))); - String changelog = Files.readString(PROJECT_ROOT.resolve("CHANGELOG.md")); - Matcher planned = Pattern.compile("^## v([0-9][^ \\n]*)\\s*[\\u2014\\-]\\s*Planned\\b", Pattern.MULTILINE) - .matcher(changelog); - if (planned.find()) { - targets.add(planned.group(1)); + String pomVersion = effectiveVersion(PROJECT_ROOT.resolve("core/pom.xml")); + if (pomVersion.endsWith("-SNAPSHOT")) { + return Set.of(latestPublishedRelease()); } - // The latest PUBLISHED release: the topmost dated CHANGELOG entry - // (## vX.Y.Z — YYYY-MM-DD). During the post-release -SNAPSHOT dev window the - // pom carries the next dev version (e.g. 2.0.1-SNAPSHOT) while the install - // snippets correctly keep advertising the version that is actually on Maven - // Central (2.0.0) — so a user copies a coordinate that resolves today, not one - // that only lands with the next tag. find() returns the first (newest) dated - // entry; a "— Planned" entry is skipped because it has no date. + return Set.of(pomVersion); + } + + /** + * The latest published release: the topmost dated {@code CHANGELOG.md} entry + * ({@code ## vX.Y.Z — YYYY-MM-DD}). {@code find()} returns the newest such + * entry; a {@code — Planned} entry is skipped because it carries no date. + */ + private String latestPublishedRelease() throws Exception { + String changelog = Files.readString(PROJECT_ROOT.resolve("CHANGELOG.md")); Matcher released = Pattern.compile("^## v([0-9][^ \\n]*)\\s*[\\u2014\\-]\\s*\\d{4}-\\d{2}-\\d{2}", Pattern.MULTILINE) .matcher(changelog); - if (released.find()) { - targets.add(released.group(1)); - } - return targets; + assertThat(released.find()) + .describedAs("CHANGELOG.md must contain a dated release entry (## vX.Y.Z — YYYY-MM-DD) to anchor the install snippets") + .isTrue(); + return released.group(1); } /** From ac49d38ffea10688703ad085ff15d46394086a47 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 13 Jul 2026 12:16:05 +0100 Subject: [PATCH 3/3] test(japicmp): add a controlled Stable/Internal exclude verification A git-revert-safe script that proves the gate enforces the Stable surface and excludes the Internal surface: it reduces a Stable public method to package-private and asserts the gate FAILS (METHOD_LESS_ACCESSIBLE), then does the same to an Internal method and asserts the gate PASSES. A trap always reverts the source. Run it after changing the japicmp or moving a type between tiers. --- scripts/japicmp-verify-excludes.sh | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100755 scripts/japicmp-verify-excludes.sh diff --git a/scripts/japicmp-verify-excludes.sh b/scripts/japicmp-verify-excludes.sh new file mode 100755 index 00000000..aa1f2f05 --- /dev/null +++ b/scripts/japicmp-verify-excludes.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Controlled verification that the japicmp binary-compatibility gate enforces the +# Stable public surface and excludes the Internal surface exactly as +# docs/api-stability.md defines it (§ 1 tiers, § 4 package map, "Binary- +# compatibility enforcement"). Run it after changing the japicmp in +# core/pom.xml, or after moving a type between the Stable and Internal tiers. +# +# It applies one controlled break at a time, runs the gate, checks the outcome, +# and ALWAYS reverts the source (even on interrupt) via a trap. Requires a clean +# working tree for the two touched files. The engine version must be off the +# japicmp baseline (a -SNAPSHOT dev version) or the gate short-circuits. +# +# Expected: a Stable break FAILS the gate; an Internal break does NOT. +# +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$HERE/.." && pwd)" +cd "$REPO_ROOT" +MVNW="$REPO_ROOT/mvnw" + +# A Stable public method (document.style is a Stable package) and an Internal +# public method (document.layout is @Internal). Both are compile-safe to reduce to +# package-private: rgba has no main callers, compile() has no cross-package caller. +STABLE_FILE="core/src/main/java/com/demcha/compose/document/style/DocumentColor.java" +INTERNAL_FILE="core/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java" + +LOGDIR="$REPO_ROOT/target/japicmp-verify" +mkdir -p "$LOGDIR" +MVN_ARGS=(-B -ntp clean -P japicmp -Dmaven.test.skip=true -Djacoco.skip=true verify -pl :graph-compose-core) + +revert() { git checkout -- "$STABLE_FILE" "$INTERNAL_FILE" 2>/dev/null || true; } +trap revert EXIT + +fail=0 + +echo "== [1/2] Stable break must FAIL the gate ==" +perl -0pi -e 's/ public static DocumentColor rgba/ static DocumentColor rgba/' "$STABLE_FILE" +if "$MVNW" "${MVN_ARGS[@]}" > "$LOGDIR/stable.log" 2>&1; then + echo " FAIL: the gate PASSED a Stable-surface break (DocumentColor.rgba)"; fail=1 +elif grep -q "DocumentColor.rgba.*METHOD_LESS_ACCESSIBLE" "$LOGDIR/stable.log"; then + echo " OK: the gate failed the build (DocumentColor.rgba METHOD_LESS_ACCESSIBLE)" +else + echo " FAIL: the gate failed, but not on the rgba break — see $LOGDIR/stable.log"; fail=1 +fi +git checkout -- "$STABLE_FILE" + +echo "== [2/2] Internal break must NOT fail the gate ==" +perl -0pi -e 's/ public LayoutGraph compile\(/ LayoutGraph compile(/' "$INTERNAL_FILE" +if "$MVNW" "${MVN_ARGS[@]}" > "$LOGDIR/internal.log" 2>&1; then + echo " OK: the gate passed (LayoutCompiler.compile is excluded via document.layout.**)" +else + echo " FAIL: the gate FAILED an Internal-surface break — see $LOGDIR/internal.log"; fail=1 +fi +git checkout -- "$INTERNAL_FILE" + +echo "" +if [ "$fail" = "0" ]; then + echo "VERIFY PASS — Stable enforced, Internal excluded." +else + echo "VERIFY FAIL — see the logs above." +fi +exit "$fail"