ci(templates): diff the templates API against 2.0.0 and the last release - #681
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
docs/api-stability.mdlists everytemplates.*package as Stable, but japicmp only ever diffedgraph-compose-core. DeletingTextOrnaments.spacedUpper(String)— deprecated in 2.4.0, its removal promised no earlier than 3.0 — passed every check.What changed
templates/pom.xmlgets ajapicmpprofile (same plugin version as core) with one execution per published baseline:japicmp-against-major-floor→graph-compose-templates:2.0.0(japicmp.baseline.floor), the GA surface;japicmp-against-previous-release→2.3.0(japicmp.baseline.previous), everything added since. A floor-only diff cannot see it:SectionAllocationis absent from 2.0.0, so re-typingtitleOrpasses the floor and fails only this execution.@Internalmarker: everytemplates.*package is Stable, and nothing carries it.ignoreMissingOldVersion=false. japicmp 0.26.2 defaults it totrue, which logs an unresolvable baseline as a warning, skips the diff, still writes a report, and passes.japicmp.baselineincluded) must name a published release strictly older than the working version, whichVersionConsistencyGuardTestenforces. As defence in depth — neither replaces that rule — each path drops our cached artifacts from the local repository before the gate resolves, andpublish.ymlruns the gate (one reactor run over core and templates, after installing the fonts/emoji companions core resolves at test scope) before theclean installthat seeds the repository with the release being published.docs/api-stability.md, Opening a major). While majorXhas no release of its own — the wholeX.0.0-SNAPSHOTcycle and theX.0.0release commit — the templates pins name the previous major's floor and last release andjapicmp.break.binaryisfalse: both diffs run and are reported, because a major is allowed to break. TheX.0.0publish therefore checks those diffs against releases already on Central: no circular wait forX.0.0, and no self-comparison.-PostReleaseOnlyends the bootstrap by itself —Update-JapicmpMajorBaselinemoves the floor onto anX.0.0release and turns the switch back totrue, beside the previous-release move — so strict same-major enforcement resumes withX.0.1-SNAPSHOT. The guard derives all three values from the CHANGELOG and fails the build until the poms match.publish.ymland 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 existingBinary Compatibilityjob diffs both modules in one reactor run and also runs on a newtemplatespath filter (templates/src/**,templates/pom.xml); both report directories are uploaded. The job name is unchanged, so branch protection is unaffected.-PostReleaseOnlyStep 3a movesjapicmp.baseline.previousonto the release just published, unit-checked inrelease-script-check.ymlagainst a synthetic pom and the realtemplates/pom.xml; that workflow now also runs whentemplates/pom.xmlchanges, and requires Step 3a to call every mover it lifts — a helper nothing calls moves no pin.VersionConsistencyGuardTest(pin derivation, the break switch, and no pin equal to the version being built — in every gated pom) andChangelogVersionParsingTest(+10 derivation cases).BinaryCompatibilityGateGuardTest(7 tests, in the always-on guards job) holds the wiring: executions bound toverify/cmpwith theiroldVersion; break-on-binary astrueor the property; no skip element, nooverrideCompatibilityChangeParameters, nojapicmp.skipanywhere the build reads; fail-on-unresolvable and no exclude beyond@Internalfor templates; each path's module list,-P japicmpatverify, report checks as uncommentedtest -slines, cached-artifact drops, and the publish ordering; plus the PR job's exported path filter,needs: changesand nocontinue-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/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, andcore/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):-DskipTestsTextOrnaments.spacedUpper(String)…TextOrnaments.spacedUpper(java.lang.String):METHOD_REMOVEDjoinPipe(String...)→(CharSequence...); deprecated 13-argSpacing(...)package-private;CvIdentity(CvName, Contact, List)→CollectionMETHOD_REMOVED,CONSTRUCTOR_LESS_ACCESSIBLE,CONSTRUCTOR_REMOVEDSectionAllocation.titleOr(CvSection, String)→CharSequence…titleOr(…CvSection,java.lang.String):METHOD_REMOVEDpublic static String additiveProbe(String)-Djapicmp.baseline.previous=2.2.99-Djapicmp.skip=truejapicmp skipped.×3, Maven passes with no reports; the report checks exit 1 and Step 5b's check throwsjapicmp gate incomplete: … is missingEach 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:
3.0.0, artifact installed asclean installdoesversion 3.0.0 with the previous version 3.0.0, same jar as old and new archive3.0.0deleted from the repository3.0.03.0.02.0.0version 3.0.0 with the previous version 2.3.0and… 2.0.0, BUILD SUCCESSignoreMissingOldVersionremoved,overrideCompatibilityChangeParametersadded,japicmp.skipset, the break switch hard-coded; in the PR job thetemplatesoutput dropped,changesdropped fromneeds,continue-on-erroradded, templates dropped from-pl, a report check deleted, a report check commented out, the cached-artifact drop removed; in Step 5b templates and-P japicmpdropped, the post-run check deleted, the pre-run clear deleted, the baseline drop removed; inpublish.ymljapicmp dropped, a report check dropped, the gate moved afterclean install. Every file was restored to its checksum../mvnw -B -ntp clean "-Dtest=<guards-job list>,ChangelogVersionParsingTest,CiGateCoverageGuardParsingTest" test -pl :graph-compose-core→ 106 tests, 0 failures../mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-templates→ BUILD SUCCESS, 747 core tests (2 skipped) + 113 templates tests.extract-api --from-reactor --check,check-stability-doc,check-claims --check,check-routes→ green.-DryRunfull cut prints Step 5b with both modules;-PostReleaseOnly -DryRuncompletes; 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 anX.0.0release moves the floor and the switch); therelease-script-check.ymlstep runs green locally.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-*andgraph-compose-testingremain ungated.