Skip to content

ci(templates): diff the templates API against 2.0.0 and the last release - #681

Merged
DemchaAV merged 6 commits into
developfrom
chore/templates-binary-compat
Sep 12, 2026
Merged

ci(templates): diff the templates API against 2.0.0 and the last release#681
DemchaAV merged 6 commits into
developfrom
chore/templates-binary-compat

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Why

docs/api-stability.md lists every templates.* package as Stable, but japicmp only ever diffed graph-compose-core. Deleting TextOrnaments.spacedUpper(String) — deprecated in 2.4.0, its removal promised no earlier than 3.0 — passed every check.

What changed

  • templates/pom.xml gets a japicmp profile (same plugin version as core) with one execution per published baseline:
    • japicmp-against-major-floorgraph-compose-templates:2.0.0 (japicmp.baseline.floor), the GA surface;
    • japicmp-against-previous-release2.3.0 (japicmp.baseline.previous), everything added since. A floor-only diff cannot see it: SectionAllocation is absent from 2.0.0, so re-typing titleOr passes the floor and fails only this execution.
    • Breaks on any binary incompatibility; source-only ones are reported (core's phased policy). The only exclude is the per-element @Internal marker: every templates.* package is Stable, and nothing carries it.
    • ignoreMissingOldVersion=false. japicmp 0.26.2 defaults it to true, which logs an unresolvable baseline as a warning, skips the diff, still writes a report, and passes.
  • A release can never be its own baseline. japicmp resolves a pin equal to the module's own version to the artifact the build just produced — measured on a simulated 3.0.0 cut, where it reported "No incompatible changes found while checking backward compatibility of version 3.0.0 with the previous version 3.0.0" and passed, from the reactor and from a local repository the release had been installed into alike. So every pin in every gated pom (core's japicmp.baseline included) must name a published release strictly older than the working version, which VersionConsistencyGuardTest enforces. As defence in depth — neither replaces that rule — each path drops our cached artifacts from the local repository before the gate resolves, and publish.yml runs the gate (one reactor run over core and templates, after installing the fonts/emoji companions core resolves at test scope) before the clean install that seeds the repository with the release being published.
  • Opening a major has a policy (docs/api-stability.md, Opening a major). While major X has no release of its own — the whole X.0.0-SNAPSHOT cycle and the X.0.0 release commit — the templates pins name the previous major's floor and last release and japicmp.break.binary is false: both diffs run and are reported, because a major is allowed to break. The X.0.0 publish therefore checks those diffs against releases already on Central: no circular wait for X.0.0, and no self-comparison. -PostReleaseOnly ends the bootstrap by itself — Update-JapicmpMajorBaseline moves the floor onto an X.0.0 release and turns the switch back to true, beside the previous-release move — so strict same-major enforcement resumes with X.0.1-SNAPSHOT. The guard derives all three values from the CHANGELOG and fails the build until the poms match.
  • Every path that runs the gate proves it ran. A japicmp execution that does not run — switched off, unbound, or not selected — writes no report and fails nothing, so the pull-request job, publish.yml and Step 5b each require every execution's XML report; Step 5b deletes the reports first, so one left by an earlier run cannot stand in.
  • ci.yml: the existing Binary Compatibility job diffs both modules in one reactor run and also runs on a new templates path filter (templates/src/**, templates/pom.xml); both report directories are uploaded. The job name is unchanged, so branch protection is unaffected.
  • The pin moves with the release: -PostReleaseOnly Step 3a moves japicmp.baseline.previous onto the release just published, unit-checked in release-script-check.yml against a synthetic pom and the real templates/pom.xml; that workflow now also runs when templates/pom.xml changes, and requires Step 3a to call every mover it lifts — a helper nothing calls moves no pin.
  • Guards: VersionConsistencyGuardTest (pin derivation, the break switch, and no pin equal to the version being built — in every gated pom) and ChangelogVersionParsingTest (+10 derivation cases). BinaryCompatibilityGateGuardTest (7 tests, in the always-on guards job) holds the wiring: executions bound to verify/cmp with their oldVersion; break-on-binary as true or the property; no skip element, no overrideCompatibilityChangeParameters, no japicmp.skip anywhere the build reads; fail-on-unresolvable and no exclude beyond @Internal for templates; each path's module list, -P japicmp at verify, report checks as uncommented test -s lines, cached-artifact drops, and the publish ordering; plus the PR job's exported path filter, needs: changes and no continue-on-error. Neither class decides for itself what to cover: the executions each pom must keep and the baseline pins each must declare are both required to cover every module discovered to declare the profile, so a module joining the gate cannot join it unchecked — which is what would let a third gated pom pin the version being built.
  • Docs: docs/api-stability.md (enforcement section and the bootstrap policy), CONTRIBUTING.md (job description, trigger, guard list), docs/contributing/release-process.md (5b, -PostReleaseOnly), CHANGELOG.md ### Build, and core/pom.xml's baseline comment, which described an equal pin as a no-op.

Verification

Gate runs: ./mvnw -B -ntp -P japicmp verify -pl :graph-compose-core,:graph-compose-templates (sabotage runs with -Dmaven.test.skip=true, since templates tests call the sabotaged members):

Change Result
none — the CI command, -DskipTests BUILD SUCCESS; 319 / 21 / 8 diff rows (core / templates floor / templates previous)
delete TextOrnaments.spacedUpper(String) floor: …TextOrnaments.spacedUpper(java.lang.String):METHOD_REMOVED
joinPipe(String...)(CharSequence...); deprecated 13-arg Spacing(...) package-private; CvIdentity(CvName, Contact, List)Collection floor: METHOD_REMOVED, CONSTRUCTOR_LESS_ACCESSIBLE, CONSTRUCTOR_REMOVED
SectionAllocation.titleOr(CvSection, String)CharSequence floor passes; previous: …titleOr(…CvSection,java.lang.String):METHOD_REMOVED
add public static String additiveProbe(String) BUILD SUCCESS, both executions
-Djapicmp.baseline.previous=2.2.99 BUILD FAILURE: artifact could not be resolved
-Djapicmp.skip=true japicmp skipped. ×3, Maven passes with no reports; the report checks exit 1 and Step 5b's check throws japicmp gate incomplete: … is missing

Each sabotaged source was restored to its HEAD blob (hash-checked).

The first cut of a new major, simulated in a throwaway worktree at 3.0.0 with an isolated repository:

Configuration Result
both pins 3.0.0, artifact installed as clean install does passesversion 3.0.0 with the previous version 3.0.0, same jar as old and new archive
same, with the installed 3.0.0 deleted from the repository still passes — the local repository was not the only route
reactor run, repository never held 3.0.0 still passes — japicmp resolves the build's own artifact
on the fixed tree: both pins 3.0.0 guard fails: the floor must be 2.0.0
on the fixed tree: the bootstrap pins the policy derives guard passes; the gate reports version 3.0.0 with the previous version 2.3.0 and … 2.0.0, BUILD SUCCESS
  • Guards hold the wiring: 26 edits over thirteen runs each fail the guard method that owns them — in the poms a stale pin, a templates pin equal to the version being built, core's pin equal to it, the previous-release execution unbound, a Stable package excluded, ignoreMissingOldVersion removed, overrideCompatibilityChangeParameters added, japicmp.skip set, the break switch hard-coded; in the PR job the templates output dropped, changes dropped from needs, continue-on-error added, templates dropped from -pl, a report check deleted, a report check commented out, the cached-artifact drop removed; in Step 5b templates and -P japicmp dropped, the post-run check deleted, the pre-run clear deleted, the baseline drop removed; in publish.yml japicmp dropped, a report check dropped, the gate moved after clean install. Every file was restored to its checksum.
  • ./mvnw -B -ntp clean "-Dtest=<guards-job list>,ChangelogVersionParsingTest,CiGateCoverageGuardParsingTest" test -pl :graph-compose-core106 tests, 0 failures.
  • ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-templatesBUILD SUCCESS, 747 core tests (2 skipped) + 113 templates tests.
  • Knowledge: extract-api --from-reactor --check, check-stability-doc, check-claims --check, check-routes → green.
  • Release script: parses; the -DryRun full cut prints Step 5b with both modules; -PostReleaseOnly -DryRun completes; Step 3a's movers and Step 5b's report list, clear loop and check loop lifted by AST and run (clear → gate → check order; passes with the reports, throws without; only an X.0.0 release moves the floor and the switch); the release-script-check.yml step runs green locally.
  • The CI job's Maven steps rehearsed from an empty local repository in a clean worktree → BUILD SUCCESS; the only GraphCompose artifacts fetched were the baselines.

Lane: build/CI — no production code change.

Notes. graph-compose-core's gate behaviour is unchanged: it still diffs its floor alone, and still runs with japicmp's default, so a core baseline it cannot resolve is skipped with a warning and still leaves a report. What changed for it is the pin rule it is now held to, and a comment that had described an equal pin as a no-op. graph-compose-render-* and graph-compose-testing remain ungated.

docs/api-stability.md lists every templates.* package as Stable, but
japicmp only ever diffed graph-compose-core: a public templates method
could be deleted, narrowed or re-typed in a 2.x minor with every check
green.

templates/pom.xml carries a `japicmp` profile with one execution per
published baseline. japicmp-against-major-floor diffs against 2.0.0 and
holds the GA surface. japicmp-against-previous-release diffs against the
latest release (2.3.0) and holds everything added since, which a
floor-only diff cannot: SectionAllocation is absent from 2.0.0, so
re-typing SectionAllocation.titleOr(CvSection, String) passes the floor
and fails only the previous-release diff. Every templates.* package is
Stable, so the only exclusion is the per-element @internal marker, which
nothing carries. ignoreMissingOldVersion is set to false: japicmp
defaults it to true, which skips a baseline it cannot resolve with a
warning and passes, so a mistyped or unpublished pin now fails instead.

The gate runs wherever core's does. The Binary Compatibility PR job diffs
core and templates in one reactor invocation, and also runs when only
templates/src or templates/pom.xml changed; cut-release.ps1 Step 5b
diffs both before the tag; publish.yml diffs templates on the tagged
commit. cut-release.ps1 -PostReleaseOnly moves the previous pin onto the
release it just published (Update-JapicmpPreviousBaseline, Step 3a),
unit-checked in release-script-check.yml.

VersionConsistencyGuardTest derives both pins - the floor is X.0.0 of
the working version, the previous pin the newest dated final release of
that major below it - and holds japicmp.version in lockstep with core.
BinaryCompatibilityGateGuardTest reads the wiring back: every execution
the gate relies on, break-on-binary-incompatibility, no skip switch,
fail-on-unresolvable for templates, no exclusion beyond @internal, and
every gated module named by the PR job, its path filter, the release
script and the publish workflow. It runs in the always-on guards job.
japicmp writes no report for an execution it skips, and skipping fails
nothing. A japicmp.skip property in a pom or .mvn/maven.config, an
execution unbound from the lifecycle, or a profile left inactive each
left the gate reporting success having diffed nothing, and the wiring
guard only looked for a few named switches.

The Binary Compatibility job, publish.yml and cut-release.ps1 Step 5b now
end by checking that each execution the gate relies on left its XML
report; Step 5b deletes those reports first, so one left by an earlier
run cannot stand in. BinaryCompatibilityGateGuardTest requires the checks
on all three paths, and also rejects japicmp.skip anywhere the build
reads, overrideCompatibilityChangeParameters, continue-on-error on the
job, the job losing `changes` from its needs, and a path that stops
running the profile at verify. Its path-filter probe now sits inside a
package, where the sources are.

release-script-check.yml also runs when templates/pom.xml changes, since
the pin the script moves lives there. The docs describe what the guard
holds rather than what it would catch, name graph-compose-testing among
the ungated modules, and state that the first cut of a new major fails
the templates gate until it is decided how the gate runs for it. Comments
that restated docs/api-stability.md now point at it.
An execution that does not run - switched off, unbound from the
lifecycle, or not selected - writes no japicmp report, and that is what
the report checks rely on. A baseline japicmp cannot resolve is
different: under the plugin's default it skips the diff with a warning
and still writes a report. The templates gate fails on that case
(ignoreMissingOldVersion=false); the engine gate does not yet. The
comments, the docs and the CHANGELOG said that any skipped execution
writes no report, which claimed the checks covered both.

BinaryCompatibilityGateGuardTest also stops accepting the checks as mere
text: each workflow report check has to be an uncommented `test -s`
line, and Step 5b has to clear the reports, run the gate and then check
them, in that order, so deleting either loop or commenting a check out
now fails it.
japicmp resolves a baseline pin that equals the module's own version to
the artifact the build just produced. Measured on a simulated 3.0.0 cut
with both pins at 3.0.0: it reported "No incompatible changes found while
checking backward compatibility of version 3.0.0 with the previous
version 3.0.0" and passed - from the reactor, and from a local repository
the release had just been installed into, alike. The fallback that made
both pins equal the version being released is the one the first cut of a
new major would have taken.

The pins now always name a published release strictly older than the
working version. While a major has no release of its own - the
X.0.0-SNAPSHOT cycle and the X.0.0 release commit - they name the
previous major's floor and last release, and japicmp.break.binary is
false: both diffs run and are reported, because a major is allowed to
break. They become X.0.0, and the switch true, at the first
-PostReleaseOnly after X.0.0 is published and dated in the CHANGELOG, so
strict same-major enforcement resumes with X.0.1-SNAPSHOT.
VersionConsistencyGuardTest derives floor, previous and the switch from
the CHANGELOG, and rejects a pin that is not strictly older than the
version being built.

Two further lines of defence, neither of which replaces that rule: every
path drops our cached artifacts from the local repository before the gate
resolves, and publish.yml runs the gate - now one reactor run over core
and templates - before the clean install that seeds the repository with
the release being published. BinaryCompatibilityGateGuardTest holds those
drops and that ordering, and docs/api-stability.md states the bootstrap
policy the X.0.0 cycle, its publish and its first post-release bump each
follow.
The strictly-older rule reached graph-compose-templates only, so nothing
bounded core's japicmp.baseline - and core/pom.xml still described a
baseline equal to the working version as a no-op. It is not: japicmp
resolves such a pin to the build's own artifact and passes, which is the
hole this branch closed for templates. VersionConsistencyGuardTest now
holds every pin in every gated pom strictly older than the version being
built, and the comment states what was measured.

-PostReleaseOnly can also finish a major transition on its own:
Update-JapicmpMajorBaseline moves the floor onto an X.0.0 release and
turns japicmp.break.binary back on, beside the previous-release move, so
the bump no longer leaves the poms in a state the Step 3b guard rejects
with thirteen poms already rewritten. release-script-check.yml lifts both
movers by AST and unit-checks that only an X.0.0 release moves the floor.

publish.yml installs graph-compose-fonts and graph-compose-emoji before
the gate. The gate now runs before the reactor install and without -am, so
core's test-scope companions would otherwise have to come from Central -
which is the resolution failure ci.yml builds them from source to avoid.
Neither is a gated artifact, so seeding them cannot supply a baseline.
… exist

Two guards named what to cover instead of deriving it.
BinaryCompatibilityGateGuardTest and VersionConsistencyGuardTest iterate
hard-coded maps, so a third module joining the japicmp profile - render-pdf is
the direction ROADMAP.md:71 commits to - would have no execution bound to
verify, no baseline pin checked and no report proved. It could pin the version
being built, which japicmp resolves to this build's own artifact, and every
guard would stay green. Both maps are now required to cover gatedModules(),
the set one of them already discovers, so the day a module joins the gate the
build says so. gatedModules() becomes package-visible to carry that, the way
CiGateCoverageGuardTest.jobBlocks already is.

release-script-check.yml asserted only that something calls
Update-JapicmpPreviousBaseline. Dropping the Update-JapicmpMajorBaseline call
from Step 3a - leaving the variable defined, as a refactor would - kept every
content assertion passing while the next X.0.0 bump silently stopped moving
the floor and the break switch. The step now requires every mover it lifts to
be called.
@DemchaAV
DemchaAV merged commit a4f20e3 into develop Sep 12, 2026
14 checks passed
@DemchaAV
DemchaAV deleted the chore/templates-binary-compat branch September 12, 2026 07:41
DemchaAV added a commit that referenced this pull request Sep 12, 2026
Brings the 2.4.0 engine work onto the promotion branch: native letter
spacing (#676), opt-in list hanging indent (#674), the resolved timeline
rail (#671-#673), the row-child margin fix, the RTL documentation
corrections (#679, #680) and the templates japicmp gate (#681).

Eight files conflicted. CHANGELOG.md is a union of both v2.4.0 sections,
with the branch-local "### Deprecated" folded into the house heading
"### Deprecations" and the sections ordered the way released entries are.
The other seven are generated and were regenerated from the merged source
rather than resolved by side: knowledge/api/templates.json and .md through
extract-api --from-reactor, and the five cv preview PDFs by re-rendering
their example classes.

Five qa baselines moved, all from f75def6, which stops a row child's
horizontal margin being taken off twice. Each of the four layout snapshots
changes by exactly one node's own horizontal margin - HeadingRule_EXPERIENCE
+9.0, EducationHeadingRule +11.285, FooterDueIcon -3.479 (a negative margin)
and FooterSite +1.693 (a right margin) - with startPage and endPage
unchanged, so no page ownership moved. cobalt_rota keeps its geometry
snapshot and moves only in pixels, inside composed table cells, which emit
fragments rather than PlacedNodes and so cannot appear in a layout snapshot;
the changed region is the day-header and note cells. One of 126 pixel
baselines changed, verified by checksum before and after.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant