First glance at GitLab CI
It's nice! I have set up the pipeline for this site.
I have prior experience with GitHub Actions, and I have never known how they actually work under the hood and what those magic "actions" do. What I like about GitLab CI the most, is that it simply works with Docker containers.
Build and deploy
This site is a VitePress-based project and is built with Node.js. Initially, the CI consisted only from build
job:
build:
image: node:20-alpine
script:
- corepack enable
- pnpm i
- pnpm vitepress build
artifacts:
paths:
- .vitepress/dist
That's all. In order to deploy it on GitLab Pages, I also added pages
job:
stages:
- build
- deploy
build:
stage: build
image: node:20-alpine
script:
- corepack enable
- pnpm i
- pnpm vitepress build
artifacts:
paths:
- .vitepress/dist
pages:
stage: deploy
script:
- mv .vitepress/dist/ public/
artifacts:
paths:
- 'public/'
Thank you for reading this article, bye~ Let's overcomplicate things?
Set up testing
Excuse me, why do you ever need to set up testing for a personal blog project?
Reasonable question. Well, from the beginning I wanted to add some scripting feature to the site, and it had some algorithmic logic. I wrote implementation and tested it in browser, and it was buggy. I don't like to debug things manually. Instead, I write unit tests and debug such functionality with instant feedback loop (thanks to Vitest).
So, yeah, I have added Vitest to the project, and wanted to integrate it into CI. The first, naive way to do so was to deplicate build
job, but with running pnpm test
:
stages:
- test
- build
- deploy
default:
image: node:20-alpine
test:
stage: test
script:
- corepack enable
- pnpm i
- pnpm test
build:
stage: build
script:
- corepack enable
- pnpm i
- pnpm build
artifacts:
paths:
- .vitepress/dist
pages:
stage: deploy
script:
- mv .vitepress/dist/ public/
artifacts:
paths:
- 'public/'
It worked, but the whole pipeline took 1m 7s, where
test
job took 22sbuild
job took 27sdeploy
job took 17s (!)
I was concerned about the duration and tried to optimise the pipeline.
Optimising
Without looking to the internal timing of jobs (which was a mistake), I thought that the main strategy is to avoid to doing installation several times. Installation consists of calling corepack enable
(to install a proper version of pnpm
), and pnpm install
(to install project dependencies). The artifacts of these operations should be available between jobs. There are two tools to achieve it: artifacts and caching. I don't like the first way, because if I specify e.g. node_modules
as an artifact, GitLab will save it as a pipeline artifact. I don't want to store garbage, so I decided to proceed with caching.
I don't know how to cache result of corepack enable
. This command downloads pnpm
and sets up pnpm
binary in the environment. Where it stores downloaded pnpm
and which file it patches to achieve pnpm
availability - IDK. I researched a bit, didn't find anything useful and decided not caching the Corepack step.
However, caching of pnpm
artifacts is more CI-friendly. Its artifacts are:
- Store, i.e.
.pnpm-store
. It is a global (in scope of OS) store of downloaded packages that could be shared between multiple projects. node_modules
. It is a directory of a project's installed dependencies, andpnpm
links each package to.pnpm-store
.
Initially I came up with a setup
stage that prepares node_modules
and shares it between jobs:
stages:
- setup
- test
- build
- deploy
default:
image: node:20-alpine
Setup:
stage: setup
script:
- corepack enable
- pnpm config set store-dir .pnpm-store
- pnpm install
cache:
key:
files:
- pnpm-lock.yaml
paths:
- node_modules
test:
stage: test
script:
- corepack enable # it is necessary to set up `pnpm` again
- pnpm test
cache:
key:
files:
- pnpm-lock.yaml
paths:
- node_modules
policy: pull
build:
stage: build
script:
- corepack enable
- pnpm build
artifacts:
paths:
- .vitepress/dist
cache:
key:
files:
- pnpm-lock.yaml
paths:
- node_modules
policy: pull
pages:
stage: deploy
script:
- mv .vitepress/dist/ public/
artifacts:
paths:
- 'public/'
It was running for... 1m 22s! (previous pipeline took 1m 7s).
setup
job took 22stest
job took 18sbuild
job took 24sdeploy
job took 17s
This doesn't look like an optimisation at all.
I also noticed that most of the time of each job is not the actual execution of my scripts, but containers setup. For example, if you take a look into e.g. the logs for test
job, you will see that running corepack enable
and pnpm test
took only 3s!
After that, I decided to reduce amount of jobs, and also to cache .pnpm-store
instead of node_modules
as a more general solution.
If we are in a monorepo setup, where each package has its own
node_modules
, it makes more sense to cache.pnpm-store
as a single entrypoint.
stages:
- test
- build
- deploy
default:
image: node:20-alpine
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
before_script:
- corepack enable
- pnpm config set store-dir .pnpm-store
- pnpm i
test:
stage: test
script:
- pnpm test
build:
stage: build
script:
- pnpm build
artifacts:
paths:
- .vitepress/dist
pages:
stage: deploy
script:
- mv .vitepress/dist/ public/
artifacts:
paths:
- 'public/'
What's noticable:
- Single cache for all jobs
- Single setup (
before_script
) for all jobs - Each job looks less verbose
Pipeline execution time was 1m 11s, which is... still not fast.
Combine jobs + collect report
As a nice minor improvement, I wanted to leverage GitLab's unit tests reports feature. Also, the idea of combining test and build stages into a single job seemed a good optimisation strategy, since the major part of each job is its container setup, therefore, reducing the amount of jobs saves time.
After setting up test:ci
script:
{
"scripts": {
"test:ci": "vitest run --reporter junit > vitest.xml"
}
}
I came up with this CI:
stages:
- build
- deploy
default:
image: node:20-alpine
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
build:
stage: build
script:
- corepack enable
- pnpm config set store-dir .pnpm-store
- pnpm i
- pnpm test:ci
- pnpm build
artifacts:
paths:
- vitest.xml
- .vitepress/dist
reports:
junit: vitest.xml
pages:
stage: deploy
script:
- mv .vitepress/dist/ public/
artifacts:
paths:
- public/
This pipeline took 47s.
Finally
I have gone from a simple build-deploy pipeline to a (test and build)-deploy pipeline with caching of installed packages between pipelines, which is quite nice. I like GitLab CI and would like to solve more challenging problems with it.
Although same pipeline would work maybe 2-times faster with GitHub Actions, it isn't a big deal for a hobby project. After all, GitLab CI is more understandable, and I got nice test reports. although idk how much it is useful for me now
Thanks for reading my first post, btw!