TeamCity conditional build configurations - master vs other branches

TeamCity doesn’t currently support conditional build steps (Vote on this issue). So how can you have different steps for a master builds vs. other branches?

This is not an uncommon scenario - you might want to perform similar build steps for all builds, but for a master build there may be some extra steps (eg. publishing NuGet/NPM packages to a repository, triggering an external activity) that you don’t want to run for a branch build (eg. a build for a pull request).

It is possible to work around this limitation by creating multiple build configurations - one for master and another for non-master branches. But how can you do that efficiently? One approach is to enable Versioned Settings using Kotlin.

Kotlin is a language created by JetBrains that targets the JVM (and is now the recommended language for Android development). But it’s also one of the language choices when you enable Versioned Settings in TeamCity. (The other is XML, which will look familiar if you’ve ever played with TeamCity’s meta-runners).

I’m not a Kotlin expert, but I managed to figure out that you can use it to generate the different variations, such that when TeamCity parses the Kotlin, it ends up creating multiple build configurations.

One thing to be aware of is that TeamCity parses the Kotlin up-front to create the build configurations. It doesn’t evaluate it as the build runs (so it isn’t possible to have an expression that tests an environment variable or parameter)

The following example shows generating two build configurations - “Build_CI_Master” and “Build_CI_Branches”. The difference is that the Master build configuration enables package indexing. The build configurations use different branch filters to control whether they apply to master or non-master branches.

import jetbrains.buildServer.configs.kotlin.v2018_2.*
import jetbrains.buildServer.configs.kotlin.v2018_2.buildFeatures.nuGetPackagesIndexer
import jetbrains.buildServer.configs.kotlin.v2018_2.buildSteps.PowerShellStep
import jetbrains.buildServer.configs.kotlin.v2018_2.buildSteps.powerShell
import jetbrains.buildServer.configs.kotlin.v2018_2.projectFeatures.nuGetFeed
import jetbrains.buildServer.configs.kotlin.v2018_2.triggers.vcs
import jetbrains.buildServer.configs.kotlin.v2018_2.ui.*

version = "2018.2"

val variations = listOf("Master", "Branches")
project {

    for (type in variations) {
        buildType(KotlinExample(type))
    }

    features {
        nuGetFeed {
            id = "repository-nuget-project_feed"
            name = "project_feed"
            description = ""
        }
    }
}

class KotlinExample(val variant: String) : BuildType({
    id("Build_CI_${variant}".toId())

    name = "CI $variant"

    buildNumberPattern = "1.0.%build.counter%"

    vcs {
        add(DslContext.settingsRoot.id!!)
    }

    triggers {
        add {
            vcs {
                if (variant == "Master") {
                    branchFilter = "+:<default>"
                } else {
                    branchFilter = """
                            +:*
                            -:<default>
                        """.trimIndent()
                }
            }
        }
    }

    steps {

        step {
            /* additional steps */
        }
    }

    features {
        if (variant == "Master") {
            nuGetPackagesIndexer {
                feed = "project_feed/project_feed"
            }
        }
    }
})

Written on June 21, 2019