You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/05/03 11:58:45 UTC

(grails-static-website) 05/09: Add SiteMap multi-major helpers for the multi-version page redesign

This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch multi-version-support
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git

commit 794b1605123e7573e8b05c4e457899a88c23614c
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sat May 2 22:13:29 2026 -0400

    Add SiteMap multi-major helpers for the multi-version page redesign
    
    Adds four query methods that drive the upcoming multi-major rendering changes on the home, downloads, and documentation pages:
    
      highestStablePerMajor(File): the highest stable release per major - the cross-major reference point used to decide which pre-releases are still relevant.
    
      latestStablePerMinorLine(File): one entry per X.Y minor line, pinned to the highest patch on that line. Drives one card per minor line on the redesigned downloads page.
    
      latestPreReleasePerMajor(File): the latest M/RC per major, suppressed once a stable at or above it has shipped. Apache release policy still requires the milestone/RC artefacts to be linked, so this map drives that section. New-major M/RCs surface even when their major has no stable yet.
    
      activeMinorLines(File): which X.Y lines should each get a card. Always includes every minor line of the highest stable major; if that major has fewer than two minor lines, also includes the previous major's lines so the page never shrinks to a single card right after a new major ships its first minor. Once the new major grows a second minor line, the previous major's lines roll off automatically.
    
    13 SiteMapSpec scenarios cover the today / 8.0.0 ship-day / 8.1.0 ship-day transitions, pre-release suppression, brand-new-major pre-releases, companion artefact lookup, and legacy 'releases:' key compatibility.
    
    No callers consume these helpers yet - the next commit wires them into the downloads page redesign.
    
    Assisted-by: claude-code:claude-opus-4-7
---
 .../website/model/documentation/SiteMap.groovy     | 107 ++++++++
 .../website/model/documentation/SiteMapSpec.groovy | 288 +++++++++++++++++++++
 2 files changed, 395 insertions(+)

diff --git a/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy b/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy
index 12c687f4eb3..bbe08a6cf62 100644
--- a/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy
+++ b/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy
@@ -95,4 +95,111 @@ class SiteMap {
             )
         }
     }
+
+    /**
+     * @return the highest stable {@link ReleaseVersion} per major version,
+     *         keyed by major. Used as the cross-major reference point for
+     *         deciding which pre-releases are still relevant (a pre-release
+     *         is suppressed once a stable release at or above it has shipped).
+     */
+    static Map<Integer, ReleaseVersion> highestStablePerMajor(File releases) {
+        Map<Integer, ReleaseVersion> result = new LinkedHashMap<Integer, ReleaseVersion>()
+        stableVersions(releases).each { ReleaseVersion v ->
+            // stableVersions is sorted descending, so the first encounter per
+            // major is automatically the highest one for that major.
+            if (!result.containsKey(v.major)) {
+                result[v.major] = v
+            }
+        }
+        result
+    }
+
+    /**
+     * @return the highest stable {@link ReleaseVersion} per minor line,
+     *         keyed by the {@code "X.Y"} string. Today (with 7.0.0..7.0.10
+     *         and 7.1.0 published) this returns
+     *         {@code ["7.0": 7.0.10, "7.1": 7.1.0]}. The redesigned downloads
+     *         page renders one card per entry, oldest line on the right.
+     */
+    static Map<String, ReleaseVersion> latestStablePerMinorLine(File releases) {
+        Map<String, ReleaseVersion> result = new LinkedHashMap<String, ReleaseVersion>()
+        stableVersions(releases).each { ReleaseVersion v ->
+            String key = "${v.major}.${v.minor}".toString()
+            if (!result.containsKey(key)) {
+                result[key] = v
+            }
+        }
+        result
+    }
+
+    /**
+     * @return the latest pre-release {@link ReleaseVersion} per major, but only
+     *         when that pre-release is still relevant (no stable at or above
+     *         it has been published). Today this typically returns at most one
+     *         entry (the upcoming next-minor RC). Once stable ships, the entry
+     *         drops automatically. Apache release policy still requires
+     *         /download.html and /documentation.html to link to the released
+     *         milestone/RC artefacts - this map drives that section.
+     */
+    static Map<Integer, ReleaseVersion> latestPreReleasePerMajor(File releases) {
+        Map<Integer, ReleaseVersion> highestStables = highestStablePerMajor(releases)
+        Map<Integer, ReleaseVersion> result = new LinkedHashMap<Integer, ReleaseVersion>()
+        preReleaseVersions(releases).each { ReleaseVersion v ->
+            if (result.containsKey(v.major)) {
+                return
+            }
+            ReleaseVersion stableForMajor = highestStables[v.major]
+            if (stableForMajor == null || v > stableForMajor) {
+                result[v.major] = v
+            }
+        }
+        result
+    }
+
+    /**
+     * Computes which {@code "X.Y"} minor lines should each get their own card
+     * on the home page hero, the downloads page, and the documentation page.
+     *
+     * <p>Rule (chosen so the slot count adapts to the release cadence rather
+     * than a static config):
+     * <ul>
+     *   <li>Always include every minor line of the highest major that has at
+     *       least one stable release. So today, with the highest stable major
+     *       being 7 and minor lines 7.0 and 7.1 both published, the result is
+     *       {@code ["7.1", "7.0"]} - two slots.</li>
+     *   <li>If that highest major has fewer than two minor lines (e.g. just
+     *       after 8.0.0 ships), also include every minor line of the previous
+     *       major so we don't drop from two cards to one. Once the new major
+     *       grows a second minor line of its own (8.1.0), the previous major's
+     *       lines roll off automatically.</li>
+     * </ul>
+     *
+     * <p>Returns lines in descending version order
+     * ({@code ["8.0", "7.1", "7.0"]} after 8.0.0 ships).
+     */
+    static List<String> activeMinorLines(File releases) {
+        Map<String, ReleaseVersion> latestPerLine = latestStablePerMinorLine(releases)
+        if (latestPerLine.isEmpty()) {
+            return Collections.<String> emptyList()
+        }
+        int highestMajor = latestPerLine.values()*.major.max() as int
+        List<String> latestMajorLines = latestPerLine.keySet()
+                .findAll { it.startsWith("${highestMajor}.") }
+                .toSorted { a, b -> b <=> a }
+        if (latestMajorLines.size() >= 2) {
+            return latestMajorLines
+        }
+        // Highest major has 0 or 1 minor line; pull in the previous major so
+        // the page doesn't shrink to a single card right after a new major
+        // ships its first minor.
+        List<Integer> majorsWithStables = latestPerLine.values()*.major.unique().toSorted().reverse()
+        if (majorsWithStables.size() < 2) {
+            return latestMajorLines
+        }
+        int previousMajor = majorsWithStables[1]
+        List<String> previousMajorLines = latestPerLine.keySet()
+                .findAll { it.startsWith("${previousMajor}.") }
+                .toSorted { a, b -> b <=> a }
+        return latestMajorLines + previousMajorLines
+    }
 }
diff --git a/buildSrc/src/test/groovy/website/model/documentation/SiteMapSpec.groovy b/buildSrc/src/test/groovy/website/model/documentation/SiteMapSpec.groovy
new file mode 100644
index 00000000000..8e396860c05
--- /dev/null
+++ b/buildSrc/src/test/groovy/website/model/documentation/SiteMapSpec.groovy
@@ -0,0 +1,288 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package website.model.documentation
+
+import spock.lang.Specification
+import spock.lang.TempDir
+
+import java.nio.file.Path
+
+/**
+ * Verifies the multi-major helpers introduced for the multi-version downloads
+ * /documentation page redesign: latestStablePerMinorLine, latestPreReleasePerMajor,
+ * highestStablePerMajor, and activeMinorLines.
+ */
+class SiteMapSpec extends Specification {
+
+    @TempDir
+    Path tempDir
+
+    private File releasesFile(String yaml) {
+        File f = tempDir.resolve('releases.yml').toFile()
+        f.text = yaml
+        f
+    }
+
+    void 'latestStablePerMinorLine groups by major.minor and picks highest patch per line'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.0
+  - version: 7.0.5
+  - version: 7.0.10
+  - version: 7.1.0
+'''.stripIndent())
+
+        when:
+            Map<String, ReleaseVersion> result = SiteMap.latestStablePerMinorLine(releases)
+
+        then:
+            result.keySet() as List == ['7.1', '7.0']
+            result['7.0'].versionText == '7.0.10'
+            result['7.1'].versionText == '7.1.0'
+    }
+
+    void 'latestStablePerMinorLine excludes pre-releases'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.0
+  - version: 7.1.0-RC1
+  - version: 7.1.0
+'''.stripIndent())
+
+        when:
+            Map<String, ReleaseVersion> result = SiteMap.latestStablePerMinorLine(releases)
+
+        then:
+            result['7.1'].versionText == '7.1.0'
+            !result.values()*.versionText.any { it.contains('RC') }
+    }
+
+    void 'highestStablePerMajor returns one entry per major'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 6.2.3
+  - version: 7.0.0
+  - version: 7.0.10
+  - version: 7.1.0
+'''.stripIndent())
+
+        when:
+            Map<Integer, ReleaseVersion> result = SiteMap.highestStablePerMajor(releases)
+
+        then:
+            result.keySet() as List == [7, 6]
+            result[7].versionText == '7.1.0'
+            result[6].versionText == '6.2.3'
+    }
+
+    void 'latestPreReleasePerMajor includes a pre-release that has not been superseded by stable'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.0
+  - version: 7.0.10
+  - version: 7.1.0-RC1
+'''.stripIndent())
+
+        when:
+            Map<Integer, ReleaseVersion> result = SiteMap.latestPreReleasePerMajor(releases)
+
+        then:
+            result.containsKey(7)
+            result[7].versionText == '7.1.0-RC1'
+    }
+
+    void 'latestPreReleasePerMajor suppresses a pre-release once stable has shipped at or above it'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.1.0-RC1
+  - version: 7.1.0
+'''.stripIndent())
+
+        when:
+            Map<Integer, ReleaseVersion> result = SiteMap.latestPreReleasePerMajor(releases)
+
+        then:
+            !result.containsKey(7)
+    }
+
+    void 'latestPreReleasePerMajor exposes a brand new majors first M/RC even when no stable exists for that major'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.1.0
+  - version: 8.0.0-M1
+'''.stripIndent())
+
+        when:
+            Map<Integer, ReleaseVersion> result = SiteMap.latestPreReleasePerMajor(releases)
+
+        then:
+            result.containsKey(8)
+            result[8].versionText == '8.0.0-M1'
+    }
+
+    void 'activeMinorLines today (highest major has 2 minor lines) returns just those lines'() {
+
+        given: 'today: 7.0.x line plus 7.1.x line shipped, no 8.x stable yet'
+            File releases = releasesFile('''
+coreReleases:
+  - version: 6.2.3
+  - version: 7.0.0
+  - version: 7.0.10
+  - version: 7.1.0
+'''.stripIndent())
+
+        when:
+            List<String> lines = SiteMap.activeMinorLines(releases)
+
+        then:
+            lines == ['7.1', '7.0']
+    }
+
+    void 'activeMinorLines after 8.0.0 ships pulls in the previous majors lines so we keep at least two cards'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.10
+  - version: 7.1.0
+  - version: 8.0.0
+'''.stripIndent())
+
+        when:
+            List<String> lines = SiteMap.activeMinorLines(releases)
+
+        then:
+            lines == ['8.0', '7.1', '7.0']
+    }
+
+    void 'activeMinorLines drops the previous majors lines once the new major grows a second minor'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.10
+  - version: 7.1.0
+  - version: 8.0.0
+  - version: 8.1.0
+'''.stripIndent())
+
+        when:
+            List<String> lines = SiteMap.activeMinorLines(releases)
+
+        then:
+            lines == ['8.1', '8.0']
+    }
+
+    void 'activeMinorLines returns an empty list when no stable releases exist'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.0-M1
+  - version: 7.0.0-RC1
+'''.stripIndent())
+
+        when:
+            List<String> lines = SiteMap.activeMinorLines(releases)
+
+        then:
+            lines == []
+    }
+
+    void 'companionArtifactsFor returns an empty list for a major with no entry'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.0
+companionArtifacts:
+  '7':
+    - artifactId: grails-redis
+      version: '5.0.1'
+      mirrorDirectory: redis
+      releaseNotesRepo: apache/grails-redis
+      displayName: Grails Redis Plugin
+'''.stripIndent())
+
+        expect:
+            SiteMap.companionArtifactsFor(releases, 8) == []
+            SiteMap.companionArtifactsFor(releases, 6) == []
+    }
+
+    void 'companionArtifactsFor returns the parsed CompanionArtifact list for a populated major'() {
+
+        given:
+            File releases = releasesFile('''
+coreReleases:
+  - version: 7.0.0
+companionArtifacts:
+  '7':
+    - artifactId: grails-redis
+      version: '5.0.1'
+      mirrorDirectory: redis
+      releaseNotesRepo: apache/grails-redis
+      displayName: Grails Redis Plugin
+    - artifactId: grails-quartz
+      version: '4.0.1'
+      mirrorDirectory: quartz
+      releaseNotesRepo: apache/grails-quartz
+      displayName: Grails Quartz Plugin
+'''.stripIndent())
+
+        when:
+            List<CompanionArtifact> result = SiteMap.companionArtifactsFor(releases, 7)
+
+        then:
+            result.size() == 2
+            result[0].artifactId == 'grails-redis'
+            result[0].version == '5.0.1'
+            result[0].mirrorDirectory == 'redis'
+            result[0].releaseNotesRepo == 'apache/grails-redis'
+            result[0].displayName == 'Grails Redis Plugin'
+            result[1].artifactId == 'grails-quartz'
+    }
+
+    void 'versions accepts the legacy releases: key during the migration window'() {
+
+        given:
+            File releases = releasesFile('''
+releases:
+  - version: 7.0.0
+  - version: 7.1.0
+'''.stripIndent())
+
+        when:
+            List<ReleaseVersion> result = SiteMap.versions(releases)
+
+        then:
+            result*.versionText == ['7.0.0', '7.1.0']
+    }
+}