52
loading...
This website collects cookies to deliver better user experience
Pull Request
This is executed each time a pull request is made or updated; it builds the application and runs unit and integration tests.
Build Release
This is executed each time a change is merged (or pushed) to the main branch; it also builds the applications and runs unit and integration tests, but it also prepares the application to be deployed–this means packaging the application, uploading it to our deployment system, and associating some metadata with it.
Release
This is executed after the previous step completes successfully, and deploys the application to one of the environments. If the environment is the last one before production, it also runs automated acceptance tests.
build.ps1
, which is always located in the root of the application’s repository, and pass it some strictly defined arguments. These argument always include the ‘target’, or ‘what to do’, and, depending on the stage, arguments like ‘which environment’ and so forth.dotnet publish
and dotnet test
. Deploying an application already involves using external tooling to be able to communicate with the deployment system. In addition, our build scripts do things beside building and testing, like:Task("PullRequest")
.IsDependentOn("Build")
.IsDependentOn("Test");
Task("Build")
.Does(() =>
{
DotNetCoreBuild("MyApplication.sln");
});
Task("Test")
.Does(() =>
{
DotNetCoreTest("test/MyApplication.Tests/MyApplication.Tests.csproj");
});
RunTarget("PullRequest");
PullRequest
task, which is dependent on Build
and Test
(in that order), so it will run dotnet build MyApplication.sln
and dotnet test test/MyApplication.Tests/MyApplication.Tests.csproj
.build.ps1
come in? Well, you can’t just run a Cake script; you need a runner. The build.ps1
script’s job is to bootstrap this. It downloads the Cake runner from NuGet and invokes the runner, which will then compile and execute the Cake script.build.ps1
, get a syntax error, fix it, rinse and repeat..nuspec
files), and as far as I’m aware, this also doesn’t let you define tasks.Root / "src" / "MyApplication" / "MyApplication.csproj"
).class Build: NukeBuild
{
public static int Main() => Execute<Build>(x => x.PullRequest);
[Solution]
Solution Solution;
Target PullRequest =>
_ => _.Triggers(Build)
.Triggers(Test);
Target Build =>
_ => _.Executes(
() => DotNetTasks.DotNetBuild(o => o.SetProjectFile(Solution))
);
Target Test =>
_ => _.Executes(
() => DotNetTasks.DotNetTest(o => o.SetProjectFile(Solution))
);
}
win10-x64
runtime while treating warnings as errors.abstract class MyBuild: NukeBuild
{
Target Publish =>
_ => _.Executes(() => { /* publish code goes here */ });
}
class Build: MyBuild
{
Target BuildRelease =>
_ => _.Triggers(Publish);
}
Build
class now exposes two targets: Publish
and BuildRelease
. Job done, right? Well, not so fast. What about optional features? For example, not all of our projects have automated acceptance tests. If every build exposed a target for running acceptance tests, you’d have no idea whether that target is there because of the base class or because it actually does something useful.LogDebug
, LogInformation
, LogError
, and so forth. It’ll usually also have something like a Log
or Write
method, which has the same parameters, preceded by a ‘level’ parameter. The implementation of LogDebug
and similar methods is usually to call that Log
method with the respective log level.enum LogLevel
{
Debug,
Information,
Warning,
// etc.
}
interface ILog
{
void Log(LogLevel level, string message);
void LogDebug(string message);
void LogInformation(string message);
// etc.
}
class Log: ILog
{
public void Log(LogLevel level, string message) { /* omitted for brevity */ }
public void LogDebug(string message) => Log(LogLevel.Debug, message);
}
ILog
needs to implement LogDebug
and friends, even though that implementation will likely be exactly the same as the existing ones.interface ILog
{
void Log(LogLevel level, string message);
void LogDebug(string message) => Log(LogLevel.Debug, message);
void LogInformation(string message) => Log(LogLevel.Information, message);
// etc.
}
ILog
needs to implement is the Log
method. This also means that you can add new methods to the interface without breaking existing implementations.Build
class can ‘inherit’ from multiple interfaces, where each interface represents a ‘component’, which can have its own targets and parameters. Each interface is ultimately derived from a well-known interface (INukeBuild
). NUKE understands this mechanism of defining targets, and will find targets defined on all ‘inherited’ interfaces.interface IDefaultBuild: INukeBuild
{
Target Publish => _ => _.Executes(() => /* publish code goes here */);
}
interface IBuildWithAcceptanceTests: INukeBuild
{
Target RunAcceptanceTests => _ => _.Executes(() => /* code to run acceptance tests goes here */);
}
class Build: NukeBuild, IDefaultBuild, IBuildWithAcceptanceTests
{
public static int Main() => Execute<Build>();
}
IBuildWithAcceptanceTests
interface, the build gets a whole new set of features, whereas if you choose to not implement it, the features are not in your way.Release
target — first we release the application into an environment, and then we run acceptance tests. How do we combine this with the fact that acceptance tests are optional? Triggers
on the Release
target, because that would fail if the acceptance test component hasn’t been implemented. We could use TriggeredBy
from the acceptance test target, but that will fail if the release component hasn’t been implemented. We don’t want to force people to implement particular components; they’re there if they are useful to you, and nothing more.TryTriggeredBy<T>
. This lets you express ‘if the build is a T
, this target is triggered by these targets’. This is just what we need. There are similar methods for DependsOn
, DependentOn
, Triggers
, and even After
and Before
.interface IDefaultBuild: INukeBuild
{
Target Release => _ => _.Executes(() => /* omitted for brevity */);
}
interface IBuildWithAcceptanceTests: INukeBuild
{
Target RunAcceptanceTests =>
_ => _.TryTriggeredBy<IDefaultBuild>(b => b.Release)
.Executes(() => /* omitted for brevity */);
}
IDefaultBuild
and IBuildWithAcceptanceTests
, the RunAcceptanceTests
will automatically be triggered by the Release
target. I call that pretty sweet.Unless you install Cake.Bakery, and how you should do that depends on how you’re running your Cake script. Even then, it’s fairly limited. ↩