Writing a CI/CD pipeline
Use a CI/CD pipeline to enforce controls, run tests, and automate deployments. This tutorial will help you write a simple pipeline to get started.
- GitLab CI/CD Syntax Documentation for all configuration options for a
.gitlab-ci.ymlfile. - Example project This tutorial in action.
- GitLab Runner Images Workshop-published base images for use in CI jobs.
In this tutorial, we'll put together a very simple Build → Test → Deploy pipeline that will automate checks and deployment of a simple application hosted on Cloud.gov.
Sections
Each section of the .gitlab-ci.yml file has a specific job, starting with pipeline-wide settings, and moving on to individual jobs.
Workflow
The workflow section lets us set rules about when any jobs in this pipeline should run.
The first rule targets an event condition, matched whenever a Merge Request is created. The second rule is more of a filter, satisfied whenever the targeted branch matches the $CI_DEFAULT_BRANCH branch — typically main — and running for any of several events: new commits pushed, scheduled pipelines triggered, manual executions via the "New pipeline" button (Build > Pipeplines page), and so on.
While only the first rule that matches will trigger the pipeline, successive events can trigger "duplicate" pipelines depending on your configuration. A common pitfall is to trigger a pipeline for both push and merge request events, resulting in duplicates whenever an MR is created. See the troubleshooting guide to avoiding duplicate pipelines if you're experiencing this issue.
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Defaults
The defaults section helps set up some common configuration options, so that they do not need to be specified on each job. Here, we are ensuring that yarn dependencies are always installed through the combination of a default before_script and a default cache section that ensures that each job does not need to do a full install of all yarn packages.
default:
cache:
key:
files:
- package.json
paths:
- node_modules
before_script:
- yarn install --frozen-lockfile
Stages
In a CI/CD pipeline, the simplest way to order jobs is through the use of stages. All jobs in earlier stages must complete before later stages will start. Stages are defined as a top level stages key and an array of stage names.
stages:
- build
- test
- deploy
Jobs
Jobs do not have their own entry in the pipeline file, but instead consist of all entries that aren't reserved for another purpose. Jobs have several common items that are usually provided:
imagedefines the docker image that will be run for this job.script, along withbefore_scriptandafter_script, defines the steps that are run for each job.variablesdefine settings to be passed into each job.servicesdefine other docker images that should run to provide additional services to the job, such as databases.
See the GitLab CI Syntax help document for more information on the many other options available.
Build
Jobs in the build stage typically run first — it depends on whether and how you defined stages and we put build first above — and they should do things such as installing dependencies, compiling code and assets, or building container images for use in later stages.
build-assets:
stage: build
image: node:lts-trixie-slim
script:
- npx gulp build
artifacts:
paths:
- public/assets
Test
For our test stage, we will run an automated accessibility scan. These are an excellent first step towards 508 compliance, but not a substitute for proper manual accessibility testing.
We are using the pa11y-ci image published by the Workshop team. See the GitLab Runner Images repository for a full list of these images. Images from Docker Hub or other public registries can also be used.
pa11y-check:
stage: test
image: "${CI_REGISTRY}/workshop/runner/images/pa11y-ci:4"
variables:
PA11Y_TARGET_URL: "http://localhost:3000"
script:
# start the local webserver to scan against - this must run in the background to allow the next step to proceed
- yarn serve
# this image bundles a default pa11y-ci config that can be used for most use cases
- pa11y-ci -c /home/workshop/pa11yci.js
Deploy
Finally, we'll deploy the application to our staging environment. We only want to run this job when we're on the main branch, so we'll add a rules section to control when it runs.
deploy-staging:
stage: deploy
inherit:
default: false
image: "${CI_REGISTRY}/workshop/runner/images/cf-cli:8"
dependencies: ["build-assets"]
variables:
SPACE_NAME: <cloud.gov space-name>
ORG_NAME: <cloud.gov org-name>
before_script:
- cf api https://api.fr.cloud.gov
# cf auth assumes there are CI variables named CF_USERNAME and CF_PASSWORD to provide credentials
- cf auth
- cf target -o $ORG_NAME -s $SPACE_NAME
script:
- cf push
rules:
# don't deploy on scheduled jobs
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
# don't deploy if we haven't configured the service user
- if: '$CF_USERNAME == null'
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Putting it all together
---
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
default:
cache:
key:
files:
- package.json
paths:
- node_modules
before_script:
- yarn install --frozen-lockfile
stages:
- build
- test
- deploy
build-assets:
stage: build
image: node:lts-trixie-slim
script:
- npx gulp build
artifacts:
paths:
- public/assets
pa11y-check:
stage: test
image: "${CI_REGISTRY}/workshop/runner/images/pa11y-ci:4"
variables:
PA11Y_TARGET_URL: "http://localhost:3000"
script:
# start the local webserver to scan against - this must run in the background to allow the next step to proceed
- yarn serve
# this image bundles a default pa11y-ci config that can be used for most use cases
- pa11y-ci -c /home/workshop/pa11yci.js
deploy-staging:
stage: deploy
inherit:
default: false
image: "${CI_REGISTRY}/workshop/runner/images/cf-cli:8"
dependencies: ["build-assets"]
variables:
SPACE_NAME: <cloud.gov space-name>
ORG_NAME: <cloud.gov org-name>
before_script:
- cf api https://api.fr.cloud.gov
# cf auth assumes there are CI variables named CF_USERNAME and CF_PASSWORD to provide credentials
- cf auth
- cf target -o $ORG_NAME -s $SPACE_NAME
script:
- cf push
rules:
# don't deploy on scheduled jobs
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
# don't deploy if we haven't configured the service user
- if: '$CF_USERNAME == null'
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH