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: