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 repository is hosted on GitHub
-
The CI system being used is either Azure Pipelines or GitHub Actions
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. |
The Shared Build tasks provide additional features when the build is running in one of the supported CI systems.
Currently, the following CI systems are supported
-
Azure Pipelines
-
GitHub Actions
The task CI
is the entry point for running a build inside a CI system.
Generally, the build definition in a CI system will be
./build.sh --target CI
The following steps are executed when the build is running in a CI pipeline:
-
The build number is set to the version being build (only works on Azure Pipelines)
-
When the build is running for the main branch (
main
ormaster
) or a release branch (release/*
):-
A GitHub Release for the version being built is created (including a automatically generated change log)
-
For builds of the main branch, the GitHub release is created as draft with the tag
vNext
-
For builds of release branches, a non-draft GitHub release is created
-
-
The built NuGet packages are pushed to configured NuGet feeds (see Uploading NuGet packages)
-
-
When building a Pull Request, the PR’s milestone is automatically set
-
The milestone name is derived from the current major & minor version (
v${MAJOR}.${MINOR}
) -
If a milestone with the expected name does not yet exist, it will be created automatically
-
-
The
Pack
tasks publishes the generated NuGet packages as pipeline artifacts -
The
Test
task publishes the test results (and the coverage report) as pipeline artifacts -
The
GenerateChangeLog
task publishes the change log as pipeline artifact
To use the shared build task in GitHub actions, the following settings need to be configured workflow:
-
The workflow’s
permissions
need to allowwrite
access to-
issues
andpull-requests
: This is required for automatically creating and assigning milestones to Pull Requests -
actions
: This is required for uploading pipeline artifacts -
contents
: This is required for generating the changelog (read access would be sufficient) and creating GitHub releases (requireswrite
permission)
-
-
The access token for accessing the GitHub API needs to be specified as environment variable
GITHUB_ACCESSTOKEN
. The automatically generated token provided by GitHub Actions can be used for this.
To make it available, addGITHUB_ACCESSTOKEN: ${{ secrets.GITHUB_TOKEN }}
to the workflow’senv
section -
The current pull request number needs to be made available as environment variable
PR_NUMBER
(GitHub Actions has no predefined variable that provides this).
To make the PR number available, addPR_NUMBER: ${{ github.event.number }}
to the workflow’senv
section -
For uploading artifacts, the following environment variables need to be made available.
-
ACTIONS_CACHE_URL
-
ACTIONS_RUNTIME_TOKEN
-
ACTIONS_RUNTIME_URL
-
ACTIONS_RESULTS_URL
-
This can be achieved using a
github-script
step before running the Cake build:- name: Set up environment variables uses: actions/github-script@v7 with: script: | core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); core.exportVariable('ACTIONS_RUNTIME_URL', process.env.ACTIONS_RUNTIME_URL || ''); core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
-
-
Since Nerdbank.GitVersioning is used for calculating the version from the repository’s history, the
checkout
step needs to be configure to not use shallow clones- name: Check out uses: actions/checkout@v2 with: fetch-depth: 0
An example of a full GitHub Actions workflow can be seen below:
name: CI Build
# Trigger build for pushes to the main branch and all release branches
# As well as all Pull Requests targeting these branches
on:
push:
branches:
- master
- release/*
pull_request:
branches:
- main
- master
- release/*
# Adding the "workflow_dispatch" trigger allows the workflow to be started manually from the GitHub Web UI
workflow_dispatch:
permissions:
# Write permissions to issues and PRs is required for automatically setting the PR milestone
issues: write
pull-requests: write
# Write permissions for actions is required for uploading pipeline artifacts
actions: write
# Read access to the repo is required for generating the change log
# Write access to the repo is required for creating/update GitHub releases
contents: write
env:
BUILD_CONFIGURATION: Release
# Disable telemetry and "Welcome" message of dotnet CLI
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
# Expose the Pull Request number as environment variable (there is no predefined variable for this unfortunately)
PR_NUMBER: ${{ github.event.number }}
GITHUB_ACCESSTOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build:
name: "Build"
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v2
with:
fetch-depth: 0 # Disable shallow clones (Nerdbank.GitVersioning requires the full history to calculate the version)
- name: Set up environment variables
uses: actions/github-script@v7
with:
script: |
// The 'ACTIONS_*' variables are required by the Cake build for uploading artifacts
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
core.exportVariable('ACTIONS_RUNTIME_URL', process.env.ACTIONS_RUNTIME_URL || '');
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
- name: Run Cake Build
run: |-
./build.sh --target CI --configuration $BUILD_CONFIGURATION