The what and why of CI
anes
Posted on February 22, 2023
This tutorial only talks about what and why. There is no implementation. That will follow in another tutorial
What is continuous integration (CI)
"Continuous integration (CI) is the practice of automating the integration of code changes from multiple contributors into a single software project" says Atlassian. But what does that even mean?
Continous integration is a lot of people and resources working together, to automate code release as much as possible. That includes the programmer writing his code and pushing it into a branch, which is named after the git conventions. Those are:
-
feature/[description-kebap-cased]
for features -
bugfix/[description-kebap-cased]
for bugfixes -
hotfix/[description-kebap-cased]
for hotfixes
Those then have to run trough a "QA" (quality assurance) phase, where another programmer reviews the code and shows what can be changed. Important is, that the branch includes new tests for whatever feature was added. If it was a bugfix, there should be "regression tests", which test if the bug is still happening or not.
At the same time, a CI-Tool is running in the background, which executes the tests and returns if they passed or not. That can look as follows:
If you are interested in seeing this in action, check out Ananke. I'll write an article about it, so stay tuned!
Why to set up a proper CI
Setting up a proper CI is probably the most important step of a project that is meant to last and be expanded for years to come.
It makes the code:
- Maintainable(er)
- Readable(er)
- Bug free(er)
- Secure(er)
Notice the er at the end? That's because it's not a catch-all solution that makes code automatically perfect. But having a good QA (quality assurance), having tests, having automatic linters, having tools like brakeman to check for obvious security risks makes you less prone to those issues. This is what a tech stack may look for my projects and other Rails projects (like Ananke):
Everything highlighted is only used to make the code long term maintainable
Maintainability
First and foremost: The code is better maintainable. Good and readable tests can act as a sort of "Handbook" for the code.
Real life example
Let's say, I am new in a project and I need to extend the timer_units_controller.rb
but I have no idea how it works or what it does. I can just go into the time_units_controller_spec.rb
and read the tests. They explain in (hopefully) English what every function is supposed to do.
Insurance
Next, tests act as insurance that everything you did still works.
Often times when a programmer implements a new feature, it can cause other stuff to break. By writing good tests that make sure everything does what it should, we can prevent that kind of stuff from happening.
Readability
Style checks, such as eslint
, stylelint
etc. should de a part of your CI. They make sure that the code comforts to a certain style, so a project doesn't mix a hundred different styles by the hundred different developers that worked on it. Keep in mind: You can never completely get rid of code written in a certain style, but you should try to.
Fastcheck files
What I like to do is make a bin/fastcheck
file, which executes all "fast" running tests, such as linters. A fastcheck file may look as follows:
#!/bin/sh
set -e
passing=true
check() {
if [ $? -ne 0 ]; then
passing=false
fi
}
echo "Running quick checks..."
if [[ $* == *--fix* ]]; then
echo "Autocorrecting"
bundle exec rubocop -A -c .rubocop.yml
check
npx stylelint --fix "**/*.scss"
else
echo "Note: to autocorrect, run with --fix"
check bundle exec rubocop -D -c .rubocop.yml --fail-fast
check
check npx stylelint "**/*.scss"
fi
check
bundle exec brakeman -q -z --no-summary --no-pager
check
if [ "$passing" = true ]; then
echo "All checks passed"
else
echo "Some checks failed"
exit 1
fi
Manual quality assurance
Besides that, a good and "harsh" QA (quality assurance) process makes sure that the code has been seen by at least four eyes and upholds certain standards.
Bug prevention
Everyone wants to prevent bugs in their application, but sadly it almost never works: Creating a bug-free application.
Sadly, it's almost impossible to achieve. No matter how good you think your application is, there will always be bugs. That doesn't mean, that we just give up and let bugs run rampant in our application: we give it our best to prevent them.
How to prevent bugs with CI
And that's where a good CI in combination with good versioning comes in. Having tests, which also try to cover edge-cases makes finding bugs really easy.
Let's run trough a scenario: Someone implements a new feature, where the user can also pick a background color for his profile page. To do that they get a color picker, but also an input field. Now that input field takes hex codes in this format: #FFFFFF
. What the other developer didn't think about was, that #FFF
is also a valid hex code. He pushed his code, opened a PR and requested a review by us. We look trough the code and see, that there is no test to check if #FFF
properly works. We write that into our code review and request changes. After his push we see: The new test fails, because the short format is not supported.
That's how testing should work: You write the tests for your new feature and try to simulate very single edge-case. Then you write the code that makes the tests pass.
That is called the red, green & refactor process. I wont go into that in this article, but its definitely a topic you should look up.
Another useful took, which needs good versioning: git bisect
. It binary searches trough your commit history to find new bugs. This is not exactly the topic of this article, but still important to be mentioned as it shows the importance of a clean git history.
Regression tests
Something really important are regression tests. You write those in the same PR in which you fix a bug.
The goal of regression tests is, to simulate what you did to make the code bug but expect it to not have that bug anymore. That's how we can prevent a bug from re-appearing.
In practice, that can look as follows:
Security
This pro is actually closely tied to the one before. It's the same concept of "let's write specs that try to exploit the code". That doesn't only prevent a security flaw from appearing, but also from reappearing.
Automatic security checks
A lot of frameworks have libraries/plugins that can automatically check for (obvious) security flaws. For example: Rails has Brakeman. You may have already noticed it in my fastcheck
file:
bundle exec brakeman -q -z --no-summary --no-pager
Stuff like SQL injections can quickly be caught by brakeman. That is really useful and may or may not (it definitely has already) catch developers implementing places where SQL injections are possible.
With all that said: It shouldn't give you a false sense of security. You do probably still have exploits in your application.
Automatic software updates
For everyone that has read my article about the demonstration of security flaws already knows how bad things can turn out, because a library has issues. If I would need to summarize this topic into one word: Log4Shell.
The problem with 3rd party software is: When they mess up (security wise), your software can be affected.
Luckily, often times libraries give their best to fix those exploits as quickly as possible. That doesn't matter to you tho, because you still have the exploitable version.
How can we (automatically) prevent those issues? What I like to do is also use tools such as depfu, which creates pull requests in a set schedule, which contain updates to your projects' dependencies.
Disadvantages to having a CI
Even tho I'm a big fan of having a CI, and having version updates and having tests there are obvious disadvantages and this (slightly opinionated) tutorial wouldn't be complete without listing those.
In my opinion there are a few of them:
- Money
- Money
- Money
Okay, now jokes aside. The disadvantages are mainly money. If you want a more serious list:
- Money (I'm serious)
- (Short term) Development time (which is basically money)
The money problem
The issue with having a good CI is that both upkeep cost and development time go up.
Upkeep cost
With upkeep cost I mean stuff like: running the tests on a CI tool such as semaphoreCI and paying for other tools such as depfu.
If you don't work on big projects, the costs still stay very low. From personal expirience: I pay about two dollars monthly for my personal projects (for semaphoreCI). I'm still using the free tier of depfu.
Development speed
In the short term, development speed can seriously be impacted by a CI. The QA process alone can sometime take up to weeks, if everyone that can review doesn't have time.
On the other hand you also have the writing and maintaining of a good test environment. It's really important to go all in when writing tests, because having them not test everything can cause the devs to have a false sense of security and check less if stuff actually still works.
Important: In the long term tho, I believe that a good CI improves development speed, as you don't need to worry as much about a lot of stuff such as: Keeping the codebase clean, keeping it bug-free, keeping the versions up-to-date, keeping the software relatively secure etc.
Conclusion
CI's play a very important in dev-ops environments today, which is why upcoming devs need to learn how to handle CI. I hope this article gave you a bit of an insight into how CI works, and why we do it!
If you are more interested in setting this up by yourself in projects, I currently have an article about that in the drafts.
And as always: Happy hacking :)
Posted on February 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.