This is an automated email from the ASF dual-hosted git repository.

jdaugherty pushed a commit to branch rework/publish-grails-skills
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit e1d358be7849035f9f9455e26c124dffc8f0df89
Author: James Daugherty <[email protected]>
AuthorDate: Thu Jul 2 08:53:14 2026 -0400

    Restructure skills publishing into separate gradle projects
---
 .agents/skills/README.md                           | 35 ++++++------
 build.gradle                                       | 62 ----------------------
 gradle/publish-root-config.gradle                  |  3 ++
 gradle/rat-root-config.gradle                      |  2 +-
 grails-skills/developer/build.gradle               | 29 ++++++++++
 .../developer/src/main/resources/SKILL.md          |  1 +
 grails-skills/upgrade-guide-8/build.gradle         | 29 ++++++++++
 .../upgrade-guide-8/src/main/resources/SKILL.md    |  1 +
 settings.gradle                                    |  7 +++
 skills/grails-8-upgrade/SKILL.md                   |  1 -
 skills/grails-developer/SKILL.md                   |  1 -
 11 files changed, 89 insertions(+), 82 deletions(-)

diff --git a/.agents/skills/README.md b/.agents/skills/README.md
index c727193d0d..a951e40c71 100644
--- a/.agents/skills/README.md
+++ b/.agents/skills/README.md
@@ -6,28 +6,29 @@ Licensed to the Apache Software Foundation (ASF) under one or 
more contributor l
 
 # Grails Agent Skills
 
-The canonical Grails agent skills live in this directory. Skills that are only 
useful while working in the Grails framework repository stay here and are not 
published through SkillsJars.
+The canonical Grails agent skills live in this directory. Skills that are only 
useful while working in the Grails framework repository stay here and are not 
published.
 
-The repository root `skills/` directory contains normal skill directories for 
app-facing skills that are useful when building or upgrading end-user Grails 
applications. Each published directory contains only a `SKILL.md` symbolic link 
back to the canonical source in `.agents/skills`, so 
[SkillsJars](https://www.skillsjars.com/) can discover and package the skills 
without duplicating content. SkillsJars deploys from public GitHub repositories 
by scanning `skills/**/SKILL.md` and then publ [...]
+App-facing skills that are useful when building or upgrading end-user Grails 
applications are published to Maven Central as individual artifacts. Each 
published skill has its own Gradle project under `grails-skills/`. The project 
packages the skill's `SKILL.md` into a jar via a symlink back to the canonical 
source in this directory, so there is no content duplication.
 
-| Published skill | Source skill | Use |
-|-----------------|--------------|-----|
-| `grails-developer` | `.agents/skills/grails-developer` | Building current 
Grails web applications, REST APIs, GORM models, controllers, services, views, 
plugins, and tests |
-| `grails-8-upgrade` | `.agents/skills/grails-8-upgrade` | Upgrading Grails 
applications from Grails 7.x to Grails 8 |
+| Published skill | Gradle project | Maven coordinates | Source skill | Use |
+|-----------------|----------------|-------------------|--------------|-----|
+| `grails-developer` | `grails-skills/developer` | 
`org.apache.grails.skills:grails-developer` | `.agents/skills/grails-developer` 
| Building current Grails web applications, REST APIs, GORM models, 
controllers, services, views, plugins, and tests |
+| `grails-8-upgrade` | `grails-skills/upgrade-guide-8` | 
`org.apache.grails.skills:grails-8-upgrade` | `.agents/skills/grails-8-upgrade` 
| Upgrading Grails applications from Grails 7.x to Grails 8 |
 
-The repository-specific `hibernate-developer`, `test-fixer`, and 
`violation-fixer` skills are intentionally not linked from `skills/` because 
they are for Grails framework development. Contributors already receive them 
from this repository when working on Grails core.
+The repository-specific `hibernate-developer`, `test-fixer`, 
`violation-fixer`, `groovy-developer`, and `java-developer` skills are 
intentionally not published because they are for Grails framework development. 
Contributors already receive them from this repository when working on Grails 
core.
 
-Run this check before publishing or updating a skill:
+Each `grails-skills/<name>/src/main/resources/SKILL.md` is a symbolic link to 
the canonical source under `.agents/skills/<name>/SKILL.md`. Edit the canonical 
source; the published jar always packages the current content.
 
-```bash
-./gradlew verifySkillsJarsSources
-```
+To add a new published skill:
 
-After changes are merged to the public branch, publish them from SkillsJars by 
submitting:
+1. Add the canonical `SKILL.md` under `.agents/skills/<name>/`.
+2. Create a Gradle project under `grails-skills/<project>/` with a 
`build.gradle` that applies `java` and `org.apache.grails.buildsrc.publish`, 
sets `group = 'org.apache.grails.skills'`, and declares `pomArtifactId`, 
`pomTitle`, and `pomDescription`.
+3. Symlink `grails-skills/<project>/src/main/resources/SKILL.md` to the 
canonical source.
+4. Register the project in `settings.gradle` and add it to the 
`publishedProjects` list in `gradle/publish-root-config.gradle`.
 
-| Field | Value |
-|-------|-------|
-| GitHub Org | `apache` |
-| GitHub Repo | `grails-core` |
+Build a skill jar locally with:
 
-Expected coordinates use the `apache__grails-core__<skill-name>` artifact 
pattern, for example 
`com.skillsjars:apache__grails-core__grails-developer:<date>-<commit>`.
+```bash
+./gradlew :grails-skills-developer:build
+./gradlew :grails-skills-developer:publishToMavenLocal
+```
diff --git a/build.gradle b/build.gradle
index 6e5908e012..36c73e7c5d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -119,68 +119,6 @@ tasks.register('publishAllToMavenLocal') {
     }
 }
 
-tasks.register('verifySkillsJarsSources') {
-    description = 'Verifies the SkillsJars publication paths point at the 
app-facing Grails skill sources.'
-    group = 'verification'
-
-    def skillsJarsDir = layout.projectDirectory.dir('skills')
-    def publishedSkills = [
-            'grails-8-upgrade': 
'../../.agents/skills/grails-8-upgrade/SKILL.md',
-            'grails-developer': 
'../../.agents/skills/grails-developer/SKILL.md',
-    ]
-
-    inputs.dir(layout.projectDirectory.dir('.agents/skills'))
-    inputs.dir(skillsJarsDir)
-
-    doLast {
-        if (!skillsJarsDir.asFile.directory) {
-            throw new GradleException('The SkillsJars publication path must be 
a skills directory containing app-facing skill symlinks.')
-        }
-
-        def actualSkillNames = skillsJarsDir.asFile.listFiles().findAll { File 
file -> file.directory }.collect { File file -> file.name } as Set
-        def expectedSkillNames = publishedSkills.keySet() as Set
-        def missingSkillNames = expectedSkillNames - actualSkillNames
-        def extraSkillNames = actualSkillNames - expectedSkillNames
-        if (missingSkillNames || extraSkillNames) {
-            throw new GradleException("SkillsJars must publish exactly 
${expectedSkillNames}, but missing ${missingSkillNames} and found extra 
${extraSkillNames}.")
-        }
-
-        publishedSkills.each { String skillName, String expectedSkillTarget ->
-            def skillDir = skillsJarsDir.dir(skillName).asFile
-            if (!skillDir.directory) {
-                throw new GradleException("The SkillsJars skill ${skillName} 
must be a directory containing SKILL.md.")
-            }
-
-            def skillLink = skillsJarsDir.file("${skillName}/SKILL.md").asFile
-            def skillPath = skillLink.toPath()
-            def actualTarget = null
-
-            if (java.nio.file.Files.isSymbolicLink(skillPath)) {
-                actualTarget = 
java.nio.file.Files.readSymbolicLink(skillPath).toString().replace('\\', '/')
-            }
-            else if (skillLink.isFile()) {
-                actualTarget = skillLink.getText('UTF-8').trim().replace('\\', 
'/')
-            }
-
-            if (actualTarget != expectedSkillTarget) {
-                throw new GradleException("The SkillsJars skill ${skillName} 
marker must be a symlink to ${expectedSkillTarget}, but found ${actualTarget ?: 
'no link target'}.")
-            }
-
-            def gitMode = providers.exec {
-                commandLine 'git', 'ls-files', '-s', 
"skills/${skillName}/SKILL.md"
-            }.standardOutput.asText.get().trim().split(' ')[0]
-            if (gitMode != '120000') {
-                throw new GradleException("The SkillsJars skill ${skillName} 
marker must be committed as a symlink with git mode 120000, but found mode 
${gitMode ?: 'none'}.")
-            }
-
-            def sourceSkillFile = 
layout.projectDirectory.file(expectedSkillTarget.substring(6))
-            if (!sourceSkillFile.asFile.file) {
-                throw new GradleException("The SkillsJars skill ${skillName} 
target ${expectedSkillTarget} does not exist.")
-            }
-        }
-    }
-}
-
 apply {
     from layout.projectDirectory.file('gradle/publish-root-config.gradle')
     from layout.projectDirectory.file('gradle/rat-root-config.gradle')
diff --git a/gradle/publish-root-config.gradle 
b/gradle/publish-root-config.gradle
index 123bafc419..6959e78e58 100644
--- a/gradle/publish-root-config.gradle
+++ b/gradle/publish-root-config.gradle
@@ -140,6 +140,9 @@ def publishedProjects = [
         'grails-data-graphql-core',
         // wrapper
         'grails-wrapper',
+        // agent skills
+        'grails-skills-developer',
+        'grails-skills-upgrade-guide-8',
         // profiles
         'grails-profiles-base',
         'grails-profiles-plugin',
diff --git a/gradle/rat-root-config.gradle b/gradle/rat-root-config.gradle
index c160aac011..23d127f38b 100644
--- a/gradle/rat-root-config.gradle
+++ b/gradle/rat-root-config.gradle
@@ -26,7 +26,7 @@ tasks.named('rat') {
             'CHECKSUMS', // build artifact for storing checksums for easy 
verification
             'PUBLISHED_ARTIFACTS', // build artifact for storing published 
artifacts coordinates & paths
             'licenses/**', // licenses directory excluded
-            'skills/**', // SkillsJars discovery symlinks; licensed skill 
sources live under .agents/skills
+            'grails-skills/*/src/main/resources/SKILL.md', // published skill 
symlinks; licensed skill sources live under .agents/skills
             '.claude/skills/**', // Claude skill aliases; licensed skill 
sources live under .agents/skills
             '.git-blame-ignore-revs', // git configuration isn't code
             
'grails-web-common/src/main/groovy/org/grails/web/json/JSONObject.java',
diff --git a/grails-skills/developer/build.gradle 
b/grails-skills/developer/build.gradle
new file mode 100644
index 0000000000..1d2f8a2f6f
--- /dev/null
+++ b/grails-skills/developer/build.gradle
@@ -0,0 +1,29 @@
+/*
+ *  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.
+ */
+plugins {
+    id 'java'
+    id 'org.apache.grails.buildsrc.publish'
+}
+
+group = 'org.apache.grails.skills'
+version = projectVersion
+
+ext {
+    pomArtifactId = 'grails-developer'
+    pomTitle = 'Apache Grails® Developer Agent Skill'
+    pomDescription = 'Agent skill for building Apache Grails® applications: 
controllers, services, GORM, views, plugins, and testing with Spock and Geb. 
The canonical SKILL.md source lives under .agents/skills/grails-developer.'
+}
diff --git a/grails-skills/developer/src/main/resources/SKILL.md 
b/grails-skills/developer/src/main/resources/SKILL.md
new file mode 120000
index 0000000000..fdc0c1877e
--- /dev/null
+++ b/grails-skills/developer/src/main/resources/SKILL.md
@@ -0,0 +1 @@
+../../../../../.agents/skills/grails-developer/SKILL.md
\ No newline at end of file
diff --git a/grails-skills/upgrade-guide-8/build.gradle 
b/grails-skills/upgrade-guide-8/build.gradle
new file mode 100644
index 0000000000..5a0b7d7e91
--- /dev/null
+++ b/grails-skills/upgrade-guide-8/build.gradle
@@ -0,0 +1,29 @@
+/*
+ *  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.
+ */
+plugins {
+    id 'java'
+    id 'org.apache.grails.buildsrc.publish'
+}
+
+group = 'org.apache.grails.skills'
+version = projectVersion
+
+ext {
+    pomArtifactId = 'grails-8-upgrade'
+    pomTitle = 'Apache Grails® 8 Upgrade Agent Skill'
+    pomDescription = 'Agent skill for upgrading Apache Grails® applications 
from Grails 7.x to Grails 8. The canonical SKILL.md source lives under 
.agents/skills/grails-8-upgrade.'
+}
diff --git a/grails-skills/upgrade-guide-8/src/main/resources/SKILL.md 
b/grails-skills/upgrade-guide-8/src/main/resources/SKILL.md
new file mode 120000
index 0000000000..54634cb5ff
--- /dev/null
+++ b/grails-skills/upgrade-guide-8/src/main/resources/SKILL.md
@@ -0,0 +1 @@
+../../../../../.agents/skills/grails-8-upgrade/SKILL.md
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index b0b8843788..1a1ffb303b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -224,6 +224,10 @@ include(
 
         // wrapper
         'grails-wrapper',
+
+        // Agent Skills (published for consumption by AI coding agents)
+        'grails-skills-developer',
+        'grails-skills-upgrade-guide-8',
 )
 
 project(':grails-bom').projectDir = file('grails-bom/default')
@@ -249,6 +253,9 @@ project(':grails-async-rxjava2').projectDir = 
file('grails-async/rxjava2')
 project(':grails-async-rxjava3').projectDir = file('grails-async/rxjava3')
 project(':grails-async').projectDir = file('grails-async/plugin')
 
+project(':grails-skills-developer').projectDir = 
file('grails-skills/developer')
+project(':grails-skills-upgrade-guide-8').projectDir = 
file('grails-skills/upgrade-guide-8')
+
 project(':grails-dependencies-starter-web').projectDir = 
file('grails-dependencies/starter-web')
 project(':grails-dependencies-test').projectDir = 
file('grails-dependencies/test')
 project(':grails-dependencies-assets').projectDir = 
file('grails-dependencies/assets')
diff --git a/skills/grails-8-upgrade/SKILL.md b/skills/grails-8-upgrade/SKILL.md
deleted file mode 120000
index d4c2988030..0000000000
--- a/skills/grails-8-upgrade/SKILL.md
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/grails-8-upgrade/SKILL.md
\ No newline at end of file
diff --git a/skills/grails-developer/SKILL.md b/skills/grails-developer/SKILL.md
deleted file mode 120000
index 9128af3297..0000000000
--- a/skills/grails-developer/SKILL.md
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/grails-developer/SKILL.md
\ No newline at end of file

Reply via email to