Through this article, we’re going nice and slow, from what this is all about, to how we can automate our workflow in order to make it more efficient.

Disclaimer: The tool we’ll be using is Gitlab CI as it has and currently still is to date a part of Nimble Ways’ tech stack.

Understanding the principles of Continuous Integration

As mentioned above, it all starts with a question; what is continuous integration about?

CI (short for continuous integration) is a development practice where developers integrate code into a shared repository frequently (at least once a day if not more). Each of these integrated code snippets can be tested and built automatically. This practice increases the chances of detecting and locating errors quickly and easily, therefore producing a reliable and trustworthy code. Automated testing is usually involved but it is not strictly part of CI.

Let’s suppose you’re currently working on a project, where you and 3 other developers collaborate. If you’re pushing your code on that develop/master branch at least once a day, you are already full-filling the number one rule of CI. See? Not bad for a start. You have most likely been doing it as CI has become a best practice for software development in recent years.

Configure a Gitlab Pipeline

Moving on to Gitlab CI, and why we use it as our main tool.

It sure is undeniable how various the CI tools market is. Each has its pros and cons depending on the needs. Gitlab is overall easy to use and to cope with. In order to configure your project Gitlab Pipeline, the file structure is minimalist and instinctive. You will be writing in YAML, but worry not, chances are you will likely get a hold on it before you even realize. Let’s take a look at this file (.gitlab-ci.yml at the root of your project directory)

# This is a Docker image that will be used for your jobs
# If you don't specify one, it will use Ruby 2.1 by default
image: node:alpine

# For temporary storage in order to speed up invocations of susequent runs
# For example, we cache node modules to speed up future builds
# Not to confuse with the use of artifacts
cache:
	paths:
    - node_modules

# Stages involved in the pipeline
stages:
	- some_stage

# Job definition
my_job
	stage: some_stage
    script:
    	- echo "This is your script"
Basic example of .gitlab-ci.yml configuration file

Keep in mind that:

  • A job is a set of instructions that a runner has to execute.
  • Runners are virtual machines that pick up jobs through the coordinator API of Gitlab CI. They are responsible for running the code described in your gitlab-ci.yml file.
  • A Gitlab pipeline is a set of jobs split between stages
  • A stage defines when and how to run jobs
YAML, short for “YAML Ain’t markup language”, was specifically created to work well for common use cases such as configuration files, log files and cross-language sharing files and data sharing.

Other pros about picking Gitlab CI over other tools is the possibility of building and testing Docker-based projects using Docker Engine. After creating your application Docker image, you’ll get to test push it to a remote repository after testing it, and finally to deploy your app on a server from the pushed image.

Gitlab also allows parallelizing jobs, which makes the integration process faster. You may create as many jobs as your heart desire, define when to run them, and even outputs to download if necessary.

Back to the previous example, let’s suppose we want to check for linter errors in parallel to launching our unit tests and independently. We’ll define for the same stage two different jobs, each with its own script.

# ...

stages:
	- test
    
lint_testing:
	stage: test
    script:
    	- yarn install
        - echo "Running linter check..."
        - yarn lint
        - echo "Finished linter check !"
        
run_tests:
	stage: test
	script:
    	- echo "Running tests..."
        - yarn test
        - echo "Finished running tests !"

Notice how both jobs have the same stage name, with different scripts. The Gitlab pipeline translates graphically as below,

Let’s suppose that we also want to build our project as part of our Gitlab pipeline and only after the tests succeed, this calls for another stage for which we define building and artifacts to download afterward.

Start by adding under “stages” another one for “build”.

# ...

run_build_dev:
	stage: build
    when: on_success
    only:
    	refs:
        	-master
    script:
    	- echo "Building for dev env..."
        - yarn build:dev
        - echo "Finished build !"
    artifacts:
    	paths:
        	- dist/

run_build_staging:
	stage: build
    when: on_success
    only:
    	refs:
        	-master
    script:
    	- echo "Building for staging env..."
        - yarn build:staging
        - echo "Finished build !"
    artifacts:
    	paths:
        	- dist/

This way, we define the building job only when tests succeed, and only on the master branch, as we do not particularly need to run build on each and every branch. The outcome of our four defined jobs will look as below;

Our artifacts can either be found by clicking on the job in question, or on the main page of your Gitlab project.

Applying best practices to your product development workflow

Continuous integration calls for a set of best practices. The major and first one is the maintenance of a single source repository. Your project should be build-able on a simple checkout, therefore containing all necessary artifacts for such action, without any additional dependencies or manual steps. Secondly, your team members should at least once a day commit their work, as we mentioned above. This way, conflict risks are lowered. Also, make sure your builds are self-testing. Tests are made to make sure your app is behaving as it is supposed to.

Speaking of tests, I personally would recommend using linters for spelling checks as a way to unify the code syntax across the team. Beyond the detection of errors and a quality approach, the use of a style-guide makes it possible to depersonalize the source code and to considerably facilitate branch merging operations. In a recent project, we added linter checking as part of our Gitlab pipeline. We also used a framework called Jest to test our modules and to mock module dependencies while at it. This also came with a configuration file that enabled us to monitor our test coverage.

Linters are static source code analysis tools. They can detect errors, syntax and noncompliance issues.

Overall, I say there is no right answer to what to specifically do as with your Gitlab pipeline. It depends on how you define your projects, what and when you need it. The guidelines are here to help you to get a quick hold on what these echoic words are about. You, however, will have to continuously challenge your project and team members. Nobody likes time consumption, so always make sure to test your CI implementation and adjust it until you find a suiting combo.