Skip to content
Merged
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
67 changes: 51 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,25 @@ jobs:
# in qa) run in build-and-test below, which now also covers docs-only PRs.
run: |
./mvnw -B -ntp clean \
"-Dtest=EnginePdfBoundaryTest,DocumentationCoverageTest,CanonicalSurfaceGuardTest,PackageMapGuardTest,VersionConsistencyGuardTest,CiGuardListGuardTest,CiGateCoverageGuardTest,CodeQlScopeGuardTest,AgentsGuideGuardTest,BenchmarkDependencyInstallGuardTest" \
"-Dtest=EnginePdfBoundaryTest,DocumentationCoverageTest,CanonicalSurfaceGuardTest,PackageMapGuardTest,VersionConsistencyGuardTest,CiGuardListGuardTest,CiGateCoverageGuardTest,BinaryCompatibilityGateGuardTest,CodeQlScopeGuardTest,AgentsGuideGuardTest,BenchmarkDependencyInstallGuardTest" \
test -pl :graph-compose-core

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 @@ -133,6 +135,9 @@ jobs:
core:
- 'core/src/**'
- 'core/pom.xml'
templates:
- 'templates/src/**'
- 'templates/pom.xml'
perf:
- 'core/src/**'
- 'render-pdf/**'
Expand Down Expand Up @@ -478,9 +483,13 @@ jobs:

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'
# japicmp diffs the graph-compose-core and graph-compose-templates public
# surfaces (each module carries a `japicmp` profile), so it only matters when
# one of them changed (`core/src/**`, `core/pom.xml`, `templates/src/**` or
# `templates/pom.xml`). Both are diffed in one reactor invocation: templates
# compiles against the core built in the same run, and a core change can move
# the surface templates exposes.
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 @@ -506,21 +515,47 @@ jobs:
- name: Install graph-compose-emoji (resolved at test scope by core)
run: ./mvnw -B -ntp -f emoji/pom.xml -DskipTests install

- name: Drop cached GraphCompose artifacts before the gate resolves
# The gate must diff against the PUBLISHED baselines. Anything of ours
# already in the runner's cached repository could stand in for one, so
# the pinned coordinates are fetched from Central for this run. The
# modules under test come from the reactor, never from the repository.
run: |
rm -rf ~/.m2/repository/io/github/demchaav/graph-compose-core
rm -rf ~/.m2/repository/io/github/demchaav/graph-compose-templates

- 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
# Each module's `japicmp` profile resolves the published releases its
# pom pins from Maven Central and diffs them against the freshly-built
# artifact: graph-compose-core against `japicmp.baseline` (the 2.x
# floor), graph-compose-templates against `japicmp.baseline.floor` and
# `japicmp.baseline.previous` (the floor and the latest release). Fails
# the job on any binary-incompatible modification to either public
# surface. Source-incompatible changes are reported only (phased policy).
run: ./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core,:graph-compose-templates

- name: Prove every japicmp execution ran
# A japicmp execution that does not run — switched off, unbound from the
# lifecycle, or left out of the reactor — writes no report and fails
# nothing, so the step above can pass having diffed nothing. Each execution
# the gate relies on must have left its XML report;
# BinaryCompatibilityGateGuardTest keeps this list in step with the poms.
# (A baseline japicmp cannot resolve still leaves a report; the templates
# gate fails on it through ignoreMissingOldVersion=false.)
run: |
ls -l core/target/japicmp templates/target/japicmp
test -s core/target/japicmp/japicmp-against-baseline.xml
test -s templates/target/japicmp/japicmp-against-major-floor.xml
test -s templates/target/japicmp/japicmp-against-previous-release.xml

- 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
53 changes: 40 additions & 13 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,44 @@ jobs:
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE

- name: Install graph-compose-fonts (resolved at test scope by core)
# The gate below runs before the reactor install and without `-am`, so core's
# test-scope companions resolve from Central unless they are built here first,
# and a tree may pin a companion version that is not published yet. Neither is
# a gated artifact, so seeding them cannot supply a baseline.
run: ./mvnw -B -ntp -f fonts/pom.xml -DskipTests install

- name: Install graph-compose-emoji (resolved at test scope by core)
run: ./mvnw -B -ntp -f emoji/pom.xml -DskipTests install

- name: Drop cached GraphCompose artifacts before the gate resolves
# Nothing of ours may stand in for a published baseline, so the pinned
# coordinates are fetched from Central for this run. This runs before the
# install below for the same reason: the release being published must not
# be in the local repository while its own baselines resolve.
run: |
rm -rf ~/.m2/repository/io/github/demchaav/graph-compose-core
rm -rf ~/.m2/repository/io/github/demchaav/graph-compose-templates

- 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). One reactor run
# diffs graph-compose-core and graph-compose-templates against the releases
# their poms pin, exactly as the pull-request job does; templates resolves
# the engine from the reactor rather than from the local repository, which
# is what lets this run before the install below.
run: ./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core,:graph-compose-templates

- name: Prove every japicmp execution ran
# A japicmp execution that does not run writes no report and fails nothing,
# so each one the gate relies on must have left its report.
run: |
test -s core/target/japicmp/japicmp-against-baseline.xml
test -s templates/target/japicmp/japicmp-against-major-floor.xml
test -s templates/target/japicmp/japicmp-against-previous-release.xml

- name: Build, test and install to local m2 (verify + seed the deploys)
# Re-verify the tagged commit before publishing (defence in depth
# against a tag pushed from a broken branch). `install` — not `verify` —
Expand All @@ -127,21 +165,10 @@ jobs:
# profile never publishes. Each per-module `-P release deploy` below
# builds in isolation and resolves its inter-module deps (Maven resolves
# test-scope deps even with -DskipTests) from this local m2, so the
# unpublished tests-jar no longer fails the deploy.
# unpublished tests-jar no longer fails the deploy. It runs after the gate
# above, so the artifacts it installs cannot serve as baselines.
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: Plan the deploy set (start_at resume)
id: plan
# A partial Central publication CANNOT be blindly re-dispatched: the deploys
Expand Down
98 changes: 98 additions & 0 deletions .github/workflows/release-script-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ on:
paths:
- 'scripts/cut-release.ps1'
- '.github/workflows/release-script-check.yml'
# Carries the japicmp previous-release pin the script moves.
- 'templates/pom.xml'
pull_request:
paths:
- 'scripts/cut-release.ps1'
- '.github/workflows/release-script-check.yml'
- 'templates/pom.xml'

permissions:
contents: read
Expand Down Expand Up @@ -351,3 +354,98 @@ 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 japicmp previous-release pin has to move onto the version just published
# at every cut (docs/api-stability.md, Binary-compatibility enforcement), 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)
$want = @('Update-JapicmpPreviousBaseline', 'Update-JapicmpMajorBaseline')
$fn = $ast.FindAll({
param($n)
$n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $want -contains $n.Name
}, $true)
$missing = $want | Where-Object { $_ -notin ($fn | ForEach-Object { $_.Name }) }
if ($missing) { throw "cut-release.ps1 no longer defines: $($missing -join ', ')" }
Invoke-Expression (($fn | ForEach-Object { $_.Extent.Text }) -join "`n")
function Note($m) { Write-Host " $m" }
$DryRun = $false

# A function nothing calls moves no pin. Assert the post-release path still
# invokes every one of them: the failure this guards is a helper surviving a
# refactor that drops the call, which every content assertion below would still
# pass — the pin it moves simply stops moving.
$calls = $ast.FindAll({
param($n)
$n -is [System.Management.Automation.Language.CommandAst] -and
$want -contains $n.GetCommandName()
}, $true)
$called = $calls | ForEach-Object { $_.GetCommandName() } | Select-Object -Unique
$uncalled = $want | Where-Object { $_ -notin $called }
if ($uncalled) { throw "nothing in cut-release.ps1 calls: $($uncalled -join ', ')" }

function New-Pom($previous) {
$file = Join-Path ([IO.Path]::GetTempPath()) ("pom-" + [guid]::NewGuid().ToString() + ".xml")
Set-Content -Path $file -NoNewline -Value (@(
'<project>',
' <version>2.4.1-SNAPSHOT</version>',
' <properties>',
' <japicmp.baseline.floor>2.0.0</japicmp.baseline.floor>',
" <japicmp.baseline.previous>$previous</japicmp.baseline.previous>",
' <japicmp.break.binary>false</japicmp.break.binary>',
' </properties>',
'</project>'
) -join "`n")
return $file
}

$pom = New-Pom '2.3.0'
if (-not (Update-JapicmpPreviousBaseline $pom '2.4.0')) { throw 'moving the pin reported no change' }
$after = Get-Content $pom -Raw
if ($after -notmatch '<japicmp\.baseline\.previous>2\.4\.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\.4\.1-SNAPSHOT</version>') { throw "the project version was rewritten:`n$after" }

if (Update-JapicmpPreviousBaseline $pom '2.4.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.4.1-SNAPSHOT</version>`n</project>"
if (Update-JapicmpPreviousBaseline $bare '2.4.0') { throw 'a pom without the pin reported a change' }
if (Update-JapicmpPreviousBaseline (Join-Path ([IO.Path]::GetTempPath()) 'no-such-pom.xml') '2.4.0') {
throw 'a missing pom reported a change'
}

# And on the real templates pom, so the pattern is proven against the file it
# has to move rather than only against one written to fit it.
$real = Join-Path ([IO.Path]::GetTempPath()) ("templates-pom-" + [guid]::NewGuid().ToString() + ".xml")
Copy-Item templates/pom.xml $real
if (-not (Update-JapicmpPreviousBaseline $real '9.9.9')) { throw 'the real templates pom: the pin did not move' }
$expected = [regex]::Replace((Get-Content templates/pom.xml -Raw),
'<japicmp\.baseline\.previous>[^<]*</japicmp\.baseline\.previous>',
'<japicmp.baseline.previous>9.9.9</japicmp.baseline.previous>')
if ((Get-Content $real -Raw) -ne $expected) { throw 'the real templates pom: the move changed more than the pin' }

# Opening a major is the one release that moves the floor and turns the break
# switch back on; every other release leaves both alone (docs/api-stability.md,
# Opening a major). Until that release ships, the pins name the previous major.
$bootstrap = New-Pom '2.4.0'
if (-not (Update-JapicmpMajorBaseline $bootstrap '3.0.0')) { throw 'the X.0.0 release did not move the floor' }
$opened = Get-Content $bootstrap -Raw
if ($opened -notmatch '<japicmp\.baseline\.floor>3\.0\.0</japicmp\.baseline\.floor>') {
throw "the floor did not move onto the release that opened the major:`n$opened"
}
if ($opened -notmatch '<japicmp\.break\.binary>true</japicmp\.break\.binary>') {
throw "the break switch did not return to true:`n$opened"
}
if (Update-JapicmpMajorBaseline $bootstrap '3.0.0') { throw 'a second major move reported a change' }
if (Update-JapicmpMajorBaseline (New-Pom '2.4.0') '3.0.1') { throw 'a patch release must not move the floor' }

Write-Host 'japicmp baseline: previous moves, floor stays except at X.0.0, idempotent, the real pom moves, and the cut calls both.'
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,29 @@ follow semantic versioning; release dates are ISO 8601.
Per [`docs/api-stability.md`](docs/api-stability.md) § 3 it is Stable-tier, so it is removed
no earlier than 3.0 and not before a full minor has shipped with the deprecation in place.

### Build

- **`graph-compose-templates` is under the binary-compatibility gate.** japicmp used to diff
`graph-compose-core` alone, so a Stable templates method could be deleted in a minor with
every check green. The module now carries its own `japicmp` profile, run by the same CI job,
the publish workflow and the release script, and diffs each build against two published
releases: the 2.x floor (`2.0.0`), which holds the GA surface, and the latest release
(`2.3.0` today), which holds everything added since. `cut-release.ps1 -PostReleaseOnly`
moves the second pin after each release. Every `templates.*` package is Stable, so the only
exclusion is the per-element `@Internal` marker, and nothing carries it. A baseline the gate
cannot resolve fails the build; japicmp's default would skip that diff with a warning and
pass. Each path that runs the gate then checks that every execution left its report, since
one that does not run — switched off, unbound, or not selected — writes none and fails
nothing. A pin may never name the version being built: japicmp resolves such a pin to the
artifact the build just produced and reports no differences, so both pins stay strictly
older than the working version, every path drops our cached artifacts before it resolves,
and the publish workflow runs the gate before the `install` that seeds the repository.
While a major has no release of its own, the pins name the previous major's floor and last
release and `japicmp.break.binary` is `false` — those diffs are reported, not enforced —
until the first post-release bump after `X.0.0` ships. `VersionConsistencyGuardTest` holds
the pins and that switch to the CHANGELOG, and `BinaryCompatibilityGateGuardTest` holds the
executions, their settings, the trigger, the report checks and the publish ordering in place.

### Documentation

- **The timeline recipe describes the finished model.** `LEADING | AXIS | CONTENT`, what the
Expand Down
Loading
Loading