Embracing Visual Studio Team Services for CI/CD of NuGet Packages: Part 2
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
totrue
- 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
anddotnet pack
tasks
- This is passed into my
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
andtime
- 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.majorVersion
,nuget.minorVersion
,nuget.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!
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!