ci(templates): gate the templates surface, against the major floor and the last release - #579
Open
DemchaAV wants to merge 3 commits into
Open
ci(templates): gate the templates surface, against the major floor and the last release#579DemchaAV wants to merge 3 commits into
DemchaAV wants to merge 3 commits into
Conversation
docs/api-stability.md lists every templates.* package as Stable, but japicmp only ever diffed graph-compose-core: a removed or narrowed public method in a preset, a data record, or a shared widget could ship in a 2.x minor unnoticed. templates/pom.xml carries the same `japicmp` profile as the engine pom, pinned to its own published floor of the major (graph-compose-templates:2.0.0). The PR-time Binary Compatibility job diffs core and templates in one reactor run whenever either module's sources or pom changed; cut-release.ps1 step 5b runs the same pair before the tag is cut, and the publish workflow diffs templates on the tagged commit before deploying. VersionConsistencyGuardTest holds the two japicmp.version pins in lockstep. The current surface is binary- and source-compatible with 2.0.0 (additions only); narrowing a public method to package-private fails the build with METHOD_LESS_ACCESSIBLE.
…ast release A single floor baseline protects only the surface the major shipped with. A method first published 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. core/pom.xml and templates/pom.xml pin japicmp.baseline.floor and japicmp.baseline.previous, diffed in their own executions of the japicmp profile. cut-release.ps1 -PostReleaseOnly moves the previous pin onto the version just published (Update-JapicmpPreviousBaseline, step 3a), which is also the only point at which that release is on Central. The pins are derived rather than trusted: japicmpBaselinesTrackTheWorkingMajorAndTheLatestRelease reads the floor from the module's own version and the previous pin from the newest dated CHANGELOG release of that major, and bothJapicmpBaselineExecutionsSurviveInEachGatedPom asserts neither execution has been dropped — properties alone would let the second diff stop running with CI green. release-script-check.yml drives the mover through the same AST lift the other release-script checks use. Both pins stay inside the working major, so opening the next one leaves them naming a version nobody has published; ignoreMissingOldVersion turns that into a skipped execution rather than an unresolvable-artifact failure, and the cycle is free to make the breaks a major is for. Enforcement resumes at its first -PostReleaseOnly.
The baseline guard compared versions by parsing each segment with Integer.parseInt, which throws NumberFormatException on a segment wider than an int. In a guard that lands as a stack trace where the assertion message naming the wrong pin should be, and it is an uncaught path CodeQL is right to flag. Nothing there needs the numeric value, only the order, so segments are compared as digit strings: longer wins once leading zeros are stripped, equal lengths compare lexicographically. Two cases pin it — a segment no int could hold still orders correctly, and leading zeros do not change the order.
This was referenced Aug 17, 2026
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.md§ 4 lists everytemplates.*package —CvDocumentand the section records, the components and widgets, all 17 CV presets and their cover-letter twins, invoice and proposal — as Stable: no binary break outside a major release. But japicmp only ever diffedgraph-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; the policy existed, the gate did not.Extending that gate exposed a second, larger hole in the strategy it was about to inherit. A single floor baseline (
2.0.0) protects only the surface the major shipped with. A method first published 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 getsNoSuchMethodError— andMarkdownInline.appendTransformedandSectionAllocation, both added after 2.0.0, sat in exactly that gap. Fixing it here rather than later is the point: this PR is what would otherwise cement the one-baseline strategy as policy across two modules.What changed
japicmpprofile:japicmp.baseline.floor(the major's first release — holds the GA surface) andjapicmp.baseline.previous(the latest published release — holds everything added since).core/pom.xmlandtemplates/pom.xmlcarry the same profile; templates has no Internal package tree, so its only exclude is the per-element@Internalmarker.cut-release.ps1 -PostReleaseOnlystep 3a runsUpdate-JapicmpPreviousBaselineover both poms, setting the pin to the version just cut — which is also the first moment that release is on Central. It never moves onto a-SNAPSHOTor an-rc, and it is idempotent.VersionConsistencyGuardTest.japicmpBaselinesTrackTheWorkingMajorAndTheLatestReleasereads the floor off the module's own version (<major>.0.0) and the previous pin off the newest dated CHANGELOG release of that major older than the working version.bothJapicmpBaselineExecutionsSurviveInEachGatedPomasserts neither<execution>has been deleted — checking properties alone would let the second diff quietly stop running with CI green, which is the same hole in a tidier pom.newestFinalReleaseBefore's logic is string-driven, soChangelogVersionParsingTestholds the shapes (snapshot cycle, release commit, major boundary, nothing older, out-of-order entries).3.0.0-SNAPSHOTleaves them naming an unpublished3.0.0;ignoreMissingOldVersionmakes that a skipped execution rather than an unresolvable-artifact failure, and the cycle is free to make the breaks a major is for. Pinning back to 2.x instead would fail the build on every intentional break. Enforcement resumes at the new major's first-PostReleaseOnly. This is the report-only posture the 2.0 transition ran under, now automatic instead of a hand-edited pom..github/workflows/ci.ymlgains atemplatespath filter, andbinary-compatdiffs both modules in one reactor invocation on a core or templates change, uploading both reports;publish.ymlgates templates on the tagged commit;cut-release.ps1step 5b covers both before the tag is cut;release-script-check.ymldrives the baseline mover through the same AST lift the other release-script checks use.api-stability.mdno longer says the floor alone delivers the Stable promise, and documents the two pins, the guard, and the major-boundary behaviour;release-process.mdstep 7 names the pin move;CONTRIBUTING.mdandROADMAP.md/post-2.0-engineering.mdfollow. CHANGELOG entry underv2.2.1 — Planned › Build.Verification
./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core,:graph-compose-templates→ BUILD SUCCESS, four executions (floor + previous × two modules); the reports confirm the pairs actually resolved:2.2.1-SNAPSHOTvs2.0.0and vs2.2.0for each module.MarkdownInline.appendTransformed→ BUILD FAILURE…appendTransformed(…):METHOD_REMOVEDfromjapicmp-against-previous-release, while the floor report for the same build readsBinary incompatible changes: No. That is the exact case a single floor pin passes.CvDocument.slotOf(CvSection)to package-private → BUILD FAILUREMETHOD_LESS_ACCESSIBLEfromjapicmp-against-major-floor. Both sabotages reverted.2.1.1) and a wrong floor (2.1.0) each failjapicmpBaselinesTrackTheWorkingMajorAndTheLatestRelease; deleting thejapicmp-against-previous-releaseexecution id failsbothJapicmpBaselineExecutionsSurviveInEachGatedPom.9.9.9→[WARNING] Could not resolve…, BUILD SUCCESS — the major-boundary window.:graph-compose-core(VersionConsistencyGuardTest, ChangelogVersionParsingTest, CiGuardListGuardTest, CiGateCoverageGuardTest, CiGateCoverageGuardParsingTest, CodeQlScopeGuardTest, ReleaseAssetStepGuardTest, ReleaseScriptInstallListGuardTest, ReleaseScriptRoadmapGuardTest, CanonicalSurfaceGuardTest, DocumentationCoverageTest, AgentsGuideGuardTest, BenchmarkDependencyInstallGuardTest, PackageMapGuardTest, BetaAnnotationDocumentationTest) → Tests run: 108, Failures: 0 (+3 new).cut-release.ps1parses clean and the liftedUpdate-JapicmpPreviousBaselinemoves the pin, leaves the floor and the project version untouched, and is idempotent. All three workflows parse.Lane: build/CI — no runtime code; no public API change.
Limits: the gate covers
graph-compose-coreandgraph-compose-templates. The remaining published modules (render-pdf/render-docx/render-pptx/testing/wrapper) join once each has a documented Stable / Internal split to gate against.