Sam Bunting

Using GitLab CI to Prevent Merging Broken Code

How you can use the GitLab CI to make sure you don't merge in unfinished or broken code!

February 07, 2022

GitLab

CI/CD

Ever merged a branch to only find that things are broken when when you pull the branch with the newly merged changes? Could be things like the codebase doesn’t build anymore, or unit-tests have all of a sudden started to fail. If only this could be prevented somehow 🤔…

Well, turns out - it can! You can take advantage of GitLab CI to make sure you aren’t merging anything bad!

If you’re unfamiliar with what GitLab CI is - think of it as a way of automating tasks surrounding your project. There are absolutely a ton of use-cases for CI/CD, such as deploying your website to a server, compressing assets in one of the project folders, creating new releases for NPM packages and a whole lot more.

However, in our case - we’d use it to review our code and make sure everything is all working ok before we merge it into another branch.

Getting Started

If you are using a self-hosted version of GitLab, you’ll need to install a runner - you can view the documentation here to see how you can get one setup https://docs.gitlab.com/runner/install/.

First thing you will need to do is create a .gitlab-ci.yml file in the root of your repository. GitLab will automatically look for this file and use it as the reference for what jobs need to be ran.

Within the file, we need to define what image we are going to use for the runner. Runners use Docker to complete jobs - and we need to provide the Docker image we want to use.

For this example, I’m going to use one of the Node images - as that comes pre-installed with Node and NPM. There may be a better image which suits your needs, so have a look around and see which image works for you.

image: node:17.3.1-alpine

Next - we need to define the stages of the CI process. These define which jobs should be ran at the same time. Some jobs may require a previous job to be completed, and splitting them into stages will mean it will ensure that the previous stage is complete before it starts the next.

In our case, we will have two stages:

  • dependencies - Install the dependencies and make sure the runner has everything it needs to run other jobs/stages.
  • review - Review the codebase, make sure that the code is linted, unit-tests pass etc.

Stages can be defined by adding to our .gitlab-ci.yml file:

image: node:17.3.1-alpine

stages:
  - dependencies
  - review

It’s also worth saying - they will run in the order as they are defined, so if we we’re to reverse the order, the dependencies stage would run after the review stage.

Installing Dependencies

As mentioned above, the ‘dependencies’ stage would be used for installing dependencies - in order for us to do that, we would need to define a job. Jobs are specific tasks which the GitLab runner would do.

image: node:17.3.1-alpine

stages:
  - dependencies
  - review

# The name of the job, 'dependencies'
dependencies:
  # Define the stage we want this job to run at - in this case 'dependencies'
  stage: dependencies 
  # We only are going to use this job on merge requests, so only run this job on merge requests
  only:
    - merge_requests
  # Run the NPM install command to install dependencies for the project
  script:
    - npm install
  # Define the node_modules folder to keep and pass between jobs and stages
  artifacts:
    paths:
    - node_modules

This job will run npm install on merge requests, and keep the node_modules folder. This means we can use it again for other jobs in the review stage.

Review Stage

Now we have all of the dependencies all sorted out - we can move onto the review stage - where, as the name suggests, it will review our code 😄.

This is where things can start to go into all sorts of directions from here, as projects use different tools, and have different ways of running them.

However, as long as you follow the same format, you can pretty much execute any script! In the example I have below, I’ve defined some variables which you would change in square brackets:

[JOB_NAME]:
  # We would want this to run alongside other review jobs 
  stage: review
  # We only want this to run only on merge request pipelines
  only:
    - merge_requests
  script:
    [SCRIPT CONTENTS]

I’ve added an example (or two) in the form of checking the code is linted, and unit tests pass to our .gitlab-ci.yml file example:

image: node:17.3.1-alpine

stages:
  - dependencies
  - review

# The name of the job, 'dependencies'
dependencies:
  # Define the stage we want this job to run at - in this case 'dependencies'
  stage: dependencies 
  # We only are going to use this job on merge requests, so only run this job on merge requests
  only:
    - merge_requests
  # Run the NPM install command to install dependencies for the project
  script:
    - npm install
  # Define the node_modules folder to keep and pass between jobs and stages
  artifacts:
    paths:
    - node_modules

linting:
  # We would want this to run alongside other review jobs 
  stage: review
  # We only want this to run only on merge request pipelines
  only:
    - merge_requests
  script:
    - npm run lint

unit-tests:
  # We would want this to run alongside other review jobs 
  stage: review
  # We only want this to run only on merge request pipelines
  only:
    - merge_requests
  script:
    - npm test

And… That’s it! Commit the file, and push it up to GitLab. Once you create a merge request - you should see the GitLab CI kick in, and show the progress on the two stages we have.

The dependencies stage should run first, and then once that’s done, any jobs we have in the review stage should run at the same time, ensuring you have the available runners.

If one of these jobs fail - it will notify you, and you will be sure not to merge it!

Final Thoughts

Over the past few weeks and months, I’ve been trying to better myself when it comes to GitLab pipelines, and take as much advantage as I can with them. They are seriously powerful and quite useful! GitLab CI can be an absolute rabbit hole when it comes to what it can do and all the different ways of doing it. But I’m slowly figuring it out - even if it’s one job at a time 😆

Something which I’ve been trying to do is look into how you can make use of splitting out jobs and stages - and this article is a good example of how and why you should do it. While yes, you can have absolutely everything in one job, there is absolutely no stopping you. But at the same time I feel that jobs should only really have one job each such as running unit tests. I feel they shouldn’t really also install dependencies.

GitLab CI is absolutely massive - and even a bit confusing, even this article will be confusing to some I’m sure, but hopefully you’ve learned something.

That's the article!

I really hope you enjoyed this post or found it useful in some way!

Previous Article

So... I Rebuilt my Website...

New year, new codebase I guess.

January 03, 2022

Next Article

An Update

Y'alright Sam lad? What 'ave you been doin'?

October 27, 2023