Skip to content

Latest commit

 

History

History
956 lines (604 loc) · 28.2 KB

README.md

File metadata and controls

956 lines (604 loc) · 28.2 KB

C# script tool for

NuGet TeamCity.csi GitHub

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

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.