Day One, Jenkins and Macminicolo
Using a Mac server to coordinate a team of developers and avoid the sombrero
We get to work with a lot of great companies. When the developers of Day One came to Macminicolo, I was especially excited. Their award winnning app for Mac and iOS is one of my favorite apps of all time. In fact, I've joked with Ben and Paul claiming that I am the head of their public relations, paid or not.
I asked them if they would share how they are using their Mac mini at Macminicolo and they were kind enough to write it up for all of us. So here is the secret sauce for making the 2012 Mac App of the Year:
A good development workflow is critical to producing a high-quality app, especially if there is more than one developer involved. At Day One our current team is comprised of four full time developers and one designer. The great majority of our work is focused on our Mac and iOS apps. This post will share the workflow we currently use for developing Day One.
We've been using Xcode and GitHub since we began writing Day One two years ago. We would write code, commit it and then produce a build when ready to submit to the App Store. Easy. But as additional developers joined the team I quickly realized that we were making silly mistakes. We were forgetting to test shared code on both Mac and iOS. Builds would sometimes fail. We would forget to run static analysis before a release. We were all occasionally introducing logic errors. These are all natural mistakes, especially when knowledge is distributed among multiple people in a team.
I sat down and made a list of what I wanted to change: I wanted builds to happen automatically. I wanted code reviews. I wanted automated unit tests and static analysis. I wanted to catch new compiler warnings that I inadvertently introduced. I wanted to know when a build was failing. What I wanted was continuous integration.
In a nutshell, continuous integration (CI) involves a server watching for changes to a repository. When the server detects a change it automatically begins a build and runs tests. The result is real-time feedback about the state of your application.
Have you ever committed a small change without actually building it, only to scratch your head in bewilderment the next day when your app won't build? Have you ever pulled down someone else's changes and cursed them for breaking the build? CI solves these problems. A CI server doesn't forget to run the unit tests. It doesn't turn off static analysis because it gets tired of it taking too long to run. It doesn't forget to run your cross-platform unit tests on both iOS and Mac.
How Do I Get This Goodness?
The best CI server I've found for Mac and and iOS projects is Jenkins. Jenkins is flexible, cross-platform, open-source build server with a very active community. This community has built an impressive array of plugins that cover everything from Xcode to IRC to GitHub integration.
In order to use Jenkins to build iOS and Mac projects you need a Mac. One option is to repurpose your local development machine as a Jenkins server, but this presents a few obvious problems: I use a laptop for development and sometimes I take it upstairs or out of my local network. I also don't want expensive builds to start running in the background right as I'm testing some new animations. Instead, I recommend a hosted solution. A hosted Mac Mini is a perfect solution: cheap, fast, available. We have a quad-core i7 with 16 GB RAM hosted at Macminicolo. The connection is insanely fast and we've had zero downtime.
Once you've got the hardware, the initial Jenkins install is really straightforward. Jenkins has a native OS X installer package that will take care of everything, including creating a new jenkins user on your system and setting up Jenkins to start automatically when your computer starts up.
In order to see where CI fits in, it might help to understand our overall development workflow for Day One. This workflow is designed around three major systems:
GitHub: Source code management. GitHub has great support for private organizations. Developers in our organization can create their own private forks and issue pull requests back to the main repository. The best part is that these private forks inherit the access permissions of the main repository, which means they are not public, but still remain accessible to everyone in the organization. I'll explain why this is great a bit later.
Grove.io: Hosted IRC. IRC is a great option for teams to communicate, but can be a pain to setup and maintain. Grove handles all of these pain points and provides some nice features such as searchable, server-side archiving of all conversations. It will also notify you by email if someone mentions you in a room and you're not in there.
Jenkins: Continuous integration server. Archives our builds, runs unit tests, integrates with GitHub and Grove.
With those systems in place, here's how we go from feature to implementation:
- Determine a feature to build.
- Write high-level requirements and create mockups. These are added to a GitHub issue.
- Issue is assigned to a developer.
- If they haven't already, developer forks the main Day One repository.
- Developer creates a feature-specific branch in their fork. They'll do all their work for this feature on this branch. If more than one developer is working on this feature, those developers can all commit to this one branch in the forked repository (i.e. they don't have to create their own branch in their own repo in order to collaborate). As development progresses it's simple for the designer to checkout this branch to make sure things are progressing to spec.
- When the feature is finished, the developer issues a pull request back to the main repository.
- As soon as the pull request is created, GitHub (via a service hook) notifies our Jenkins server of the change.
- Jenkins checks out the pull request and sets the status of the pull request to "Building". This status shows up in the pull request on GitHub so that other developers know whether that pull request is safe to merge or not.
- Jenkins performs the build, runs unit tests, etc and determines if the build passes. It sets the status of the PR to either "Success" or "Failure".
- Jenkins sends a message to our Grove IRC channel about the build result.
- If the build fails, the developer makes appropriate fixes and updates the pull request with new commits. Jenkins detects these new commits and goes through the build process again.
- If the build succeeds, then it's time for a human code review. Any code that goes into the main repository needs to be reviewed by another developer. GitHub makes reviewing a pull request easy by collapsing all file changes into a single diff view. The reviewer can make line-by-line comments.
- Upon passing code review, the reviewer merges the pull request into the main repository.
- GitHub sends an IRC message about the merge.
So far we've found that this process strikes the right balance. It automates the repetitive aspects, provides protection but is still lightweight and flexible. The last thing you want in a development process is for it to get in the way of real work. Don't waste valuable development time conforming to a complicated process. Instead, change your process to make it work for your team.
Plugins are the key to getting the most out of Jenkins. Here are some that we use:
Your Jenkins server will need to sign your builds. I recommend that you create an Apple ID specifically for your build server and then add it to your iOS/Mac team.
I also recommend doing the same for the GitHub integration. Create a new GitHub user for your build server and add that user to your organization with the appropriate permissions.
Our build process is a continual work of progress. Here are some ideas we've had for future enhancements:
- Whenever Jenkins finishes a build of our Mac app, have it copy the app to a shared Dropbox folder. We can then just double-click on the app on our local machines to run the latest build. Don't even have to open Xcode!
- Create a special Jenkins job that automatically uploads the latest version of our iOS app to Test Flight for distribution to our awesome beta testers.
- Fail the build on any compiler warnings or static analyzer warnings. We still have some lingering issues in our projects that we need to resolve before we can enable this, but this is definitely a goal!
Macminicolo, a Las Vegas based company, has been hosting Mac minis since their introduction in January 2005. We are the leaders in this niche market and are known for our personal service and advanced data center. We currently host hundreds of Mac minis for satisfied customers located in 56 different countries around the world. Find us on Twitter @macminicolo or on our company blog.
Pricing and Options