diff --git a/buildSrc/src/main/groovy/website/model/documentation/DownloadPage.groovy b/buildSrc/src/main/groovy/website/model/documentation/DownloadPage.groovy index aca360238ca..f38f67aeb63 100644 --- a/buildSrc/src/main/groovy/website/model/documentation/DownloadPage.groovy +++ b/buildSrc/src/main/groovy/website/model/documentation/DownloadPage.groovy @@ -164,6 +164,61 @@ class DownloadPage { } } + /** + * Renders a "plugins-only" card for a Grails major version that has + * Apache-released companion plugins but no matching {@code coreReleases:} + * entry yet. The card mirrors the visual treatment of {@link #renderDownload} + * but omits the core source / binary / wrapper / release-notes block - only + * the companion plugins are listed, each with their Source / SHA512 / ASC + * verification links and a release-notes link. + * + *

Returns the empty string for an empty {@code companions} list so that + * callers can safely render unconditionally. + * + * @param major the Grails major version this card represents (e.g. 8) + * @param companions companion plugins published for this upcoming major + */ + @CompileDynamic + static String renderCompanionsOnlyCard(int major, List companions) { + if (!companions) { + return '' + } + renderHtml { + div(class: 'guide-group') { + div(class: 'guide-group-header') { + img(src: '[%url]/images/download.svg', alt: "Apache Grails ${major} plugins") + h2("Apache Grails ${major} Plugins") + } + ul { + companions.each { CompanionArtifact c -> + li { + a( + href: sourceUrl(c.version, c.artifactId, '', c.mirrorDirectory), + "${c.displayName} ${c.version} Source" + ) + a( + href: sourceVerificationUrl(c.version, c.artifactId, '.sha512', c.mirrorDirectory), + 'SHA512' + ) + a( + href: sourceVerificationUrl(c.version, c.artifactId, '.asc', c.mirrorDirectory), + 'ASC' + ) + } + } + companions.each { CompanionArtifact c -> + li { + a( + href: "https://github.com/${c.releaseNotesRepo}/releases/tag/v${c.version}", + "${c.displayName} ${c.version} Release Notes" + ) + } + } + } + } + } + } + /** * @return {@code true} if this version line is distributed through the * Apache mirrors ({@code major >= 7}). Older versions fell back to @@ -187,14 +242,18 @@ class DownloadPage { * grid (one card per active minor line, e.g. 7.0 and 7.1 today, plus * 8.0 once that ships), a "Pre-release" grid below it for any * Apache-released milestones / RCs that haven't been superseded by - * stable, the older-versions dropdown, and a "Get Started" two-column - * footer with Application Forge + SDKMAN install instructions. + * stable, an "Upcoming Plugins" grid for Apache-released companion + * plugins whose target Grails major hasn't shipped yet (e.g. a Grails 8 + * plugin published before any Grails 8 core release), the older-versions + * dropdown, and a "Get Started" two-column footer with Application Forge + * + SDKMAN install instructions. */ @CompileDynamic static String mainContent(File releases) { List currentLines = SiteMap.activeMinorLines(releases) Map latestPerLine = SiteMap.latestStablePerMinorLine(releases) Map preReleasesPerMajor = SiteMap.latestPreReleasePerMajor(releases) + List orphanCompanionMajors = SiteMap.orphanCompanionMajors(releases) renderHtml { div(class: 'header-bar chalices-bg') { @@ -255,6 +314,30 @@ class DownloadPage { } } + if (!orphanCompanionMajors.isEmpty()) { + h2( + class: 'release-section-header column-header', + 'Plugins for upcoming Apache Grails releases' + ) + p( + 'Per Apache release policy, the plugins below are official Apache releases ' + + 'published to Maven Central with full source and signature artifacts. They ' + + 'target an upcoming Grails major version that has not yet had a release ' + + 'recorded on this site; once that version ships these plugins will move under ' + + 'its download card automatically.' + ) + div(class: 'release-grid') { + orphanCompanionMajors.each { Integer major -> + mkp.yieldUnescaped( + DownloadPage.renderCompanionsOnlyCard( + major, + SiteMap.companionArtifactsFor(releases, major) + ) + ) + } + } + } + h2(class: 'release-section-header column-header', 'Older Versions') p('You can download previous versions as far back as Grails 0.1.') p( diff --git a/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy b/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy index bbe08a6cf62..89987d25346 100644 --- a/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy +++ b/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy @@ -96,6 +96,44 @@ class SiteMap { } } + /** + * Returns the Grails major versions that have entries under + * {@code companionArtifacts:} but no entry of any kind (stable or + * pre-release) under {@code coreReleases:}. These are companion plugins + * that have shipped ahead of their target Grails core release. + * + *

The downloads page renders a dedicated section for these "orphan" + * companions so they're discoverable until the matching core release + * lands. Once any release for that major is appended to + * {@code coreReleases:} (including a milestone or RC) the major drops out + * of this list automatically and the existing per-major card picks up its + * companions through {@link #companionArtifactsFor}. + * + *

Result is sorted descending so the highest upcoming major is rendered + * first. Companion entries with an empty list are skipped (they would + * produce an empty card). + * + * @param releases the {@code conf/releases.yml} file + * @return descending list of orphan major versions; never null + */ + static List orphanCompanionMajors(File releases) { + assert releases.exists() + Set coreMajors = versions(releases)*.major as Set + def model = releases.newInputStream().withCloseable { + new Yaml().load(it) as Map + } + Map section = (model.companionArtifacts ?: [:]) as Map + List orphans = [] + section.each { key, value -> + Integer major = (key as String).toInteger() + List entries = value as List + if (entries && !coreMajors.contains(major)) { + orphans << major + } + } + orphans.toSorted().reverse() + } + /** * @return the highest stable {@link ReleaseVersion} per major version, * keyed by major. Used as the cross-major reference point for diff --git a/buildSrc/src/test/groovy/website/model/documentation/SiteMapSpec.groovy b/buildSrc/src/test/groovy/website/model/documentation/SiteMapSpec.groovy index 8e396860c05..1906e3fb7eb 100644 --- a/buildSrc/src/test/groovy/website/model/documentation/SiteMapSpec.groovy +++ b/buildSrc/src/test/groovy/website/model/documentation/SiteMapSpec.groovy @@ -270,6 +270,123 @@ companionArtifacts: result[1].artifactId == 'grails-quartz' } + void 'orphanCompanionMajors returns an empty list when no companionArtifacts section exists'() { + + given: + File releases = releasesFile(''' +coreReleases: + - version: 7.0.0 + - version: 7.1.0 +'''.stripIndent()) + + expect: + SiteMap.orphanCompanionMajors(releases) == [] + } + + void 'orphanCompanionMajors returns an empty list when every companion major has a stable core release'() { + + given: + File releases = releasesFile(''' +coreReleases: + - version: 7.0.0 + - version: 7.1.0 +companionArtifacts: + '7': + - artifactId: grails-redis + version: '5.0.1' + mirrorDirectory: redis + releaseNotesRepo: apache/grails-redis + displayName: Grails Redis Plugin +'''.stripIndent()) + + expect: + SiteMap.orphanCompanionMajors(releases) == [] + } + + void 'orphanCompanionMajors returns an empty list when the companion major has only a pre-release in coreReleases'() { + + given: 'a companion major that is also represented by a milestone in coreReleases' + File releases = releasesFile(''' +coreReleases: + - version: 7.1.0 + - version: 8.0.0-M1 +companionArtifacts: + '8': + - artifactId: grails-publish + version: '1.0.0-M1' + mirrorDirectory: grails-publish + releaseNotesRepo: apache/grails-gradle-publish + displayName: Grails Publish Gradle Plugin +'''.stripIndent()) + + expect: 'the companion is not orphan because the existing pre-release card will pick it up' + SiteMap.orphanCompanionMajors(releases) == [] + } + + void 'orphanCompanionMajors returns the major when companions exist but no core release of any kind exists for that major'() { + + given: + File releases = releasesFile(''' +coreReleases: + - version: 7.1.0 +companionArtifacts: + '7': + - artifactId: grails-redis + version: '5.0.1' + mirrorDirectory: redis + releaseNotesRepo: apache/grails-redis + displayName: Grails Redis Plugin + '8': + - artifactId: grails-publish + version: '1.0.0-M1' + mirrorDirectory: grails-publish + releaseNotesRepo: apache/grails-gradle-publish + displayName: Grails Publish Gradle Plugin +'''.stripIndent()) + + expect: + SiteMap.orphanCompanionMajors(releases) == [8] + } + + void 'orphanCompanionMajors returns multiple majors in descending order'() { + + given: + File releases = releasesFile(''' +coreReleases: + - version: 7.1.0 +companionArtifacts: + '8': + - artifactId: grails-publish + version: '1.0.0-M1' + mirrorDirectory: grails-publish + releaseNotesRepo: apache/grails-gradle-publish + displayName: Grails Publish Gradle Plugin + '9': + - artifactId: grails-experimental + version: '0.1.0' + mirrorDirectory: experimental + releaseNotesRepo: apache/grails-experimental + displayName: Grails Experimental Plugin +'''.stripIndent()) + + expect: + SiteMap.orphanCompanionMajors(releases) == [9, 8] + } + + void 'orphanCompanionMajors skips majors whose companion list is empty'() { + + given: + File releases = releasesFile(''' +coreReleases: + - version: 7.1.0 +companionArtifacts: + '8': [] +'''.stripIndent()) + + expect: + SiteMap.orphanCompanionMajors(releases) == [] + } + void 'versions accepts the legacy releases: key during the migration window'() { given: