Hi Everyone!

This post is continuation of the series about a build automation tool called Nuke . Over the time, I will updated this page with links to individual posts :

Getting Started with Nuke

This Post - Write your first building block in Nuke

Manage your Package Version using Nuke

Manage your Package Release using Nuke in Github

In our last post, we have created a new build project using Nuke. In this post, first we will see what are the changes in the project structure and then we will write our first building block in Nuke. We will also see how to generate a new workflow for Github Actions and lastly we will run our build project from local system.

Table of Contents

Effective changes

The setup will create a number of files in your repository and – if you’ve chosen so – add the build project to your solution file. Below, you can examine the structure of added files and what they are used for:

<root-directory>
├── .nuke                           # Root directory marker
│   ├── build.schema.json           # Build schema file
│   └── parameters.json             # Default parameters file
│
├── build
│   ├── .editorconfig               # Common formatting
│   ├── _build.csproj               # Build project file
│   ├── _build.csproj.DotSettings   # ReSharper/Rider formatting
│   ├── Build.cs                    # Default build implementation
│   ├── Configuration.cs            # Enumeration of build configurations
│   ├── Directory.Build.props       # MSBuild stop files
│   └── Directory.Build.targets
│
├── build.cmd                       # Cross-platform bootstrapping
├── build.ps1                       # Windows/PowerShell bootstrapping
└── build.sh                        # Linux/Shell bootstrapping

Write your first “Target” block in Nuke

Target properties are the building blocks of a Nuke project. Inside a Build class, you can define your build steps as Target properties. The implementation for a build step is provided as a lambda function through the Executes method:

Build.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Nuke.Common;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.ProjectModel;
using static Nuke.Common.Tools.DotNet.DotNetTasks;

class Build : NukeBuild
{
    public static int Main() => Execute<Build>(x => x.Clean);

    [Solution(GenerateProjects = true)]
    readonly Solution Solution;

    Target Clean => _ => _
        .Description($"Cleaning Project.")
        .Executes(() =>
        {
           DotNetClean(c => c.SetProject(Solution.src.Sundry_HelloWorld));
        });
}
In the above code, we have defined a Clean target. We gave some nice description to the target and we are executing DotNetClean. If you have noticed, we are using strongly typed Solution.src.Sundry_HelloWorld to reference the project file instead of string literal. We are going to discuss more about this later.

You can make use of asynchronous execution by adding the async as well for example:

Build.cs
1
2
3
4
5
6
7
8
9
10
11
using Nuke.Common;
class Build : NukeBuild
{
    public static int Main() => Execute<Build>();

    Target MyTarget => _ => _
        .Executes(async () =>
        {
            await Console.Out.WriteLineAsync("Hello!");
        });
}

Please note, Async targets are just a convenience feature that allows you using async APIs in a straightforward way. Behind the scenes, they are still run synchronously.

We are going to use async in coming Target with more complex tasks.

Generate the build script for “Github Actions”

Now, we have defined our first Target. We are going to generate the build script for Github Actions. In general, this is most annoying thing to do as you have to write it in Yml file. But we are going to do it in a very simple way by just adding a attribute to the Build class:

Build.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using Nuke.Common;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.ProjectModel;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
using Nuke.Common.CI.GitHubActions;

[GitHubActions(
    "continuous",
    GitHubActionsImage.UbuntuLatest,
    AutoGenerate = true,
    FetchDepth = 0,
    OnPushBranches = new[] { "main", "dev", "releases/**" },
    OnPullRequestBranches = new[] { "releases/**" },
    InvokedTargets = new[] {
        nameof(Clean),
   },
    EnableGitHubToken = true,
    ImportSecrets = new[] { nameof(MyGetApiKey), nameof(NuGetApiKey) }
)]

class Build : NukeBuild
{
    public static int Main() => Execute<Build>(x => x.Clean);

    [Solution(GenerateProjects = true)]
    readonly Solution Solution;

    [Parameter("MyGet Api Key"), Secret]
    readonly string MyGetApiKey;

    [Parameter("Nuget Api Key"), Secret]
    readonly string NuGetApiKey;

    Target Clean => _ => _
        .Description($"Cleaning Project.")
        .Executes(() =>
        {
           DotNetClean(c => c.SetProject(Solution.src.Sundry_HelloWorld));
        });
}

Let’s try to understand the above code. We have added a GitHubActions attribute to the Build class. This attribute is used to generate the build script for Github Actions. We have provided the following parameters to the attribute:

  • Name - This is the name of the workflow. It will be used to generate the workflow file name.
  • Image - This is the image that will be used to run the build. In our case, we are using UbuntuLatest.
  • AutoGenerate - This is a boolean value that indicates whether the build script should be generated or not. In our case, we are setting it to true.
  • FetchDepth - This is the number of commits that will be fetched from the repository. In our case, we are setting it to 0, which means all the commits will be fetched from all the branches and tags.
  • OnPushBranches - This is an array of branches that will trigger the build on push. In our case, we are setting it to main, dev and releases/**.
  • OnPullRequestBranches - This is an array of branches that will trigger the build on pull request. In our case, we are setting it to releases/**.
  • InvokedTargets - This is an array of targets that will be invoked when the build is triggered. In our case, we are setting it to Clean.
  • EnableGitHubToken - This is a boolean value that indicates whether the GITHUB_TOKEN should be enabled or not. In our case, we are setting it to true.
  • ImportSecrets - This is an array of secrets that will be imported from the repository. In our case, we are setting it to MY_GET_API_KEY and NUGET_API_KEY.

Now, build your project, this is very important step and then go to your root folder from your Window terminal and run below command to generate the build script:

nuke

This will generate the following yml file named “continuous.yml” :

continuous.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# ------------------------------------------------------------------------------
# <auto-generated>
#
#     This code was generated.
#
#     - To turn off auto-generation set:
#
#         [GitHubActions (AutoGenerate = false)]
#
#     - To trigger manual generation invoke:
#
#         nuke --generate-configuration GitHubActions_continuous --host GitHubActions
#
# </auto-generated>
# ------------------------------------------------------------------------------

name: continuous

on:
  push:
    branches:
      - main
      - dev
      - 'releases/**'
  pull_request:
    branches:
      - 'releases/**'

jobs:
  ubuntu-latest:
    name: ubuntu-latest
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Cache .nuke/temp, ~/.nuget/packages
        uses: actions/cache@v2
        with:
          path: |
            .nuke/temp
            ~/.nuget/packages
          key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj') }}
      - name: Run './build.cmd Clean'
        run: ./build.cmd Clean
        env:
          MyGetApiKey: ${{ secrets.MY_GET_API_KEY }}
          NuGetApiKey: ${{ secrets.NUGET_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Let’s write all the “Target”

Now, we have basic foundation. Let’s write all the targets that we need to build our project. We will write the following targets:

  • Clean - This target will clean the project.

  • Restore - This target will restore the project.

  • Compile - This target will build the project.

  • Pack - This target will pack the project and generate the artifact (Nuget package) to specific folder.

  • PublishToGithub - This target will publish the package to Github, Only if the build is triggered from the dev branch or the pull request.

  • PublishToMyGet - This target will publish the package to MyGet, only if the build is triggered from the release/** branch.

  • PublishToNuGet - This target will publish the package to NuGet, only if the build is triggered from the main branch.

Lets write the above targets in our Build.cs file:

build.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
using System.Linq;

using Nuke.Common;
using Nuke.Common.IO;
using Nuke.Common.Git;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.GitVersion;
using Nuke.Common.Utilities.Collections;
using Nuke.Common.CI.GitHubActions;
using Nuke.Common.Tools.NerdbankGitVersioning;

using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
[GitHubActions("continuous",
    GitHubActionsImage.UbuntuLatest,
    AutoGenerate = true,
    FetchDepth = 0,
    OnPushBranches = new[] 
    {
        "main", 
        "dev",
        "releases/**"
    },
    OnPullRequestBranches = new[] 
    {
        "releases/**" 
    },
    InvokedTargets = new[]
    {
        nameof(Pack),
    },
    EnableGitHubToken = true,
    ImportSecrets = new[] 
    { 
        nameof(MyGetApiKey), 
        nameof(NuGetApiKey) 
    }
)]

class Build : NukeBuild
{
    public static int Main() => Execute<Build>(x => x.Pack);
    
    [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
    readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;

    [Parameter("MyGet Feed Url for Public Access of Pre Releases")]
    readonly string MyGetNugetFeed;
    [Parameter("MyGet Api Key"), Secret]
    readonly string MyGetApiKey;

    [Parameter("Nuget Feed Url for Public Access of Pre Releases")]
    readonly string NugetFeed;
    [Parameter("Nuget Api Key"), Secret]
    readonly string NuGetApiKey;

    [Parameter("Copyright Details")]
    readonly string Copyright;

    [Parameter("Artifacts Type")]
    readonly string ArtifactsType;

    [Parameter("Excluded Artifacts Type")]
    readonly string ExcludedArtifactsType;

    [GitVersion]
    readonly GitVersion GitVersion;

    [GitRepository]
    readonly GitRepository GitRepository;

    [Solution(GenerateProjects = true)]
    readonly Solution Solution;

    static GitHubActions GitHubActions => GitHubActions.Instance;
    static AbsolutePath ArtifactsDirectory => RootDirectory / ".artifacts";

    string GithubNugetFeed => GitHubActions != null
         ? $"https://nuget.pkg.github.com/{GitHubActions.RepositoryOwner}/index.json"
         : null;


    Target Clean => _ => _
      .Description($"Cleaning Project.")
      .Before(Restore)
      .Executes(() =>
      {
          DotNetClean(c => c.SetProject(Solution.src.Sundry_HelloWorld));
          EnsureCleanDirectory(ArtifactsDirectory);
      });
    Target Restore => _ => _
        .Description($"Restoring Project Dependencies.")
        .DependsOn(Clean)
        .Executes(() =>
        {
            DotNetRestore(
                r => r.SetProjectFile(Solution.src.Sundry_HelloWorld));
        });

    Target Compile => _ => _
        .Description($"Building Project with the version.")
        .DependsOn(Restore)
        .Executes(() =>
        {
            DotNetBuild(b => b
                .SetProjectFile(Solution.src.Sundry_HelloWorld)
                .SetConfiguration(Configuration)
                .SetVersion(GitVersion.NuGetVersionV2)
                .SetAssemblyVersion(GitVersion.AssemblySemVer)
                .SetInformationalVersion(GitVersion.InformationalVersion)
                .SetFileVersion(GitVersion.AssemblySemFileVer)
                .EnableNoRestore());
        });

    Target Pack => _ => _
    .Description($"Packing Project with the version.")
    .Requires(() => Configuration.Equals(Configuration.Release))
    .Produces(ArtifactsDirectory / ArtifactsType)
    .DependsOn(Compile)
    .Triggers(PublishToGithub, PublishToMyGet, PublishToNuGet)
    .Executes(() =>
    {
        DotNetPack(p =>
            p
                .SetProject(Solution.src.Sundry_HelloWorld)
                .SetConfiguration(Configuration)
                .SetOutputDirectory(ArtifactsDirectory)
                .EnableNoBuild()
                .EnableNoRestore()
                .SetCopyright(Copyright)
                .SetVersion(GitVersion.NuGetVersionV2)
                .SetAssemblyVersion(GitVersion.AssemblySemVer)
                .SetInformationalVersion(GitVersion.InformationalVersion)
                .SetFileVersion(GitVersion.AssemblySemFileVer));
    });

    Target PublishToGithub => _ => _
       .Description($"Publishing to Github for Development only.")
       .Requires(() => Configuration.Equals(Configuration.Release))
       .OnlyWhenStatic(() => GitRepository.IsOnDevelopBranch() || GitHubActions.IsPullRequest)
       .Executes(() =>
       {
           GlobFiles(ArtifactsDirectory, ArtifactsType)
               .Where(x => !x.EndsWith(ExcludedArtifactsType))
               .ForEach(x =>
               {
                   DotNetNuGetPush(s => s
                       .SetTargetPath(x)
                       .SetSource(GithubNugetFeed)
                       .SetApiKey(GitHubActions.Token)
                       .EnableSkipDuplicate()
                   );
               });
       });

    Target PublishToMyGet => _ => _
       .Description($"Publishing to MyGet for PreRelese only.")
       .Requires(() => Configuration.Equals(Configuration.Release))
       .OnlyWhenStatic(() => GitRepository.IsOnReleaseBranch())
       .Executes(() =>
       {
           GlobFiles(ArtifactsDirectory, ArtifactsType)
               .Where(x => !x.EndsWith(ExcludedArtifactsType))
               .ForEach(x =>
               {
                   DotNetNuGetPush(s => s
                       .SetTargetPath(x)
                       .SetSource(MyGetNugetFeed)
                       .SetApiKey(MyGetApiKey)
                       .EnableSkipDuplicate()
                   );
               });
       });
    Target PublishToNuGet => _ => _
       .Description($"Publishing to NuGet with the version.")
       .Requires(() => Configuration.Equals(Configuration.Release))
       .OnlyWhenStatic(() => GitRepository.IsOnMainOrMasterBranch())
       .Executes(() =>
       {
           GlobFiles(ArtifactsDirectory, ArtifactsType)
               .Where(x => !x.EndsWith(ExcludedArtifactsType))
               .ForEach(x =>
               {
                   DotNetNuGetPush(s => s
                       .SetTargetPath(x)
                       .SetSource(NugetFeed)
                       .SetApiKey(NuGetApiKey)
                       .EnableSkipDuplicate()
                   );
               });
       });
}
Couple of things to note here:

  • Requires : This allows you to specify parameter requirement that must be met before the target is executed. In this case, we are saying that the target can only be executed if the configuration parameter is set to Release. This is because we don’t want to publish a debug version of our packages.

  • OnlyWhenStatic : This allows you to specify conditions that must be met before the target is executed. In our example for PublishToGithub case, we are saying that the target can only be executed if the branch is set to develop or if it is a pull request. This is because we want to publish this only for internal users.

  • Produces : This allows you to specify the output of the target. In our example, we are saying that the output of the target is the artifacts directory.

  • Triggers : This allows you to specify the targets that should be executed after the current target. In our example, we are saying that the target Pack should trigger the targets PublishToGithub, PublishToMyGet and PublishToNuGet.

  • DependsOn : This allows you to specify the targets that should be executed before the current target. In our example, we are saying that the target Pack should depend on the targets Compile and Restore.

  • Parameter : This attribute allows you to specify the parameters that should be passed to the target. You can specify the parameters in the following ways:

    • Through Command-Line : You can specify the parameters from the command-line through their kebab-case names prefixed with a double-dash. For example, if you want to specify the Configuration parameter, you can do so by running the following command:

      1
      nuke --configuration Release

    • Through Parameter Files : You can specify the parameters in a parameter file. The parameter file is located in .nuke folder.

      .nuke/parameters.json
      1
      2
      3
      4
      {
          "$schema": "./build.schema.json",
          "Configuration": "Release"
      }

      Besides the default parameters.json file, you can create additional profiles following the parameters.{name}.json naming pattern. These profiles can be loaded on-demand like : nuke --profile {name} [other-profiles...]

    • Through Environment Variables : You can specify the parameters through environment variables. My recommendation would be to keep The environment variables prefixed with NUKE_ and keep the parameter name in uppercase. For example, if you want to specify the Configuration parameter, you can do so by setting the environment variable NUKE_CONFIGURATION to Release. Nuke will automatically pick up the environment variables and use them as parameters.

  • Secret : This attribute allows you to specify the parameters that should be passed to the target as secrets such as API Key, password. In our example, we are specifying the NuGetApiKey as a secret.

  • Solution : This attribute allows you to specify the solution file that should be used for the target. In our example, we are saying that the solution file that should be used for the target is Sundry HelloWorld.sln. We are also mentioning the project name should be automatically inferred from the solution file. Hence we used GenerateProjects = true.

  • GitVersion : This attribute allows you to get the version of the build. In our example, we are saying that we want to get the version of the build from GitVersion. we will discuss more about GitVersion in the next post.

  • GitRepository : This attribute allows you to get the repository information.

New Requirement! 😈

As we understand from the above, Github will trigger this scripts when we push to the main, release/** and dev branches.

Also it will trigger when we submit pull request for the release/** branch.

But, I don’t want to trigger this Action when we push Readme.md file to any of the branches. So, to achieve this, we have to add paths-ignore: in the yml file. But Nukedoesn’t support this feature yet. So, we have to add it manually.

To do that first, let’s disable the auto-generation of the yml file by updating AutoGenerate property of the GitHubActions attribute, like below:

Build.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using Nuke.Common;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.ProjectModel;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
using Nuke.Common.CI.GitHubActions;

[GitHubActions(
    "continuous",
    GitHubActionsImage.UbuntuLatest,
    AutoGenerate = false,
    FetchDepth = 0,
    OnPushBranches = new[] { "main", "dev", "releases/**" },
    OnPullRequestBranches = new[] { "releases/**" },
    InvokedTargets = new[] {
        nameof(Clean),
   },
    EnableGitHubToken = true,
    ImportSecrets = new[] { nameof(MyGetApiKey), nameof(NuGetApiKey) }
)]

class Build : NukeBuild
{
    /* Omitted */
}

Now, update the yml file by adding paths-ignore: as below:

continuous.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# ------------------------------------------------------------------------------
# <auto-generated>
#
#     This code was generated.
#
#     - To turn off auto-generation set:
#
#         [GitHubActions (AutoGenerate = false)]
#
#     - To trigger manual generation invoke:
#
#         nuke --generate-configuration GitHubActions_continuous --host GitHubActions
#
# </auto-generated>
# ------------------------------------------------------------------------------

name: continuous

on:
  push:
    branches:
      - main
      - dev
      - 'releases/**'
    paths-ignore:
      - '**/README.md'
  pull_request:
    branches:
      - 'releases/**'
    paths-ignore:
      - '**/README.md'

jobs:
  ubuntu-latest:
    name: ubuntu-latest
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Cache .nuke/temp, ~/.nuget/packages
        uses: actions/cache@v2
        with:
          path: |
            .nuke/temp
            ~/.nuget/packages
          key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj') }}
      - name: Run './build.cmd Pack'
        run: ./build.cmd Pack
        env:
          MyGetApiKey: ${{ secrets.MY_GET_API_KEY }}
          NuGetApiKey: ${{ secrets.NUGET_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/upload-artifact@v1
        with:
          name: .artifacts
          path: .artifacts

Run your first Nuke build

At this point, we have a working build script. You can run the build script by executing the following command in the terminal but make sure you have Git repository initialized.

1
nuke

Here is the output of the above command:

As we can see, we are getting an error that, could not inject value for GitVersion. This is because we haven’t installed the GitVersion tool yet.

In the next article, we will add the GitVersion tool and we will learn how to manage a version of a package using the same .