Stafini
BlogFlashcardsProjectsResume

DevOps with GitLab CI/CD - Continuous Integration with GitLab CI

What is Continuous Integration (CI)

Continuous Integration (CI) is a development practice that automates the integration of code changes into a shared repository. Key elements include:

  • Automated Builds: CI systems automatically build the entire project when code changes are pushed.

  • Code Repository: Developers contribute code changes to a shared version control repository.

  • Continuous Testing: Automated tests ensure new changes don't introduce issues.

  • Early Issue Detection: CI helps identify and address integration issues early in development.

  • Optional Deployment Automation: Some CI systems automate deployment after successful builds and tests.

Benefits

  • Faster Feedback: Rapid feedback to developers on code changes.

  • Reduced Risks: Lower risk of integration problems through frequent integration.

  • Consistent Environments: Builds and tests in consistent environments.

  • Improved Collaboration: Encourages collaboration with a shared codebase.

CI Tools

Popular CI tools include Jenkins, Travis CI, CircleCI, and GitLab CI/CD.

Setup Project for CI

For this project we will use Vite to bootstrap, we won't delve too much into what does our project do, instead focusing on continuous integration tasks such as:

  • Code lint
  • Unit test and test coverage
  • Build
  • Deploy

And we will also set them up in order

Get started by running the command

npm create vite@latest vite-ci -- --react-ts
or
yarn create vite vite-ci --template react-ts

Install project depedencies and start it.

cd vite-ci
yarn
yarn dev

Lastly we must push our project to Gitlab repository, create a new project called vite-ci if you have not.

We are going to use SSH so we need to add our SSH key to account. Go to SSH page by open Preferences then SSH, or you can access with the link: https://gitlab.com/-/profile/keys. Click on Add a new key

Then run these commands to commit and push to gitlab

git init
git remote set-url origin [email protected]:your_username/your_project_name.git
git add .
git commit -m 'Init project'
git push -uf origin main

The lint step

Code linting involves automatically checking code for style and formatting issues, ensuring a consistent and clean codebase. In Continuous Integration (CI), incorporating code linting is crucial for maintaining a unified coding style across the project, detecting potential issues early in development, facilitating collaboration by enforcing standardized code structures, and improving overall code quality by addressing problems before they escalate. It automates the adherence to coding standards, contributing to a more efficient and error-resistant development process.

Start by creating .gitlab-ci.yml file for CI configuration, then commit and push it.

lint app:
  image: node:18
  script:
    - yarn install
    - yarn lint

Noted

  • The image is different from previous post due to alpine does not have node installed by default so we won't be able to run yarn if we use it
  • The line yarn install is very important, previously we learn that evertime a job runs, it uses a fresh docker image so if we don't run yarn install there is node dependecy to run our project, the job will fail.

Navigate to Jobs page to check how it runs.

How about testing if it works properly by making linting fail, you can just add an unused variable to see how it goes

It fails as expected and we're done with the first step in automating workflow.

The test step

Unit testing involves testing individual components or functions of a software application in isolation to ensure their correct functionality. The importance of unit testing lies in its ability to verify that each part of the code performs its specific function accurately, detect and address bugs at an early stage of development, provide a safety net for refactoring, contribute to overall code quality by encouraging modular and well-organized code, and enhance maintainability by facilitating quick validation of changes through automated tests. Unit testing is a fundamental practice that ensures the reliability and robustness of software systems.

Since we're using Vite as frontend base build tool, we need to install vitest for unit test run, guide can be founded here https://vitest.dev/guide/. Also update tsconfig.json to run Typescript file

// tsconfig.json
"compilerOptions": {
    "types": ["vitest/importMeta"]
}

Again unit test is not our focus in this post so let's just create a simple test file.

// sum.ts
export function sum(a: number, b: number) {
  return a + b
}

// sum.test.ts
import { expect, test } from 'vitest'
import { sum } from './sum'

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

Also update package.json scripts with 2 lines

"test": "vitest",
"coverage": "vitest run --coverage"

Feel free to try running testing with vitest at your local

Next we are required to update CI configuration.

stages:
  - lint
  - test

lint app:
  image: node:18-alpine
  stage: lint
  script:
    - yarn install
    - yarn lint
  artifacts:
    paths:
      - $CI_PROJECT_DIR
    exclude:
      - .git
      - .git/**/*

test app:
  image: node:18-alpine
  stage: test
  script:
    - yarn coverage

Note

  • We use artifacts to copy folder from lint job to test since we don't want to run yarn install again due to everytime a job is done its container will be destroy.
  • $CI_PROJECT_DIR is variable for the full path the repository is cloned to, and where the job runs from. If the GitLab Runner builds_dir parameter is set, this variable is set relative to the value of builds_dir.
  • exclude is to prevent files from being added to an artifacts archive, avoid WARNING: Part of .git directory is on the list of files to archive.

The build step

Essentially, every time we want to deploy our application, we must perform the build step to create a version suitable for deployment. I won't delve into the details of what it does, as that would be too lengthy. However, for each committed change that is merged, there is a chance the build will fail, causing the production to fail as well. To avoid this, we could run the build command locally, but it wouldn't be ideal and would consume a significant amount of time for other developers. Instead, we can leverage GitLab to automatically run the build command every time we commit our code.

This should be the last job in our workflow and it is very easy to do, the completed yml file should look like this

stages:
  - lint
  - test
  - build

lint app:
  image: node:18-alpine
  stage: lint
  script:
    - yarn install
    - yarn lint
  artifacts:
    paths:
      - $CI_PROJECT_DIR
    exclude:
      - .git
      - .git/**/*

test app:
  image: node:18-alpine
  stage: test
  script:
    - yarn coverage

build app:
  image: node:18-alpine
  stage: build
  script:
    - yarn build

Check pipeline one last time to make sure everything works well

Merge request setup

During this post, we always push directly to main branch to test our pipeline, it is ok as we are the only one work on this and it is faster to test by pushing directly, but by best practice you should not always do it instead of creating a merge request. Let's update project setting for merge request along pipeline run.

This setting makes every MR can only be merged if pipelines are success

Pipeline Structure

  1. Source Stage:

    • Integrate with version control.
    • Trigger on code commits.
  2. Build Stage:

    • Compile code and create artifacts.
    • Run unit tests and code analysis.
  3. Test Stage:

    • Perform integration and functional testing.
    • Check code quality.
  4. Deployment Stage:

    • Define deployment environments.
    • Deploy to specified environments.
  5. Post-Deployment Stage:

    • Run additional automated tests.
    • Set up monitoring and notifications.
  6. Cleanup Stage:

    • Remove unnecessary artifacts.
    • Release resources.

Additional Considerations:

  • Parallel execution for efficiency.
  • Conditional steps based on criteria.
  • Manual approval for critical stages.
  • Artifact versioning strategy.

With that in mind, we will make a Merge request for one final change at our pipeline configuration:

stages:
  - .prev
  - build

test app:
  image: node:18-alpine
  stage: .prev
  script:
    - yarn install
    - yarn lint
    - yarn coverage
  artifacts:
    paths:
      - $CI_PROJECT_DIR
    exclude:
      - .git
      - .git/**/*

build app:
  image: node:18-alpine
  stage: build
  script:
    - yarn build

Noted

  • We merge the lint and test job into one stage since they can run in one job use one Docker image

If you check the pipeline page you can see there are two pipelines, one for the MR, other is for main branch when MR is merged


Happy Coding 🍺🍺🍺 !!!