Skip to content

ci(templates): gate the templates surface, against the major floor and the last release - #579

Open
DemchaAV wants to merge 3 commits into
feature/cv-constructor-layerfrom
chore/templates-japicmp-gate
Open

ci(templates): gate the templates surface, against the major floor and the last release#579
DemchaAV wants to merge 3 commits into
feature/cv-constructor-layerfrom
chore/templates-japicmp-gate

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Why

docs/api-stability.md § 4 lists every templates.* package — CvDocument and 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 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; 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 gets NoSuchMethodError — and MarkdownInline.appendTransformed and SectionAllocation, 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

  • Each gated module pins two baselines, diffed in their own execution of the japicmp profile: japicmp.baseline.floor (the major's first release — holds the GA surface) and japicmp.baseline.previous (the latest published release — holds everything added since). core/pom.xml and templates/pom.xml carry the same profile; templates has no Internal package tree, so its only exclude is the per-element @Internal marker.
  • The previous pin moves with the release. cut-release.ps1 -PostReleaseOnly step 3a runs Update-JapicmpPreviousBaseline over 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 -SNAPSHOT or an -rc, and it is idempotent.
  • The pins are derived, not trusted. VersionConsistencyGuardTest.japicmpBaselinesTrackTheWorkingMajorAndTheLatestRelease reads 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. bothJapicmpBaselineExecutionsSurviveInEachGatedPom asserts 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, so ChangelogVersionParsingTest holds the shapes (snapshot cycle, release commit, major boundary, nothing older, out-of-order entries).
  • A major boundary stands the gate down by construction. Both pins stay inside the working major, so opening 3.0.0-SNAPSHOT leaves them naming an unpublished 3.0.0; ignoreMissingOldVersion makes 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.
  • Where it runs: .github/workflows/ci.yml gains a templates path filter, and binary-compat diffs both modules in one reactor invocation on a core or templates change, uploading both reports; publish.yml gates templates on the tagged commit; cut-release.ps1 step 5b covers both before the tag is cut; release-script-check.yml drives the baseline mover through the same AST lift the other release-script checks use.
  • Docs corrected where they overclaimed: api-stability.md no longer says the floor alone delivers the Stable promise, and documents the two pins, the guard, and the major-boundary behaviour; release-process.md step 7 names the pin move; CONTRIBUTING.md and ROADMAP.md / post-2.0-engineering.md follow. CHANGELOG entry under v2.2.1 — Planned › Build.

Verification

  • ./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core,:graph-compose-templatesBUILD SUCCESS, four executions (floor + previous × two modules); the reports confirm the pairs actually resolved: 2.2.1-SNAPSHOT vs 2.0.0 and vs 2.2.0 for each module.
  • The hole, demonstrated on the finished config. Deleting MarkdownInline.appendTransformedBUILD FAILURE …appendTransformed(…):METHOD_REMOVED from japicmp-against-previous-release, while the floor report for the same build reads Binary incompatible changes: No. That is the exact case a single floor pin passes.
  • Floor still enforces independently: narrowing CvDocument.slotOf(CvSection) to package-private → BUILD FAILURE METHOD_LESS_ACCESSIBLE from japicmp-against-major-floor. Both sabotages reverted.
  • Guards fail closed, each driven red before being left green: a stale previous pin (2.1.1) and a wrong floor (2.1.0) each fail japicmpBaselinesTrackTheWorkingMajorAndTheLatestRelease; deleting the japicmp-against-previous-release execution id fails bothJapicmpBaselineExecutionsSurviveInEachGatedPom.
  • An unpublished baseline skips instead of failing: pinning 9.9.9[WARNING] Could not resolve…, BUILD SUCCESS — the major-boundary window.
  • Guard suite in :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.ps1 parses clean and the lifted Update-JapicmpPreviousBaseline moves 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-core and graph-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.

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.
@DemchaAV DemchaAV changed the title ci(templates): put the templates public surface under the japicmp gate ci(templates): gate the templates surface, against the major floor and the last release Aug 17, 2026
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.
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.

2 participants