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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ on how to do that, including how to develop and test locally and the versioning

## Release Notes

### TBD
*Released*: TBD
(Earliest compatible LabKey version: 26.7.0)
- Change logic for getting properties on TeamCity to be compatible with Gradle 10
- More small updates in `CreateModule`, `InstallRPackage`, `RestoreFromTrash` and `SetUpProperties` for compatibility
- Add `listExternalDependencies` task to help in converting `showDiscrepancies` to be compatible with Gradle 10
- Update to Gradle 9.8.0
- Update `getStandardVCSProperties` to use non-deprecated command execution method
- Update check for `isOnTeamCity` to be more reliable
- Add new `runClientLibBuilds` task so clientLibs are automatically built if the enlistment exists

### 11.0.0
*Released*: 15 September 2026
(Earliest compatible LabKey version: 26.7.0)
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dependencies {
}

group = 'org.labkey.build'
project.version = "11.1.0-SNAPSHOT"
project.version = "11.1.0-teamCityProps-SNAPSHOT"

gradlePlugin {
plugins {
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Expand Down
18 changes: 18 additions & 0 deletions src/main/groovy/org/labkey/gradle/plugin/LabKey.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package org.labkey.gradle.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.labkey.gradle.plugin.extension.LabKeyExtension
import org.labkey.gradle.task.ListExternalDependencies
import org.labkey.gradle.util.GroupNames
import org.labkey.gradle.util.ModuleFinder
import org.labkey.gradle.util.BuildUtils

Expand All @@ -30,6 +32,7 @@ class LabKey implements Plugin<Project>
public static final String SOURCES_CLASSIFIER = "sources"
public static final String JAVADOC_CLASSIFIER = "javadoc"
public static final String FAT_JAR_CLASSIFIER = "all"
public static final String LIST_EXTERNAL_DEPENDENCIES_TASK = "listExternalDependencies"

@Override
void apply(Project project)
Expand All @@ -44,6 +47,7 @@ class LabKey implements Plugin<Project>
}

addConfigurations(project)
addTasks(project)

LabKeyExtension labKeyExt = project.extensions.create("labkey", LabKeyExtension)
labKeyExt.setDirectories(project)
Expand Down Expand Up @@ -72,6 +76,20 @@ class LabKey implements Plugin<Project>
project.configurations.remotePipelineJars.setDescription("Dependencies required for running remote pipeline jobs")

}

private static void addTasks(Project project)
{
project.tasks.register(LIST_EXTERNAL_DEPENDENCIES_TASK, ListExternalDependencies) {
ListExternalDependencies task ->
task.group = GroupNames.HELP
task.description = "Lists the external module versions resolved for the 'external' configuration (used by the showDiscrepancies task)"
task.projectPath.set(project.path)
task.moduleVersions.set(project.configurations.named("external")
.flatMap { it.incoming.resolutionResult.rootComponent }
.map { ListExternalDependencies.getModuleVersions(it) })
task.outputFile.set(project.layout.buildDirectory.file("externalDependencies.txt"))
}
}
}


Expand Down
107 changes: 105 additions & 2 deletions src/main/groovy/org/labkey/gradle/plugin/NpmRun.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.DeleteSpec
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskProvider
import org.labkey.gradle.plugin.extension.LabKeyExtension
import org.labkey.gradle.plugin.extension.NpmRunExtension
Expand All @@ -41,8 +42,15 @@ class NpmRun implements Plugin<Project>
public static final String NODE_MODULES_DIR = "node_modules"
public static final String WEBPACK_DIR = "webpack"
public static final String ENTRY_POINTS_FILE = "src/client/entryPoints.js"
public static final String UI_COMPONENTS = "labkey-ui-components"
public static final String UI_PREMIUM = "labkey-ui-premium"
public static final String JS_API = "labkey-api-js"
public static final String CLIENT_API_DIR = "clientAPIs"

private static final String EXTENSION_NAME = "npmRun"
private static final String BUILD_CLIENT_LIBS_TASK = "npmRunBuildClientLibs"
private static final String NODE_PLUGIN_ID = "com.github.node-gradle.node"
private static final String NPM_TASK_CLASS = "com.github.gradle.node.npm.task.NpmTask"

static boolean isApplicable(Project project)
{
Expand All @@ -58,7 +66,7 @@ class NpmRun implements Plugin<Project>
void apply(Project project)
{
// This brings in nodeSetup and npmInstall tasks. See https://github.com/node-gradle/gradle-node-plugin
project.apply plugin: 'com.github.node-gradle.node'
project.apply plugin: NODE_PLUGIN_ID
project.extensions.create(EXTENSION_NAME, NpmRunExtension)

configurePlugin(project)
Expand Down Expand Up @@ -130,7 +138,20 @@ class NpmRun implements Plugin<Project>
task.mustRunAfter "npmInstall"
}

configureBuildTask(project.tasks.named("npm_run_${project.npmRun.buildDev}"))
def npmRunBuildDevTask = project.tasks.named("npm_run_${project.npmRun.buildDev}")
configureBuildTask(npmRunBuildDevTask)
if (project.hasProperty("runClientLibBuilds")) {
List<File> clientLibDirs = getClientLibDirs(project)
TaskProvider<Task> clientLibsTask = getClientLibsBuildTask(project, clientLibDirs)
if (clientLibsTask != null)
npmRunBuildDevTask.configure { Task task ->
task.dependsOn(clientLibsTask)
// Rebuild this module when the library build outputs change
task.inputs.files(clientLibDirs.collect { new File(it, "dist") })
.withPropertyName("clientLibs")
.withPathSensitivity(PathSensitivity.RELATIVE)
}
}
if (BuildUtils.useServerNode(project) && project.path !== BuildUtils.getServerProject(project).path) {
project.tasks.named('npmSetup').configure
{
Expand All @@ -152,6 +173,88 @@ class NpmRun implements Plugin<Project>
TaskUtils.configureTaskIfPresent(project, "processWebappResources", { dependsOn(runCommand) })
}


/**
* @return the client library enlistments under clientAPIs, in dependency order
*/
private static List<File> getClientLibDirs(Project project)
{
return [getJSAPIDir(project), getUIComponentsDir(project), getUIPremiumDir(project)].findAll { it != null }
}

/**
* Returns the root project's task for building the client library enlistments under clientAPIs, registering
* it if this is the first module to ask for it. This way the libraries are built once per build, not once per module.
* The builds themselves are NpmTasks registered in the first module so they use the npm and node from that module's
* node configuration, which the root project doesn't have.
* @return the task provider, or null if there are no client library enlistments
*/
private static TaskProvider<Task> getClientLibsBuildTask(Project project, List<File> clientLibDirs)
{
if (clientLibDirs.isEmpty())
return null

Project rootProject = project.rootProject
if (rootProject.tasks.names.contains(BUILD_CLIENT_LIBS_TASK))
return rootProject.tasks.named(BUILD_CLIENT_LIBS_TASK)

// The node plugin is loaded by the build script, which this plugin's classloader can't see, so get NpmTask from the plugin's classloader
Class<? extends Task> npmTaskClass = project.plugins.getPlugin(NODE_PLUGIN_ID).class.classLoader.loadClass(NPM_TASK_CLASS) as Class<? extends Task>
List<TaskProvider<Task>> libTasks = []
clientLibDirs.each { File libDir ->
List<TaskProvider<Task>> previousLibTasks = new ArrayList<>(libTasks)
libTasks.add(project.tasks.register(getTaskNameFromDirName(libDir.name), npmTaskClass) { Task task ->
task.group = GroupNames.NPM_RUN
task.description = "Runs 'npm run build' in ${libDir}"
task.workingDir.set(libDir)
task.args.set(["run", "build"])
task.dependsOn(previousLibTasks)
task.inputs.files(project.fileTree(dir: libDir, includes: ["src/**/*", NPM_PROJECT_FILE, NPM_PROJECT_LOCK_FILE, TYPESCRIPT_CONFIG_FILE, "package.config.js", "webpack.config.js"]))
.withPropertyName("clientLibSources")
.withPathSensitivity(PathSensitivity.RELATIVE)
task.outputs.dir(new File(libDir, "dist"))
.withPropertyName("clientLibDist")
})
}

return rootProject.tasks.register(BUILD_CLIENT_LIBS_TASK) { Task task ->
task.group = GroupNames.NPM_RUN
task.description = "Runs 'npm run build' in the client library enlistments under ${CLIENT_API_DIR}"
task.dependsOn(libTasks)
}
}

private static String getTaskNameFromDirName(String dirName)
{
switch (dirName) {
case JS_API: return "npmRunBuild_api"
case UI_PREMIUM: return "npmRunBuild_premium"
default: return "npmRunBuild_" + dirName
}
}

static File getUIComponentsDir(Project project)
{
return getClientLibsDir(project, UI_COMPONENTS + "/packages/components")
}

static File getUIPremiumDir(Project project)
{
return getClientLibsDir(project, UI_PREMIUM)
}

static File getJSAPIDir(Project project)
{
return getClientLibsDir(project, JS_API)
}

static File getClientLibsDir(Project project, String clientLibName) {
File file = new File(project.rootProject.rootDir, "${CLIENT_API_DIR}/${clientLibName}")
if (file.exists())
return file
return null
}

private static void addTasks(Project project)
{
if (project.file(NPM_PROJECT_FILE).exists())
Expand Down
32 changes: 20 additions & 12 deletions src/main/groovy/org/labkey/gradle/plugin/TeamCity.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ class TeamCity extends Tomcat

project.tasks.named("stopLabKey").configure {
it.doLast { Task task ->
ensureShutdown(task.logger, debugPort)
TeamCity.ensureShutdown(task.logger, debugPort)
}
}

project.tasks.named("stopTomcat").configure {
it.doLast { Task task ->
ensureShutdown(task.logger, debugPort)
TeamCity.ensureShutdown(task.logger, debugPort)
}
}

Expand All @@ -120,7 +120,7 @@ class TeamCity extends Tomcat
task.group = GroupNames.TEST_SERVER
task.description = "Kill Chrome processes"
task.doLast {
killChrome(it.ant)
TeamCity.killChrome(it.ant)
}
}

Expand All @@ -129,7 +129,7 @@ class TeamCity extends Tomcat
task.group = GroupNames.TEST_SERVER
task.description = "Kill Firefox processes"
task.doLast {
killFirefox(it.ant)
TeamCity.killFirefox(it.ant)
}
}

Expand All @@ -149,13 +149,19 @@ class TeamCity extends Tomcat

project.tasks.register("validateConfiguration") {
Task task ->
// Captured here because the extension (which references the project) cannot be used from a task
// action with the configuration cache
boolean isValid = extension.isValidForTestRun()
String validationMessages = extension.validationMessages.join('; ')
String branchIsDefault = extension.getTeamCityProperty('teamcity.build.branch.is_default')
String branch = extension.getTeamCityProperty('teamcity.build.branch')
task.doFirst
{
if (!extension.isValidForTestRun())
throw new GradleException("TeamCity configuration problem(s): ${extension.validationMessages.join('; ')}")
if (!isValid)
throw new GradleException("TeamCity configuration problem(s): ${validationMessages}")

task.logger.info("teamcity.build.branch.is_default: ${extension.getTeamCityProperty('teamcity.build.branch.is_default')}")
task.logger.info("teamcity.build.branch: ${extension.getTeamCityProperty('teamcity.build.branch')}")
task.logger.info("teamcity.build.branch.is_default: ${branchIsDefault}")
task.logger.info("teamcity.build.branch: ${branch}")
}
}

Expand Down Expand Up @@ -251,7 +257,7 @@ class TeamCity extends Tomcat
task.description = "Run a test suite on the TeamCity server"
task.doLast(
{
killFirefox(task.ant)
TeamCity.killFirefox(task.ant)
}
)
}
Expand All @@ -263,7 +269,9 @@ class TeamCity extends Tomcat
}
}

private static void killChrome(AntBuilder ant)
// Not private and called qualified by class name from task actions because, with the configuration cache, those
// closures are restored without this plugin as their owner, so unqualified calls would be looked up on the task
static void killChrome(AntBuilder ant)
{
if (SystemUtils.IS_OS_WINDOWS)
{
Expand Down Expand Up @@ -293,7 +301,7 @@ class TeamCity extends Tomcat
}
}

private static void killFirefox(AntBuilder ant)
static void killFirefox(AntBuilder ant)
{
if (SystemUtils.IS_OS_WINDOWS)
{
Expand Down Expand Up @@ -432,7 +440,7 @@ class TeamCity extends Tomcat
}
}

private static void ensureShutdown(Logger logger, String debugPort)
static void ensureShutdown(Logger logger, String debugPort)
{
if (!debugPort.isEmpty())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import java.nio.charset.StandardCharsets

class TeamCityExtension
{
public static final String TEAMCITY_VERSION_ENV_VAR = "TEAMCITY_VERSION"

String databaseName
Boolean dropDatabase = false
List<DatabaseProperties> databaseTypes = new ArrayList<>()
Expand All @@ -37,15 +39,40 @@ class TeamCityExtension
setValidationMessages()
}

/**
* TeamCity's Gradle init script declares the 'teamcity' property on the root project. Look for it explicitly
* on the given project and then on the root project, since implicit lookup of properties in parent projects is
* deprecated (and will fail in Gradle 10).
* @param project the project whose TeamCity properties are wanted
* @return the map of TeamCity properties, or null if not running on TeamCity
*/
static Map getTeamCityMap(Project project)
{
def extraProperties = project.extensions.extraProperties
if (extraProperties.has('teamcity'))
return (Map) extraProperties.get('teamcity')
if (project != project.rootProject)
return (Map) project.rootProject.findProperty('teamcity')
return (Map) project.findProperty('teamcity')
}

/**
* TeamCity sets the TEAMCITY_VERSION environment variable for all builds run on its agents. This is more reliable
* than checking for the 'teamcity' property, which TeamCity's Gradle init script does not always populate
* (for example, with some configuration cache setups).
* @param project the current project
* @return true if the build is running on a TeamCity agent
*/
static boolean isOnTeamCity(Project project)
{
return project.hasProperty('teamcity')
return project.providers.environmentVariable(TEAMCITY_VERSION_ENV_VAR).isPresent()
}

static Object getTeamCityProperty(Project project, String name, Object defaultValue)
{
if (isOnTeamCity(project))
return project.teamcity[name] != null ? project.teamcity[name] : defaultValue
Map teamcity = getTeamCityMap(project)
if (teamcity != null)
return teamcity[name] != null ? teamcity[name] : defaultValue
else if (project.hasProperty(name))
return project.property(name)
else
Expand All @@ -54,10 +81,11 @@ class TeamCityExtension

static Properties getTeamCityProperties(Project project)
{
if (isOnTeamCity(project))
Map teamcity = getTeamCityMap(project)
if (teamcity != null)
{
def tcProps = new Properties()
tcProps.putAll(project.teamcity)
tcProps.putAll(teamcity)
return tcProps
}
else
Expand Down Expand Up @@ -109,7 +137,7 @@ class TeamCityExtension
return getTeamCityProperty(project, "tomcatJavaHome", System.getenv("JAVA_HOME"))
}

Boolean isValidForTestRun()
boolean isValidForTestRun()
{
return validationMessages.isEmpty()
}
Expand Down
Loading
Loading