Skip to main content

An official website of the United States government

Here’s how you know

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.

Related Documentation

In this tutorial, we'll put together a very simple BuildTestDeploy 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.

warning

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.

.gitlab-ci.yml
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.

.gitlab-ci.yml
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.

.gitlab-ci.yml
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:

  • image defines the docker image that will be run for this job.
  • script, along with before_script and after_script, defines the steps that are run for each job.
  • variables define settings to be passed into each job.
  • services define 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.

.gitlab-ci.yml
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.

.gitlab-ci.yml
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.

.gitlab-ci.yml
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

.gitlab-ci.yml
---
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

GSA.gov

An official website of the U.S. General Services Administration

Looking for U.S. government information and services?
Visit USA.gov