Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,19 @@ 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
outputs:
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:
Expand Down Expand Up @@ -124,6 +126,9 @@ jobs:
core:
- 'core/src/**'
- 'core/pom.xml'
templates:
- 'templates/src/**'
- 'templates/pom.xml'
perf:
- 'core/src/**'
- 'render-pdf/**'
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
22 changes: 16 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions .github/workflows/release-script-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 (@(
'<project>',
' <version>2.3.1-SNAPSHOT</version>',
' <properties>',
' <japicmp.baseline.floor>2.0.0</japicmp.baseline.floor>',
" <japicmp.baseline.previous>$previous</japicmp.baseline.previous>",
' </properties>',
'</project>'
) -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 '<japicmp\.baseline\.previous>2\.3\.0</japicmp\.baseline\.previous>') {
throw "the previous-release pin did not move:`n$after"
}
if ($after -notmatch '<japicmp\.baseline\.floor>2\.0\.0</japicmp\.baseline\.floor>') {
throw "the major floor must not move at a cut:`n$after"
}
if ($after -notmatch '<version>2\.3\.1-SNAPSHOT</version>') { 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 "<project>`n <version>2.3.1-SNAPSHOT</version>`n</project>"
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.'
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ Almost all work targets **`develop`**, the ongoing 2.x line. The `1.x` branch ta
- `Architecture and Documentation Guards` &mdash; fast canonical / engine-boundary guard tests, fail-first gate (always runs)
- `Build and run tests (JDK 17)`, `(JDK 21)`, `(JDK 25)` &mdash; full `mvnw verify` in parallel matrix across the supported JVMs
- `Examples Generation Smoke Test` &mdash; regenerates every runnable example and uploads the PDFs as a CI artifact
- `Binary Compatibility` &mdash; PR-only japicmp diff of the `graph-compose-core` surface
- `Binary Compatibility` &mdash; PR-only japicmp diff of the `graph-compose-core` and `graph-compose-templates` surfaces
- `Performance Smoke Check` &mdash; PR-only coarse benchmark to catch performance regressions
- `CI Gate` &mdash; 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** &mdash; on the baseline JDK alone, and without example generation &mdash; 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** &mdash; on the baseline JDK alone, and without example generation &mdash; 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.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** &mdash; split `LayoutCompiler` and `TextFlowSupport` along their natural seams into individually-tested collaborators, with layout output unchanged.
- **Per-module binary-compatibility baselines** &mdash; 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** &mdash; `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)

Expand Down
Loading
Loading