Stephen A. Fuqua (saf)

a Bahá'í, software engineer, and nature lover in Austin, Texas, USA

Splitting TeamCity Kotlin Into Multiple Files

Motivation

I don’t like having a single large file for a TeamCity project, which is the default when exporting a project. It violates the Single Responsibility Principle (SRP). For maintenance, I would rather find each element of interest — whether a sub-project, template, build step, or vcs root — in its own small file, so that I don’t have to hunt inside a large file. And I would rather add new files than modify existing ones.

Is This a Good Idea?

This note about non-portable DSL explains the basic structure when you want to use multiple files. And yet I never noticed it while hunting in detail for help on this topic a week ago; only just stumbled on it while writing this blog piece. It seems to imply that using multiple files is “non-portable,” but apparently I have been using the portable DSL: “The portable format does not require specifying the uuid”, which I’ve not been doing.

There is a small risk that I could do something drastic and lose my build history without a uuid. Since I also have server backups, I’m not too worried. And in all of my experiments I’ve not been able to find any problems with this approach so far.

Starting Point

The official help page has the following sample settings.kts file:

import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script

version = "2020.1"

project {
  buildType {
    id("HelloWorld")
    name = "Hello world"
    steps {
        script {
            scriptContent = "echo 'Hello world!'"
        }
    }
  }
}

File Structure

An approach to splitting this could result in the following structure:

.teamcity directory
|-- _self
   |-- buildTypes
      |-- EchoHelloWorld.kt
   |-- Project.kt
|-- pom.xmls
|-- settings.kts

Some conventions to note here:

  • Per the Kotlin Coding Conventions, the directory names correspond to packages, the packages are named with camelCase rather than PascalCase, but the file / class name is in PascalCase.
  • Whereas the single file has the Kotlin script extension .kts, the individual files have plain .kt, except for settings.kts.
  • Root-level project files are in the _self directory. The TeamCity help pages mention this as _Self, but I prefer _self as it reinforces the Kotlin coding convention.
  • When converting from a single portable script to multiple scripts, be sure to set the package name correctly at the top of each file. Otherwise you will likely trip yourself up with compilation errors, unless you explicitly reference the package name in an import.

The individual files are shown below, not including pom.xml; there is no reason to modify it. Note the package imports section, containing both local packages and the jetbrains packages.

EchoHelloWorld.kt

package _self.buildTypes

import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script

object EchoHelloWorld : BuildType ({
    id("HelloWorld")
    name = "Hello world"

    steps {
        script {
            scriptContent = "echo 'Hello world!'"
        }
    }
})

Project.kt

I could have named this HelloWorldProject.kt, but Project.kt is short, simple, and unambiguous in the root of the Self directory.

When bringing the project definition over from the settings file, it needs to be converted into a class, so we replace project { ...} with a class declaration that inherits from the JetBrains Project class, as you see in the snippet below. We are creating this in the _self directory, so we also need to declare that our class is in the _self package, and we need to import appropriate packages from JetBrains. Note that the first import statement came directly from the settings file, but the second one was not there. We need to add this second import in order for the file to know what the Project class is.

package _self

import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.Project

object HelloWorldProject : Project({
    buildType(_self.buildTypes.EchoHelloWorld)
})

settings.kts

import jetbrains.buildServer.configs.kotlin.v2019_2.*

version = "2020.1"
project(_self.HelloWorldProject)

Enriching with a VCS Root

To further demonstrate, let’s add a new file defining a Git VCS root.

.teamcity directory
|-- _self
   |-- buildTypes
      |-- EchoHelloWorld.kt
   |-- vcsRoots
      |-- HelloWorldRepo.kt
   |-- Project.kt
|-- pom.xml
|-- settings.kts

HelloWorldRepo.kt

See the previous post’s Managing Secure Data section for important information on the accessToken variable. Note that the GitHub organization name is specified as a variable — allowing a developer to test in a fork (substitute user’s username for organization) before submitting a pull request.

package installer.vcsRoots

import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot

object HelloWorldRepo : GitVcsRoot({
    name = "Hello-World"
    url = "https://github.com/%github.organization%/Hello-World.git"
    branch = "%git.branch.default%"
    userNameStyle = GitVcsRoot.UserNameStyle.NAME
    checkoutSubmodules = GitVcsRoot.CheckoutSubmodules.IGNORE
    serverSideAutoCRLF = true
    useMirrors = false
    authMethod = password {
        userName = "%github.username%"
        password = "%github.accessToken%"
    }
})

Next Steps

Hoping to cover in a future post…

  • Templates are just specialized BuildTypes.
  • Build Features
  • Generate XML for further validation

Posted with : DevOps Tools and Practices, Tech, Software Development Life Cycle