Skip to content

Shared build logic based on Cake.Frosting

License

Notifications You must be signed in to change notification settings

ap0llo/shared-build

Repository files navigation

SharedBuild

shared build?branchName=master

This repository contains shared build logic common to some of my open source projects.

The build tasks are based on Cake.Frosting

Usage

To use the shared build tasks in a project, perform the following steps. This guide assumes you’re already familiar with Cake.Frosting.

  1. Create a new Cake.Frosting project.

  2. Add the Azure Artifacts feed to your nuget.config (the package is not available on NuGet.org).

  3. Add a package reference to the Grynwald.SharedBuild package to your build project.

  4. In the build project’s Main() method, load the shared build tasks using the UseSharedBuild() method:

    public static int Main(string[] args)
    {
        return new CakeHost()
            .UseSharedBuild<BuildContext>()
            .Run(args);
    }
  5. You’ll need to provide a build context type that implements IBuildContext.

  6. The package provides a default implementation of the build context named DefaultBuildContext.

  7. 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.

  8. Set up your repository to match the assumptions the shared build tasks make. See Assumptions for details.

  9. Install Required tools

  10. Configure package upload

Assumptions

The shared build tasks make some assumptions about the repository being built

  1. The output structure follows the expected Project Output Structure

  2. The repository uses Nerdbank.GitVersioning for versioning

  3. The repository’s main Visual Studio solution file is located in the root of the repository.

Project Output Structure

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:

  1. 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 variable BUILD_BINARIESDIRECTORY is used instead.

  2. Within the root output directory, there is a separate directory for each configuration (Debug or Release)

  3. The build output for each C# project is built to a directory named after the project, e.g. Binaries/Debug/MyProject

  4. NuGet packages for all projects are built to a common output directory at <RootOutputDirectory>/<Configuration>/packages

  5. 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):

  1. Code coverage reports will be written to <RootOutputDirectory>/<Configuration>/CodeCoverage/Report.

  2. Code coverage history files (generated by ReportGenerator) will be written to <RootOutputDirectory>/<Configuration>/CodeCoverage/History.

  3. A change log will be generated to <RootOutputDirectory>/changelog.md (using the changelog tool).

Required tools

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:

Uploading NuGet packages

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)

Example

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)

Credentials

In order to upload packages, the build requires credentials which need to be provided as environment variables.

  1. 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)
  2. For uploads to nuget.org, the API key is required to be available in the environment variable NUGET_ORG_APIKEY

  3. For uploads to MyGet, the API key is required to be available in the environment variable MYGET_APIKEY

Overriding and skipping tasks

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.

About

Shared build logic based on Cake.Frosting

Resources

License

Stars

Watchers

Forks

Contributors 4

  •  
  •  
  •  
  •  

Languages