This repository contains shared build logic common to some of my open source projects.
The build tasks are based on Cake.Frosting
To use the shared build tasks in a project, perform the following steps. This guide assumes you’re already familiar with Cake.Frosting.
-
Create a new Cake.Frosting project.
-
Add the Azure Artifacts feed to your
nuget.config
(the package is not available on NuGet.org). -
Add a package reference to the
Grynwald.SharedBuild
package to your build project. -
In the build project’s
Main()
method, load the shared build tasks using theUseSharedBuild()
method:public static int Main(string[] args) { return new CakeHost() .UseSharedBuild<BuildContext>() .Run(args); }
-
You’ll need to provide a build context type that implements
IBuildContext
. -
The package provides a default implementation of the build context named
DefaultBuildContext
. -
By default, the
UseSharedBuild()
method will import all tasks. You can filter these tasks by specifying a task filter, see Overriding and skipping tasks for details. -
Set up your repository to match the assumptions the shared build tasks make. See Assumptions for details.
-
Install Required tools
The shared build tasks make some assumptions about the repository being built
-
The output structure follows the expected Project Output Structure
-
The repository uses Nerdbank.GitVersioning for versioning
-
The repository’s main Visual Studio solution file is located in the root of the repository.
The Shared Build package makes some assumptions about the build output structure of a repository.
These assumptions can be customized by overriding the IBuildContext.Output
property.
The default output structure is assumed to be:
-
The root output directory is a directory named
Binaries
located in the repository for a local build. When running a build in Azure Pipelines, the value of the environment variableBUILD_BINARIESDIRECTORY
is used instead. -
Within the root output directory, there is a separate directory for each configuration (
Debug
orRelease
) -
The build output for each C# project is built to a directory named after the project, e.g.
Binaries/Debug/MyProject
-
NuGet packages for all projects are built to a common output directory at
<RootOutputDirectory>/<Configuration>/packages
-
Test Results for all projects are written to a common output directory at
<RootOutputDirectory>/<Configuration>/TestResults
The easiest way to configure projects to use the default ouptut structure is using a Directory.Builds.props
file in the root of a repository and defining the following properties:
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<!-- Output paths -->
<BaseOutputPath Condition="'$(BUILD_BINARIESDIRECTORY)' != '' ">$(BUILD_BINARIESDIRECTORY)</BaseOutputPath>
<BaseOutputPath Condition="'$(BaseOutputPath)' == '' ">$(MSBuildThisFileDirectory)Binaries/</BaseOutputPath>
<BaseOutputPath Condition="!HasTrailingSlash('BaseOutputPath')">$(BaseOutputPath)/</BaseOutputPath>
<OutputPath>$(BaseOutputPath)$(Configuration)/$(MSBuildProjectName)/</OutputPath>
<PackageOutputPath>$(BaseOutputPath)$(Configuration)/packages/</PackageOutputPath>
<VSTestResultsDirectory>$(BaseOutputPath)TestResults/</VSTestResultsDirectory>
</PropertyGroup>
In addition to these directories, the shared build tasks will produce the following output directories (that do not have to be configured in the csprojs):
-
Code coverage reports will be written to
<RootOutputDirectory>/<Configuration>/CodeCoverage/Report
. -
Code coverage history files (generated by ReportGenerator) will be written to
<RootOutputDirectory>/<Configuration>/CodeCoverage/History
. -
A change log will be generated to
<RootOutputDirectory>/changelog.md
(using the changelog tool).
The Shared Build tasks require the following tools to be installed into the Cake Build. You can use any of Cake’s mechanisms to install tools. If you’d like to reference the tools using a .NET Local tool manifest, you can use the Cake.DotNetLocalTools module.
The following tools need to be installed:
The Shared Build tasks support uploading NuGet packages from builds to NuGet.org, Azure Artifacts or MyGet.
Where packages are uploaded to is determined by the PushTargets
property of the build context.
To define one or more push targets when using a build context derived from DefaultBuildContext
, override the property and return one or more IPushTarget
instances.
The package provides PushTarget
as default implementation of IPushTarget
which can be used to easily define upload targets.
The KnownPushTargets
class offers factory methods to create IPushTarget
instances for well-known targets (currently only nuget.org)
class BuildContext : DefaultBuildContext
{
public override IReadOnlyCollection<IPushTarget> PushTargets { get; } = new IPushTarget[]
{
new PushTarget(
// type: The type of hosting provider to push to. Can be either Azure Artifacts, Nuget.org or MyGet
type: PushTargetType.AzureArtifacts,
// feedUrl: The url of the NuGet package feed to push to, for nuget.org, use "https://api.nuget.org/v3/index.json"
feedUrl: "https://pkgs.dev.azure.com/ap0llo/OSS/_packaging/PublicCI/nuget/v3/index.json",
// isActive: Provide a function to determine when to upload packages to this source
// In the example below, packages are uploaded to Azure Artifacts for every build of master or a release branch
isActive: context => context.Git.IsMasterBranch || context.Git.IsReleaseBranch
),
// 'KnownPushTargets' provides factory methods for known targets to avoid having to repeat the feed url in every project
// In the example below, packages are uploaded to nuget when the current build is build a release branch
KnownPushTargets.NuGetOrg(isActive: context => context.Git.IsReleaseBranch)
};
public BuildContext(ICakeContext context) : base(context)
{ }
}
Note that the task to upload packages will only run when the build is running in a continuous integration environment (property IBuildContext.IsRunningInCI
)
In order to upload packages, the build requires credentials which need to be provided as environment variables.
-
Upload to Azure Artifacts will only work when the build is running in Azure Pipelines. The pipeline’s access token needs to be made available to the build by mapping it into the environment, e.g.
steps: - task: PowerShell@2 displayName: Cake Build inputs: filePath: './build.ps1' arguments: '--target CI --configuration $(buildConfiguration)' env: SYSTEM_ACCESSTOKEN: $(System.AccessToken)
-
For uploads to nuget.org, the API key is required to be available in the environment variable
NUGET_ORG_APIKEY
-
For uploads to MyGet, the API key is required to be available in the environment variable
MYGET_APIKEY
When importing shared build tasks using the UseSharedBuild()
extension method, by default all tasks are imported.
The set of tasks that are imported can be customized by specifying a task filter.
When specified, only the tasks for which the filter function returned true
will be added to the build.
public static int Main(string[] args)
{
return new CakeHost()
// Import all tasks except the "Pack" task
.UseSharedBuild<BuildContext>(taskType => taskType != typeof(Grynwald.SharedBuild.Tasks.PackTask))
.Run(args);
}
This way tasks can be skipped. By adding a custom task with the same name, tasks from the shared build package can be replaced.
For example, to use a custom "Pack" task, skip importing the task from the package and define a custom task with the same name:
namespace Build
{
public static class Program
{
public static int Main(string[] args)
{
return new CakeHost()
.UseSharedBuild<DefaultBuildContext>(taskType => taskType != typeof(Grynwald.SharedBuild.Tasks.PackTask))
.Run(args);
}
}
// The 'TaskNames' class provides constants for the names of all built-in tasks
[TaskName(TaskNames.Pack)]
public class PackTask : FrostingTask<IBuildContext>
{
public override void Run(IBuildContext context)
{
// Custom task logic
}
}
}
Caution
|
When skipping the import of a task that is a dependency of another task, the build will fail. In that case you cannot just skip the task but must provide a (possibly empty) implementation of a task with the same name. |