diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a79df8a2..23f9feaf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -72,10 +72,11 @@ jobs:
changes:
# Path-based change detection for selective CI on pull requests. Emits the
# reverse-dependency flags the heavy jobs gate on: `code` (any build input),
- # `docs` (markdown the guard suites read and compile), `core` (the root
- # graph-compose-core module — drives japicmp), `perf`
- # (modules the smoke benchmark exercises), and `jvm` (published library modules
- # + toolchain — drives the JDK matrix width). Pushes/dispatch bypass these gates.
+ # `docs` (markdown the guard suites read and compile), `core` and `templates`
+ # (the graph-compose-core / graph-compose-templates sources + pom — the two
+ # modules under the japicmp gate), `perf` (modules the smoke benchmark
+ # exercises), and `jvm` (published library modules + toolchain — drives the
+ # JDK matrix width). Pushes/dispatch bypass these gates.
name: Detect changed paths
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
@@ -83,6 +84,7 @@ jobs:
code: ${{ steps.filter.outputs.code }}
docs: ${{ steps.filter.outputs.docs }}
core: ${{ steps.filter.outputs.core }}
+ templates: ${{ steps.filter.outputs.templates }}
perf: ${{ steps.filter.outputs.perf }}
jvm: ${{ steps.filter.outputs.jvm }}
steps:
@@ -124,6 +126,9 @@ jobs:
core:
- 'core/src/**'
- 'core/pom.xml'
+ templates:
+ - 'templates/src/**'
+ - 'templates/pom.xml'
perf:
- 'core/src/**'
- 'render-pdf/**'
@@ -393,10 +398,13 @@ jobs:
if-no-files-found: error
binary-compat:
- name: Binary Compatibility (japicmp vs pom baseline)
- # japicmp diffs the graph-compose-core public surface, so it only matters
- # when the core module (`core/src/**` or `core/pom.xml`) changed.
- if: github.event_name == 'pull_request' && needs.changes.outputs.core == 'true'
+ name: Binary Compatibility (japicmp vs pom baselines)
+ # japicmp diffs the graph-compose-core and graph-compose-templates public
+ # surfaces (both carry a `japicmp` profile), so it only matters when one of
+ # those modules (`core/**` or `templates/**` sources / pom) changed. Both are
+ # always diffed together: templates resolves core from the same reactor build,
+ # and a core change can move the templates surface it re-exports.
+ if: github.event_name == 'pull_request' && (needs.changes.outputs.core == 'true' || needs.changes.outputs.templates == 'true')
needs: [architecture-and-documentation-guards, changes]
runs-on: ubuntu-latest
env:
@@ -422,21 +430,27 @@ jobs:
- name: Install graph-compose-emoji (resolved at test scope by core)
run: ./mvnw -B -ntp -f emoji/pom.xml -DskipTests install
- - name: Compare public API against baseline
- # The `japicmp` profile resolves the baseline release pinned
- # 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).
- run: ./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core
+ - name: Compare public API against baselines
+ # The `japicmp` profile in each module resolves TWO published baselines of
+ # that module (japicmp.baseline.floor — the major's first release, holding
+ # the GA surface; japicmp.baseline.previous — the latest release, holding
+ # everything added since) and diffs each against the freshly-built
+ # artifact. The floor alone would let a method introduced after it be
+ # deleted with the diff green. Fails the job on any
+ # binary-incompatible modification to the public surface. Source-
+ # incompatible changes are reported only (phased policy). Both modules
+ # sit in one reactor invocation: the reactor orders core before
+ # templates, so templates compiles against the freshly-built core.
+ run: ./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core,:graph-compose-templates
- name: Upload japicmp report
if: always()
uses: actions/upload-artifact@v7
with:
name: japicmp-report-${{ github.run_id }}
- path: core/target/japicmp/**
+ path: |
+ core/target/japicmp/**
+ templates/target/japicmp/**
if-no-files-found: ignore
perf-smoke:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8c2f1af0..29463417 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -130,18 +130,28 @@ jobs:
# unpublished tests-jar no longer fails the deploy.
run: ./mvnw -B -ntp clean install
- - name: Verify binary compatibility against the published baseline
+ - name: Verify binary compatibility against the published baselines
# 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.
+ # the freshly built graph-compose-core against both baselines pinned in
+ # core/pom.xml — the major floor and the latest published release — 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: Verify templates binary compatibility against the published baselines
+ # Same defence for graph-compose-templates: its own `japicmp` profile
+ # diffs the freshly built jar against the two baselines pinned in
+ # templates/pom.xml. Every templates.* package is Stable per
+ # docs/api-stability.md, so only elements carrying the per-element
+ # @Internal marker are excluded (none today). Standalone pom — it
+ # resolves graph-compose-core from the local m2 the install step seeded.
+ run: ./mvnw -B -ntp -f templates/pom.xml -P japicmp -Dmaven.test.skip=true verify
+
- name: Plan the deploy set (start_at resume)
id: plan
# A partial Central publication CANNOT be blindly re-dispatched: the deploys
diff --git a/.github/workflows/release-script-check.yml b/.github/workflows/release-script-check.yml
index b94c2ff2..b72dd866 100644
--- a/.github/workflows/release-script-check.yml
+++ b/.github/workflows/release-script-check.yml
@@ -351,3 +351,69 @@ jobs:
if ((Get-Content $roadmap -Raw) -ne $after) { throw "a second promotion changed the file" }
Write-Host "roadmap promotion: 3 refusals, 1 rewrite, idempotent on re-run."
+
+ - name: Unit-check the japicmp previous-release baseline move
+ shell: pwsh
+ run: |
+ # The binary-compatibility gate diffs against two published releases: the major
+ # floor, which never moves inside a major, and the previous release, which must
+ # move to the version just published at every cut. A pin left behind leaves
+ # everything added in that release freely removable with the gate green, so the
+ # move is code that has to be exercised, not a step someone remembers. Lifted by
+ # AST like the checks above, so the code under test is the code that ships.
+ $path = (Resolve-Path scripts/cut-release.ps1).Path
+ $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$null)
+ $fn = $ast.FindAll({
+ param($n)
+ $n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq 'Update-JapicmpPreviousBaseline'
+ }, $true)
+ if (-not $fn) { throw 'cut-release.ps1 no longer defines Update-JapicmpPreviousBaseline' }
+ Invoke-Expression $fn[0].Extent.Text
+ function Note($m) { Write-Host " $m" }
+ $DryRun = $false
+
+ # A function nothing calls moves no pin. Assert the post-release path still
+ # invokes it — the failure this guards is the helper surviving a refactor that
+ # drops the call, which every content assertion below would still pass.
+ $calls = $ast.FindAll({
+ param($n)
+ $n -is [System.Management.Automation.Language.CommandAst] -and
+ $n.GetCommandName() -eq 'Update-JapicmpPreviousBaseline'
+ }, $true)
+ if (-not $calls) { throw 'nothing in cut-release.ps1 calls Update-JapicmpPreviousBaseline' }
+
+ function New-Pom($previous) {
+ $file = Join-Path ([IO.Path]::GetTempPath()) ("pom-" + [guid]::NewGuid().ToString() + ".xml")
+ Set-Content -Path $file -NoNewline -Value (@(
+ '',
+ ' 2.3.1-SNAPSHOT',
+ ' ',
+ ' 2.0.0',
+ " $previous",
+ ' ',
+ ''
+ ) -join "`n")
+ return $file
+ }
+
+ $pom = New-Pom '2.2.0'
+ if (-not (Update-JapicmpPreviousBaseline $pom '2.3.0')) { throw 'moving the pin reported no change' }
+ $after = Get-Content $pom -Raw
+ if ($after -notmatch '2\.3\.0') {
+ throw "the previous-release pin did not move:`n$after"
+ }
+ if ($after -notmatch '2\.0\.0') {
+ throw "the major floor must not move at a cut:`n$after"
+ }
+ if ($after -notmatch '2\.3\.1-SNAPSHOT') { throw "the project version was rewritten:`n$after" }
+
+ if (Update-JapicmpPreviousBaseline $pom '2.3.0') { throw 'a second move reported a change' }
+
+ $bare = Join-Path ([IO.Path]::GetTempPath()) ("pom-" + [guid]::NewGuid().ToString() + ".xml")
+ Set-Content -Path $bare -NoNewline -Value "`n 2.3.1-SNAPSHOT`n"
+ if (Update-JapicmpPreviousBaseline $bare '2.3.0') { throw 'a pom without the pin reported a change' }
+ if (Update-JapicmpPreviousBaseline (Join-Path ([IO.Path]::GetTempPath()) 'no-such-pom.xml') '2.3.0') {
+ throw 'a missing pom reported a change'
+ }
+
+ Write-Host 'japicmp baseline: previous moves, floor stays, idempotent, and the cut calls it.'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6680edb..eaeda76d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,37 @@ follow semantic versioning; release dates are ISO 8601.
### Build
+- **The templates module is under the binary-compatibility gate, and the gate now
+ protects what a release added.** `docs/api-stability.md` has listed every
+ `templates.*` package as Stable — no binary break outside a major — since the 2.0
+ module split, but japicmp only ever diffed `graph-compose-core`; a removed or
+ narrowed public method in a preset, a data record, or a shared widget would have
+ shipped in a 2.x minor with nothing looking at it. `templates/pom.xml` now carries
+ the same `japicmp` profile as the engine pom.
+
+ Both modules pin **two** baselines instead of one, each diffed in its own execution.
+ A floor pin alone protects only the surface the major shipped with: a method that
+ first ships in 2.2.0 is absent from both 2.0.0 and a 2.2.1 that deletes it, so the
+ floor diff stays green while an application compiled against 2.2.0 gets
+ `NoSuchMethodError` — and `MarkdownInline.appendTransformed` and `SectionAllocation`,
+ both added after 2.0.0, sat in exactly that gap. `japicmp.baseline.floor` keeps
+ holding the GA surface; `japicmp.baseline.previous` holds everything added since and
+ moves to the version just published in `cut-release.ps1 -PostReleaseOnly`. A pin left
+ behind is not a silent hole: `VersionConsistencyGuardTest` derives both rather than
+ trusting them — the floor from the module's own version, the previous pin from the
+ CHANGELOG — and asserts both executions are still in the pom, while the release-script
+ workflow drives the move itself. Both pins stay inside the working major, so opening
+ the next one leaves them naming a version nobody has published and the gate stands
+ down for that cycle rather than failing every break the major is for.
+
+ The PR-time `Binary Compatibility` job runs the gate whenever the core or templates
+ sources or pom changed, `cut-release.ps1` step 5b runs it before the tag is cut, and
+ the publish workflow runs it again on the tagged commit before anything is deployed.
+ The current surface is binary- and source-compatible with both baselines; removing
+ `MarkdownInline.appendTransformed` fails the build with `METHOD_REMOVED` against the
+ 2.2.0 pin while the 2.0.0 diff stays green, which is the hole the second baseline
+ closes.
+
- **The weekly benchmark run builds the modules it measures.** The JMH workflow
installed the engine from source and then let Maven resolve the rest from Central,
and one of them is not there to resolve: the benchmarks read their document fixtures
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2e2dea05..d7d7a3b7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -70,11 +70,11 @@ Almost all work targets **`develop`**, the ongoing 2.x line. The `1.x` branch ta
- `Architecture and Documentation Guards` — fast canonical / engine-boundary guard tests, fail-first gate (always runs)
- `Build and run tests (JDK 17)`, `(JDK 21)`, `(JDK 25)` — full `mvnw verify` in parallel matrix across the supported JVMs
- `Examples Generation Smoke Test` — regenerates every runnable example and uploads the PDFs as a CI artifact
- - `Binary Compatibility` — PR-only japicmp diff of the `graph-compose-core` surface
+ - `Binary Compatibility` — PR-only japicmp diff of the `graph-compose-core` and `graph-compose-templates` surfaces
- `Performance Smoke Check` — PR-only coarse benchmark to catch performance regressions
- `CI Gate` — single aggregate status check that is green when every job that ran passed
- **Selective on pull requests:** a `dorny/paths-filter` step skips the heavy jobs when a PR touches nothing that affects the build. Markdown counts as a build input, so a **docs-only PR still runs the reactor** — on the baseline JDK alone, and without example generation — because that is where the guards compiling the published snippets live. `Binary Compatibility` runs only when the core module changed, and the `Performance Smoke Check` only when core / render-pdf / templates changed. Pushes to `develop` / `main` (and manual dispatch) always run the full gate. Point branch protection at **`CI Gate`** + **`Architecture and Documentation Guards`** rather than the individual matrix legs, so a docs-only PR is not left waiting on a skipped check.
+ **Selective on pull requests:** a `dorny/paths-filter` step skips the heavy jobs when a PR touches nothing that affects the build. Markdown counts as a build input, so a **docs-only PR still runs the reactor** — on the baseline JDK alone, and without example generation — because that is where the guards compiling the published snippets live. `Binary Compatibility` runs only when the core or templates module changed, and the `Performance Smoke Check` only when core / render-pdf / templates changed. Pushes to `develop` / `main` (and manual dispatch) always run the full gate. Point branch protection at **`CI Gate`** + **`Architecture and Documentation Guards`** rather than the individual matrix legs, so a docs-only PR is not left waiting on a skipped check.
The PR cannot merge into a protected branch until all required checks are green.
7. **Address review comments**, then squash any fixup commits before merge. The maintainer merges through GitHub once review is complete.
diff --git a/ROADMAP.md b/ROADMAP.md
index 14d6ed2e..9b8f1bf9 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -49,7 +49,7 @@ The **v1.9.x** line receives critical fixes only.
Committed internal direction for the post-2.0 line: refactors, scale work, and tooling that do **not** change the public authoring API. Tracked in [docs/roadmaps/post-2.0-engineering.md](docs/roadmaps/post-2.0-engineering.md).
- **Decompose the layout hot files** — split `LayoutCompiler` and `TextFlowSupport` along their natural seams into individually-tested collaborators, with layout output unchanged.
-- **Per-module binary-compatibility baselines** — now that the 2.0 GA artifacts are published, switch `japicmp` from the single-artifact baseline to per-module baselines in break-on-incompatible mode.
+- **Per-module binary-compatibility baselines** — `japicmp` gates `graph-compose-core` and `graph-compose-templates` against their 2.0.0 floors in break-on-incompatible mode; the remaining published modules join once each has a documented Stable / Internal split.
## Later (directional)
diff --git a/core/pom.xml b/core/pom.xml
index 5e1d1bdd..2d53ee4c 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -89,16 +89,23 @@
3.8.0
17
-
+
0.26.1
-
- 2.0.0
+
+ 2.0.0
+ 2.2.0
japicmp
@@ -683,27 +692,59 @@
${japicmp.version}
- japicmp-against-baseline
+ japicmp-against-major-floor
verify
cmp
+
+
+
+ io.github.demchaav
+ graph-compose-core
+ ${japicmp.baseline.floor}
+
+
+
+
+
+ japicmp-against-previous-release
+ verify
+
+ cmp
+
+
+
+
+ io.github.demchaav
+ graph-compose-core
+ ${japicmp.baseline.previous}
+
+
+
-
-
- io.github.demchaav
- graph-compose-core
- ${japicmp.baseline}
-
-
${project.build.directory}/${project.build.finalName}.jar
+
+ true
true
true
+
+
+ 0.26.1
+ 2.0.0
+ 2.2.0
@@ -262,5 +277,88 @@
+
+
+
+ japicmp
+
+
+
+ com.github.siom79.japicmp
+ japicmp-maven-plugin
+ ${japicmp.version}
+
+
+ japicmp-against-major-floor
+ verify
+
+ cmp
+
+
+
+
+ io.github.demchaav
+ graph-compose-templates
+ ${japicmp.baseline.floor}
+
+
+
+
+
+ japicmp-against-previous-release
+ verify
+
+ cmp
+
+
+
+
+ io.github.demchaav
+ graph-compose-templates
+ ${japicmp.baseline.previous}
+
+
+
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}.jar
+
+
+
+
+ true
+ true
+ true
+ false
+ false
+ true
+ true
+
+ @com.demcha.compose.document.api.Internal
+
+
+
+
+
+
+