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

benweidig pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit 6e1317f94d4f7501b6fb5a27ea0ee7154c231e84
Author: Ben Weidig <[email protected]>
AuthorDate: Sun Apr 19 12:00:05 2026 +0200

    TAP5-2809: buildSrc cleanup/documentation, Gradle 9 preparations
---
 build.gradle                                       |  63 +++++-----
 buildSrc/README.md                                 | 139 +++++++++++++++++++++
 .../main/groovy/t5build/GenerateChecksums.groovy   |  12 ++
 buildSrc/src/main/groovy/t5build/NpmTask.groovy    | 112 +++++++++++++++++
 buildSrc/src/main/groovy/t5build/SSshExec.groovy   |  21 ----
 buildSrc/src/main/groovy/t5build/Scp.groovy        |  36 ------
 buildSrc/src/main/groovy/t5build/SshTask.groovy    |  94 --------------
 .../main/groovy/t5build/TapestryBuildLogic.groovy  |  22 ----
 .../main/groovy/tapestry.java-convention.gradle    |  12 +-
 .../src/main/groovy/tapestry.ssh-convention.gradle |  15 ---
 quickstart/build.gradle                            |  59 +++++----
 tapestry-cdi/build.gradle                          |   1 -
 12 files changed, 338 insertions(+), 248 deletions(-)

diff --git a/build.gradle b/build.gradle
index f4a9cb764..b32c83b33 100755
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,4 @@
 import t5build.GenerateChecksums
-import t5build.TapestryBuildLogic
 import org.apache.tools.ant.filters.ReplaceTokens
 
 plugins {
@@ -43,15 +42,16 @@ ext {
         'tapestry-core',
     ]
 
+    // Set via GRADLE_OPTS='-Dci=true' in CI (see Jenkinsfile).
+    // Controls version suffix, deploy target, and if integration tests will 
run headless
     continuousIntegrationBuild = Boolean.getBoolean('ci')
+
     stagingUrl = 
'https://repository.apache.org/service/local/staging/deploy/maven2/'
     snapshotUrl = 
'https://repository.apache.org/content/repositories/snapshots'
     doSign = !project.hasProperty('noSign') && 
project.hasProperty('signing.keyId')
 
     // apacheDeployUserName and apacheDeployPassword should be specified in 
~/.gradle/gradle.properties
-
-    def deployFlavor = TapestryBuildLogic.isSnapshot(project) ? 'snapshot' : 
'apache'
-
+    def deployFlavor = continuousIntegrationBuild ? 'snapshot' : 'apache'
     deployUsername = providers.gradleProperty("${deployFlavor}DeployUserName")
     deployPassword = providers.gradleProperty("${deployFlavor}DeployPassword")
     archiveDeployFolder = providers.gradleProperty('apacheArchivesFolder')
@@ -64,18 +64,18 @@ ext {
 }
 
 // Provided so that the CI server can override the normal version number for 
nightly builds.
-version = TapestryBuildLogic.tapestryVersion(project)
+version = continuousIntegrationBuild ? tapestryMajorVersion + '-SNAPSHOT' : 
tapestryMajorVersion + tapestryMinorVersion
 
 // Specific to top-level build, not set for subprojects:
 configurations {
     javadoc
-    published.extendsFrom archives, meta
 
+    published.extendsFrom archives, meta
     if (doSign) {
         published.extendsFrom signatures
     }
 
-    binaries // additional dependencies included in the binary archive
+    binaries
 }
 
 dependencies {
@@ -130,9 +130,8 @@ subprojects {
     }
 
     configurations {
-        // published -- what gets uploaded to the Nexus repository
+        // published: what gets uploaded to the Nexus repository
         published.extendsFrom archives, meta
-
         if (rootProject.doSign) {
             published.extendsFrom signatures
         }
@@ -150,8 +149,9 @@ subprojects {
 
                 pom {
                     name = project.name
-                    // TODO: find some way to get the subproject description 
here.
-                    // description =
+                    description = providers.provider {
+                        project.description
+                    }
                     url = 'https://tapestry.apache.org/'
                     licenses {
                         license {
@@ -162,13 +162,12 @@ subprojects {
                     scm {
                         connection = 
'scm:git:https://gitbox.apache.org/repos/asf/tapestry-5.git'
                         developerConnection = 
'scm:git://gitbox.apache.org/repos/asf/tapestry-5.git'
-                        url = 
'https://git-wip-us.apache.org/repos/asf?p=tapestry-5.git;a=summary'
+                        url = 
'https://git-wip-us.apache.org/repos/asf?p=tapestry-5.git'
                     }
                     // Changes the generated pom.xml so its dependencies on 
suffixed artifacts
                     // get properly updated with suffixed artifact ids
                     withXml {
-                        def artifactIdQName = new groovy.namespace.QName(
-                                'http://maven.apache.org/POM/4.0.0', 
'artifactId')
+                        def artifactIdQName = new 
groovy.namespace.QName('http://maven.apache.org/POM/4.0.0', 'artifactId')
                         def node = asNode()
                         def dependencies = node.get('dependencies')[0]
                         if (dependencies != null) {
@@ -183,6 +182,7 @@ subprojects {
                 }
             }
         }
+
         if (canDeploy) {
             repositories {
                 mavenLocal()
@@ -209,8 +209,9 @@ subprojects {
         }
     }
 
+    // Modules not in suffixedArtifactNames have no -jakarta counterpart
+    // and should not be published when artifactSuffix is empty (i.e. the 
javax branch)
     def actuallyPublish = !artifactSuffix.isEmpty() || 
suffixedArtifactNames.contains(project.name)
-    // println 'XXXXXX Actually publish? ' + actuallyPublish + ' project ' + 
project.name + ' ' + '!artifactSuffix.isEmpty() ' + !artifactSuffix.isEmpty()
     if (!actuallyPublish) {
         tasks.withType(PublishToMavenRepository).configureEach {
             it.enabled = false
@@ -233,7 +234,7 @@ subprojects {
 // Cribbed from 
https://github.com/hibernate/hibernate-core/blob/master/release/release.gradle#L19
 
 tasks.register('aggregateJavadoc', Javadoc) {
-    group = 'Documentation'
+    group = 'documentation'
     description = 'Build the aggregated JavaDocs for all modules'
 
     dependsOn configurations.javadoc
@@ -303,14 +304,14 @@ tasks.register('aggregateJavadoc', Javadoc) {
 }
 
 tasks.register('typeScriptDocs') {
-    group = 'Documentation'
+    group = 'documentation'
     description = 'Builds typedoc documentation for all TypeScript sources'
 
     dependsOn(':tapestry-core:generateTypeScriptDocs')
 }
 
 tasks.register('combinedJacocoReport', JacocoReport) {
-    group = 'Verification'
+    group = 'verification'
     description = 'Generates combined JaCoCo coverage report for all 
subprojects'
     
     def excludedProjects = [
@@ -329,7 +330,7 @@ tasks.register('combinedJacocoReport', JacocoReport) {
     def subprojectsToConsider = subprojects.findAll {
         !excludedProjects.contains(it.name)
     }
-    dependsOn subprojectsToConsider.test
+    dependsOn subprojectsToConsider.collect { it.tasks.named('check') }
 
     def classFiles = 
files(subprojectsToConsider.sourceSets.main.output).asFileTree.matching {
         exclude 'org/apache/tapestry5/internal/plastic/asm/**'
@@ -338,9 +339,12 @@ tasks.register('combinedJacocoReport', JacocoReport) {
 
     
additionalSourceDirs.from(files(subprojectsToConsider.sourceSets.main.allSource.srcDirs))
     
sourceDirectories.from(files(subprojectsToConsider.sourceSets.main.allSource.srcDirs))
-    
executionData.from(files(subprojectsToConsider.jacocoTestReport.executionData).filter
 {
-        it.exists()
+    // Glob picks up all exec files regardless of task name: test.exec, 
testNG.exec, etc.
+    executionData.from(subprojectsToConsider.collect { p ->
+        p.fileTree(p.layout.buildDirectory) { include 'jacoco/*.exec' }
     })
+
+    // Reuse the JaCoCo agent classpath that each subproject's 
jacocoTestReport task already resolved.
     jacocoClasspath = 
files(subprojectsToConsider.jacocoTestReport.jacocoClasspath)
 
     reports {
@@ -357,7 +361,7 @@ tasks.register('combinedJacocoReport', JacocoReport) {
 }
 
 tasks.register('continuousIntegration') {
-    group = 'Verification'
+    group = 'verification'
     description = 'Runs a full CI build: assembles all artifacts, runs all 
checks, and generates aggregate reports.'
 
     def dependants = [
@@ -365,20 +369,16 @@ tasks.register('continuousIntegration') {
         'tapestry-core:testWithJqueryAndRequireJsDisabled',
         'tapestry-core:testWithPrototypeAndRequireJsEnabled', 
         subprojects.check, // jQuery and Require.js enabled
-        combinedJacocoReport
+        combinedJacocoReport,
+        aggregateJavadoc
     ]
 
-    // tapestry-javadoc doesn't work with Java 8 anymore. That's why it's only 
added if != 8.
-    if (JavaVersion.current() != JavaVersion.VERSION_1_8) {
-        dependants << aggregateJavadoc
-    }
-
     dependsOn(dependants)
 }
 
 
 tasks.register('zippedSources', Zip) {
-    group = 'Release artifact'
+    group = 'release artifact'
     description = "Creates a combined Zip file of all sub-project's sources"
     
     dependsOn 'tapestry-core:compileTypeScript'
@@ -403,7 +403,7 @@ tasks.register('zippedSources', Zip) {
 }
 
 tasks.register('zippedApidoc', Zip) {
-    group = 'Release artifact'
+    group = 'release artifact'
     description = "Zip archive of the project's aggregate JavaDoc and 
TypeScript documentation"
 
     dependsOn typeScriptDocs
@@ -474,7 +474,6 @@ if (canDeploy) {
         uploads.extendsFrom archives, signatures
     }
 
-
     artifacts {
         archives zippedApidoc, zippedSources, zippedBinaries
     }
@@ -524,7 +523,7 @@ if (canDeploy) {
 }
 
 tasks.register('updateBootstrap') {
-    group = 'Maintenance'
+    group = 'maintenance'
     description = 'Updates the included Bootstrap dependencies from GitHub'
 
     doLast {
diff --git a/buildSrc/README.md b/buildSrc/README.md
new file mode 100644
index 000000000..d5c83b7f8
--- /dev/null
+++ b/buildSrc/README.md
@@ -0,0 +1,139 @@
+# buildSrc: Tapestry Build Conventions
+
+Gradle convention plugins and build utilities shared across all subprojects.
+All convention files live in `src/main/groovy/` as precompiled script plugins.
+
+---
+
+## Convention hierarchy
+
+```text
+tapestry.java-convention                    applied to every subproject by 
root build.gradle
+ └─ tapestry.testing-base-convention        JUnit Platform runner, shared test 
config
+    ├─ tapestry.junit5-convention             + Jupiter API + engine
+    │   ├─ tapestry.junit5-spock-convention       + Spock BOM / spock-core
+    │   └─ tapestry.junit4-legacy-convention      + Vintage engine (JUnit 4 
tests)
+    └─ tapestry.testng-convention             + TestNG + EasyMock + 
testng-engine
+```
+
+---
+
+## Conventions
+
+### `tapestry.java-convention`
+
+Applied automatically to every subproject by the root `build.gradle`.
+
+*   Java 11 source/target compatibility
+*   `provided` configuration (compile-only scope, like Maven 
`<scope>provided</scope>`)
+*   Dependency version constraints to keep library versions consistent across 
modules
+*   JAR manifest: `Automatic-Module-Name` (JPMS) and `LICENSE`/`NOTICE` in 
`META-INF`
+
+---
+
+### `tapestry.testing-base-convention`
+
+Foundation for all test conventions.
+
+**Do not apply directly**
+
+Use one of the specialized conventions below.
+
+Configures every `Test` task in the project via `tasks.withType(Test)`:
+
+*   JUnit Platform as the runner (required by all engines: Jupiter, TestNG, 
Vintage)
+*   `junit-platform-launcher` and the JUnit BOM on the runtime classpath
+*   Standard system properties: file encoding, locale, CI flag, Selenium wait 
timeout
+*   Consistent test logging: full exception format, pass/skip/fail events, 
progress counter
+
+### `tapestry.junit5-convention`
+
+Use when the module's test sources use JUnit 5 Jupiter (`@Test`, 
`@ExtendWith`, ...).
+
+Adds Jupiter API to the test compile classpath and the Jupiter engine at 
runtime.
+
+```groovy
+plugins {
+    id 'tapestry.junit5-convention'
+}
+```
+
+### `tapestry.junit5-spock-convention`
+
+Use when the module writes tests in Spock (Groovy BDD framework).
+
+Extends `junit5-convention` with the Spock BOM and `spock-core`.
+Spock runs on the JUnit Platform — no additional runner config needed.
+
+```groovy
+plugins {
+    id 'tapestry.junit5-spock-convention'
+}
+```
+
+### `tapestry.junit4-legacy-convention`
+
+Use **only** for modules that still have JUnit 4 test sources (`@RunWith`, 
`@Rule`, ...).
+
+Extends `junit5-convention` with the JUnit Vintage engine, so legacy tests run 
alongside any Jupiter tests without touching test source code.
+Prefer migrating tests over adding this convention to new modules.
+
+```groovy
+plugins {
+    id 'tapestry.junit4-legacy-convention'
+}
+```
+
+### `tapestry.testng-convention`
+
+Use when the module's test sources use TestNG annotations (`@Test`, 
`@BeforeMethod`, `@DataProvider`, ...).
+
+Adds TestNG and EasyMock to the test compile classpath.
+The `testng-engine` dependency at runtime lets the standard `test` task (JUnit 
Platform) discover and run TestNG unit tests automatically.
+
+```groovy
+plugins {
+    id 'tapestry.testng-convention'
+}
+```
+
+#### Modules with Selenium integration tests
+
+When a module has both TestNG unit tests and Selenium integration tests, 
_native_ TestNG **must** be used for the integration tests!
+
+This is because Selenium tests currently rely on `@BeforeTest` / 
`ITestContext` scoping that only works correctly with the native TestNG runner 
and a `testng.xml` suite file.
+Each `<test>` element gets its own server instance with the right webapp.
+
+The required additions to the module's `build.gradle`:
+
+```groovy
+// Exclude integration classes from the JUnit Platform 'test' task
+tasks.named('test') {
+    exclude '**/integration/**'   // adjust pattern to match your integration 
tests
+}
+
+// Native TestNG task: uses testng.xml for correct ITestContext grouping
+tasks.register('testNG', Test) {
+    group = 'verification'
+
+    useTestNG {
+        suiteXmlFiles << project.file('src/test/resources/testng.xml')
+    }
+}
+
+// Include in the check lifecycle
+tasks.named('check') {
+    dependsOn 'testNG'
+}
+```
+
+The `testng.xml` suite file for these modules should contain **only** 
integration `<test>` elements.
+
+Unit tests are handled by the `test` task via testng-engine by Jupiter and 
must not appear in `testng.xml`.
+
+---
+
+## Helper classes (`t5build` package)
+
+**`GenerateChecksums`**: custom Gradle task type that generates MD5/SHA-256 
checksum
+files for release archives.
diff --git a/buildSrc/src/main/groovy/t5build/GenerateChecksums.groovy 
b/buildSrc/src/main/groovy/t5build/GenerateChecksums.groovy
index dd9f9997f..9a54c0e8e 100644
--- a/buildSrc/src/main/groovy/t5build/GenerateChecksums.groovy
+++ b/buildSrc/src/main/groovy/t5build/GenerateChecksums.groovy
@@ -1,3 +1,15 @@
+// Licensed 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
+//
+// http://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 t5build
 
 import org.gradle.api.DefaultTask
diff --git a/buildSrc/src/main/groovy/t5build/NpmTask.groovy 
b/buildSrc/src/main/groovy/t5build/NpmTask.groovy
new file mode 100644
index 000000000..4312e6284
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/NpmTask.groovy
@@ -0,0 +1,112 @@
+// Licensed 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
+//
+// http://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 t5build
+
+import org.gradle.api.tasks.Exec
+import org.gradle.api.tasks.Internal
+
+/**
+ * Gradle task that runs an npm command, either via Docker or the local npm 
executable.
+ * <p>
+ * When Docker is available, the task mounts the project's {@code src/main} 
directory into
+ * a container and runs {@cpde npm} there, ensuring a consistent Node.js 
environment.
+ * <p>
+ * On Linux, the container process runs as the current host user to avoid 
file-ownership
+ * issues on mounted volumes.
+ * <p>
+ * When Docker is not available, the task falls back to invoking the local 
{@code npm} (or
+ * {@code npm.cmd} on Windows) directly.
+ * <p>
+ * The Docker image used can be overridden via the {@code nodeDockerImage} 
Gradle project
+ * property (defaults to {@code node:lts-alpine}).
+ * <p>
+ * Usage in a build script:
+ * <pre>
+ * tasks.register('npmInstall', NpmTask) {
+ *     npmArgs = ['install']
+ * }
+ * </pre>
+ */
+class NpmTask extends Exec {
+
+    private static Boolean dockerAvailableCache = null
+
+    /**
+     * Arguments passed to the npm command (e.g. {@code ['install']}).
+     */
+    @Internal
+    List<String> npmArgs = []
+
+    /**
+     * Docker image used to run npm when Docker is available.
+     * <p>
+     * Default: {@code node:lts-alpine}
+     */
+    @Internal
+    String nodeDockerImage = (project.findProperty('nodeDockerImage') ?: 
'node:lts-alpine') as String
+
+    NpmTask() {
+        workingDir = 'src/main/typescript'
+    }
+
+    @Override
+    protected void exec() {
+        commandLine(buildCommand())
+        super.exec()
+    }
+
+    private List<String> buildCommand() {
+        return isDockerAvailable() ? buildDockerCommand() : [npmExecutable()] 
+ npmArgs
+    }
+
+    private List<String> buildDockerCommand() {
+        def cmd = [
+            'docker', 'run', '--rm',
+            '-v', "${project.projectDir}/src/main:/work",
+            '-w', '/work/typescript',
+            // The node:lts-alpine image has a root-owned /.npm cache baked in.
+            // Redirect to /tmp so the container user can always write to it.
+            '-e', 'NPM_CONFIG_CACHE=/tmp/.npm']
+        if (isLinux()) {
+            cmd += ['--user', "${['id', '-u'].execute().text.trim()}:${['id', 
'-g'].execute().text.trim()}"]
+        }
+        cmd += [nodeDockerImage, 'npm'] + npmArgs
+        return cmd
+    }
+
+    private static boolean isDockerAvailable() {
+        if (dockerAvailableCache == null) {
+            try {
+                def proc = ['docker', 'info'].execute()
+                proc.waitFor()
+                dockerAvailableCache = proc.exitValue() == 0
+            } catch (Exception ignored) {
+                dockerAvailableCache = false
+            }
+        }
+        return dockerAvailableCache
+    }
+
+    private static String npmExecutable() {
+        return isWindows() ? 'npm.cmd' : 'npm'
+    }
+
+    private static boolean isWindows() {
+        return System.properties['os.name'].toLowerCase().contains('windows')
+    }
+
+    private static boolean isLinux() {
+        def os = System.properties['os.name'].toLowerCase()
+        return !os.contains('windows') && !os.contains('mac')
+    }
+}
diff --git a/buildSrc/src/main/groovy/t5build/SSshExec.groovy 
b/buildSrc/src/main/groovy/t5build/SSshExec.groovy
deleted file mode 100644
index e985b4591..000000000
--- a/buildSrc/src/main/groovy/t5build/SSshExec.groovy
+++ /dev/null
@@ -1,21 +0,0 @@
-package t5build
-
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.TaskAction
-
-class SshExec extends SshTask {
-
-    @Input
-    List<String[]> commandLines = []
-
-    void commandLine(String... commandLine) {
-        commandLines << commandLine
-    }
-
-    @TaskAction
-    void doActions() {
-        commandLines.each { commandLine ->
-            ssh(*commandLine)
-        }
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/t5build/Scp.groovy 
b/buildSrc/src/main/groovy/t5build/Scp.groovy
deleted file mode 100644
index 3280cfb9d..000000000
--- a/buildSrc/src/main/groovy/t5build/Scp.groovy
+++ /dev/null
@@ -1,36 +0,0 @@
-package t5build
-
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.SkipWhenEmpty
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-
-class Scp extends SshTask {
-
-    @InputFiles @SkipWhenEmpty
-    def source
-
-    @Input
-    String destination
-
-    @Input
-    boolean isDir = false
-
-    @TaskAction
-    void doActions() {
-        if (isDir) {
-            scpDir(source, destination)
-            return
-        }
-        project.files(source).each { doFile(it) }
-    }
-
-    private void doFile(File file) {
-        if (file.isDirectory()) {
-            file.eachFile { doFile(it) }
-        } else {
-            scpFile(file, destination)
-        }
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/t5build/SshTask.groovy 
b/buildSrc/src/main/groovy/t5build/SshTask.groovy
deleted file mode 100644
index 769b32adf..000000000
--- a/buildSrc/src/main/groovy/t5build/SshTask.groovy
+++ /dev/null
@@ -1,94 +0,0 @@
-package t5build
-
-import org.gradle.api.DefaultTask
-import org.gradle.api.file.FileCollection
-import org.gradle.api.logging.LogLevel
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
-
-abstract class SshTask extends DefaultTask {
-
-    @InputFiles
-    FileCollection sshAntClasspath
-
-    @Input
-    String host
-
-    @Input
-    String userName
-
-    // TODO: Passwords should not be plain @Input.
-    @Input
-    String password
-
-    @Input
-    boolean verbose = false
-
-    private boolean antInited = false
-
-    protected void initAnt() {
-        if (antInited) {
-            return
-        }
-        ant.taskdef(name: 'scp',
-                    classname: 
'org.apache.tools.ant.taskdefs.optional.ssh.Scp',
-                    classpath: sshAntClasspath.asPath,
-                    loaderref: 'ssh')
-
-        ant.taskdef(name: 'sshexec',
-                    classname: 
'org.apache.tools.ant.taskdefs.optional.ssh.SSHExec',
-                    classpath: sshAntClasspath.asPath,
-                    loaderref: 'ssh')
-        antInited = true
-    }
-
-    protected void withInfoLogging(Closure action) {
-        def oldLogLevel = getLogging().getLevel()
-        getLogging().setLevel([LogLevel.INFO, oldLogLevel].min())
-        try {
-            action()
-        } finally {
-            if (oldLogLevel != null) {
-                getLogging().setLevel(oldLogLevel)
-            }
-        }
-    }
-
-    protected void scpFile(Object source, String destination) {
-        initAnt()
-        withInfoLogging {
-            // TODO: This keyfile is hardcoded and uses an old algorithm (dsa)
-            ant.scp(localFile: project.files(source).singleFile,
-                    remoteToFile: "${userName}@${host}:${destination}",
-                    keyfile: "${System.properties['user.home']}/.ssh/id_dsa",
-                    verbose: verbose)
-        }
-    }
-
-    protected void scpDir(Object source, String destination) {
-        initAnt()
-        withInfoLogging {
-            ant.sshexec(host: host,
-                        username: userName,
-                        password: password,
-                        command: "mkdir -p ${destination}")
-
-            ant.scp(remoteTodir: "${userName}@${host}:${destination}",
-                    keyfile: "${System.properties['user.home']}/.ssh/id_dsa",
-                    verbose: verbose) {
-                project.files(source).addToAntBuilder(ant, 'fileSet', 
FileCollection.AntType.FileSet)
-            }
-        }
-    }
-
-    protected void ssh(Object... commandLine) {
-        initAnt()
-        withInfoLogging {
-            ant.sshexec(host: host,
-                        username: userName,
-                        password: password,
-                        command: commandLine.join(' '))
-        }
-    }
-}
diff --git a/buildSrc/src/main/groovy/t5build/TapestryBuildLogic.groovy 
b/buildSrc/src/main/groovy/t5build/TapestryBuildLogic.groovy
deleted file mode 100644
index 13f9b935a..000000000
--- a/buildSrc/src/main/groovy/t5build/TapestryBuildLogic.groovy
+++ /dev/null
@@ -1,22 +0,0 @@
-package t5build
-
-import org.gradle.api.Project
-
-class TapestryBuildLogic {
-
-    static boolean isSnapshot(Project project) {
-        return tapestryVersion(project).endsWith('SNAPSHOT')
-    }
-
-    static boolean isWindows() {
-        return System.properties['os.name'].toLowerCase().contains('windows')
-    }
-
-    static String tapestryVersion(Project project) {
-        String major = project.rootProject.ext.tapestryMajorVersion
-        String minor = project.rootProject.ext.tapestryMinorVersion
-      
-        boolean isCiBuild = 
project.rootProject.hasProperty('continuousIntegrationBuild') && 
project.rootProject.continuousIntegrationBuild
-        return isCiBuild ? major + '-SNAPSHOT' : major + minor
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/tapestry.java-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.java-convention.gradle
index d06cbb6ca..6787f2a5b 100644
--- a/buildSrc/src/main/groovy/tapestry.java-convention.gradle
+++ b/buildSrc/src/main/groovy/tapestry.java-convention.gradle
@@ -1,3 +1,9 @@
+// Base convention applied to every subproject in root build.gradle via 
`subprojects {}`
+//   - Java 11 source/target compatibility
+//   - 'provided' configuration (compile-only scope, like Maven 
<scope>provided</scope>)
+//   - Dependency version constraints to keep the classpath consistent across 
modules
+//   - JAR manifest: Automatic-Module-Name for JPMS, LICENSE/NOTICE in META-INF
+
 plugins {
     id 'java-library'
     id 'eclipse'
@@ -13,7 +19,10 @@ java {
 }
 
 configurations {
+    // 'provided': on compile + test classpaths but excluded from the runtime 
jar/war.
     provided
+
+    // 'meta': collects artifacts (e.g. sourcesJar) that should be published 
alongside the main jar.
     meta
 }
 
@@ -53,7 +62,8 @@ tasks.withType(Jar).configureEach {
         into 'META-INF'
     }
 
-    // JPMS compatibility
+    // Stable module name for JPMS (e.g. tapestry-ioc → 
org.apache.tapestry.ioc).
+    // Keeps the module name consistent even if the JAR file is renamed.
     manifest {
         attributes("Automatic-Module-Name": 
"org.apache.tapestry.${project.name}"
         .replace('tapestry-', '')
diff --git a/buildSrc/src/main/groovy/tapestry.ssh-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.ssh-convention.gradle
deleted file mode 100644
index 2f6aa7c8a..000000000
--- a/buildSrc/src/main/groovy/tapestry.ssh-convention.gradle
+++ /dev/null
@@ -1,15 +0,0 @@
-import t5build.SshTask
-
-// This configuration will hold the ant-jsch.jar at runtime
-configurations {
-    sshAntTask
-}
-
-dependencies {
-    sshAntTask libs.ant.jsch
-}
-
-// Configure all SshTask instances to use the files from the configuration
-tasks.withType(SshTask).configureEach {
-    it.sshAntClasspath = configurations.sshAntTask
-}
diff --git a/quickstart/build.gradle b/quickstart/build.gradle
index ad2136526..199f93414 100644
--- a/quickstart/build.gradle
+++ b/quickstart/build.gradle
@@ -1,50 +1,55 @@
 import org.apache.tools.ant.filters.FixCrLfFilter
 import org.apache.tools.ant.filters.ReplaceTokens
 
-task copyGradleWrapper(type: Copy) {
-    ext.srcDir = file("$buildDir/wrapper")
+tasks.register('copyGradleWrapper', Copy) {
+    group = 'build setup'
+    description = 'Copies Gradle wrapper files to archetype resources'
 
+    def srcDir = layout.buildDirectory.dir('wrapper')
     inputs.dir srcDir
-    outputs.dir file("$buildDir/resources/main/archetype-resources")
+    outputs.dir layout.buildDirectory.dir('resources/main/archetype-resources')
 
     from srcDir
-    into file("$buildDir/resources/main/archetype-resources")
-
+    into layout.buildDirectory.dir('resources/main/archetype-resources')
     exclude '.gradle'
 }
 
-task addGradleWrapper(type: Exec) {
-    workingDir "$buildDir/wrapper"
-    commandLine "${rootProject.projectDir}/gradlew", 'wrapper', 
'--gradle-version', '7.3'
+tasks.register('addGradleWrapper', Exec) {
+    group = 'build setup'
+    description = 'Generates Gradle wrapper in temporary directory'
 
+    workingDir = layout.buildDirectory.dir('wrapper')
+    commandLine = ["${rootProject.projectDir}/gradlew", 'wrapper', 
'--gradle-version', '8.14.2']
     standardOutput = new ByteArrayOutputStream()
 
-    ext.output = {
-        return standardOutput.toString()
-    }
-
     doFirst {
-        def wrapperDirectory = new File(buildDir, "wrapper")
+        def wrapperDirectory = 
layout.buildDirectory.dir('wrapper').get().asFile
         wrapperDirectory.mkdirs()
 
-        def settings = new File(wrapperDirectory, "settings.gradle")
-        new FileOutputStream(settings).close();
+        def settings = new File(wrapperDirectory, 'settings.gradle')
+        settings.createNewFile()
     }
 
-    finalizedBy 'copyGradleWrapper'
+    finalizedBy copyGradleWrapper
 }
 
-task addWrappers(dependsOn: [addGradleWrapper]) {
+tasks.register('addWrappers') {
+    group = 'build setup'
+    description = 'Coordinates wrapper generation tasks'
+
+    dependsOn addGradleWrapper
 }
 
-task processFiltered(type: Copy) {
-    ext.srcDir = file('src/main/resources-filtered')
+tasks.register('processFiltered', Copy) {
+    group = 'build'
+    description = 'Processes filtered resources with token replacement'
 
+    def srcDir = layout.projectDirectory.dir('src/main/resources-filtered')
     inputs.dir srcDir
-    outputs.dir sourceSets.main.output.resourcesDir
+    outputs.dir layout.buildDirectory.dir('resources/main')
 
     from srcDir
-    into sourceSets.main.output.resourcesDir
+    into layout.buildDirectory.dir('resources/main')
 
     filter(FixCrLfFilter)
     filter(ReplaceTokens, tokens: [
@@ -55,7 +60,7 @@ task processFiltered(type: Copy) {
         jacksonVersion: libs.versions.jackson.get(),
         log4jVersion: libs.versions.quickstart.log4j.get(),
         yassonVersion: libs.versions.quickstart.yasson.get(),
-        servletVersion: libs.versions.javax.servlet.api.get(),
+        servletVersion: libs.versions.jakarta.servlet.api.get(),
         mavenCompilerVersion: libs.versions.quickstart.maven.compiler.get(),
         mavenSurefireVersion: libs.versions.quickstart.maven.surefire.get(),
         mavenWarVersion: libs.versions.quickstart.maven.war.get(),
@@ -64,8 +69,10 @@ task processFiltered(type: Copy) {
     ])
 }
 
-processResources.dependsOn([addWrappers, processFiltered])
+tasks.named('processResources') {
+    dependsOn addWrappers, processFiltered
+}
 
-jar {
-    dependsOn(copyGradleWrapper)
-}
\ No newline at end of file
+tasks.named('jar') {
+    dependsOn copyGradleWrapper
+}
diff --git a/tapestry-cdi/build.gradle b/tapestry-cdi/build.gradle
index d40e22e16..1bfbd7a89 100644
--- a/tapestry-cdi/build.gradle
+++ b/tapestry-cdi/build.gradle
@@ -1,5 +1,4 @@
 import org.gradle.plugins.ide.idea.model.*
-import t5build.*
 
 description = "Bridge to CDI for Apache Tapestry 5 Project"
 

Reply via email to