diff --git a/.github/scripts/update-contributors.groovy b/.github/scripts/update-contributors.groovy new file mode 100755 index 0000000..f148d0c --- /dev/null +++ b/.github/scripts/update-contributors.groovy @@ -0,0 +1,73 @@ +#!/usr/bin/env groovy +@Grab('org.apache.groovy:groovy-yaml') +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import groovy.yaml.YamlBuilder +import groovy.yaml.YamlSlurper + +def token = System.getenv('GITHUB_TOKEN') +def repo = System.getenv('GITHUB_REPOSITORY') + +if (!token) { + println "GITHUB_TOKEN not set — skipping contributor update" + System.exit(0) +} + +def requestProps = [ + Authorization : "token ${token}", + 'User-Agent' : 'update-contributors-action', + Accept : 'application/vnd.github+json', +] + +// 1. Fetch all contributors via REST (sorted by contributions desc, bots excluded) +def contributors = [] +def page = 1 +while (true) { + def url = "https://api.github.com/repos/${repo}/contributors?per_page=100&page=${page}" + def batch = new JsonSlurper().parseText(url.toURL().getText(requestProperties: requestProps)) as List + if (!batch) break + contributors.addAll(batch) + page++ +} + +def humans = contributors.findAll { it.type != 'Bot' } +if (!humans) { + println "No human contributors found — skipping update" + System.exit(0) +} + +def nodeIds = humans*.node_id +def loginByNode = humans.collectEntries { [it.node_id, it.login] } + +// 2. Fetch display names via GraphQL +def gqlQuery = 'query($ids:[ID!]!){nodes(ids:$ids){...on User{id name login}}}' +def gqlBody = JsonOutput.toJson([query: gqlQuery, variables: [ids: nodeIds]]) + +def conn = 'https://api.github.com/graphql'.toURL().openConnection() as HttpURLConnection +conn.doOutput = true +conn.requestMethod = 'POST' +conn.setRequestProperty('Authorization', "token ${token}") +conn.setRequestProperty('Content-Type', 'application/json') +conn.setRequestProperty('User-Agent', 'update-contributors-action') +conn.outputStream.withWriter { it << gqlBody } + +def gqlNodes = new JsonSlurper().parseText(conn.inputStream.text).data.nodes +def nameByNode = gqlNodes.findAll().collectEntries { [(it.id): (it.name ?: it.login)] } as Map + +// 3. Build ordered map: login → display name (preserving contribution order) +def ordered = humans.collectEntries { c -> + def login = loginByNode[c.node_id] + [login, nameByNode[c.node_id] ?: login] +} as LinkedHashMap + +// 4. Update the contributors section in project.yml +def projectYmlFile = new File('project.yml') +def projectYml = new YamlSlurper().parse(projectYmlFile) as Map + +projectYml.contributors = ordered + +def yaml = new YamlBuilder() +yaml(projectYml) +projectYmlFile.text = yaml.toString() + +println "Updated ${ordered.size()} contributors in project.yml" diff --git a/.github/scripts/update-versions.groovy b/.github/scripts/update-versions.groovy new file mode 100755 index 0000000..9e528ea --- /dev/null +++ b/.github/scripts/update-versions.groovy @@ -0,0 +1,88 @@ +#!/usr/bin/env groovy +@Grab('org.apache.groovy:groovy-yaml') +import groovy.json.JsonSlurper +import groovy.yaml.YamlBuilder +import groovy.yaml.YamlSlurper + +def token = System.getenv('GITHUB_TOKEN') +def repo = System.getenv('GITHUB_REPOSITORY') + +if (!token) { + println "GITHUB_TOKEN not set — skipping version update" + System.exit(0) +} + +def requestProps = [ + Authorization: "token ${token}", + 'User-Agent' : 'update-versions-action', + Accept : 'application/vnd.github+json', +] + +def projectYmlFile = new File('project.yml') +def projectYml = new YamlSlurper().parse(projectYmlFile) as Map +def ignore = (projectYml.versions?.ignore ?: []) as List + +def entries = new JsonSlurper().parseText( + "https://api.github.com/repos/${repo}/contents/?ref=gh-pages" + .toURL().getText(requestProperties: requestProps) +) as List + +def semver = ~/^\d+\.\d+\.(\d+|x)(-.*)?$/ +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 sorted = entries + .findAll { it.type == 'dir' && (it.name as String) ==~ semver } + .collect { it.name as String } + .findAll { v -> !ignore.contains(v) } + .toSorted { a, b -> + def av = parseVer(a) + def bv = parseVer(b) + 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]) + } + +if (!sorted) { + println "No versioned directories found on gh-pages — skipping update" + System.exit(0) +} + +// ignore is preserved; only current and previous are updated +projectYml.versions.current = sorted.first() +projectYml.versions.previous = sorted + +def yaml = new YamlBuilder() +yaml(projectYml) +projectYmlFile.text = yaml.toString() + +println "Versions updated: current=${projectYml.versions.current}, total=${sorted.size()}" + +// Generate ghpages.html so the workflow can push it directly to gh-pages +def tmplFile = new File('docs/src/docs/index.tmpl') +if (tmplFile.exists()) { + def githubOrg = projectYml.github.org as String + def githubProject = projectYml.github.project as String + def optionsHtml = sorted + .collect { v -> "" } + .join('\n ') + + def tokens = [ + LATEST_VERSION : projectYml.versions.current, + OTHER_VERSIONS_OPTIONS: optionsHtml, + GITHUB_REPO_URL : "https://github.com/${githubOrg}/${githubProject}", + GITHUB_ORG_URL : "https://github.com/${githubOrg}", + PROJECT_TITLE : projectYml.project.title as String, + PROJECT_DESCRIPTION : projectYml.project.description as String, + PROJECT_ORG : projectYml.project.org as String, + REPO_SLUG : githubProject, + ] + + def html = new groovy.text.SimpleTemplateEngine().createTemplate(tmplFile.text).make(tokens).toString() + def outFile = new File('build/ghpages.html') + outFile.parentFile.mkdirs() + outFile.text = html + println "Generated build/ghpages.html" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e689a8f..6d44608 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,3 +108,10 @@ jobs: env: GRADLE_PUBLISH_RELEASE: 'false' SOURCE_FOLDER: build/docs + update-index: + if: github.repository_owner == 'gpc' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + needs: publish + uses: ./.github/workflows/update-versions.yml + permissions: + contents: write + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed65e82..b72d526 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -178,10 +178,19 @@ jobs: GRADLE_PUBLISH_RELEASE: 'true' SOURCE_FOLDER: build/docs VERSION: ${{ env.VERSION }} + update-index: + name: "Update Version Index" + needs: [ docs ] + uses: ./.github/workflows/update-versions.yml + with: + branch: ${{ github.event.release.target_commitish }} + permissions: + contents: write + secrets: inherit close: name: "To Next Version" environment: close # this step will be delayed until approved - needs: [ publish, docs, release ] + needs: [ publish, docs, release, update-index ] runs-on: ubuntu-24.04 permissions: contents: write # required for gradle.properties revert diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml new file mode 100644 index 0000000..58b20e6 --- /dev/null +++ b/.github/workflows/update-contributors.yml @@ -0,0 +1,61 @@ +name: Update Contributors + +on: + push: + branches: + - '[0-9]+.[0-9]+.x' + - main + workflow_dispatch: + +permissions: + contents: write + +env: + JAVA_DISTRIBUTION: 'liberica' + +jobs: + update-contributors: + runs-on: ubuntu-24.04 + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + + - name: "🦊 Install Groovy ${{ env.SDKMANRC_groovy }}" + run: | + curl -s "https://get.sdkman.io" | bash + source "$HOME/.sdkman/bin/sdkman-init.sh" + sdk install groovy ${{ env.SDKMANRC_groovy }} < /dev/null + echo "$HOME/.sdkman/candidates/groovy/current/bin" >> $GITHUB_PATH + + - name: "👥 Update contributors in project.yml" + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: groovy .github/scripts/update-contributors.groovy + + - name: "📝 Commit if changed" + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if ! git diff --quiet project.yml; then + git add project.yml + git commit -m "chore: update contributors [skip ci]" + git push + else + echo "No contributor changes to commit" + fi diff --git a/.github/workflows/update-versions.yml b/.github/workflows/update-versions.yml new file mode 100644 index 0000000..af81024 --- /dev/null +++ b/.github/workflows/update-versions.yml @@ -0,0 +1,83 @@ +name: Update GitHub Index Page + +on: + workflow_call: + inputs: + branch: + description: 'Branch to checkout and commit project.yml on' + type: string + default: '' + workflow_dispatch: + +permissions: + contents: write + +env: + JAVA_DISTRIBUTION: 'liberica' + +jobs: + update-versions: + runs-on: ubuntu-24.04 + steps: + - name: "📥 Checkout repository" + uses: actions/checkout@v6 + with: + ref: ${{ inputs.branch || github.ref_name }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: "Export .sdkmanrc properties" + uses: apache/grails-github-actions/export-gradle-properties@asf + with: + file: ".sdkmanrc" + prefix: "SDKMANRC_" + + - name: "Determine Java Version" + run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV + + - name: "☕️ Setup JDK" + uses: actions/setup-java@v5 + with: + distribution: ${{ env.JAVA_DISTRIBUTION }} + java-version: ${{ env.SDKMANRC_java }} + + - name: "🦊 Install Groovy ${{ env.SDKMANRC_groovy }}" + run: | + curl -s "https://get.sdkman.io" | bash + source "$HOME/.sdkman/bin/sdkman-init.sh" + sdk install groovy ${{ env.SDKMANRC_groovy }} < /dev/null + echo "$HOME/.sdkman/candidates/groovy/current/bin" >> $GITHUB_PATH + + - name: "🔢 Update versions in project.yml and generate ghpages.html" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: groovy .github/scripts/update-versions.groovy + + - name: "📝 Commit project.yml if changed" + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if ! git diff --quiet project.yml; then + git add project.yml + git commit -m "chore: update versions [skip ci]" + git push + else + echo "No version changes to commit" + fi + + - name: "🚀 Push updated index to gh-pages" + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + cp build/ghpages.html /tmp/ghpages.html + git fetch origin gh-pages + git checkout gh-pages + cp /tmp/ghpages.html ghpages.html + if ! git diff --quiet ghpages.html; then + git add ghpages.html + git commit -m "chore: update version index [skip ci]" + git push origin gh-pages + else + echo "No index changes to push to gh-pages" + fi diff --git a/build-logic/build.gradle b/build-logic/build.gradle index 746607d..0f16e7c 100644 --- a/build-logic/build.gradle +++ b/build-logic/build.gradle @@ -2,28 +2,15 @@ plugins { id 'groovy-gradle-plugin' } -file('../gradle.properties').withInputStream { is -> - extensions.extraProperties.set( - 'gradleProperties', - new Properties().tap { load(is) } - ) -} - -allprojects { project -> - gradleProperties.stringPropertyNames().each { key -> - project.extensions.extraProperties.set( - key, - gradleProperties.getProperty(key) - ) - } -} +def gradleProperties = new Properties() +file('../gradle.properties').withInputStream { gradleProperties.load(it) } 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' implementation 'org.apache.grails.gradle:grails-publish' implementation "org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:${gradleProperties.asciidoctorVersion}" } - diff --git a/build-logic/src/main/groovy/config.docs.gradle b/build-logic/src/main/groovy/config.docs.gradle index 7ce6e44..2731513 100644 --- a/build-logic/src/main/groovy/config.docs.gradle +++ b/build-logic/src/main/groovy/config.docs.gradle @@ -45,8 +45,7 @@ tasks.register('docs') { docProject.get().tasks.named('asciidoctor') ) finalizedBy( - 'copyAsciiDoctorDocs', - 'ghPagesRootIndexPage' + 'copyAsciiDoctorDocs' ) } @@ -59,40 +58,3 @@ tasks.register('copyAsciiDoctorDocs', Copy) { includeEmptyDirs = false } - -tasks.register('ghPagesRootIndexPage') { - description = 'Provides a root index page with historical versions fetched from the gh-pages branch at build time' - group = 'documentation' - - def templateFile = docProject.map { it.layout.projectDirectory.file('src/docs/index.tmpl') } - def outputFile = rootProject.layout.buildDirectory.file('docs/ghpages.html') - - inputs.file(templateFile) - outputs.file(outputFile) - - doLast { - List versions = [] - try { - def conn = URI.create('https://api.github.com/repos/gpc/grails-export/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() - } catch (Exception e) { - logger.warn("ghPagesRootIndexPage: could not fetch GitHub versions — ${e.message}") - } - - String optionsHtml = versions - ? versions.collect { v -> "" }.join('\n ') - : '' - - def out = outputFile.get().asFile - out.parentFile.mkdirs() - out.text = templateFile.get().asFile.text.replace('@OTHER_VERSIONS_OPTIONS@', optionsHtml) - } -} diff --git a/build-logic/src/main/groovy/config.project-metadata.gradle b/build-logic/src/main/groovy/config.project-metadata.gradle new file mode 100644 index 0000000..6220288 --- /dev/null +++ b/build-logic/src/main/groovy/config.project-metadata.gradle @@ -0,0 +1,10 @@ +def projectYml = new groovy.yaml.YamlSlurper().parse(rootProject.file('project.yml')) as Map + +extensions.extraProperties.set('projectTitle', projectYml.project.title as String) +extensions.extraProperties.set('projectDescription', projectYml.project.description as String) +extensions.extraProperties.set('projectOrg', projectYml.project.org as String) +extensions.extraProperties.set('githubOrg', projectYml.github.org as String) +extensions.extraProperties.set('githubProject', projectYml.github.project as String) +extensions.extraProperties.set('projectLicense', projectYml.license.name as String) +extensions.extraProperties.set('projectContributors', projectYml.contributors ?: [:]) +extensions.extraProperties.set('projectVersions', projectYml.versions ?: [:]) 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..1842112 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