This is a repository of TeamCity.csi which is an interactive tool for running C# scripts. It can be used as a TeamCity build runner or installed as a command-line tool on Windows, Linux, or macOS.
The tool requires .NET 6+ runtime.
Currently, the tool can be used as a TeamCity build runner provided in terms of TeamCity 2021.2 Early Access Program. Read the runner's documentation for more details.
After installing tool you can use this tool independently of TeamCity, to run C# scripts from the command line. TeamCity.csi is available as a NuGet package.
Before installing TeamCity.csi as a local tool dot not forget to create .NET local tool manifest file if it is not exist:
dotnet new tool-manifest
Install the tool and add to the local tool manifest:
dotnet tool install TeamCity.csi
Or install the tool for the current user:
dotnet tool install TeamCity.csi -g
Launch the tool in the interactive mode:
dotnet csi
Run a specified script with a given argument:
dotnet csi Samples/Scripts/hello.csx World
Run a single script located in the MyDirectory directory:
dotnet csi Samples/Build
Usage:
dotnet csi [options] [--] [script] [script arguments]
Executes a script if specified, otherwise launches an interactive REPL (Read Eval Print Loop).
Supported arguments:
Option | Description | Alternative form |
---|---|---|
script | The path to the script file to run. If no such file is found, the command will treat it as a directory and look for a single script file inside that directory. | |
script arguments | Script arguments are accessible in a script via the global list Args[index] by an argument index. | |
-- | Indicates that the remaining arguments should not be treated as options. | |
--help | Show how to use the command. | /? , -h , /h , /help |
--version | Display the tool version. | /version |
--source | Specify the NuGet package source to use. Supported formats: URL, or a UNC directory path. | -s , /s , /source |
--property <key=value> | Define a key-value pair(s) for the script properties called Props, which is accessible in scripts. | -p , /property , /p |
--property:<key=value> | Define a key-value pair(s) in MSBuild style for the script properties called Props, which is accessible in scripts. | -p:<key=value> , /property:<key=value> , /p:<key=value> , --property:key1=val1;key2=val2 |
@file | Read the response file for more options. |
using HostApi;
directive in a script allows you to use host API types without specifying the fully qualified namespace of these types.
Install the C# script template TeamCity.CSharpInteractive.Templates
dotnet new -i TeamCity.CSharpInteractive.Templates
Create a console project "Build" containing a script from the template build
dotnet new build -o ./Build
This projects contains the script ./Build/Program.csx. To run this script from the command line from the directory Build:
dotnet csi Build
To run this script as a console application:
dotnet run --project Build
Open the ./Build/Build.csproj in IDE and debug the script.
Please use our YouTrack to report related issues.
- Global state
- Logging
- Command Line API
- Docker API
- .NET build API
- Build a project
- Build a project using MSBuild
- Clean a project
- Pack a project
- Publish a project
- Restore a project
- Restore local tools
- Run a custom .NET command
- Run a project
- Run tests under dotCover
- Test a project
- Test a project using the MSBuild VSTest target
- Test an assembly
- Shuts down build servers
- NuGet API
- TeamCity Service Messages API
Args have got from the script arguments.
if (Args.Count > 0)
{
WriteLine(Args[0]);
}
if (Args.Count > 1)
{
WriteLine(Args[1]);
}
Properties Props have got from TeamCity system properties automatically.
WriteLine(Props["TEAMCITY_VERSION"]);
WriteLine(Props["TEAMCITY_PROJECT_NAME"]);
// This property will be available at the next TeamCity steps as system parameter _system.Version_
// and some runners, for instance, the .NET runner, pass it as a build property.
Props["Version"] = "1.1.6";
Host is actually the provider of all global properties and methods.
var packages = Host.GetService<INuGet>();
Host.WriteLine("Hello");
This method might be used to get access to different APIs like INuGet or ICommandLine.
GetService<INuGet>();
var serviceProvider = GetService<IServiceProvider>();
serviceProvider.GetService(typeof(INuGet));
Besides that, it is possible to get an instance of System.IServiceProvider to access APIs.
public void Run()
{
var serviceProvider =
GetService<IServiceCollection>()
.AddTransient<MyTask>()
.BuildServiceProvider();
var myTask = serviceProvider.GetRequiredService<MyTask>();
var exitCode = myTask.Run();
exitCode.ShouldBe(0);
}
class MyTask
{
private readonly ICommandLineRunner _runner;
public MyTask(ICommandLineRunner runner) =>
_runner = runner;
public int? Run() =>
_runner.Run(new CommandLine("whoami"));
}
WriteLine("Hello");
WriteLine();
WriteLine("Hello", Header);
Error("Error info", "Error identifier");
Warning("Warning info");
Info("Some info");
Trace("Some trace info");
// Adds the namespace "Script.Cmd" to use Command Line API
using HostApi;
// Creates and run a simple command line
"whoami".AsCommandLine().Run();
// Creates and run a simple command line
new CommandLine("whoami").Run();
// Creates and run a command line with arguments
new CommandLine("cmd", "/c", "echo", "Hello").Run();
// Same as previous statement
new CommandLine("cmd", "/c")
.AddArgs("echo", "Hello")
.Run();
(new CommandLine("cmd") + "/c" + "echo" + "Hello").Run();
("cmd".AsCommandLine("/c", "echo", "Hello")).Run();
("cmd".AsCommandLine() + "/c" + "echo" + "Hello").Run();
// Just builds a command line with multiple environment variables
var cmd = new CommandLine("cmd", "/c", "echo", "Hello")
.AddVars(("Var1", "val1"), ("var2", "Val2"));
// Same as previous statement
cmd = new CommandLine("cmd") + "/c" + "echo" + "Hello" + ("Var1", "val1") + ("var2", "Val2");
// Builds a command line to run from a specific working directory
cmd = new CommandLine("cmd", "/c", "echo", "Hello")
.WithWorkingDirectory("MyDyrectory");
// Builds a command line and replaces all command line arguments
cmd = new CommandLine("cmd", "/c", "echo", "Hello")
.WithArgs("/c", "echo", "Hello !!!");
// Adds the namespace "HostApi" to use Command Line API
using HostApi;
var exitCode = GetService<ICommandLineRunner>().Run(new CommandLine("cmd", "/c", "DIR"));
exitCode.ShouldBe(0);
// or the same thing using the extension method
exitCode = new CommandLine("cmd", "/c", "DIR").Run();
exitCode.ShouldBe(0);
// using operator '+'
var cmd = new CommandLine("cmd") + "/c" + "DIR";
exitCode = cmd.Run();
exitCode.ShouldBe(0);
// with environment variables
cmd = new CommandLine("cmd") + "/c" + "DIR" + ("MyEnvVar", "Some Value");
exitCode = cmd.Run();
exitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use Command Line API
using HostApi;
int? exitCode = await GetService<ICommandLineRunner>().RunAsync(new CommandLine("cmd", "/C", "DIR"));
// or the same thing using the extension method
exitCode = await new CommandLine("cmd", "/c", "DIR").RunAsync();
// Adds the namespace "HostApi" to use Command Line API
using HostApi;
var lines = new List<string>();
int? exitCode = new CommandLine("cmd", "/c", "SET")
.AddVars(("MyEnv", "MyVal"))
.Run(output => lines.Add(output.Line));
lines.ShouldContain("MyEnv=MyVal");
// Adds the namespace "HostApi" to use Command Line API
using HostApi;
Task<int?> task = new CommandLine("cmd", "/c", "DIR").RunAsync();
int? exitCode = new CommandLine("cmd", "/c", "SET").Run();
task.Wait();
The cancellation will kill a related process.
// Adds the namespace "HostApi" to use Command Line API
using HostApi;
var cancellationTokenSource = new CancellationTokenSource();
Task<int?> task = new CommandLine("cmd", "/c", "TIMEOUT", "/T", "120")
.RunAsync(default, cancellationTokenSource.Token);
cancellationTokenSource.CancelAfter(TimeSpan.FromMilliseconds(100));
task.IsCompleted.ShouldBeFalse();
If timeout expired a process will be killed.
// Adds the namespace "HostApi" to use Command Line API
using HostApi;
int? exitCode = new CommandLine("cmd", "/c", "TIMEOUT", "/T", "120")
.Run(default, TimeSpan.FromMilliseconds(1));
exitCode.HasValue.ShouldBeFalse();
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force"
var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build();
result.ExitCode.ShouldBe(0);
// Builds the library project, running a command like: "dotnet build" from the directory "MyLib"
result = new DotNetBuild().WithWorkingDirectory("MyLib").Build();
// The "result" variable provides details about a build
result.Errors.Any(message => message.State == BuildMessageState.StdError).ShouldBeFalse();
result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force"
var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build();
result.ExitCode.ShouldBe(0);
// Builds the library project, running a command like: "dotnet build" from the directory "MyLib"
result = new DotNetBuild().WithWorkingDirectory("MyLib").Build();
result.ExitCode.ShouldBe(0);
// Clean the project, running a command like: "dotnet clean" from the directory "MyLib"
result = new DotNetClean().WithWorkingDirectory("MyLib").Build();
// The "result" variable provides details about a build
result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Gets the dotnet version, running a command like: "dotnet --version"
NuGetVersion? version = default;
var exitCode = new DotNetCustom("--version")
.Run(message => NuGetVersion.TryParse(message.Line, out version));
exitCode.ShouldBe(0);
version.ShouldNotBeNull();
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force"
var result = new DotNetNew("mstest", "-n", "MyTests", "--force").Build();
result.ExitCode.ShouldBe(0);
// Runs tests via a command like: "dotnet msbuild /t:VSTest" from the directory "MyTests"
result = new MSBuild()
.WithTarget("VSTest")
.WithWorkingDirectory("MyTests").Build();
// The "result" variable provides details about a build
result.ExitCode.ShouldBe(0);
result.Summary.Tests.ShouldBe(1);
result.Tests.Count(test => test.State == TestState.Passed).ShouldBe(1);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force"
var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build();
result.ExitCode.ShouldBe(0);
// Creates a NuGet package of version 1.2.3 for the project, running a command like: "dotnet pack /p:version=1.2.3" from the directory "MyLib"
result = new DotNetPack()
.WithWorkingDirectory("MyLib")
.AddProps(("version", "1.2.3"))
.Build();
result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force"
var result = new DotNetNew("classlib", "-n", "MyLib", "--force", "-f", "net6.0").Build();
result.ExitCode.ShouldBe(0);
// Publish the project, running a command like: "dotnet publish --framework net6.0" from the directory "MyLib"
result = new DotNetPublish().WithWorkingDirectory("MyLib").WithFramework("net6.0").Build();
result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force"
var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build();
result.ExitCode.ShouldBe(0);
// Restore the project, running a command like: "dotnet restore" from the directory "MyLib"
result = new DotNetRestore().WithWorkingDirectory("MyLib").Build();
result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new console project, running a command like: "dotnet new console -n MyApp --force"
var result = new DotNetNew("console", "-n", "MyApp", "--force").Build();
result.ExitCode.ShouldBe(0);
// Runs the console project using a command like: "dotnet run" from the directory "MyApp"
var stdOut = new List<string>();
result = new DotNetRun().WithWorkingDirectory("MyApp").Build(message => stdOut.Add(message.Text));
result.ExitCode.ShouldBe(0);
// Checks StdOut
stdOut.ShouldBe(new[] {"Hello, World!"});
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force"
var result = new DotNetNew("mstest", "-n", "MyTests", "--force").Build();
result.ExitCode.ShouldBe(0);
// Runs tests via a command like: "dotnet test" from the directory "MyTests"
result = new DotNetTest().WithWorkingDirectory("MyTests").Build();
// The "result" variable provides details about a build
result.ExitCode.ShouldBe(0);
result.Summary.Tests.ShouldBe(1);
result.Tests.Count(test => test.State == TestState.Passed).ShouldBe(1);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force"
var exitCode = new DotNetNew("mstest", "-n", "MyTests", "--force").Run();
exitCode.ShouldBe(0);
// Creates the tool manifest and installs the dotCover tool locally
// It is better to run the following 2 commands manually
// and commit these changes to a source control
exitCode = new DotNetNew("tool-manifest").Run();
exitCode.ShouldBe(0);
exitCode = new DotNetCustom("tool", "install", "--local", "JetBrains.dotCover.GlobalTool").Run();
exitCode.ShouldBe(0);
// Creates a test command
var test = new DotNetTest().WithProject("MyTests");
var dotCoverSnapshot = Path.Combine("MyTests", "dotCover.dcvr");
var dotCoverReport = Path.Combine("MyTests", "dotCover.html");
// Modifies the test command by putting "dotCover" in front of all arguments
// to have something like "dotnet dotcover test ..."
// and adding few specific arguments to the end
var testUnderDotCover = test.Customize(cmd =>
cmd.ClearArgs()
+ "dotcover"
+ cmd.Args
+ $"--dcOutput={dotCoverSnapshot}"
+ "--dcFilters=+:module=TeamCity.CSharpInteractive.HostApi;+:module=dotnet-csi"
+ "--dcAttributeFilters=System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage");
// Runs tests under dotCover via a command like: "dotnet dotcover test ..."
var result = testUnderDotCover.Build();
// The "result" variable provides details about a build
result.ExitCode.ShouldBe(0);
result.Tests.Count(test => test.State == TestState.Passed).ShouldBe(1);
// Generates a HTML code coverage report.
exitCode = new DotNetCustom("dotCover", "report", $"--source={dotCoverSnapshot}", $"--output={dotCoverReport}", "--reportType=HTML").Run();
exitCode.ShouldBe(0);
// Check for a dotCover report
File.Exists(dotCoverReport).ShouldBeTrue();
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
var projectDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()[..4]);
Directory.CreateDirectory(projectDir);
// Creates a local tool manifest
var exitCode = new DotNetNew("tool-manifest").WithWorkingDirectory(projectDir).Run();
exitCode.ShouldBe(0);
// Restore local tools
exitCode = new DotNetToolRestore().WithWorkingDirectory(projectDir).Run();
exitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new test project, running a command like: "dotnet new mstest -n MyTests --force"
var result = new DotNetNew("mstest", "-n", "MyTests", "--force").Build();
result.ExitCode.ShouldBe(0);
// Builds the test project, running a command like: "dotnet build -c Release" from the directory "MyTests"
result = new DotNetBuild().WithWorkingDirectory("MyTests").WithConfiguration("Release").WithOutput("MyOutput").Build();
result.ExitCode.ShouldBe(0);
// Runs tests via a command like: "dotnet vstest" from the directory "MyTests"
result = new VSTest()
.AddTestFileNames(Path.Combine("MyOutput", "MyTests.dll"))
.WithWorkingDirectory("MyTests")
.Build();
// The "result" variable provides details about a build
result.ExitCode.ShouldBe(0);
result.Summary.Tests.ShouldBe(1);
result.Tests.Count(test => test.State == TestState.Passed).ShouldBe(1);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Creates a new library project, running a command like: "dotnet new classlib -n MyLib --force"
var result = new DotNetNew("classlib", "-n", "MyLib", "--force").Build();
result.ExitCode.ShouldBe(0);
// Builds the library project, running a command like: "dotnet msbuild /t:Build -restore /p:configuration=Release -verbosity=detailed" from the directory "MyLib"
result = new MSBuild()
.WithWorkingDirectory("MyLib")
.WithTarget("Build")
.WithRestore(true)
.AddProps(("configuration", "Release"))
.WithVerbosity(DotNetVerbosity.Detailed)
.Build();
// The "result" variable provides details about a build
result.Errors.Any(message => message.State == BuildMessageState.StdError).ShouldBeFalse();
result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use .NET build API
using HostApi;
// Shuts down all build servers that are started from dotnet.
var exitCode = new DotNetBuildServerShutdown().Run();
exitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use INuGet
using HostApi;
IEnumerable<NuGetPackage> packages = GetService<INuGet>().Restore(new NuGetRestoreSettings("IoC.Container").WithVersionRange(VersionRange.All));
// Adds the namespace "HostApi" to use INuGet
using HostApi;
var packagesPath = Path.Combine(
Path.GetTempPath(),
Guid.NewGuid().ToString()[..4]);
var settings = new NuGetRestoreSettings("IoC.Container")
.WithVersionRange(VersionRange.Parse("[1.3, 1.3.8)"))
.WithTargetFrameworkMoniker("net5.0")
.WithPackagesPath(packagesPath);
IEnumerable<NuGetPackage> packages = GetService<INuGet>().Restore(settings);
// Adds the namespace "HostApi" to use .NET build API and Docker API
using HostApi;
// Creates a base docker command line
var dockerRun = new DockerRun()
.WithAutoRemove(true)
.WithImage("mcr.microsoft.com/dotnet/sdk")
.WithPlatform("linux")
.WithContainerWorkingDirectory("/MyProjects")
.AddVolumes((Environment.CurrentDirectory, "/MyProjects"));
// Creates a new library project in a docker container
var exitCode = dockerRun
.WithCommandLine(new DotNetCustom("new", "classlib", "-n", "MyLib", "--force"))
.Run();
exitCode.ShouldBe(0);
// Builds the library project in a docker container
var result = dockerRun
.WithCommandLine(new DotNetBuild().WithProject("MyLib/MyLib.csproj"))
.Build();
// The "result" variable provides details about a build
result.Errors.Any(message => message.State == BuildMessageState.StdError).ShouldBeFalse();
result.ExitCode.ShouldBe(0);
// Adds the namespace "HostApi" to use Command Line API and Docker API
using HostApi;
// Creates some command line to run in a docker container
var cmd = new CommandLine("whoami");
// Runs the command line in a docker container
var result = new DockerRun(cmd, "mcr.microsoft.com/dotnet/sdk")
.WithAutoRemove(true)
.Run();
result.ShouldBe(0);
For more details how to use TeamCity service message API please see this page. Instead of creating a root message writer like in the following example:
using JetBrains.TeamCity.ServiceMessages.Write.Special;
using var writer = new TeamCityServiceMessages().CreateWriter(Console.WriteLine);
use this statement:
using JetBrains.TeamCity.ServiceMessages.Write.Special;
using var writer = GetService<ITeamCityWriter>();
This sample opens a block My Tests and reports about two tests:
// Adds a namespace to use ITeamCityWriter
using JetBrains.TeamCity.ServiceMessages.Write.Special;
using var writer = GetService<ITeamCityWriter>();
using (var tests = writer.OpenBlock("My Tests"))
{
using (var test = tests.OpenTest("Test1"))
{
test.WriteStdOutput("Hello");
test.WriteImage("TestsResults/Test1Screenshot.jpg", "Screenshot");
test.WriteDuration(TimeSpan.FromMilliseconds(10));
}
using (var test = tests.OpenTest("Test2"))
{
test.WriteIgnored("Some reason");
}
}
For more information on TeamCity Service Messages, see this page.