How to automate CI/CD tasks with C# Scripting in TeamCity

The recently released TeamCity 2021.2 introduces a new build runner — C# Script. We’ve created it for users who want to automate their routine CI/CD operations using the familiar syntax of C# and all its scripting conveniences.

TL;DR: The runner’s main advantages are:

– It’s tightly integrated with TeamCity and supports all its handy features like a detailed build log and parameterized build properties.
– It’s platform-independent and can run inside a Docker container.
– It can automatically restore NuGet packages mentioned in your scripts.
– It can access public, private, and TeamCity-internal NuGet feeds.

In this post, we explore the details of the runner’s implementation for those curious to learn what’s under its hood. To have a little practice, we go through two tutorials: basic “Hello World” and advanced Polling bot for Telegram.

Why use C# to automate tasks in .NET projects

Windows system administrators usually automate their tasks with PowerShell. PowerShell is based on .NET Core and supports all the common lightweight commands and APIs.

However, it uses a different syntax than C# — the language to go when writing for the .NET platform. And, if you create programs for .NET, it seems rational to use a single syntax for developing a program and for scripting its CI/CD processes.

This article covers the essentials of C# scripting.

Our new C# Script runner serves as a straightforward launcher of C# scenarios in your TeamCity builds.

C# Script runner’s requirements

The runner has two requirements:

  • .NET 6.0 Runtime must be available on the build agent. The easiest approach is to use a Docker container that already has it on board (but make sure to preinstall Docker on the agent(s)), or you can install it manually.
    Note that 6.0 is currently in the Beta stage. As soon as it is officially released, we will update our custom tool to support it.
  • Our custom C# Interactive shell must be distributed to build agents. If you use TeamCity On-Premises, you need to install it as an agent tool. TeamCity Cloud instances receive it automatically.  

See what features it adds above the regular C# scripting.

Tutorial: Run Hello World script

Without further ado, let’s launch a simple script with the new runner:

  1. Create a build configuration in your TeamCity 2021.2 instance and add a C# Script build step to it.
  2. You can enter a C# script’s body directly in the step settings, or specify a path to a .csx file on the build agent. In this tutorial, we’ll enter code that reports “Hello World from project <Name>” to the build log:
WriteLine("Hello World from project " + Args[0])

Here, Args represents the array of optional parameters of the script, whose values we will specify in Step 3.

  1. We only need one argument this time: enter %system.teamcity.projectName% into the Script parameters field. This predefined parameter references the name of the current TeamCity project.
    This field supports both predefined and custom parameters.
  1. We will run this step inside a Docker container — let’s define the target image name in the Docker Settings field: mcr.microsoft.com/dotnet/runtime:6.0.

Here’s how the build step settings look like:

Save the step, and you can run the build right away! Check its build log to see that the “Hello World from ProjectName” line was reported to it:

Configure C# Script build step with Kotlin DSL

If you prefer configuring TeamCity projects with Kotlin DSL, we’ve got you covered. Here’s the code for the sample build configuration described above:

object HelloWorldBuildType: BuildType({
  name = "Say hello"
  steps {
    csharpScript {
      content = "WriteLine(\"Hello World from project \" + Args[0])"
      arguments = "\"%system.teamcity.projectName%\""
      dockerImage = Settings.dockerImageRuntime
      dockerImagePlatform = CSharpScriptCustomBuildStep.ImagePlatform.Linux

It uses the following parameters:

  • content: the content of the C# script field, that is the script code itself.
  • arguments: the content of the Script parameters field. It defines optional parameters of the script. In our case, it’s %system.teamcity.projectName%, which is the name of the current project.
  • dockerImage: the content of the Run step within Docker container field. We are about to run this step inside a Docker container, and this parameter defines the name of the Docker image to use.
  • dockerImagePlatform: the value of the Docker image platform field. It states that TeamCity should prefer a Linux container whenever possible.

Automatic restore of NuGet packages

Apart from running C# scenarios across different platforms, the runner offers one more convenience: it can automatically restore necessary NuGet packages. It detects all uses of #r "nuget: ..." directives inside your script and fetches the respective packages to the build agent.

By default, the runner restores packages from NuGet.org, but you can specify other sources as a space-separated list in the NuGet package sources field. The runner also supports private and TeamCity-internal feeds.

TeamCity custom C# Interactive tool

To support all the platforms where you can run TeamCity builds (Windows, Linux, and macOS), we had to extend the regular functionality of the C# Interactive shell. The result is our custom tool that implements the unique capabilities of the runner.

This tool is designed with TeamCity specifics in mind and offers the following advantages compared to the default Microsoft solution:

  • It’s platform-independent.
  • It can access private and TeamCity-internal NuGet feeds.
  • It provides an extended REPL (Read, Evaluate, Print Loop) r# command to restore NuGet packages.

The main purpose of the tool is to provide necessary functionality to the C# Script runner, but you can also use it independently from TeamCity: as a simple command line or an interactive shell. Let’s see how to do this!

Use custom C# Interactive outside TeamCity

Install the tool on your machine:

dotnet tool install TeamCity.csi -g --version <version>

where <version> is the package version (using latest is recommended), from its NuGet repo.

Run a script from a command line, on any supported OS:

dotnet csi <scriptName>.csx

Start the interactive mode:

dotnet csi

You can find the supported arguments in the tool’s README. We plan to update and extend the custom shell in the upcoming updates. Our nearest goal is to provide tighter integration with TeamCity and cover more common use cases. All the new features will be also mentioned in the README. If you have any ideas or propositions, you are welcome to submit issues and PRs to this repo.

Tutorial: run Telegram bot to approve build

For a more advanced tutorial, we’ve created a Telegram bot that can notify developers that the new library version is compiled and gather votes of approval from them. If the compiled library is approved, TeamCity will increment its version and upload it to the target NuGet repository.

The build configuration comprises 6 steps:

  1. Launches a C# script that analyzes the latest version of the MySampleLib package and calculates the potential next version number.
  2. Compiles the next version of the MySampleLib library from the freshest sources.
  3. Tests this library.
  4. Launches a new poll via a Telegram bot. The bot will (1) notify all its subscribers about the new build and (2) create a poll that allows voting if the library produced in Step 2 is ready to be released.
  5. If all expected positive votes are received during the 30 minutes after the poll is created, creates a new MySampleLib package with the new version. Otherwise, fails the build.
  6. Publishes the lib package to NuGet.org, thus updating the previous version.

Let’s look closer at steps 1 and 4 that use the new C# Script runner.

Step 1: Calculate next library version

This step launches the following script:

using System.Linq;
Props["version"] = 
  .Restore(Args[0], "*", "net6.0")
  .Where(i => i.Name == Args[0])
  .Select(i => i.Version)
  .Select(i => new Version(i.Major, i.Minor, i.Build + 1))
  .DefaultIfEmpty(new Version(1, 0, 0))
WriteLine($"Version: {Props["version"]}", Success);
  • Line 3: we call the global function GetService to get the API required for working with NuGet.
  • Line 4: we restore the MySampleLib package. We refer to the global array Args which contains the values entered in the Script arguments field. In our case, we need to enter only one element — %demo.package%, which is the name of the package.
  • Line 7: we calculate the next version of the package by incrementing the current latest one. The result will be stored to the global dictionary Props specified in line 4, with the index version. As the dictionary is global, this value will be available to all the following steps of the build — in other C# scripts and .NET commands. It works similarly to passing this as a command-line argument: -p:version=1.0.9. Moreover, this value will be available as the system.version system parameter in TeamCity, so you can refer to it via %system.version%.
  • Line 11: we report the calculated version of the MySampleLib package to the build log (Success declares that it will be highlighted with green color), for diagnostics.

Step 4. Launch Telegram poll

This step launches the script file that:

  1. Gets a token for the Telegram bot and the value of the expected duration of the vote. We define these system parameters in the parent project in TeamCity.
    Get the token and timeout values:
if(!Props.TryGetValue("telegram.bot.token", out var token))
    throw ...

if(!Props.TryGetValue("telegram.bot.poll.timeout", out var timeoutStr) ||
   !TimeSpan.TryParse(timeoutStr, out var timeout))
    throw ...
  1. Launches a Telegram bot for voting using the libraries from the Telegram.Bot and Telegram.Bot.Extensions.Polling packages.
    Restore the packages:
#r "nuget: Telegram.Bot, 15.6.0"
#r "nuget: Telegram.Bot.Extensions.Polling, 0.2.0"
  1. In advance, every subscriber should execute the /start command to activate the bot. During the build, TeamCity will report the link to the build to the bot’s channel. The vote starts and lasts 30 minutes.
  1. If all members of the channel approve the build, it reports the names of the voters to the build log, creates a package from the build, and publishes it to the NuGet repository with the incremented version.
    Otherwise, if at least one person votes against it or 30 minutes expire, it sends the respective Error report and fails the build (in the former case, lists the names of users who voted against publishing).


Our new C# Script runner is a solution for the developers and administrators who need to automate certain stages of the builds and (1) want to rely on a familiar C# syntax or/and (2) use the logic from .NET libraries and NuGet packages.

If you have already tried this runner, we are eager to read your feedback in the comments. Stay tuned for the upcoming updates!


Happy building!

image description