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/.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/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..1cd3357d 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,42 @@ true - false + true false false true true - com.demcha.compose.document.layout.payloads - - com.demcha.compose.engine.measurement.TextMeasurementSystem - - com.demcha.compose.ConfigLoader + 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 9db9e0de..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,15 +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 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 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 { @@ -227,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); } @@ -247,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); } @@ -287,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); } } @@ -343,23 +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 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. + * 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 pomVersion = effectiveVersion(PROJECT_ROOT.resolve("core/pom.xml")); + if (pomVersion.endsWith("-SNAPSHOT")) { + return Set.of(latestPublishedRelease()); + } + 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 planned = Pattern.compile("^## v([0-9][^ \\n]*)\\s*[\\u2014\\-]\\s*Planned\\b", Pattern.MULTILINE) + Matcher released = Pattern.compile("^## v([0-9][^ \\n]*)\\s*[\\u2014\\-]\\s*\\d{4}-\\d{2}-\\d{2}", Pattern.MULTILINE) .matcher(changelog); - if (planned.find()) { - targets.add(planned.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); } /** 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/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" 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.