When I first tried to switch to Gitlab CI/CD, I was disappointed by the slow build times. Let’s see if we can do better. Let’s try building a React application.
npx create-react-app faster-gitlab-cicd
cd faster-gitlab-cicd
git remote add origin <your git repo>
git add .
git commit -m "Initial commit"
Now add a file gitlab-ci.yml
and let’s build it.
# .gitlab-ci.yml
image: node:14
stages:
- test
- build
test:
stage: test # This stage must run before the release stage
script:
- yarn
- yarn react-scripts test --watchAll=false
build:
stage: build # This stage must run before the release stage
script:
- yarn
- yarn build
We are using the basic node v14 image and running the Test and Build commands on our app. It takes 3 minutes and 23 seconds to run.
Now let’s make some changes. First, we will use a lighter weight image.
image: node:14-alpine
Then let’s create a cache for the node_modules
folder. I think Gitlab recommends creating a new cache for each pipeline build. You would have a key like this key: $CI_COMMIT_REF_SLUG
in this case. However, we will live on the edge and use the same cache until the lock file change.
cache:
key:
files:
- package-lock.json
- yarn.lock
paths:
- node_modules/
Now let’s add a job to populate our node_modules cache only when the lock files change.
node-modules:
stage: prepare
script:
- yarn
only:
changes:
- package-lock.json
- yarn.lock
With that, we can remove yarn
from the script in the test
and build
jobs. It should look like this now.
# .gitlab-ci.yml
image: node:14-alpine
stages:
- prepare
- test
- build
cache:
key:
files:
- package-lock.json
- yarn.lock
paths:
- node_modules/
setup-node:
stage: prepare
script:
- yarn
only:
changes:
- package-lock.json
- yarn.lock
test:
stage: test # This stage must run before the release stage
script:
- yarn react-scripts test --watchAll=false
build:
stage: build # This stage must run before the release stage
script:
- yarn build
Make sure that yarn.lock
is changed, so we populate the cache. My first run, which populated the cache, took 2 minutes and 57 seconds. The second run, which uses the cache, took 1 minute and 27 seconds. That’s 2 and 1/3 times faster.
Let’s add some other stuff to our pipeline just for fun.
This is a resuable rules that can come in handy in complex builds.
.release_rule:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
Here is how you can use the rule. Granted, this is a pretty silly use case. It’s just an example.
workflow:
rules:
- !reference [.release_rule]
We can create variables like this.
variables:
SAMPLE_PACKAGE: 'sample.tar.gz'
This example shows how you can get the version from package.json
and pass it along to other jobs as an environmental variable.
I also show how to create some artifacts.
prepare_release:
image: endeveit/docker-jq
stage: prepare
script:
- export VERSION=$(jq -r .version package.json)
- echo "TAG=v${VERSION}" >> variables.env
- echo "PACKAGE_REGISTRY_URL=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/sample/${VERSION}" >> variables.env
- mkdir sample # create some dummy files to package
- echo "Sample 1" > sample/sample-1.txt
- echo "Sample 2" > sample/sample-2.txt
artifacts:
reports:
dotenv: variables.env # Use artifacts:reports:dotenv to expose the variables to other jobs
paths:
- sample
This example uploads a generic package. We will use this package in the Relase job.
upload-packages:
stage: publish
image: curlimages/curl:latest
needs:
- job: prepare_release
artifacts: true
script:
- tar czvf $SAMPLE_PACKAGE sample
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $SAMPLE_PACKAGE "${PACKAGE_REGISTRY_URL}/${SAMPLE_PACKAGE}"
That last job will perform a release. It uses the generic package created previously. Make sure to create a dummy CHANGELOG.md.
release_job:
stage: publish
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "Creating release for $TAG"
release:
name: 'Release $TAG'
description: './CHANGELOG.md'
tag_name: $TAG
assets:
links:
- name: 'james digital garden'
url: 'https://jameskolean.tech'
- name: 'samples'
url: '${PACKAGE_REGISTRY_URL}/${SAMPLE_PACKAGE}'
The complete pipeline looks like this.
# .gitlab-ci.yml
image: node:14-alpine
stages:
- prepare
- test
- build
- publish
cache:
key:
files:
- package-lock.json
- yarn.lock
paths:
- node_modules/
.release_rule:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
workflow:
rules:
- !reference [.release_rule]
prepare_release:
image: endeveit/docker-jq
stage: prepare
script:
- export VERSION=$(jq -r .version package.json)
- echo "TAG=v${VERSION}" >> variables.env
- echo "PACKAGE_REGISTRY_URL=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/sample/${VERSION}" >> variables.env
- mkdir sample # create some dummy files to package
- echo "Sample 1" > sample/sample-1.txt
- echo "Sample 2" > sample/sample-2.txt
artifacts:
reports:
dotenv: variables.env # Use artifacts:reports:dotenv to expose the variables to other jobs
paths:
- sample
setup-node:
stage: prepare
script:
- yarn
only:
changes:
- package-lock.json
- yarn.lock
test:
stage: test # This stage must run before the release stage
script:
- yarn react-scripts test --watchAll=false
build:
stage: build # This stage must run before the release stage
script:
- yarn build
upload-packages:
stage: publish
image: curlimages/curl:latest
needs:
- job: prepare_release
artifacts: true
script:
- tar czvf $SAMPLE_PACKAGE sample
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $SAMPLE_PACKAGE "${PACKAGE_REGISTRY_URL}/${SAMPLE_PACKAGE}"
release_job:
stage: publish
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "Creating release for $TAG"
release:
name: 'Release $TAG'
description: './CHANGELOG.md'
tag_name: $TAG
assets:
links:
- name: 'james digital garden'
url: 'https://jameskolean.tech'
- name: 'samples'
url: '${PACKAGE_REGISTRY_URL}/${SAMPLE_PACKAGE}'
Check the Release in your Gitlab project under Deployments > Releases.