Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build-logic/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
9 changes: 9 additions & 0 deletions build-logic/src/main/groovy/config.contributors.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

def fetchContributors = tasks.register("fetchContributors", contributor.ContributorTask) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original reason I didn't add a fetch in core, and the other areas is b/c it's not accurate without a malimap. I think for plugins this is probably ok though.

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 })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flatmap is non-lazy so you're invoking the task here. We typically avoid this as it's going to happen as part of the configuration workflow. Some possible workarounds:

  1. have a task dependency on the output of the fetchContributors as an input to wherever you need it (or a provider even)
  2. have the fetch contributors write to a file so the task is cacheable and then read from that file, you can then tweak the task for when it fetches

50 changes: 40 additions & 10 deletions build-logic/src/main/groovy/config.docs.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be a better design to have a github action that just generates a contributor.yml and merges it into a branch, then it can be updated either manually or externally via github

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we should externalize the contributor.yml and make it a part of the project, how about moving all the meta-data to a project.yml and have it contain stuff like

projectTitle
projectDescription
projectOrganization
githubOrg
githubProject
contributors
historicVersions

Then it does not have to be in two separate files (gradle.properties and contributors.yml) and it can be easily read and updated by a github action?

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)(-.*)?/ }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're parsing semversioning when there are libraries to do this. There are actually multiple classes in grails-core with this, we really should centralize this logic and publish it

.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 -> "<option value=\"${v}\">${v}</option>" }.join('\n ')
: '<option value="" disabled>No previous versions available</option>'
? versions.collect { v -> "<option value=\"${v}\">${v}</option>" }.join('\n ')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just clean up the documentation tasks in grails-core and publish them. The main blocker here is we need to pull our themeing templates out from the project and host them somewhere (we talked about doing it on the grails website itself)

: '<option value="" disabled>No previous versions available</option>'

def tokens = [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gradle supports template expansion:

  1. it can do it as part of the processResources for templates being published
  2. you can do it as part of the copy task itself:

tasks.register('generateConfig', Copy) {
from 'templates/config.tpl'
into "$buildDir/generated"
rename { 'config.conf' }
expand([
version: project.version,
env: 'prod'
])
}

'@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
}
}
90 changes: 90 additions & 0 deletions build-logic/src/main/groovy/contributor/ContributorTask.groovy
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. the task should be cacheable
  2. this seems like something we should publish instead of having it in every project


@Input
abstract Property<String> getRepoOwner()

@Input
abstract Property<String> getRepoName()

@Input
@Optional
abstract Property<String> getGithubToken()

@OutputFile
abstract RegularFileProperty getYamlOutput()

@Internal
Provider<Map<String, String>> getContributors() {
yamlOutput.map { regularFile ->
def yamlFile = regularFile.asFile
if (!yamlFile.exists()) return Collections.<String, String>emptyMap()
def yaml = new YamlSlurper().parse(yamlFile) as Map<String, Serializable>
(yaml.contributors ?: [:]) as Map<String, String>
}
}

@TaskAction
void generate() {
def token = githubToken.orNull
def outputFile = yamlOutput.get().asFile
outputFile.parentFile.mkdirs()

if (!token) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're assuming Github token access to have a reproducible build. We typically don't do this as part of the grails builds.

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<Map<String, Serializable>>

def sortedNodes = contributors*.node_id
def loginMap = contributors.collectEntries { [it.node_id, it.login] } as Map<String, String>

// 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<String, String>

// 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<String, String>

YamlBuilder yaml = new YamlBuilder()
yaml contributors: finalMap
outputFile.text = yaml.toString()
}
}
3 changes: 1 addition & 2 deletions docs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
23 changes: 12 additions & 11 deletions docs/src/docs/index.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Export - Grails Plugin</title>
<title>@PROJECT_TITLE@</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
Expand Down Expand Up @@ -287,15 +287,15 @@
</head>
<body>
<header class="hero">
<a href="https://github.com/gpc/grails-export" class="github-link">
<a href="@GITHUB_REPO_URL@" class="github-link">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on GitHub
</a>
<div class="hero-content">
<h1>Export</h1>
<p class="subtitle">A Grails plugin that adds export functionality for CSV, Excel, ODS, PDF, RTF, and XML formats</p>
<h1>@PROJECT_TITLE@</h1>
<p class="subtitle">@PROJECT_DESCRIPTION@</p>
</div>
</header>

Expand All @@ -305,15 +305,16 @@
<div class="docs-grid">
<div class="doc-card">
<span class="version-badge latest">Latest Release</span>
<span class="version-badge latest">@LATEST_VERSION@</span>
<h3>Stable Version</h3>
<div class="links">
<a href="/grails-export/latest/" class="link">
<a href="/@REPO_SLUG@/latest/" class="link">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
User Guide
</a>
<a href="/grails-export/latest/gapi/" class="link">
<a href="/@REPO_SLUG@/latest/gapi/" class="link">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
Expand All @@ -326,13 +327,13 @@
<span class="version-badge snapshot">Snapshot</span>
<h3>Development Version</h3>
<div class="links">
<a href="/grails-export/snapshot/" class="link">
<a href="/@REPO_SLUG@/snapshot/" class="link">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
User Guide
</a>
<a href="/grails-export/snapshot/gapi/" class="link">
<a href="/@REPO_SLUG@/snapshot/gapi/" class="link">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
Expand Down Expand Up @@ -375,15 +376,15 @@
function update() {
var v = sel.value;
if (!v) return;
guide.href = '/grails-export/' + v + '/';
api.href = '/grails-export/' + v + '/gapi/';
guide.href = '/@REPO_SLUG@/' + v + '/';
api.href = '/@REPO_SLUG@/' + v + '/gapi/';
}
sel.addEventListener('change', update);
})();
</script>

<footer class="footer">
<p>Built by the <a href="https://github.com/gpc">Grails Plugin Collective (GPC)</a> team</p>
<p>Built by the <a href="@GITHUB_ORG_URL@">@PROJECT_ORG@</a> team</p>
</footer>
</body>
</html>
7 changes: 7 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
projectVersion=7.0.1-SNAPSHOT
grailsVersion=7.0.10
# GitHub repository metadata
githubOrg=gpc
githubProject=grails-export
# Project metadata
projectTitle=Grails Export Plugin
projectDescription=A Grails plugin that adds export functionality for CSV, Excel, ODS, PDF, RTF, and XML formats.
projectOrg=Grails Plugin Collective (GPC)
# Comma-separated list of projects to publish
projectsToPublish=grails-export
# Build dependencies
Expand Down
8 changes: 4 additions & 4 deletions gradle/publish.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ if (bintrayProperties.exists()) {
user = project.bintrayUsername
key = project.bintrayApiKey

githubSlug = 'gpc/grails-export'
githubSlug = "${project.findProperty('githubOrg')}/${project.findProperty('githubProject')}"

license {
name = 'Apache-2.0'
}

title = "Export"
title = "Grails Export Plugin"
desc = "This plugin offers export functionality supporting different formats e.g. CSV, Excel, Open Document Spreadsheet, PDF and XML and can be extended to add additional formats."

developers = [ graemerocher: 'Graeme Rocher',
Expand Down Expand Up @@ -50,7 +50,7 @@ if (githubProperties.exists()) {
}

githubPages {
repoUri = 'https://github.com/gpc/grails-export.git'
repoUri = "https://github.com/${project.findProperty('githubOrg')}/${project.findProperty('githubProject')}.git"

credentials {
username = project.hasProperty('githubApiKey') ? project.githubApiKey : ''
Expand All @@ -64,4 +64,4 @@ if (githubProperties.exists()) {

task publishDocs(dependsOn: [docs, publishGhPages]) << {
}
}
}
45 changes: 24 additions & 21 deletions plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import org.gradle.api.publish.maven.tasks.GenerateMavenPom

plugins {
id 'config.compile'
id 'config.grails-plugin'
id 'config.publish'
id 'config.testing'
id 'config.contributors'
}

version = projectVersion
Expand Down Expand Up @@ -37,28 +40,28 @@ dependencies {

extensions.configure(org.apache.grails.gradle.publish.GrailsPublishExtension) {
it.artifactId = project.name
it.githubSlug = 'gpc/grails-export'
it.githubSlug = "${githubOrg}/${githubProject}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you even need to define some of these if you define the properties by default. the publish plugin by default looks for properties

it.license.name = 'Apache-2.0'
it.title = 'Grails Export Plugin'
it.desc = 'This plugin offers export functionality supporting different formats e.g. CSV, Excel, Open Document Spreadsheet, PDF and XML and can be extended to add additional formats.'
it.title = "${projectTitle}"
it.desc = "${projectDescription}"
it.organization {
it.name = 'Grails Plugin Collective (GPC)'
it.url = 'https://github.com/gpc'
it.name = "${projectOrg}"
it.url = "https://github.com/${githubOrg}"
}
}

pluginManager.withPlugin('maven-publish') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tasks.withType(GenerateMavenPom).configureEach { pomTask ->
dependsOn(tasks.named('fetchContributors'))
doFirst {
pomTask.pom.developers { spec ->
contributorsData.get().each { String login, String displayName ->
spec.developer {
id = login
name = displayName
}
}
}
}
}
it.developers = [
graemerocher:'Graeme Rocher',
puneetbehl: 'Puneet Behl',
nwwells: 'Nathan Wells',
tulu: 'Ruben',
arturoojeda: 'Arturo Ojeda López',
fabiooshiro: 'Fabio Issamu Oshiro',
ddelponte: 'Dean Del Ponte',
cristallo: 'Cristiano Limiti',
mirweb: 'Mirko Weber',
joasgarcia: 'Joás Garcia',
frangarcia: 'Fran García',
dustindclark: 'Dustin Clark',
miq: 'Mihael Koep',
sbglasius: 'Søren Berg Glasius'
]
}
Loading