When building a devops pipeline you can go two main directions: put logic into a text-based make-like tool such as Cake, or embed your logic exclusively in a Continuous Integration server like Team City or Visual Studio Team Services. The CI route provides an incredible amount of power quickly. It can distill a breathtaking range of devops complexity to a few checkboxes thanks to 3rd party plug-ins. But it comes at a cost. Here are the 4 main reasons I prefer to put my CI logic in make-like tools.
1. Reduce Vendor Lock-In
And in the darkness bind them
On my current project we've changed CI platforms twice in the last two years due to changing InfoSec requirements. It was a large amount of work to build a cross platform mobile devops pipeline in the first place, but to rebuild it two more times after that was a staggering amount of rework.
If there's one thing I've learned through this experience it's this: my ideal CI server build definition now consists of a single task: Run Make-Like Tool. In my case that tool happens to be Cake, because it's based in C#, it's cross platform and open source, and it has a gigantic set of plugins for accomplishing a wide range of tasks in the .Net toolchain and beyond.
But regardless of which Make-Like tool you use: ant, gradle, rake, psake, fake, etc, the point is that by eliminating a CI Server's custom tools and marketplace plugins, you reduce dependence on a particular CI server, eliminate vendor lock in, and increase portability. Then should you ever be forced to move from the cloud to in-house or back the migration will be a small effort.
2. Democratize DevOps
In my experience most teams end up with one owner of the build pipeline. If something breaks or a developer adds a feature that requires build script changes then nothing can move forward until the build manager, or BM if you will, can investigate.
But what happens if the BM is on vacation? What happens if they're overloaded fighting fires, deploying builds, or fixing production servers that just went offline? What happens if they get hit by a bus?
When we moved away from custom Jenkins tasks to Cake on my last project, the build scripts became available to every developer. And because the build automation scripts were in the exact same language (C#) and location (source control) as the production system code, regular developers felt more comfortable jumping in and contributing, fixing, and extending as necessary.
The evolution reminded me of projects where database access was once owned exclusively by stored procedure writing DBA's. When those projects moved to ORM, at first there was fear that everyone would break everything. Very quickly, however, the removal of strict firewalls replaced fear with flexibility, deadlocks with agility, and hard dependencies with increased productivity. Some people became less busy, others became more so, but overall the project moved noticeably faster.
3. Reduce Impedance Mismatch
Imagine you're tasked with implementing a feature that affects both production code and the build automation process -- something like crash reporting, swapping unit testing frameworks, or incorporating build environment information into the app (e.g. use a green background if in UAT). In a world of extensive Continuous Integration Logic you (or a BM) must create a custom build definition for the feature branch for testing. Then when you move the feature to the develop branch you must remember to update the associated build definitions there. Then you have to remember to update again when you merge to master, and possibly again for release branches.
The problem is that production code is frequently tightly coupled with build automation logic whether you like it or not, yet build definitions stick rigidly with a particular branch -- they fail to move with production code.
By placing all build automation logic in scripts that live under the same source control as production code you can leverage the tight coupling instead of fighting it. Build automation logic will flow smoothly through branches and merges, and no one ever needs to remember to update build definition logic, since all build definitions are identical.
And, as a bonus feature because your scripts are under source control you get version history, you can do a blame to determine that: oh yea, it was actually you that wrote that crap.
4. Simplify Debugging
The best part of moving to make-like scripts and eliminating Continuous Integration Logic is improving the "developer experience". When things go wrong on a CI server with custom logic you can't set breakpoints, environmental differences are inaccessible, logging options are limited, and you frequently have to wait very long times to see the results of any changes (i.e. the build manager inner loop, to coin another phrase).
With Cake I get feedback fast. I can start a debugging session and inspect variables. I can temporarily remove time consuming dependent tasks whose output I happen to know is cached. I never have to wait for other people's builds to complete. Also, having IntelliSense (code completion) is a productivity boost, and having the flexibility of writing my own custom plugins in C# has been extremely powerful.
The hidden costs of Continuous Integration Logic may not exceed their convenience for every project. However, having seen the pain first-hand on several occasions, my preference will be for make-like tools on all future projects. I've also migrated all my personal projects to Cake knowing it'll be the last CI migration they'll need. That is a great feeling.
Hopefully this post has helped illuminate some hidden costs of which you were unaware. If I missed one, or you disagree, please respond in the comments to start a discussion, or hit me up on Twitter.