Month: December 2015

Upgrading to Continuous Deployment In 1 Day with Xamarin, TeamCity and HockeyApp

It’s Just So Beautiful

Every time I push a commit to my project’s GitHub remote, within about 20 minutes, my beta users get an email that says “New version of [Dan’s app] is available. Download and install now.”

A special thing about the app they’re installing is that it has been automatically verified across 6 iOS devices and versioned automatically during that 20 minutes. All I did was push the code. What would happen if your team had this same capability?

To start right away, skip to Let’s Do This – It’s only 10 steps!

What if you had this automated release pipeline set up today? If you got a critical bug report and needed to push a fix, how soon can you get that into the hands of your users?

This post contains a lot of info, but covers a lot of bases and skill levels. If you follow it closely, you’ll be up and running soon.

CI and CD speed up your dev process and increase your confidence. I learned from my work at Readify that the sooner you have a build, the cleaner your work, the better your velocity is, the more “agile” and responsive you can be to changes in requirements or bug fixes. In fact, for nearly every engagement at Readify, we nailed the devops work in the first week and it made the rest of the project smoother.

Just do it. Do it today. (Or tomorrow, if it’s late right now. But do it).

You can move to continuous integration and continuous deployment, and you can do it one day. I set it up in a couple of hours, but I’ve been doing this a lot lately, so it might take a bit longer if you are new to some or all of the tools.

Going Continuous

The following instructions create an ideal setup for a Xamarin iOS team or developer who uses GitHub, Xamarin Studio on OSX, Xamarin Test Cloud, HockeyApp, and TeamCity. You can substitute some of these technologies if you must. The general idea is the same.

The instructions for Android are almost identical, but we’ll focus on iOS for now. Once you get the iOS build running, you can always duplicate it and tell it to build/submit the apk instead.

All you are doing with a tool like TeamCity is bringing together a bunch of disparate tools and sequencing their execution. Each tool is going to provide you a command blurb of some kind, which you’re going to customize to your needs and then add to TeamCity. TeamCity is what continuously integrates new changes – the HockeyApp component adds deployment to the mix.

Installing the Stuff

Choose the CI solution that works for you, but TeamCity is a great option that I really prefer for Xamarin apps. You can get a free version of TeamCity with one build agent to practice with on your local Mac, which I highly recommend. Download TeamCity here, and then follow the setup instructions. You should be able to access your TeamCity instance at http://localhost:8111/ after you are done installing. Don’t be afraid; this part is easy! If you can’t figure out how to start TeamCity, look for this line in the installation instructions: bin/runAll.sh start

Gathering the Other Stuff

The first few steps in the build are pretty straightforward. When we submit to Xamarin Test Cloud, we’re going to need our trusty shell script command that Test Cloud creates for us. I’m going to show you how to get this working once, and then I will show you how to make it better.

  1. Have C# UITests written. Even if all your test does is wait for any element and then take a screenshot, just write the test. Use your Test Cloud hours. A basic smoke test script will save you from app crashes, and it takes less than 5 minutes, so make one!
  2. Get your upload command from Test Cloud. From http://testcloud.xamarin.com, select New Test Run and set up the test run to your liking. At the final step, you’ll get a command in a window that follows the following format. You will change the red items in a later step – for now, just save this line somewhere. Make sure you copy it exactly as-is from Test Cloud.
    mono packages/Xamarin.UITest.[version]/tools/test-cloud.exe submit
    yourAppFile.ipa YOUR_API_KEY_IS_HERE --devices 947599ea
    --series "master" --locale "en_US" --app-name "MyiOSApp"
    --user you@yourcompany.com --assembly-dir pathToTestDllFolder
    
  3. Set up an API token for your app in HockeyApp. This is done from the API Tokens page of the HockeyApp account settings area (click on your name on the top right when you’re logged in + look for API Tokens at the bottom left menu). If you haven’t set up a HockeyApp account yet, just create a free one.When you create this token, make sure the bundle id you specify for this API token precisely matches the bundle id of the app. You can find and change the Bundle ID in the Info.plist of your Xamarin iOS project. Save this token for later.

Let’s Do This

Okay, dear reader, developer with minimal devops experience. We’re going to do this together. It’s going to be fun, and a little bit difficult, and challenging. However, we’re going to learn a lot, and we’re going to transform how we build, test and deploy our Xamarin apps by the end of the day. I’ve got your back – leave a comment, or for faster answers, tweet at me @danwaters if you have any questions.

At the end of things, your TeamCity build configuration is going to consist of six Command Line steps:

  1. Restore NuGet packages for the solution (which should also include your UITest project)
  2. Auto increment the app version using PlistBuddy
  3. Use xbuild to generate the .ipa
  4. Build the UITest project and build it
  5. Upload the .ipa and .dSYM to Xamarin Test Cloud
  6. Send that bad boy to HockeyApp, and deploy out to your users.

These seven steps will get you to the peaceful bliss that is continuous deployment. It will take you a couple of tries to get each step right. If you feel the need to punch something along the way, an inner tube or other inflatable device will work great, but I’ll do my best to help keep your punching urges to a minimum. I promise that when you’ve got this running, you’ll want to hug your CI every time it increases your confidence or saves your behind.

Let’s get started!

1. Creating the Build Configuration

If you don’t have a default build agent, create one, and then create a new project and build configuration using TeamCity. Fill in the details in whatever way makes sense to you.

The next step is to set up the VCS root.

2. Configure your SSH Key

I highly recommend uploading an SSH key to your TeamCity server and using that to authenticate with GitHub, as opposed to using user credentials or one-time passwords. This takes a little time to configure the first time around, but once you’ve got it working for one repo, you don’t need to mess with it any more.

3. Configure the VCS Root

A build configuration can have several different places it goes to look for code. These are called VCS (version control system) roots, and they connect to things like GitHub.

To create a VCS root, go to your build configuration in TeamCity and choose VCS Roots on the left. By the way, just getting around TeamCity will be unfamiliar at first – the Projects menu on the top left is usually a quick way to get back. You can usually find Edit Configuration Settings when looking at any build, on the top right.

Here are a few tips for configuring a basic connection to GitHub with TeamCity:

  • Fetch URL: This format seems to work well in TeamCity:
    git@github.com/organizationName/repo.git
  • Default branch: I prefer to trigger builds on the dev branch, which means that in this field I put refs/heads/dev. Ideally you’d trigger a build like this when people merge into dev. Leave master for last stable release. If you have no idea what I’m talking about, check out this writeup from the good folks at Atlassian.
  • Username style: UserId
  • Authentication method: Uploaded key (point to the one you added in the last step)

Keep experimenting until Test Connection is successful.

4. Setting Triggers

Screen Shot 2015-11-30 at 3.08.51 PM

This is really a matter of preference on your team. The trigger I have set up is on the dev branch and will include multiple commits from each individual committer. The build I have, which tests the app in Test Cloud too, runs about 20 minutes, so including multiple commits saves lots of time if I’ve added more commits during the last build.

Since I set the default branch on the VCS root, I do not need to add any trigger rules, although for more custom scenarios you are welcome to do this.

I would also recommend disabling the trigger until you are actually ready to test the trigger. You don’t want to be making a bunch of junk commits just to test the build (which you’ll be doing a lot of), so leave this step for last.

5. Adding Build Step #1: Restore NuGet Packages

This step looks really long, but it’s not that bad. It’s just the first time we’re adding a build step, so I’ll make this detailed.

If you have a .gitignore in your repo – and you should – it means that your repository doesn’t include the binaries from your NuGet packages and you can’t build anything, so you need to do this.

A minor caveat on OSX is that any of the prebaked steps like Restore Nuget Packages or Visual Studio Build are not going to work. You have to use mono to run these commands, which is why everything is a shell command in this build. Several months ago, I had issues with nuget.exe, not existing or being findable by TeamCity, and I can’t remember if I solved this or if it just works now. Either way, anticipate that this might be a problem. For now, let’s just see if it works.

To add this build step, go to your build configuration and click Build Steps on the left, followed by Add Build Step.

Screen Shot 2015-11-30 at 3.22.22 PM

Details of the step:

  • Runner type: Command Line
  • Step name: Restore NuGet packages (or whatever you like)
  • Execute step if all previous steps finished successfully
  • Run: Custom script
  • Build script content:
    nuget restore %teamcity.build.checkoutDir%/MyProject.sln

You should not need to explicitly run nuget restore under mono (e.g. mono nuget.exe restore) but it is an option if you have problems with this syntax. The line above works for me using TeamCity on OSX.

If you’re not familiar with the %variables% these can be extremely helpful. checkoutDir points to the location on disk where the git repository is cloned for this build, and you use that as your base path for everything else. You will want to point to your .sln for this, which hopefully is in the root of your checkout directory.

Now – run the build! Let’s see if it works. Save this build step and then click the Run button on the top right. If you need to run with settings, click the ellipsis button next to it instead. You’ll see a (1) appear next to Agents on the top. If you click it, you’ll see all the currently running builds. Hopefully one is yours.

Screen Shot 2015-11-30 at 3.30.11 PM

You won’t see the “running on devices” bit yet, but soon. I promise.

Your build should be green. If you click the build status, and then click on Build Log, you can get a very verbose log of every step of the build. Get accustomed to finding the log, because you’ll be looking through it a lot.

Your build log should look a bit this, so far:

Screen Shot 2015-11-30 at 3.32.38 PM

If your build is green, congratulations! You did a ton of work just now. You got your VCS root hooked up, and got NuGet to restore your packages. But your work has just begun… let’s continue.

6. Build Step #2: Versioning the App

There are a lot of different ways to do this, different schools of thought and opinions. I just want to attach an incrementing version number to my .ipa so it can appear correctly in Test Cloud and HockeyApp. It is, in fact, the job of the build system to assign this version number, because CI is so critical in release management.

I’m just going to use the TeamCity build number for the last bit of the version, and have a major version as a configuration parameter. Remember, there are a ton of ways to do this, and this is just one such way.

First of all, let’s set our configuration parameters. One of them, we’ll read. The other, we’ll generate with a shell script and then inject into our iOS project’s Info.plist using PlistBuddy.

Go to the Parameters section of your build configuration, and then add two new parameters. Both will be environment variables.

The first environment variable, env.major_version, contains the major version of our app, which we set manually. Mine is set at 0.2, so my apps come out of TeamCity with that plus the build number, like 0.2.40. I will manually change the major version when I feel like it’s appropriate.

The second environment variable, env.ios_version, combines env.major_version with the build number and then we reference this variable later. Here’s the configuration for the two parameters:

Configuration parameters are awesome because you can use them for anything – device sets in Test Cloud, API keys, anything that you might reuse or don’t want to expose to the script directly. I’ll suggest some further uses of this later in the post.

Now that we have these two environment variables, we need to put them together.

Create a new build step, also a custom script, called Set iOS Version. For the build script content, enter the following commands:

/usr/libexec/PListBuddy -c "Set :CFBundleVersion %env.major_version%.%build.counter%" "%teamcity.build.checkoutDir%/iOS/Info.plist"
/usr/libexec/PListBuddy -c "Set :CFBundleShortVersionString %env.major_version%.%build.counter%" "%teamcity.build.checkoutDir%/iOS/Info.plist"

Ugh, that’s a bit nasty. I’m sorry. But greatness does not always come easy.

PListBuddy is a tool from Apple that should already be installed on your Mac, even though you might not be able to find it. But it’s there, in /usr/libexec. We invoke PListBuddy and set the version number to env.major_version.build.counter which results in something like 0.2.40 – three integers with a dot separator. It auto-increments with the build, and it works just fine for me.

Aside: These two commands together set two properties in the plist, both to the same value. The only difference in the two lines is CFBundleVersion versus CFBundleShortVersionString. CFBundleVersion is used for development builds in general, whereas CFBundleShortVersionString is the official version for release builds. For our purposes, these values can just be exactly the same, until you decide it should be something else. 

Test the build before moving on to the next step.

7. Build Step #3: Build the iOS App

Oh, we’re there already? Yup. This build step is another shell command, just like the last two. And guess what, it’s dead easy. You can do this.

This is all your custom script needs:

xbuild %teamcity.build.checkoutDir%/iOS/My.iOS.csproj /p:Configuration=Debug /p:Platform=iPhone /p:OutputPath=%teamcity.build.checkoutDir%/drop/ /p:BuildIpa=true

We are building to a new folder called ‘drop’ in the checkout directory. You can build to anywhere you want, but this works for now. You do need to send a developer profile signed, debug IPA to Test Cloud so that the Calabash test-server is enabled.

This line will work fine for a newer Forms app that has an iOS project in the iOS folder. Change the path as appropriate to point to the csproj for your iOS project.

Test the build before moving on to the next step.

8. Build Step #4: Build the UITest Project

Yet Another Shell Command. Assuming you have a Forms app, your UITests will be in the UITests folder. If not, change the path to point to your UITest csproj relative to the checkout directory. I chose not to put this stuff into a drop folder because it really isn’t needed.

xbuild %teamcity.build.checkoutDir%/UITests/ShredTrainer.UITests.csproj

Test the build before moving on to the next step.

9. Build Step #5: Submit to Test Cloud

Earlier in the post, I had you create your version of this command from Test Cloud:

mono packages/Xamarin.UITest.[version]/tools/test-cloud.exe submit
yourAppFile.ipa YOUR_API_KEY_IS_HERE --devices 947599ea
--series "master" --locale "en_US" --app-name "MyiOSApp"
--user you@yourcompany.com --assembly-dir pathToTestDllFolder

Now, we’re going to change it up a bit in order to fit in your environment. There are a few items that you need to change:

  • Path to test-cloud.exe: this comes down in your NuGet package with Xamarin.UITest, so you need to point to the packages folder and then again into the UITest folder, into its tools folder. You are relative to the checkout directory, so you could just do packages/Xamarin.UITest.1.2.0/tools/test-cloud.exe but you will have to come back here and hard-code the UITest version every time you update UITest. There may be another solution here that I haven’t discovered – let me know if you come up with a good idea for managing this!
  •  yourIpaFile.ipa: this should be pulled from the drop folder that you just built to in the last step.
  • YOUR_API_KEY_IS_HERE: When you generate this from Test Cloud, the API key will already exist, just make sure it corresponds to the right team. If it doesn’t, just re-generate the command from the Test Cloud frontend.
  • –devices abcdef11: This represents the devices you selected. If you don’t change this, your tests will always run on the same set of devices. This is another thing I suggest extracting to a configuration parameter, because then you can have multiple named sets and bring them into your builds.
  • –series “master”: this tells Test Cloud to add this test run to the master series, which is the default. From CI I usually like to create a smoke-tests series or something similar. You can also use test series to indicate the nature of the devices you’re testing on (e.g. broad-android-tablet-set) or the type of build (daily-build).
  • –locale “en_US”: specify any ISO-standard locale to force the devices to reboot in this language and locale.
  • –app-name “MyiOSApp”: – this is generated by the Xamarin Test Cloud upload workflow.
  • –user you@yourcompany.com: Identifies who uploaded the app.
  • assembly-dir pathToTestDllFolder: This should point to the location where the UITest binaries are after the build. Since we did a debug build, you’ll specify this folder like %teamcity.build.checkoutDir%/UITests/bin/Debug/

We can tack on a few more switches to this command:

--dsym %teamcity.build.checkoutDir%/drop/my.app.dSYM

This will upload the dsym as well, so you can get better stack traces. Change the path to the dsym as necessary. As configured, you’ll find it in the drop folder after the build step.

--category "myCategory" --category "myOtherCategory"

This line will include and execute tests that have [Category] attributes matching these names. In this case, both myCategory tests and myOtherCategory tests will run, but nothing else.

--test-param screencapture:true

This doesn’t work on iOS, but for Android, you can capture the screen recording and play it back inside of Test Cloud.

Tip: If you want to make this more readable, extract some things to configuration parameters, and use the line continuation character to break up the commands (just a backslash \ on OSX).

Test the build before you continue. If you want, you can just execute the Test Cloud step by disabling the other steps in the build on the Build Steps screen. Test Cloud will use the last build as copied to your checkout directory. It will save you time on the workflow, getting this right, to run this step by itself rather than as part of the full build.

Once your build status goes to “Running on X devices,” then you know you’ve got the integration working properly. If you have problems here, tweet me as I mentioned in the beginning of the post.

10. Build Step #6: Deploy to HockeyApp

This final command is pretty easy, and contains an example of the line continuation character I mentioned in the last step. In this case, for this build step I set the working directory to the drop folder I created, so all paths are relative to that. We only have one path, which is the path to the .ipa.

set -x
curl \
-F "status=2" \
-F "notify=1" \
-F "notes=Build %build.counter%" \
-F "notes_type=0" \
-F "ipa=@<strong>myApp.ipa</strong>" \
-F "release_type=0" \
-H "X-HockeyAppToken: <strong>myAppToken1234517257ABCDEF</strong>" \
https://rink.hockeyapp.net/api/2/apps/upload

Just replace your API token. You can expand this to include other trickery I haven’t taken the time to do yet, like set your HockeyApp release notes to the last commit message, or something like that.

You can see more about this interface from the HockeyApp docs.

notify=1 will send all of your users an email that there is a new version. Set it to 0 to turn off.

Just like in the last step, try disabling all of the steps except for this one to see if your HockeyApp deployment works on its own. You don’t want to wait for Test Cloud to test this step in case your app token is wrong or it can’t find the ipa file.

We’re Done

Yep! Amazing! Try it out for yourself. Next time you make a commit, push it, and then go look in TeamCity and watch the status of the build. If it breaks, go check out the Build Log and try to troubleshoot it yourself. Tweet at me if you need some help and I’ll try to be of assistance if I can!

Enhancements

Once you have this basic build working, there are a few parting ideas.

  • Things like the device set, test series, and other things can be extracted to configuration parameters that you can pass into the build. That way you can “store” your device sets in these parameters and then access them in TeamCity using the % syntax, quickly letting you retarget the devices you’re testing on without needing to select them again in Test Cloud.
  • Including multiple commits in a build can keep your build times down.
  • Only run smoke tests in CI. You want quick feedback, but try to stick to the 20-minute promise. Keep the CI build short enough that you aren’t losing productivity. Test the most basic scenarios. Another good option is to chain a longer build onto successful CI builds that targets more tests or more scenarios, but you consider the build mostly good as long as the CI build passes. You can move current-interest scenarios into the Quick CI build to verify them a few times and then move them into the longer build later.
  • Use categories and the –category switch when uploading Xamarin UITests. Use cucumber profiles when uploading Calabash tests.
  • If you look at the Actions menu on your build configuration, you can copy it or even use it as a template for future builds.

With these tools in place and a great build strategy, you have the flexibility and the power you need to deploy your mobile apps continuously. Experiment with different combinations of test series, category selection, and device selection to bring your ideas to life, such as “I want a smoke test every check-in on six devices, and a weekly deployed build with all the tests on more devices.”

Once you see that first successful email from HockeyApp or this system finds your first bug, you’ll never go back!

Happy Dev-Opsing!