How-To's TeamCity Tips & Tricks

Kotlin DSL for Beginners: Recommended Refactorings

Imagine you have just switched your TeamCity project over to Kotlin DSL. Your builds run successfully, but what next? What small refactorings can you apply to your Kotlin DSL code to help keep it clean and tidy?

In this article, we’ll discuss the next steps you can take.

1. Remove disabled build steps, triggers & requirements

If you used the UI before switching to Kotlin DSL, there is a significant chance your build scripts contain a fair number of disabled settings, like obsolete build steps, triggers, and requirements.

Instead of keeping those settings in your build scripts, let’s simply delete them (you can always restore them from your VCS history if needed).

Build steps & triggers – code example

Build Steps, triggers, and build features have an enabled field, that developers can disable, as shown here:

ant {
   enabled = false
   targets = "run-tests dist"
}
  1. Open the BuildStep or Trigger class, which corresponds to your Kotlin DSL version, in IntelliJ IDEA.
  2. Perform a search for usages of the enabled field.
  3. Delete the found write results with enabled = false.

Build steps & triggers – video

Requirements – code example

Agent requirements can be disabled by calling the disableSettings() method, as shown here:

requirements {
   matches("teamcity.agent.jvm.os.name", ".*dows.*", "RQ_8343")
}

disableSettings("RQ_8343")
  1. Search for usages of those disabledSettings() calls in IntelliJ IDEA.
  2. Delete the calls, including the corresponding requirement, or the whole requirements block if it’s empty after removal.
  3. Note: If the settings come from a template, be sure to check the consequences of enabling/disabling the template feature.

Requirements – video

2. Remove unnecessary parameters

TeamCity’s configuration parameters are often used (via the UI) to avoid the duplication of settings. However, when using Kotlin DSL, you should try removing these config parameters, add them as constants to a Kotlin Singleton object, and then reference the constants.

Config parameters – code example

Before:

params {
   param("DockerImagePostfix", "STAGING")
}

dockerCommand {
   commandType = build {
       // ...
       namesAndTags = "myName-%DockerImagePostfix%:latest"
   }
}

After

enum class DockerImagePostfix {
   DEV, STAGING, PROD
}

dockerCommand {
    commandType = build {
       // ...
       namesAndTags = "myName-${DockerImagePostfix.STAGING}:latest"   
   }
}

Config parameters – video

3. Remove templates

If you are configuring complex projects through the TeamCity UI, you are likely using templates to reuse common build steps, triggers or requirements across your project. Instead of using templates, you can refactor them into easily reusable and traceable extension functions.

Template removal – code example

  1. Decide which template feature (build step, trigger, requirement) you want to migrate. Migrate them one after another, rather than all at once.
    Here’s an example for build steps:
object Build : BuildType({
   templates(CommonSteps, CommonTriggers)
   // …
})

object CommonSteps : Template({
   name = "Common steps"

   steps {
       gradle {
           tasks = "%tasks%"
       }
   }

    params {
       param("tasks", "clean build")
    }

})
  1. Write a Kotlin extension function for your build step (or trigger, or requirement) in a separate Kotlin file, to replicate the behavior of your templated build step. The code could look like this:
    fun BuildSteps.compileProject(init: GradleBuildStep.() -> Unit = {}) {
       gradle {
           name = "Compiles project"
           tasks = "clean build"
    
           init(this)
       }
    }
  2. Use the extension function when creating your BuildTypes:
    object Build : BuildType({
       name = "Build"
    
       steps {
           compileProject()
       }
    })
  3. Once you’ve migrated all your template features, remove the template references in your Kotlin code.

Template removal – video

4. Review agent requirements

Agent requirements can be a pain in large projects. It can make sense to periodically review your requirements and extract them to extension functions for better readability and traceability, just as you do with templates.

Agent requirement – code example

  1. Search for usages of the requirements{} function in your IDE.
object Build : BuildType({
   name = "Build"

   //...

   requirements {
       contains("teamcity.agent.jvm.os.name", "Windows")

   }
})
  1. Review the requirements and refactor commonly used ones into extension functions.
fun Requirements.windows() {
   contains("teamcity.agent.jvm.os.name", "Windows")
}

object Build : BuildType({
   name = "Build"

   requirements {
     windows()
   }
})

Agent requirement – video

5. Bonus step: disable UI editing

As a bonus step, if you are confident enough in your team’s usage of Kotlin DSL, you can even consider disabling editing in the UI for your TeamCity projects by setting the following internal property (Go to: Administration | Server Administration | Diagnostics | Internal Properties):

teamcity.ui.settings.readOnly=true

What’s next

By applying the refactorings from this article, you will already be taking a tremendous step toward keeping your Kotlin DSL code neat and tidy.

Next you could try to make your project completely self-contained, which means getting rid of all absoluteId() references in your Kotlin Scripts.

As this is potentially a larger, more complex undertaking, we’ll cover how to do this in a future blog post.

If you have any questions or feedback about this tutorial, please feel free to send an email to marco@jetbrains.com.

image description