These Days Proper CI is Table Stakes

December 11, 2019

There is a lot of variety when it comes to Continuous Integration and Continuous Deployment (CI/CD) configurations. But I am constantly amazed by how many projects fail to even implement the absolute minimum CI configuration.

In this article, I outline what I believe to be the absolute, bare minimum continuous integration that should exist on practically every project. These are table stakes. You ought to be embarrassed if your project doesn’t do these things.

Or at least, you should be embarrassed if your project still doesn’t do these things tomorrow. Because now that you know, implementing each of these steps should take just a few minutes.

Code Style Validation

Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.
Rob Pike, co-creator of Go

Every project should check the format of the code automatically in CI. This should probably be the first step executed in your CI pipeline.

Of course not everyone agrees on how code should be formatted. There are countless debates to be had on the ideal code format.

  • Tabs vs spaces?
  • Cuddle curly braces or not?
  • Spaces around operators?
  • Blank line at the beginning of a function?
  • How many …

Shut up! This is stupid! Pick a style. Which style barely matters. What matters is that you and the rest of your team spend their time and mental energy writing code instead of arguing about style. So just pick one style, and commit to it. There are really only two important things to consider when choosing a style:

  1. Choose a style that won’t be annoying when you change teams or jobs, or when new people join your team.

    If your language has a standard style, like Go does, use that. If there’s no industry standard for your language, pick a reasonable one. For JavaScript, ESLint’s default style is a good choice.

  2. Don’t spend more than 30 minutes making a choice. Really, 5 minutes should be enough.

    Just pick a style. If you absolutely need buy-in from your team, for political reasons, give them a short list of options (2-5 at most), then let them vote (by email/web form if possible). Do this as quickly as possible. Don’t focus on getting a perfect answer, focus on getting team buy-in.

Once a style is chosen, add a command to your CI script to verify that the code conforms to the style standard. If it does not conform, the CI pipeline should fail.

It can be as simple as:

eslint "lib/**"

or

go fmt ./...

You should not have the CI pipeline auto-commit re-formatted code. This makes your revision history incredibly messy, and can even lead to errors in some cases.

Static Analysis & Linters

A computer is like a mischievous genie. It will give you exactly what you ask for, but not always what you want.
Joe Sondow
Code style validation could fit under this heading, but I like to think of them as distinct, because they serve different purposes. Where style validation primarily serves to make code consistent for human consumption, I think of static analysis and linters as serving the needs of the program itself.

Practically every language comes with a large selection of tools for static analysis. These tools can be configured to check for correctness in simple cases, such as misused shadowed variables, unchecked error conditions, or null pointer exceptions. The variety and complexity of the available tools will depend heavily on which language(s) you use in your project.

Adding such tools to your CI pipeline is usually as simple as adding a single command. If you have a more mature static analysis tool, it may be possible to run many checks simultaneously with a single tool. Do a search for “[your language] linters” or “[your language] static analysis” to find a list of such tools, then choose the ones that make sense, and add them to your CI pipeline today.

Unit Tests

If you don’t like unit testing your product, most likely your customers won’t like to test it either.
Anonymous

Every project should have unit tests.

Most projects should approach 100% test coverage.

Most projects do approach 0% test coverage.

Don't let this get you down. Even if you have zero tests in your project right now, adding one is simple. Just add a single test that does nothing, or tests simply for `true`, and start running it in your CI pipeline. Often the biggest obstacle to running unit tests is not having them set up in CI. So end the chicken-and-egg problem today by running an empty test suite in CI.

This also works well if you have an outdated test suite with broken tests. Disable the broken tests, and run only the working tests in CI. Do this today!

If you have integration tests which fail in CI, due to a missing database, for example, find a way to distinguish them from your unit tests, so that the unit tests will still be run in your CI pipeline, but the integration tests can be run in the appropriate environment.

Running a partial or even empty test suite has two important benefits:

  1. The biggest, as alluded to above, is the psychological difference between running tests in CI and not doing so. By running tests in CI, you drastically lower the bar to entry for any future developer to add a unit test, even if they do so only sparingly.
  2. Simply running tests, even if they don’t actually test code, is a test of its own. In most languages, this forces the program to be compiled or interpreted, which can catch a certain number of runtime bugs that otherwise might not be noticed until production.

Build the Code

Yes we can!
Bob the Builder

Even if you are not using continuous delivery or continuous deployment, you should at least build your code in your CI pipeline.

For some languages (particularly compiled languages), simply running unit tests can do the same for you. Even so, explicitly building a project can still be a valuable exercise.

This should take moments, at most. You already have a build command, and you probably run it several times per day. Just add it as a final step in your CI pipeline.

Getting There

In summary, you should be able to, immediately (within the next hour) start running style validation, static analysis and linters, unit tests, and a final build step in your CI pipeline. Even if you have no CI configured at all today, it should only take an extra half an hour or so, to subscribe to Travis-CI, CircleCI, or GitLab to set up a minimal CI pipeline.

The bigger problem, especially for large projects, is that getting the code to pass the new CI requirements can be a challenge.

If your code fails any of these tests, skip those tests for now. The most important thing is simply to get a minimal CI pipeline in place. For some projects, that may mean that literally the only thing your CI pipeline does is to build the project. That’s a fine starting point, as long as you don’t stop there.

Here I offer some suggestions on incrementally implementing the above steps, in the typical order of ease. Adjust to your own project as needed.

  1. Build the Code

    If your project is not buildable right now, this very instant, then you have more serious problems than this post can help with. Let me just suggest Trunk-based development for now.

  2. Code Style Validation

    Introducing style validation usually results in a lot of insignificant changes. Indentation will change, lines may be wrapped differently, etc. For most projects, the easiest way to make this transition is with a single pull request, that does nothing but reformat the code. Create a new branch, run your new code formatter, and commit all the changes at once, then merge to master. On a large team, it’s probably best to inform your teammates that this will be happening, so they can be prepared, in case any outstanding pull requests will need to be merged with the code reformat.

  3. Unit Tests

    This is usually only a problem if there’s an outdated test suite (you know the kind: half of the tests fail, or they require a specific environment that Bob knows, but he left 6 months ago, and didn’t tell anyone how to set it up).

    If you find yourself in this situation, my advice is start running the tests in CI, then immediately disable every test that doesn’t pass in CI. Some may slightly freak out about disabling tests, but if the tests aren’t trustworthy, they aren’t worth anything just now. But don’t delete them!

    Once you have all easily passing tests running in CI, merge to master, so the entire team benefits from running tests in CI. From this point on, any broken test is a reason to block a merge or halt a deployment!

    Now go back to the tests you disabled and perform triage. Some tests are likely integration tests–that is, they depend on some external resource, such as a database, to pass. These should be configured to run on demand in the proper environment, but not in your CI pipeline–at least not until you have your CI pipeline configured to run integration tests. Others may be simply outdated or no longer necessary. Either update them to pass, or delete them.

  4. Static Analysis & Linters

    Fixing errors caught by linters and static analysis tools is not as simple as running a code formatter. This class of problem often requires a certain amount of brain power, which precludes automation.

    And for a large code base, this can be a very involved process. I’ve spent literally weeks, on some code bases, fixing all linter errors.

    The solution here is to use a tool, if your language provides one, that causes your CI pipeline to fail if new violations are added, while not complaining about old violations. If your language doesn’t provide this functionality, you can hack it together with some fancy bash-fu, or you can use a service such as SonarQube that provides this capability (and many more) for you.

Next Steps

Now that the embarrassment of not having a minimal CI pipeline in place is behind you, what’s next?

There are countless ways a CI/CD pipeline can be used to improve your development team’s productivity. Some are common, some may be unique to your specific needs. The bottom line is to never stop improving! This is your starting point. Add CD if you don’t have it. Add review environments. Maybe add a stage to catch forgotten cleanups. The sky is the limit!

If you found this article helpful, please subscribe to be notified of new posts.


comments powered by Disqus
© 2020