diff --git a/build-logic/build.gradle b/build-logic/build.gradle index 746607d..d1bf315 100644 --- a/build-logic/build.gradle +++ b/build-logic/build.gradle @@ -20,6 +20,7 @@ allprojects { project -> dependencies { implementation platform("org.apache.grails:grails-bom:${gradleProperties.grailsVersion}") + implementation 'org.apache.groovy:groovy-yaml' implementation 'cloud.wondrify:asset-pipeline-gradle' implementation "com.adarshr:gradle-test-logger-plugin:${gradleProperties.testLoggerVersion}" implementation 'org.apache.grails:grails-gradle-plugins' diff --git a/build-logic/src/main/groovy/config.contributors.gradle b/build-logic/src/main/groovy/config.contributors.gradle new file mode 100644 index 0000000..89a21a0 --- /dev/null +++ b/build-logic/src/main/groovy/config.contributors.gradle @@ -0,0 +1,9 @@ + +def fetchContributors = tasks.register("fetchContributors", contributor.ContributorTask) { + repoOwner.convention(providers.gradleProperty('githubOrg')) + repoName.convention(providers.gradleProperty('githubProject')) + githubToken.convention(providers.environmentVariable('GITHUB_TOKEN')) + yamlOutput.convention(layout.buildDirectory.file("generated/contributors.yml")) +} + +extensions.add("contributorsData", fetchContributors.flatMap { it.contributors }) diff --git a/build-logic/src/main/groovy/config.docs.gradle b/build-logic/src/main/groovy/config.docs.gradle index 7ce6e44..17ab08e 100644 --- a/build-logic/src/main/groovy/config.docs.gradle +++ b/build-logic/src/main/groovy/config.docs.gradle @@ -65,34 +65,64 @@ tasks.register('ghPagesRootIndexPage') { group = 'documentation' def templateFile = docProject.map { it.layout.projectDirectory.file('src/docs/index.tmpl') } - def outputFile = rootProject.layout.buildDirectory.file('docs/ghpages.html') + def outputFile = rootProject.layout.buildDirectory.file('docs/ghpages.html') inputs.file(templateFile) outputs.file(outputFile) doLast { + def githubOrg = rootProject.findProperty('githubOrg') as String + def githubProject = rootProject.findProperty('githubProject') as String + List versions = [] + String latestVersion = "" try { - def conn = URI.create('https://api.github.com/repos/gpc/grails-export/contents/?ref=gh-pages').toURL().openConnection() + def conn = URI.create("https://api.github.com/repos/${githubOrg}/${githubProject}/contents/?ref=gh-pages").toURL().openConnection() conn.setRequestProperty('Accept', 'application/vnd.github+json') conn.setRequestProperty('User-Agent', 'gradle-docs-build') def parsed = new groovy.json.JsonSlurper().parse(conn.inputStream) versions = (parsed as List) - .findAll { it.type == 'dir' } - .collect { it.name as String } - .findAll { it ==~ /\d+\.\d+\.(\d+|x)(-.*)?/ } - .sort() - .reverse() + .findAll { it.type == 'dir' } + .collect { it.name as String } + .findAll { it ==~ /\d+\.\d+\.(\d+|x)(-.*)?/ } + .toSorted { a, b -> + // Parse into [major, minor, patch, qualifier] + // patch 'x' becomes -1 so it sorts below any numeric patch + // qualifier '' (stable) sorts above any pre-release string + def parseVer = { String v -> + def m = v =~ /^(\d+)\.(\d+)\.(x|\d+)(?:-(.+))?$/ + if (!m) return [0, 0, -2, ''] + [m[0][1] as int, m[0][2] as int, m[0][3] == 'x' ? -1 : m[0][3] as int, m[0][4] ?: ''] + } + def av = parseVer(a) + def bv = parseVer(b) + return bv[0] <=> av[0] ?: bv[1] <=> av[1] ?: bv[2] <=> av[2] ?: + (!av[3] && bv[3] ? -1 : av[3] && !bv[3] ? 1 : bv[3] <=> av[3]) // RC and other postfix versions + } + latestVersion = versions ? versions.first() : '' } catch (Exception e) { logger.warn("ghPagesRootIndexPage: could not fetch GitHub versions — ${e.message}") } String optionsHtml = versions - ? versions.collect { v -> "" }.join('\n ') - : '' + ? versions.collect { v -> "" }.join('\n ') + : '' + + def tokens = [ + '@LATEST_VERSION@' : latestVersion, + '@OTHER_VERSIONS_OPTIONS@': optionsHtml, + '@GITHUB_REPO_URL@' : "https://github.com/${githubOrg}/${githubProject}", + '@GITHUB_ORG_URL@' : "https://github.com/${githubOrg}", + '@PROJECT_TITLE@' : "${projectTitle}", + '@PROJECT_DESCRIPTION@' : "${projectDescription}", + '@PROJECT_ORG@' : "${projectOrg}", + '@REPO_SLUG@' : githubProject, + ] def out = outputFile.get().asFile out.parentFile.mkdirs() - out.text = templateFile.get().asFile.text.replace('@OTHER_VERSIONS_OPTIONS@', optionsHtml) + def content = templateFile.get().asFile.text + tokens.each { token, value -> content = content.replace(token, value) } + out.text = content } } diff --git a/build-logic/src/main/groovy/contributor/ContributorTask.groovy b/build-logic/src/main/groovy/contributor/ContributorTask.groovy new file mode 100644 index 0000000..cb2b856 --- /dev/null +++ b/build-logic/src/main/groovy/contributor/ContributorTask.groovy @@ -0,0 +1,90 @@ +package contributor + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import groovy.yaml.YamlBuilder +import groovy.yaml.YamlSlurper + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class ContributorTask extends DefaultTask { + + @Input + abstract Property getRepoOwner() + + @Input + abstract Property getRepoName() + + @Input + @Optional + abstract Property getGithubToken() + + @OutputFile + abstract RegularFileProperty getYamlOutput() + + @Internal + Provider> getContributors() { + yamlOutput.map { regularFile -> + def yamlFile = regularFile.asFile + if (!yamlFile.exists()) return Collections.emptyMap() + def yaml = new YamlSlurper().parse(yamlFile) as Map + (yaml.contributors ?: [:]) as Map + } + } + + @TaskAction + void generate() { + def token = githubToken.orNull + def outputFile = yamlOutput.get().asFile + outputFile.parentFile.mkdirs() + + if (!token) { + logger.warn("No GITHUB_TOKEN — skipping contributor fetch for ${repoOwner.get()}/${repoName.get()}") + YamlBuilder emptyYaml = new YamlBuilder() + emptyYaml contributors: [:] + outputFile.text = emptyYaml.toString() + return + } + + // 1. Fetch from REST (sorted by contributions, bots excluded) + def restUrl = "https://api.github.com/repos/${repoOwner.get()}/${repoName.get()}/contributors" + def contributors = new JsonSlurper().parseText( + restUrl.toURL().getText(requestProperties: [Authorization: "token $token"]) + ).findAll { it.type != 'Bot' } as List> + + def sortedNodes = contributors*.node_id + def loginMap = contributors.collectEntries { [it.node_id, it.login] } as Map + + // 2. Fetch display names via GraphQL + def gqlQuery = 'query($ids: [ID!]!) { nodes(ids: $ids) { ... on User { id name login } } }' + def body = JsonOutput.toJson([query: gqlQuery, variables: [ids: sortedNodes]]) + + def connection = "https://api.github.com/graphql".toURL().openConnection() as HttpURLConnection + connection.with { + doOutput = true + requestMethod = 'POST' + setRequestProperty('Authorization', "bearer $token") + outputStream.withWriter { it << body } + } + + def gqlResponse = new JsonSlurper().parseText(connection.inputStream.text) + def nameLookup = gqlResponse.data.nodes.collectEntries { [it.id, it.name ?: it.login] } as Map + + // 3. Write YAML output (login → display name, preserving contribution order) + def finalMap = sortedNodes + .findAll { loginMap[it] } + .collectEntries { id -> [loginMap[id], nameLookup[id] ?: loginMap[id]] } as Map + + YamlBuilder yaml = new YamlBuilder() + yaml contributors: finalMap + outputFile.text = yaml.toString() + } +} diff --git a/docs/build.gradle b/docs/build.gradle index f077b23..04c53f4 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -17,10 +17,9 @@ def asciidoctorAttributes = [ idprefix : '', idseparator : '-', version : project.version, - projectUrl : 'https://github.com/gpc/grails-export', + projectUrl : "https://github.com/${rootProject.findProperty('githubOrg')}/${rootProject.findProperty('githubProject')}", sourcedir : "${rootProject.allprojects.find { it.name == 'grails-export' }.projectDir}/src/main/groovy", grailsVersion : grailsVersion -// grailsDocBase : "https://grails.apache.org/docs/${resolveGrailsDocsDirName(project.grailsVersion)}" ] tasks.named('asciidoctor', AsciidoctorTask) { diff --git a/docs/src/docs/index.tmpl b/docs/src/docs/index.tmpl index dfa0759..de0efa5 100644 --- a/docs/src/docs/index.tmpl +++ b/docs/src/docs/index.tmpl @@ -3,7 +3,7 @@ - Export - Grails Plugin + @PROJECT_TITLE@ @@ -287,15 +287,15 @@
- + View on GitHub
-

Export

-

A Grails plugin that adds export functionality for CSV, Excel, ODS, PDF, RTF, and XML formats

+

@PROJECT_TITLE@

+

@PROJECT_DESCRIPTION@

@@ -305,15 +305,16 @@
Latest Release + @LATEST_VERSION@

Stable Version