From 68c605c08cc89c15588a8b5b68cdfb56b02a37aa Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 20 Oct 2025 09:38:33 +0200 Subject: [PATCH] build: inline guide release-dropdown task from core Grails Gradle Plugins `7.0.0` stopped publishing these classes, inline them to retain the release dropdown functionality. --- buildSrc/build.gradle | 1 - .../dropdown/CreateReleaseDropDownTask.groovy | 240 ++++++++++++++++++ .../grails/doc/dropdown/Snapshot.groovy | 82 ++++++ .../doc/dropdown/SoftwareVersion.groovy | 101 ++++++++ .../grails/doc/git/FetchTagsTask.groovy | 78 ++++++ plugin-core/docs/build.gradle | 3 - 6 files changed, 501 insertions(+), 4 deletions(-) create mode 100644 buildSrc/src/main/groovy/grails/doc/dropdown/CreateReleaseDropDownTask.groovy create mode 100644 buildSrc/src/main/groovy/grails/doc/dropdown/Snapshot.groovy create mode 100644 buildSrc/src/main/groovy/grails/doc/dropdown/SoftwareVersion.groovy create mode 100644 buildSrc/src/main/groovy/grails/doc/git/FetchTagsTask.groovy diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 77ca98ecc..619c8ae20 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -57,5 +57,4 @@ dependencies { implementation 'org.apache.grails:grails-gradle-plugins' implementation "org.nosphere.apache.rat:org.nosphere.apache.rat.gradle.plugin:${versions.get('ratVersion')}" implementation "org.gradle.crypto.checksum:org.gradle.crypto.checksum.gradle.plugin:${versions.get('gradleCryptoChecksumVersion')}" - implementation 'org.apache.grails:grails-docs-core' } diff --git a/buildSrc/src/main/groovy/grails/doc/dropdown/CreateReleaseDropDownTask.groovy b/buildSrc/src/main/groovy/grails/doc/dropdown/CreateReleaseDropDownTask.groovy new file mode 100644 index 000000000..d45a14255 --- /dev/null +++ b/buildSrc/src/main/groovy/grails/doc/dropdown/CreateReleaseDropDownTask.groovy @@ -0,0 +1,240 @@ +/* + * 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 grails.doc.dropdown + +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.StandardCopyOption +import java.nio.file.attribute.BasicFileAttributes + +import javax.inject.Inject + +import groovy.transform.CompileStatic + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Duplicates the documentation and modifies source files to add a release dropdown to the documentation + * @since 6.2.1 + */ +@CompileStatic +@CacheableTask +abstract class CreateReleaseDropDownTask extends DefaultTask { + + @Input + final Property docBaseUrl + + @Input + final Property githubSlug + + @InputDirectory + @PathSensitive(PathSensitivity.RELATIVE) + final DirectoryProperty sourceDocsDirectory + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + final RegularFileProperty gitTags + + @Input + final Property projectVersion + + @Input + final Property minimumVersion + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + final ConfigurableFileCollection filesToAddDropdowns + + @OutputDirectory + final DirectoryProperty modifiedPagesDirectory + + @Input + final Property versionHtml + + @Input + final Property additionalPath + + @Inject + CreateReleaseDropDownTask(ObjectFactory objects, Project project) { + group = 'documentation' + githubSlug = objects.property(String).convention(project.provider { + project.findProperty('githubSlug') as String ?: 'apache/grails-core' + }) + sourceDocsDirectory = objects.directoryProperty().convention(project.layout.buildDirectory.dir('manual')) + projectVersion = objects.property(String).convention(project.provider { project.version as String }) + filesToAddDropdowns = objects.fileCollection() + modifiedPagesDirectory = objects.directoryProperty().convention(project.layout.buildDirectory.dir('modified-pages')) + gitTags = objects.fileProperty().convention(project.layout.buildDirectory.file('git-tags.txt')) + minimumVersion = objects.property(SoftwareVersion).convention(new SoftwareVersion(major: 5)) + docBaseUrl = objects.property(String).convention('https://grails.apache.org/docs') + versionHtml = objects.property(String).convention(project.provider { '

Version: ' + projectVersion.get() + '

' }) + additionalPath = objects.property(String).convention('') + } + + /** + * Add the release dropdown to the documentation*/ + @TaskAction + void createReleaseDropDown() { + Path targetOutputDirectory = modifiedPagesDirectory.get().asFile.toPath() + if (Files.exists(targetOutputDirectory)) { + targetOutputDirectory.deleteDir() + } + Files.createDirectories(targetOutputDirectory) + + String projectVersion = projectVersion.get() + + final List result = gitTags.get().asFile.readLines()*.trim() + List softwareVersions = parseSoftwareVersions(projectVersion, result) + logger.lifecycle("Detected Project Version: ${projectVersion} and Software Versions: ${softwareVersions*.versionText.join(',')}") + + Path guideDirectory = sourceDocsDirectory.get().asFile.toPath() + + Map filesToAddDropdown = filesToAddDropdowns.collectEntries { [it.absolutePath, it.toPath()] } + Files.walkFileTree(guideDirectory, new SimpleFileVisitor() { + + @Override + FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Path targetDir = targetOutputDirectory.resolve(guideDirectory.relativize(dir)) + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir) + } + FileVisitResult.CONTINUE + } + + @Override + FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path targetFile = targetOutputDirectory.resolve(guideDirectory.relativize(file)) + String absolutePath = targetFile.toAbsolutePath().toString() + if (filesToAddDropdown.containsKey(absolutePath)) { + String pageRelativePath = guideDirectory.toFile().relativePath(file.toFile()) + String selectHtml = select(options(projectVersion, pageRelativePath, softwareVersions)) + + final String versionWithSelectHtml = "

Version: ${selectHtml}

" + targetFile.toFile().text = file.text.replace(versionHtml.get(), versionWithSelectHtml) + + filesToAddDropdown.remove(absolutePath) + } else { + Files.copy(file, targetFile, StandardCopyOption.REPLACE_EXISTING) + } + FileVisitResult.CONTINUE + } + + @Override + FileVisitResult visitFileFailed(Path file, IOException e) throws IOException { + throw new GradleException("Unable to copy file: ${file}", e) + } + }) + } + + /** + * Generate the options for the select tag. + * + * @param version The current version of the documentation + * @param pageRelativePath The relative path for the page to add the dropdown to + * @param softwareVersions The list of software versions to include in the dropdown + * @return The list of options for the select tag + */ + private List options(String version, String pageRelativePath, List softwareVersions) { + List options = [] + + final String snapshotHref = docBaseUrl.get() + '/snapshot/' + additionalPath.get() + pageRelativePath + options << option(snapshotHref, 'SNAPSHOT', version.endsWith('-SNAPSHOT')) + + softwareVersions + .forEach { softwareVersion -> + final String versionName = softwareVersion?.versionText + final String href = docBaseUrl.get() + '/' + (versionName?.endsWith('-SNAPSHOT') ? 'snapshot' : versionName) + '/' + additionalPath.get() + pageRelativePath + options << option(href, versionName, version == versionName) + } + + options + } + + /** + * Generate the select tag + * + * @param options The List of options tags for the select tag + * @return The select tag with the options + */ + private String select(List options) { + String selectHtml = /' + selectHtml + } + + /** + * Generate the option tag + * + * @param value The URL to navigate to + * @param text The version to display + * @param selected Whether the option is selected + * + * @return The option tag + */ + private String option(String value, String text, boolean selected = false) { + if (selected) { + return "" + } else { + return "" + } + } + + /** + * Parse the software versions from the resultant JSON + * + * @param result List of all tags in the repository. + * @param minimumVersion Minimum SoftwareVersion to include in the list. Default version is 0.0.0 + * @return The list of software versions + */ + private List parseSoftwareVersions(String projectVersion, List tags) { + def minimum = minimumVersion.get() + + LinkedHashSet combined = ["v${projectVersion}" as String] + combined.addAll(tags) + + combined.findAll { it ==~ /^v\d.*/ } + .collect { it.replace('v', '') } + .collect { SoftwareVersion.build(it) } + .findAll { it >= minimum } + .toSorted() + .unique() + .reverse() + } +} diff --git a/buildSrc/src/main/groovy/grails/doc/dropdown/Snapshot.groovy b/buildSrc/src/main/groovy/grails/doc/dropdown/Snapshot.groovy new file mode 100644 index 000000000..336fdbaed --- /dev/null +++ b/buildSrc/src/main/groovy/grails/doc/dropdown/Snapshot.groovy @@ -0,0 +1,82 @@ +/* + * 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 grails.doc.dropdown + +class Snapshot implements Comparable, Serializable { + + private static final long serialVersionUID = 1L + + private String text + + int getMilestoneVersion() { + text.replace('M', '').toInteger() + } + + int getReleaseCandidateVersion() { + text.replace('RC', '').toInteger() + } + + boolean isBuildSnapshot() { + text.endsWith('BUILD-SNAPSHOT') + } + + boolean isReleaseCandidate() { + text.startsWith('RC') + } + + boolean isMilestone() { + text.startsWith('M') + } + + Snapshot(String text) { + this.text = text + } + + @Override + int compareTo(Snapshot o) { + + if (this.buildSnapshot && !o.buildSnapshot) { + return 1 + } else if (!this.buildSnapshot && o.buildSnapshot) { + return -1 + } else if (this.buildSnapshot && o.buildSnapshot) { + return 0 + } + + if (this.releaseCandidate && !o.releaseCandidate) { + return 1 + } else if (!this.releaseCandidate && o.releaseCandidate) { + return -1 + } else if (this.releaseCandidate && o.releaseCandidate) { + return this.releaseCandidateVersion <=> o.releaseCandidateVersion + } + + if (this.milestone && !o.milestone) { + return 1 + } else if (!this.milestone && o.milestone) { + return -1 + } else if (this.milestone && o.milestone) { + return this.milestoneVersion <=> o.milestoneVersion + } + + return 0 + } +} + diff --git a/buildSrc/src/main/groovy/grails/doc/dropdown/SoftwareVersion.groovy b/buildSrc/src/main/groovy/grails/doc/dropdown/SoftwareVersion.groovy new file mode 100644 index 000000000..852969c97 --- /dev/null +++ b/buildSrc/src/main/groovy/grails/doc/dropdown/SoftwareVersion.groovy @@ -0,0 +1,101 @@ +/* + * 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 grails.doc.dropdown + +class SoftwareVersion implements Comparable, Serializable { + + private static final long serialVersionUID = 1L + + int major + int minor + int patch + + Snapshot snapshot + + String versionText + + static SoftwareVersion build(String version) { + String[] parts = version.split('\\.') + SoftwareVersion softVersion + if (parts.length >= 3) { + softVersion = new SoftwareVersion() + softVersion.versionText = version + softVersion.major = parts[0].toInteger() + softVersion.minor = parts[1].toInteger() + if (parts.length > 3) { + softVersion.snapshot = new Snapshot(parts[3]) + } else if (parts[2].contains('-')) { + String[] subparts = parts[2].split('-') + softVersion.patch = subparts.first() as int + softVersion.snapshot = new Snapshot(subparts[1..-1].join('-')) + return softVersion + } + + // Filter out invalid patches (e.g. 1.0.RC4) + if (parts[2].isInteger()) { + softVersion.patch = parts[2].toInteger() + } + } + softVersion + } + + boolean isSnapshot() { + snapshot != null + } + + @Override + int compareTo(SoftwareVersion o) { + int majorCompare = this.major <=> o.major + if (majorCompare != 0) { + return majorCompare + } + + int minorCompare = this.minor <=> o.minor + if (minorCompare != 0) { + return minorCompare + } + + int patchCompare = this.patch <=> o.patch + if (patchCompare != 0) { + return patchCompare + } + + if (this.isSnapshot() && !o.isSnapshot()) { + return -1 + } else if (!this.isSnapshot() && o.isSnapshot()) { + return 1 + } else if (this.isSnapshot() && o.isSnapshot()) { + return this.getSnapshot() <=> o.getSnapshot() + } else { + return 0 + } + } + + @Override + String toString() { + return 'SoftwareVersion{' + + 'major=' + major + + ', minor=' + minor + + ', patch=' + patch + + ', snapshot=' + snapshot + + /, versionText='/ + versionText + /'/ + + '}' + } +} diff --git a/buildSrc/src/main/groovy/grails/doc/git/FetchTagsTask.groovy b/buildSrc/src/main/groovy/grails/doc/git/FetchTagsTask.groovy new file mode 100644 index 000000000..1208dbf3e --- /dev/null +++ b/buildSrc/src/main/groovy/grails/doc/git/FetchTagsTask.groovy @@ -0,0 +1,78 @@ +/* + * 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 grails.doc.git + +import java.nio.charset.StandardCharsets +import java.time.LocalDate +import java.time.LocalDateTime + +import javax.inject.Inject + +import groovy.transform.CompileStatic + +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Exec +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile + +@CompileStatic +@CacheableTask +abstract class FetchTagsTask extends Exec { + + @Input + final Property cacheDate // allows for forcing refreshing the tags, defaults to once a day + + @Input + final Property defaultTag // if no git repo present + + @OutputFile + final RegularFileProperty tagsFile + + @Inject + FetchTagsTask(ObjectFactory objectFactory, Project project) { + group = 'documentation' + cacheDate = objectFactory.property(LocalDateTime).convention(LocalDate.now().atStartOfDay()) + tagsFile = objectFactory.fileProperty().convention(project.layout.buildDirectory.file('git-tags.txt')) + defaultTag = objectFactory.property(String).convention(project.provider { "v${project.version as String}" as String }) + + commandLine('git', 'tag', '-l', '--sort=-creatordate') + ignoreExitValue = !project.rootProject.layout.projectDirectory.dir('.git').asFile.exists() + + def output = new ByteArrayOutputStream() + standardOutput = output + + doLast { + File file = tagsFile.get().asFile + if (!file.parentFile.exists()) { + file.mkdirs() + } + + if (ignoreExitValue) { + logger.lifecycle('not a git repo, so assuming a default tag of {}', defaultTag.get()) + } + + file.text = ignoreExitValue ? defaultTag.get() : new String(output.toByteArray(), StandardCharsets.UTF_8).trim() + } + } +} diff --git a/plugin-core/docs/build.gradle b/plugin-core/docs/build.gradle index 85524df97..a2af3fef4 100644 --- a/plugin-core/docs/build.gradle +++ b/plugin-core/docs/build.gradle @@ -17,9 +17,6 @@ * under the License. */ -import grails.doc.dropdown.CreateReleaseDropDownTask -import grails.doc.dropdown.SoftwareVersion - plugins { id 'groovy' // For groovydoc task }