M. Barry

In this multi-part blog series, I'll walk through my experience using VSTS Build and Release against a public GitHub repository.

  • Part 1 : Preparing your Project
  • Part 2 : Creating your Build
  • Part 3 : Creating your Release Pipeline
  • Part 4 : Final Thoughts and Takeaways

Note: All examples will relate to a library I recently created for personal and educational purposes, Unofficial.Owlet.

Part 2: Creating Your Build

Now that we've covered creating a NuGet package from our shared library, we are ready to look at putting together our continuous integration pipeline.

Visual Studio Team Services Account

If you do not yet have one, register a new organization and create your VSTS account at https://visualstudio.microsoft.com/team-services/.

Create your Team Project

You should be able to create a new Team Project once you're on your account landing page in the top right, or use an existing Team Project. For our example, I'm going to show you how to integrate a GitHub project's source code into VSTS, but you could also push your code directly to VSTS.

Note: While not a requirement, I've joined the Public Projects preview so that I can share a Build/Release badge in my GitHub README, and that others can see my Build and Release pipeline in production.

Create a Build Definition

Click on Builds within your new Team Project and click New Pipeline. Here we can select GitHub source. You'll need to allow VSTS access to your GitHub account to see your public and private repositories.

Once you've selected the source branch for your build pipeline, you'll be asked to choose a starting template. I like to start with an Empty process to choose exactly what I want to happen.

Adding Build Tasks

By clicking on the + icon on the Phase 1 section, we can start to choose our build tasks.

I chose the following:

  • .NET Core Tool Installer
    • Allows you to specify a specific .NET Core SDK for building your project
  • PowerShell (to setup some build variables)
  • .NET Core (for building our assembly)
  • .NET Core (for explicitly packing)
    • Separate step so we can add debugging symbols
  • Publish Build Artifacts
    • Saves our build pipeline for our release pipeline later

Packing

You might be thinking, but wait, we added <GeneratePackageOnBuild> in the last post, so shouldn't our Build task be enough? You're not wrong. It definitely generates the .nupkg in that step.

However, this is for learning, right? There's another additional step I wanted to add to my VSTS build process which was to create a .symbols.nupkg package - which includes the .pdb and .dll together.

To achieve this, I could have also added a <IncludeSymbols> property of true to my .csproj file to generate an additional symbols package every time dotnet build runs for my project. Or, I can specify it on-demand with

dotnet pack --include-symbols

So now I get output from my pack step like:

Successfully created package '\Unofficial.Owlet.0.1.4-CI-20180722-132452.nupkg'.
Successfully created package '\Unofficial.Owlet.0.1.4-CI-20180722-132452.symbols.nupkg'.

If we take a more complete look at my pack command, it looks like:

--output "$(Build.ArtifactStagingDirectory)"
--configuration $(BuildConfiguration)
--no-build
--include-symbols
/p:PackageVersion=$(nuget.MajorVersion).$(nuget.MinorVersion).$(nuget.PatchVersion)$(nuget.CISuffix)

I wanted to specify my VSTS-provided variable of Build.ArtifactStagingDirectory so that my .nupkg files drop in a specific folder that I can easily save for later.

I can specify Debug or Release version to pack - if I'm creating symbols, I'm going to want Debug.

I specify --no-build because I had just performed a dotnet build in the previous task. However, I can let pack run my build, too (including a NuGet restore).

Then, I specify which package version I want applied to my source. This contains a concatenation of Major/Minor/Patch/CISuffix. These get evaluated by VSTS before passing the parameter to the VSTS build agent. More on this soon.

Publishing Artifacts

The goal of our CI build is to always have a usable (working or not) artifact. This artifact is tied to your specific build number, and represents the compiled source at that time. You can have either a single artifact or multiple artifacts for your build - you provide the path for a file (or files) and specify a name for your artifact.

I specify the path $(Build.ArtifactStagingDirectory)\ as my artifact source location (in there are my two .nupkg files from the previous task), provide a name Unofficial.Owlet, and publish to VSTS itself.

When my future builds complete, I could go to the build summary page, and download any of the published artifacts by name.

Build artifacts are also a requirement to utilize Release Management pipelines - where we specify a Build Artifact as our assets to deploy to release environments. We'll tackle that in the next post.

Variables

The variable configuration for my build looks like this:

I specify:

  • DOTNET_SKIP_FIRST_TIME_EXPERIENCE to true
    • This saves some time when running on a VSTS Hosted Agent. It will not pre-fetch a lot of NuGet packages on first run. If you're running your own build agent, skip this.
  • BuildConfiguration
    • This is passed into my dotnet build and dotnet pack tasks
  • nuget.
    • CISuffix
      • This is a special variable appended to my NuGet packages created by the CI build. More on this later.
    • majorVersion
    • minorVersion
    • patchVersion
  • date and time
    • This gets initialized by a PowerShell task by the build agent
    • You'll notice these variables are consumed by the nuget.CISuffix value

Some of these variables are marked as "Settable at queue time", meaning when I manually queue a new build, I can change some of these variables. The only values I am interested in changing during a manual queue are:

  • BuildConfiguration
  • nuget.majorVersionnuget.minorVersionnuget.patchVersion

Everything else is constant, or is driven by other variables.

Build Options

If you click the Options tab, you can now set the build number, and enable status badges.

The build number format I am using is related to user-provided variables, as well as a datetime component.

$(nuget.majorVersion).$(nuget.minorVersion).$(nuget.patchVersion)-$(date:yyyy.MMdd)$(rev:.r)

This will produce a build number like:

0.1.4-2018.0722.1
0.1.4-2018.0717.2
0.1.4-2018.0717.1
0.1.1-2018.0708.2
...

Then, of course, the status badges. Use these image URLs when adding an indicator for the health of your project. I placed mine in my README near the top - as that's fairly common practice to give end-users an indication of the maintenance of your library.

Date and Time Variables

I tried to set my variables date and time to be :

  • date = $(date:yyyy.MMdd)
  • time = $(date:HHmmss)

But it never evaluated when the build was queued. So I had to resort to a simple PowerShell script to set them at runtime:

Now I can use these as part of my CISuffix variable!

-CI-$(date)-$(time)

So that every single build artifact is stamped with the semantic version as well as the date/time of the CI build.

Unofficial.Owlet.0.1.4 -CI-20180722-132452.nupkg

Triggers

Since we want this as a continuous integration pipeline, let's set that up next. Click on the Triggers tab. Feel free to poke around the multitude of settings here, but you can schedule CI based on specific branch changes, Pull Request validations, based off another build definition, or on a set schedule.

Queuing Our Build

Now that everything is set up, let's give it a shot!

build-queue.png

Set our major/minor/patch values, then click Queue!

Our build should start, and we should see it complete in a few seconds!

Build Complete!

Now we have a working build, producing an artifact with and without symbols for our shared library. Try and download your artifact - or the latest of our sample library here.

Preparing Our Release Pipeline

Next time, we'll visit configuring our release pipeline consuming our build artifact and deploying to multiple NuGet provider feeds!