In my last post we talked about setting up CI & CD for your Xamarin.Android project. Let’s continue on with our cross-platform story today by targeting Windows 10.
This is the second in a 3-part series on how you can embrace Continuous Deployment/Delivery as part of your Agile practices your mobile development environment. If you haven’t already, check out part 1 for the primer! (Also I kinda assume you already did this anyway.)
Windows 10
Assuming you already have your VSTS project created from Part 1 and your repo structure as shown there as well, let’s just jump right in to what this will look like for the Windows 10 deployment of your app, shall we? As you might imagine, VSTS does really well with this. It wasn’t until recently, however that they updated the Hosted build agents with the UWP SDK allowing cloud builds of your app so thankfully now that they’ve done that this process becomes even easier.
Step 1 – Set up Continuous Integration builds
As before, we first must set up our builds to run on every check-in. To do this, follow the same steps you did in Part 1, but this time choose the ‘Universal Windows Platform’ build template:
The next screen is the same as the Android one; make sure you choose the Continuous Integration check box, and set up the repo & branch you want to target just like you did previously.
In my setups today I remove the Index Sources & Publish Symbols step, leaving my setup looking like this:
I also use the following for the Visual Studio Build task’s MSBuild Arguments:
/p:AppxBundlePlatforms=\”$(BuildPlatform)\”
Note the targets for the Nuget Restore and VS Build steps in this configuration closely. They will restore & build every sln file in the repo if you leave the as-is. This means if you have configured different solutions anywhere, they’ll be built. If you don’t want to do this, then target those tasks to building only the SLNs you want. Also notice again in this definition we have the Publishing of Artifacts to a Drop folder on the Server. Why do we do this? Right. Because they need to be placed on the server for the Release Definition we create later to pick them up and send them off to HockeyApp.
Let’s configure the platforms. Since we know we want to build for Mobile and Big Windows, we do the following to our definition:
On the ‘Options’ tab, check ‘MultiConfiguration’, put in ‘BuildPlatform’ and select ‘Parallel’. Then:
Head to the ‘Variables’ tab and put a new ‘BuildPlatform’ variable in with value ‘x86,ARM’. After this you need to tell the VS Build task to use the build platform by putting
$(BuildPlatform)
in to the ‘Platform’ field of the Visual Studio Build task.
What these steps will do is kick off parallel builds of the x86 and ARM configurations for your solution on every CI. You’ll see later how we use these outputs. You could also add x64 in here if you wanted.
There was something a bit unique I wanted to do for my Windows 10 app that makes this process cool, but also more complicated.
Versioning.
When I do a new build of the app for the Store, I do it on one development machine and I check the packages.appxmanifest file back in to source control. This keeps the build version as a marker in source and then allows me to rev the revision version when I do CD out to HockeyApp. The process basically flows like this:
- Build package on Release machine, setting/incrementing the app version as desired.
- Check appxmanifest file in to source (contains the version number used to build the package). This number *always* ends in .0 because that’s what’s required for store submissions.
- On CI, set the revision number of the current version number in the appxmanifest file to match the Build ID of the current build (we don’t check this back in).
- When CI builds, it’ll stamp the resulting appx file with this version number.
In my opinion this is a great way to version your CD builds of the app as it shows what “base version” of the app this rev was built from. You know it was built after the one in the store with the corresponding Major.Minor.Build matching the one of your CD installation.
To do this we turn to the all-powerful PowerShell. Yup, VSTS has the ability to run PowerShell scripts as part of your build process. It gives you near-infinite flexibility!
Let’s just jump right in to what this process ends up looking like for the Windows 10 builds:
1: Param([String]$buildNumber = \'\')
2:
3: if (-Not $buildNumber) {
4: Write-Host \"buildNumber must be specified\"
5: exit 1
6: }
7:
8: # get the manifest file paths
9: $filepath = Get-Item -Path \"Windows.AppPackage.appxmanifest\"
10: Write-Host \"Updating the Store App Package \'$filepath\' \"
11: # update the identity value
12: $XMLfile=NEW-OBJECT XML
13: $XMLfile.Load($filepath.Fullname)
14: # pull out the current version
15: $curVersion = [Version]$XMLFile.Package.Identity.Version
16: Write-Host \"Current Version: $curVersion\"
17: # bump 4th digit (revision version)
18: $newVersion = [Version]($curVersion.Major.ToString() + \'.\' + $curVersion.Minor + \'.\' + $curVersion.Build + \'.\' + $buildNumber)
19: Write-Host \"New Version: $newVersion\"
20: [Environment]::SetEnvironmentVariable(\"LastBuildNumber\", $newVersion.ToString(), \"User\")
21: Write-Host (\"Env variable \'LastBuildNumber\' set to \" + [Environment]::GetEnvironmentVariable(\"LastBuildNumber\", \"User\"))
22: # set back in to the file
23: $XMLFile.Package.Identity.Version = [String]$newVersion
24: # set the file as read write
25: Set-ItemProperty $filepath.Fullname -name IsReadOnly -value $false
26: $XMLFile.save($filepath.Fullname)
1) Send in the buildNumber so we can put this as the Revision number in the package’s version. You’ll see where we get this later.
9) Pick out the manifest file for the Windows 10 package. Tune this path to your needs – remember it’s relative to the repo root at the time the script’s activating.
The remainder of the script is simply parsing the manifest XML, getting the version within the manifest, creating a new version object and putting the buildNumber in for the Revision spot of the Version, then writing that back in to the manifest file. It’s pretty straightforward.
Take this file, save it as a ps1 file and push it up to your repo. Mine’s named prebuild.ps1.
Once you’ve done this, it’s time to add the build step that will execute it and give it the build number to use. VSTS makes this pretty straightforward:
Again, once you click the ‘Add’ button you’ve gotta click “Close” yourself. Now we’re to the part where we configure the step to run the script:
Remember, move the PowerShell task to be the first in execution, then click the ‘…’ (aka Browse) button in the filename field. You’ll get a live browse of your repo and can simply pick the file.
Now we need to configure the arguments by simply setting them to:
-buildNumber $(Build.BuildId)
which passes the Build’s BuildId (a whole number that increments with each build) to the script as the buildNumber parameter. Exactly what we want. We don’t use BuildVersion here because that is an arbitrary format that might have dots in it thereby resulting in an invalid version if we were to append that to the end of our Version object in the script.
So now each CI build of our Windows app will have a unique build number which makes it ideal for Continuous Deployment.
Packaging for HockeyApp
The nice thing about the default behavior of a Release build for a UWP is it will automatically result in an appx/appxbundle file which is exactly what we want for the purpose of Continuous Deploy. However there’s one caveat here. HockeyApp doesn’t support appx deployment for Big Windows. Rather, what they want you to do is ZIP up the artifacts for Big Windows (appx, cert, ps1 installation file, etc) and submit that as your Big Windows build. So we have some more manual work to do before we’re ready to set up the deployment. Of course, this takes shape in the form of another Powershell script.
1: Param([string] $buildVersion = \'\')
2:
3: Write-Host \"Build Platform: \'$env:BuildPlatform\'\"
4:
5: if (($env:BuildPlatform) -eq \"x86\")
6: {
7: if (-Not $buildVersion) { $buildVersion = [Environment]::GetEnvironmentVariable(\"LastBuildNumber\", \"User\") }
8:
9: Write-Host \"Using build number $buildVersion\"
10:
11: $targetDir = Get-Item -Path (\".appxWindows.App_\" + $buildVersion + \"_Test\")
12: Write-Host \"targetDir: $targetDir\"
13:
14: $outputFile = $pwd.Path + \"\" + $buildVersion + \".zip\"
15: Write-Host ($outputFile)
16:
17: Add-Type -Assembly System.IO.Compression.FileSystem
18: $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
19: [System.IO.Compression.ZipFile]::CreateFromDirectory($targetDir.Fullname,
20: $outputFile, $compressionLevel, $false)
21: }
There’s one assumption specific to how I do my CIs that’s built in to this script – the check for a BuildPlatform of x86. Since Big Windows takes x86 or x64, I only ever build ARM and x86 of my Windows UWPs as part of the CI build process. And I don’t much care to zip up and send the ARM version off to HockeyApp for the Big Windows deployment. So, if we’re not building x86, this script just bails out.
You’ll notice on line 7 I check to see if you sent in a build number; if you didn’t I pull a User Environment Variable denoting what the last build number was. If you were paying attention, I set this variable on line 20 of our prebuild script. This means that I don’t have to pass the build number to this script if I don’t want to.
Line 11 is where some more magic happens. If you’ve done any packaging for store you know that when you package, you can specify where it drops the appx. This is, in fact, stored in your Solution/project as Metadata, so make sure you have it specified to something (relative!) that will work on a build server. The format of the resulting folder is shown here, which is what we want to ZIP up and make our Deployment for Big Windows. And the rest is just zipping up that folder and sending to a ZIP file at the build root named .zip. Make note of this.
Save this file, push it to your repo, create a PowerShell task (last one in our build definition this time), and target this file for it. You don’t need to send the Build ID as an argument this time if you don’t want because, like I pointed out, we cache it in an environment variable anyway.
So thus far, we’ve versioned our package and ZIP’d up an archive for Big Windows. What’s the next step? Slap it all in a server-side drops folder, right.
Copying output to Drops
This is the step that makes your binaries available to the Release definition that will send them to HockeyApp, so it’s another important piece. Let’s have a look at how we make this work with the custom stuff we had to do with the Big Windows ZIP.
The kicker here is when we created the UWP build definition, it added a task “Publish Build Artifacts” and put them in ‘drops’. The problem is we just created a new artifact that is not a build artifact, and we need IT also put in drops. So, scrap that task and put in a different one:
Again, don’t forget to click ‘Close’ after ‘Add’.
The difference here is you’re also copying some stuff to the drops location, not just arbitrarily grabbing build output. This is what we want to grab the Big Windows ZIP our PowerShell script creates.
Which is to say for ARM builds, grab any resulting appx files (to submit to the MOBILE deployment) and also include any ZIP files at the root of the repo (there will be only one). Store all the things you find in to a ‘drop’ folder server-side.
Boom, our CI is now fully ready to go to build a uniquely-versioned package and set it up for handoff to a Release Management definition.
Step 2 – Set up Release to HockeyApp
As with Android, now we need to kick things off to HockeyApp. There’s a good and bad to this. The good is HockeyApp’s fully ready for a quick integration for Windows Mobile. The bad is there’s a lot of manual effort involved in the Big Windows integration, unfortunately. So let’s start with the easy one:
Windows Mobile -> HockeyApp
As with Android, create a new Release definition with a HockeyApp task, configuring it with your AppID and HockeyApp connection. For the binaries field, we simply point it at the APPX files that were copied to the ‘drop’ folder from our CI build and voila! To choose the APPX files, just enter
$(System.DefaultWorkingDirectory)New Universal Windows Platform definition 1drop***_ARM.appx
in to the Binary File Path area of the task.
That was easy.
Big Windows -> HockeyApp
Update! As of March 30th, 2016, the following steps are no longer necessary. HockeyApp now allows a .zip file containing a Big Windows 10 appx/appxbundle to be auto-parsed to determine version information! So you need only submit the .zip file created above as the binary to HockeyApp and the magic flows just as it did for your Windows Mobile appx.
To understand why this is more arduous you have to understand what HockeyApp does in the case of our Android deploy (previously) and the Windows Mobile one above. They basically took the version number stamped on/in the package that was submitted, created a new version entry for the app corresponding to the AppID parameter, then put the binaries in that version entry for that app.
For Big Windows, there’s no such “automatic” flow. So we have to do these determinations and calls manually. Well, we know the version – it’s the filename of the .zip file. We know the AppID – we have it set up in HockeyApp. So now it’s just a matter of making some REST calls per HockeyApp’s API and getting stuff up there. How? PowerShell of course.
1: Param(
2: [string]$appID, # HockeyApp App ID
3: [string]$apiToken #HockeyApp App Token
4: )
5:
6: $zipFile = Get-Item *.zip | Select-Object -first 1
7:
8: if ($zipFile) {
9: Write-Host \"Add version to HockeyApp...\"
10: $create_url = \"https://rink.hockeyapp.net/api/2/apps/$($appID)/app_versions/new\"
11:
12: $buildVersion = $zipFile.BaseName
13: Write-Host \"Detected version: $($buildVersion)\"
14:
15: $body = @{
16: bundle_version = $buildVersion
17: }
18:
19: #Create new version and get response object
20: $response = Invoke-RestMethod -Method POST -Uri $create_url -Header @{ \"X-HockeyAppToken\" = $apiToken } -Body $body
21:
22: Write-Host \"Submit zip to version placeholder...\"
23: $update_url = \"https://rink.hockeyapp.net/api/2/apps/$($appID)/app_versions/$($response.id)\"
24:
25: Write-Host \"Change directory to $($zipFile.Directory.FullName)\"
26: cd \"$($zipFile.Directory.FullName)\"
27: #Upload ZIP file
28: $curlCommand = @\'
29: curl -k -F \"ipa=@
30: \'@ + $($zipFile.Name) + @\'
31: \" -X PUT -F \"status=2\" -F \"notify=1\" -H \"X-HockeyAppToken:
32: \'@ + $($apiToken) + \'\" \' + $($update_url)
33:
34: Write-Host \"Executing $($curlCommand)\"
35: $output = (cmd /c $curlCommand 2`>`&1)
36: }
37: else {
38: Write-Host \"Skipped.\"
39: }
While writing this script I found one wildcard: curl. Turns out the ‘curl’ embedded with PowerShell/windows doesn’t use the usual syntax and, frankly, I just didn’t want to fight it. So, I downloaded the real curl binary and have it in my repo. I’ve attached it to this post for your convenience to do the same. I highly recommend this route vs swimming upstream and trying to make this work with the curl alias PowerShell introduces.
The parameters to this script, as you see, are the appID in HockeyApp and a HockeyApp access token. Once we have these it’s off to the races.
6) Get the first (hopefully only!) .zip file in the working directory (‘drop’ folder). If we didn’t find one, bail.
12) Use the base name of the discovered .zip file as the version we’re going to tell HockeyApp to create.
28) Put together the curl command that sends the file up to HockeyApp
And boom, it arrives there. Take this script, save it and push it to your repo. Now create a PowerShell task in our Release definition to run it:
Because our working folder is important (it’s where curl runs from, etc) let’s make sure we set that correctly as well.
There’s one more thing we need to do – can you guess? Here’s a hint: where’s curl come from?
Yup, we have to push that curl.exe to our repo, but we also need to get it to the drop folder to be used in the Release script execution for Big Windows!
No matter, this is as simple as heading back to your CI Build definition and adding “curl.exe” to the bottom of your “Copy and Publish” task. But wait, since we also need the SCRIPT we just wrote to be run during release, we’ve also gotta push that to the ‘drop’ folder:
**binARM$(BuildConfiguration)*ARM*.appx
*.zip
curl.exe
win10tohockeyapp.ps1
One more tweak. What if the push to the HockeyApp Mobile endpoint fails? Do we want to stop the release of Big Windows? In my case, no. So I elected to have the Mobile deploy not nix the Big Windows deploy in case of a failure by checking the ‘Continue on error’ box:
Now if all goes well, you should be able to kick off a CI for Windows and end up with a new Windows Mobile and Big Windows version out to your HockeyApp account! Enjoy!