teamcity-csharp-interactive
teamcity-csharp-interactive copied to clipboard
The cross platform build automation system: C# scripts + powerful API
C# script tool for 
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.
Prerequisites
The tool requires .NET 6+ runtime.
Use Inside TeamCity
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.
Use Outside TeamCity
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.
Debug scripts easy!
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.
Report and Track Issues
Please use our YouTrack to report related issues.
Usage Scenarios
- Global state
- Using Args
- Using Props dictionary
- Using the Host property
- Get services
- Service collection
- Logging
- Write a line to a build log
- Write a line highlighted with "Header" color to a build log
- Write an empty line to a build log
- Log an error to a build log
- Log a warning to a build log
- Log information to a build log
- Log trace information to a build log
- Command Line API
- Build command lines
- Run a command line
- Run a command line asynchronously
- Run and process output
- Run asynchronously in parallel
- Cancellation of asynchronous run
- Run timeout
- Docker API
- Build a project in a docker container
- Running in docker
- .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
- Restore NuGet a package of newest version
- Restore a NuGet package by a version range for the specified .NET and path
- TeamCity Service Messages API
- TeamCity integration via service messages
Using Args
Args have got from the script arguments.
if (Args.Count > 0)
{
WriteLine(Args[0]);
}
if (Args.Count > 1)
{
WriteLine(Args[1]);
}
Using Props dictionary
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";
Using the Host property
Host is actually the provider of all global properties and methods.
var packages = Host.GetService<INuGet>();
Host.WriteLine("Hello");
Get services
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.
Service collection
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"));
}
Write a line to a build log
WriteLine("Hello");
Write an empty line to a build log
WriteLine();
Write a line highlighted with "Header" color to a build log
WriteLine("Hello", Header);
Log an error to a build log
Error("Error info", "Error identifier");
Log a warning to a build log
Warning("Warning info");
Log information to a build log
Info("Some info");
Log trace information to a build log
Trace("Some trace info");
Build command lines
// 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 !!!");
Run a command line
// 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);
Run a command line asynchronously
// 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();
Run and process output
// 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");
Run asynchronously in parallel
// 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();
Cancellation of asynchronous run
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();
Run timeout
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();
Build a project
// 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);
Clean a project
// 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);
Run a custom .NET command
// 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();
Test a project using the MSBuild VSTest target
// 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);
Pack a project
// 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);
Publish a project
// 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);
Restore a project
// 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);
Run a project
// 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!"});
Test a project
// 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);
Run tests under dotCover
// 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();
Restore local tools
// 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);
Test an assembly
// 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);
Build a project using MSBuild
// 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);
Shuts down build servers
// 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);
Restore NuGet a package of newest version
// Adds the namespace "HostApi" to use INuGet
using HostApi;
IEnumerable<NuGetPackage> packages = GetService<INuGet>().Restore(new NuGetRestoreSettings("IoC.Container").WithVersionRange(VersionRange.All));
Restore a NuGet package by a version range for the specified .NET and path
// 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);
Build a project in a docker container
// 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);
Running in docker
// 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);
TeamCity integration via service messages
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.