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 havenode
installed by default so we won't be able to runyarn
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 runyarn 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 fromlint
job totest
since we don't want to runyarn 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 Runnerbuilds_dir
parameter is set, this variable is set relative to the value ofbuilds_dir
.exclude
is to prevent files from being added to an artifacts archive, avoidWARNING: 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
-
Source Stage:
- Integrate with version control.
- Trigger on code commits.
-
Build Stage:
- Compile code and create artifacts.
- Run unit tests and code analysis.
-
Test Stage:
- Perform integration and functional testing.
- Check code quality.
-
Deployment Stage:
- Define deployment environments.
- Deploy to specified environments.
-
Post-Deployment Stage:
- Run additional automated tests.
- Set up monitoring and notifications.
-
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
andtest
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 🍺🍺🍺 !!!