VSIX Continuous Delivery using Cake, AppVeyor and MyGet

Yesterday, I posted a deep dive into continuous integration for Visual Studio extensions using Cake and AppVeyor. While this is pretty awesome, and can really make testing a lot easier, we can extend this even further!

This post will be covering the continuous delivery part of the CI/CD pipeline, using our Cake scripts and AppVeyor builds to publish to MyGet.

Summary and Plan

If you haven't read the previous post on setting up CI, you should do that first!

So, at the moment, we have a simple Cake script to build VSIX packages from our Visual Studio extension. We can use this on the command line to build the package, plus using AppVeyor to upload new packages on every build. We're now going to update this to additionally publish our extension to MyGet on every build.

We will do this by calling the MyGet REST API from within our script, uploading the VSIX directly to MyGet, where it will be available from the feed.

MyGet

If you haven't used MyGet before, it's an awesome hosted package repository service that lets you store, publish and manage packages in a range of formats. While plenty of people use it for its customisable NuGet feeds, today we'll be using MyGet's built-in support for VSIX packages.

MyGet's VSIX feeds are a valid extension source for Visual Studio so your users can simply add your feed as a source and Visual Studio will use it as an additional source for extensions, much like the VS Marketplace.

Updates window in Visual Studio

Again, I won't be going into a lot of detail on setting up MyGet itself, as they also have excellent Getting Started documentation for working with VSIX feeds. Once you have your feed set up, you will need two things: the Feed URL and your API Key.

Feed properties

Now, in order to protect your API key, you should never put it directly in your Cake script. Instead, open the settings page for your AppVeyor project and click the Environment tab on the left. Under environment variables, you can add a new variable called (in our case) MYGET_VSIX_API_KEY, and set the value to the API key you got from MyGet. This way, in your Cake script you can use the simple EnvironmentVariable(string) method to get the value of this key.

Note also that if you would prefer to keep the variables in your appveyor.yml, you can also use AppVeyor's integrated support for secure variables and store the variables securely in your build configuration.

The steps from here on will use these environment variables for the URL and Key, similar to the screenshot.

Versioning

One small complication of this approach is that if we are continuously delivering updated packages, we need to bump the version number before Visual Studio will allow an installed extension to be updated. There are two ways to achieve this.

MSBuild

If you have an MSBuild task that returns a value (such as GitTools/GitVersion or Nerdbank.GitVersioning), we can use a special substitution string in the extension's .vsixmanifest file. If the MSBuild task returns a value for the current version, we could simply add a string in the form of |YourProjectName;TargetToCall| into the .vsixmanifest where we want the version to be inserted. This will generally be part of the /PackageManifest/Metadata/Identity element's Version attribute.

Manifest patching

To avoid using a separate MSBuild task for the version, we can also just add the desired version directly to the manifest file. While you can do this with XSLT, the easiest way to do this in a Cake script is with a dead simple MagicChunks transformation. First, we add a task to update the version:

Task("Update-Manifest-Version")  
    .WithCriteria(() => shouldPublishToMyGet) // this is optional, but recommended
    .Does(() => 
{
    var versionInfo = GitVersion();
    var targetVersion = versionInfo.MajorMinorPatch + "." + versionInfo.CommitsSinceVersionSourcePadded;
    context.TransformConfig(
        "./src/source.extension.vsixmanifest",
        new TransformationCollection {
            { "PackageManifest/Metadata/Identity/@Version", targetVersion }
        }
    );
});

and add the task as a dependency in the Build task:

Task("Build")  
    .IsDependentOn("Clean")
    .IsDependentOn("Restore")
    .IsDependentOn("Update-Manifest-Version")
// trimmed

Now, our manifest will be updated with the correct version right before the extension is built and packaged, resulting in a VSIX with a higher version than the previous build, which means all new builds will appear as updates to our users in Visual Studio.

Upload to MyGet

Currently, Cake doesn't "natively" support uploading VSIX packages to MyGet. Fortunately, Cake is still just C# which means we can easily consume the official Microsoft HttpClient library to upload to MyGet. To fetch and include the .NET HttpClient library, just add the following two lines to the top of your script:

#addin "System.Net.Http"
using System.Net.Http;  

You can now use a HttpClient object just like any other C# app.

Building a MyGet client

To make this functionality reusable, we have a simple MyGetClient type that we then #load in to upload our VSIX. You can download a copy of this Cake script and #load it into your own scripts to introduce a simple MyGetClient type.

Using this helper type, a task to upload to MyGet is as simple as the following:

Task("Publish-Extension")  
    .IsDependentOn("Post-Build")
    .WithCriteria(() => ShouldPublishToMyGet)
    .Does(() => 
{
    var client = MyGetClient.GetClient(EnvironmentVariable("MYGET_VSIX_API_URL"), EnvironmentVariable("MYGET_VSIX_API_KEY"));
    var response = client.UploadVsix(GetFile("./dist/Cake.VisualStudio.vsix"));
    Information("VSIX Upload {0}", response.IsSuccessStatusCode ? "succeeded." : "failed.");
});

The awesome guys over at MyGet even helped us out with this client!

Now, we update the task that AppVeyor uses:

Task("AppVeyor")  
    .IsDependentOn("Upload-Artifact")
    .IsDependentOn("Publish-Extension");

The next time AppVeyor runs, it will call the task, add the Publish-Extension task to the graph and will attempt to upload the built VSIX direct to MyGet. From there, MyGet will automatically add the package to your feed, and it will show up as an update to users very shortly after. Adding a feed is super-easy as well, with a separate section in the Options window.

Adding a custom gallery feed

Cake for Visual Studio updates

You can see this in action in the Cake for Visual Studio extension. You'll find all the code you see here is pulled from the GitHub repo and check the most recent PRs to see not only our "old" AppVeyor artifact uploading, but also the uploads to MyGet. You can read more about using our MyGet feed in this blog post on the Cake blog.

Comments

comments powered by Disqus