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 + + + + + + +