Best Practices for a Continuous Integration Build
Continuous Integration (CI) can bring new levels of productivity and harmony to your app development. As a collaborative approach, Continuous Integration helps developers working on different portions of software maintain continuity and proactively resolve issues before they ever reach users. As a result, your team can work more efficiently, avoid redundancies, and stay on schedule for a more seamless deployment (and very happy clients). Frankly, we are not surprised you’ve taken an interest.
But how do you actually, well, do it? Great question! Let’s take a look at some best practices that can help you improve your CI process.
1. Maintain a single source of truth
Continuity is, in essence, a complete path that allows information to flow effectively and efficiently. It’s also a beautiful thing—and really the core of CI. Without it, you can’t gain any of the efficiencies promised in the approach. So, a useful starting place for implementing CI is designating a single, central repository for clean code and a solid system for versioning. Your development team will need a clear plan for pull requests (PRs) and coding merging—one that is specifically designed with CI in mind. You will also want to make sure that this repository is as secure as possible since it’s your single source of truth for the project.
2. It’s ok to be pushy
While there are plenty of tools to enhance the quality of your CI implementation, no factor is more important than the team’s buy-in on the approach. Why? Because for CI to work, every developer must push their code frequently.
In the context of CI, “push” is more than sharing code to a repository. It also includes the process of testing and merging code. Without frequent updates to the central repository, you can’t increase the pace of progress and stay competitive against other teams that have successfully implemented CI. It’s worth acknowledging that this process does approach Continuous Deployment, though we’re going to cover CD in more depth in a later post.
Regardless, developers should break their changes into smaller portions, do their best to push multiple times a day, and PR once a day. The ideal we’ve seen on a typical project is for your team to make a commitment to a daily PR.
3. Keep it continuous (automate, automate, automate)
So where do these commits actually go? Before they are merged, they need to undergo testing and validation. You can streamline nearly every aspect of CI by building a pipeline for compiling code, executing tests, and deploying in a production-like environment. By implementing an automated pipeline, you can start to see the speed benefits of CI kick in, as well as reducing certain types of errors. A CI pipeline also creates visibility across the entire process, so you can analyze the source of errors and avoid them in the future.
Every time a developer commits code, it automatically undergoes necessary testing (more on that later). Particularly in an agile environment where you need to release updates at a high frequency, the pipeline keeps everyone not only uniform but also moving forward.
But how can you put your first CI pipeline in place? That’s going to depend on the scale of your build and the complexity of testing and deployment involved. For smaller teams and operations, buying a pipeline is an excellent option. For example, on many of our projects across platforms, we’ve found CircleCI effective. Some alternative solutions include GitHub Actions, Jenkins, Bitrise, and GitLab CI. For more complex builds, you may need to build a customized pipeline or outsource that build to a team of professionals.
4. Know your tests – and when to run them
Let’s review the different types of testing you need to run as part of CI.
Unit tests check the business logic, or basic correctness, of code. Typically, you run these at a high frequency (often with every commit) to detect bugs.
Running unit tests as part of a CI pipeline has additional benefits. Not only will bugs be caught that might otherwise slip into a product, but the tests themselves can also have problems. Running the suite more often incentivizes the team to keep the test suite relevant and healthy.
This one is a pain because it’s both expensive and inconvenient. It also happens to be incredibly crucial. Whereas unit tests focus squarely on the code, integration tests widen the scope of testing to include pieces of your infrastructure like the network, database, or file system. These tests are going to indicate issues bigger than bugs, like a problematic change in your environment. CI allows a dev team to split out slower and more expensive tests so that they run overnight or in a more controlled fashion. This maintains a high-quality check on the product without slowing down day-to-day development.
You’re not done just yet. Once you validate your high-level architecture, you still need to test your builds within a simulation of the full production environment. The ultimate goal of any app is to provide the best experience for your end-users. That includes the portion they interact with, as well as the systems that are holding the whole thing up. Those systems include:
- Virtualization tools (if you’re running your own private cloud)
- Containers and container orchestration systems like Kubernetes
- Load balancers
- DNS servers
- Proxy servers
- Cloud object stores (AWS S3, Google GCS, etc)
Make sure that the system tests you run accurately simulate your production environment in conjunction with these elements (as applicable). A common approach to achieve this is by using lower environments that are a copy of production.
With all the changes that occur during a project—especially one with multiple developers working through multiple phases—there is always a chance that your app will not work as intended. In fact, it’s not uncommon for the same feature to break multiple times, in response to different, apparently unrelated, changes. This regression in functionality can cause all sorts of headaches and problems.
Regression testing addresses these issues by checking that existing core functionality hasn’t been damaged. A regression testing suite is often built from the previous tests you’ve successfully run in the past, such as unit or integration tests.
The upshot of regression testing is that frequent problem areas remain under test, and hidden problems are caught sooner. The team also gets a psychological boost from avoiding the feeling of “this same thing keeps breaking and I keep fixing it.” That kind of failure is very draining to a team’s sense of progress.
Keep it sorted with Agile
On larger projects, all these tests can become a bit overwhelming. You can automate all of these tests within your CI pipeline. For the best results, we typically work within an agile framework.
However, in an agile environment, multiple developers can trigger build cycles in unpredictable orders. With a rapid moving team and continuous integration, there is no “endpoint” at which to insert quality checks like integration testing. One common mistake is to push integration tests off, close to release points. However it is better to push them earlier in the process, so they run as part of merging code early and often. This gives the product team more time to deal with breakages and adjust processes.
It’s also a good idea to run these tests in separate environments so integration testing doesn’t slow down unit test feedback. That way, developers can check their code logic freely and frequently while longer integration tests run in a separate test suite and in different environments.
5. Amplify feedback – and keep it blameless
The reality is, builds are going to break. It’s part of the process! A culture of blame creates an atmosphere of fear that discourages team members from making commits. Frequent commits are a necessary component of effective CI in the first place. Along with those commits is keeping the lines of communication open. Part of the CI philosophy is to keep these communications “blameless,” or rather, focused on the opportunities for improvement rather than individual vilification.
In practice, this means that throughout the project you will want to have a system in place for sharing user feedback and learnings across the team, so everyone stays up to date. Blameless retros are a good tool, and there are many retrospective guides and styles. The important thing is that the team regularly discusses, in a future-focused way, how to improve.
It’s also important to make sure the team checks in, both at regular intervals and the close of the project, to reflect on these learnings and identify necessary improvements. No one can change the past, and on a healthy team, you should assume everyone is doing the best they know at the moment to contribute. So a healthy team stays focused on learning and improving continuously, just like their software is tested continuously.
If you’d like to learn more about blameless CI, check out Vivek Ganesan’s Blameless Continuous Integration: A Small Step Towards Psychological Safety of Agile Teams.
We’re big CI fans. Want to talk shop?
Continuous Integration isn’t something that happens overnight. It takes time and effort to get CI right for your specific needs, but that effort pays dividends when it comes to quality builds, greater responsiveness – and, the holy grail, client and user satisfaction.
Here at the Ranch, we use CI/CD on our projects as much as possible. It helps us adapt quickly to changes and deliver quality code to our customers. And if there’s one thing we love, it’s nerding out on technical topics. So if you want to learn more about our approach to CI, or how you can better implement it for your teams, don’t be shy – schedule a meeting today!
Here are some resources that I’ve used in the past if you’d like to learn more:
- CI Best Practices
- CI vs CD
- How to Implement an Effective CI/CD Pipeline
- The “Bible” for CI, though some concepts have been updated over time
- CD Best Practices
- CI/CD Best Practices
- 6 Best Practices for Integration Testing and CI