Send With Confidence
Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.
Time to read: 9 minutes
# Our main pipeline.yml CICD flow | |
steps: | |
# We do other steps on every push to a PR and merge to master such as building a Docker image, | |
# building our JS/HTML/CSS assets, running unit tests, deploying to environments, etc. | |
# We have the option to trigger selected Cypress tests against our testing/feature branch environment | |
# for all nonmaster branches before deploying to staging | |
# We will later parse out the Yes/glob file path values vs. No values when forming the Cypress --spec option | |
- block: ':cypress: Choose Cypress specs to run in testing environment :cypress:' | |
branches: '!master' | |
fields: | |
- select: 'Do you want to run all the specs?' | |
key: 'runAll' | |
default: 'no' | |
options: | |
- label: 'Yes' | |
value: 'cypress/integration/**/*' | |
- label: 'No' | |
value: 'no' | |
- select: 'Do you want to run all the Alerts specs?' | |
key: 'runAlerts' | |
default: 'no' | |
options: | |
- label: 'Yes' | |
value: 'cypress/integration/P1/sendgridOnly/Alerts/*' | |
- label: 'No' | |
value: 'no' | |
- select: 'Do you want to run all the Mail Settings specs?' | |
key: 'runMailSettings' | |
default: 'no' | |
options: | |
- label: 'Yes' | |
value: 'cypress/integration/MailSettings/*' | |
- label: 'No' | |
value: 'no' | |
- select: 'Do you want to run all the Sender Authentication specs?' | |
key: 'runSenderAuthentication' | |
default: 'no' | |
options: | |
- label: 'Yes' | |
value: 'cypress/integration/SenderAuthentication/**/*' | |
- label: 'No' | |
value: 'no' | |
# More page specs to run | |
# Based on our Yes/No answers to the selected Cypress tests, we will parse out and form the Cypress --specs option | |
# and pass along environment variables when we trigger the Cypress pipeline to run based on environment and selected specs | |
- label: ':buildkite: Triggering Cypress tests if necessary' | |
branches: '!master' | |
command: './.buildkite/runCypress.sh' | |
env: | |
CYPRESS_TEST_ENV: 'testing' | |
PARALLELISM: '${PARALLELISM}' | |
# More steps to deploy our web assets to an S3 bucket backed by CloudFront | |
# Similar Cypress specs selector and trigger after we deploy to staging but before we deploy to production for the master branch builds |
runCypress.sh
to run after that select step to parse out the selected “Yes” or “No” values. We do this to form a list of comma-separated spec paths to run and append as an option, --spec
, to our eventual Cypress command that runs in a Docker container in a triggered pipeline. We export environment variables such as the formed list of specs in “CYPRESS_SPECS” and the current test environment in “CYPRESS_TEST_ENV” to be used in the pipeline we are triggering at the end of script with buildkite-agent pipeline upload "$DIRNAME"/triggerCypress.yml
.
#!/bin/bash | |
set -e | |
# Get where the script is currently running from | |
DIRNAME=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) | |
# Get spec selection values; its value will be the Cypress spec's relative path i.e. cypress/integration/SenderAuthentication/**/* | |
RUNALL=$(buildkite-agent meta-data get "runAll") | |
RUNALERTS=$(buildkite-agent meta-data get "runAlerts") | |
RUNMAILSETTINGS=$(buildkite-agent meta-data get "runMailSettings") | |
RUNSENDERAUTHENTICATION=$(buildkite-agent meta-data get "runSenderAuthentication") | |
# ...More Cypress Yes/No answers... | |
SPECVALUES=( | |
"${RUNALL}" | |
"${RUNALERTS}" | |
"${RUNMAILSETTINGS}" | |
"${RUNSENDERAUTHENTICATION}" | |
# ...More RUN*... | |
) | |
# Filter all selected specs that do not equal "no" | |
SELECTEDSPECS=() | |
for i in "${SPECVALUES[@]}"; do [[ $i != "no" ]] && SELECTEDSPECS+=("$i"); done | |
# Skip if no specs selected | |
if [[ ${#SELECTEDSPECS[@]} = 0 ]]; then | |
echo "Skipping Cypress specs for the ${CYPRESS_TEST_ENV} environment!" | |
exit 0 | |
fi | |
# Form comma-separated specs string i.e. cypress/integration/SenderAuthentication/**/*,cypress/integration/MailSettings/* | |
FORMATTEDSELECTEDSPECS=$( | |
IFS=$',' | |
echo "${SELECTEDSPECS[*]}" | |
) | |
# Trigger Cypress pipeline to run | |
# The exported variables such as CYPRESS_SPECS, CYPRESS_TEST_ENV, PARALLELISM, ASYNC will go to triggerCypress.yml | |
# and eventually set up the environment variables in pipeline.cypress.yml | |
export CYPRESS_SPECS="$FORMATTEDSELECTEDSPECS" | |
export CYPRESS_TEST_ENV="$CYPRESS_TEST_ENV" | |
export PARALLELISM=$PARALLELISM | |
if [[ "$CYPRESS_TEST_ENV" = "testing" ]]; then | |
# Do not block after testing/feature branch deploys based on triggered testing Cypress tests | |
export ASYNC=true | |
else | |
# Block deploys to production based on triggered staging Cypress tests | |
export ASYNC=false | |
fi | |
buildkite-agent pipeline upload "$DIRNAME"/triggerCypress.yml |
runCypress.sh
, we recall that script triggers the second pipeline to run by calling the triggerCypress.yml
file with assigned environment variable values. The triggerCypress.yml
file looks something like this. You’ll notice the “trigger” step and interpolation of values into the build messages are helpful for debugging and dynamic step names.
pipeline.cypress.yml
file like so:
Dockerfile.cypress
and docker-compose.cypress.yml
use those environment variables exported from our pipelines to then use the proper Cypress command from our application’s package.json
pointing to the right test environment and running the selected spec files. The snippets below show our general approach that you can expand on and improve to be more flexible.
We were able to run over 200 tests in around 5 minutes for one of our application repos.It then spreads out all the Cypress tests to run in parallel across those machines while maintaining the recording of each of the tests for a specific build run. This boosted our test run times dramatically!
--ci-build-id
option in addition to the parallel
option so it knows which unique build run to associate with when parallelizing tests across machines.Set up your commands to be flexible and configurable based on environment variable values.Once you have your tests running in Docker with your CI provider (and if you pay for the Dashboard Service), you can take advantage of parallelizing your tests across multiple machines. You may have to modify existing tests and resources so they are not dependent on another to avoid any tests stomping on each other.
Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.
# This is triggered from our runCypress.sh script from the main pipeline.yml's Cypress select and trigger steps | |
steps: | |
- trigger: 'cypress-trigger' # We have a separate pipeline called cypress-trigger to run our Cypress tests | |
label: ':cypress: Triggered $CYPRESS_SPECS specs against the $CYPRESS_TEST_ENV environment :cypress:' | |
async: '$ASYNC' | |
build: | |
commit: '$BUILDKITE_COMMIT' | |
message: '$BUILDKITE_MESSAGE' | |
branch: '$BUILDKITE_BRANCH' | |
# Refer to environment variables exported from runCypress.sh for where these are coming from when we call this trigger step | |
env: | |
CYPRESS_TEST_ENV: '$CYPRESS_TEST_ENV' | |
CYPRESS_SPECS: '$CYPRESS_SPECS' | |
ASYNC: '$ASYNC' | |
PARALLELISM: '$PARALLELISM' |
# Whether we plan to use this in a separate pipeline for scheduled Cypress test runs or for triggering tests in a separate Cypress pipeline | |
# all we have to do is change up the environment variable values for things to work | |
steps: | |
- label: ':npm: :docker: Build Cypress Docker image' | |
command: | |
# Building Cypress Docker image with application/test code | |
# We need to tag latest and Buildkite version on the container | |
- docker-compose -f docker-compose.cypress.yml build cypress | |
- docker tag <private_docker_registry_path>:${VERSION} <private_docker_registry_path>:latest | |
# Pushing images to private registry with Buildkite versioning and latest tags | |
- docker push <private_docker_registry_path>:${VERSION} | |
- docker push <private_docker_registry_path>:latest | |
env: | |
# Adds unique version tag through Buildkite build ID so we can reference it in future steps | |
VERSION: '$BUILDKITE_BUILD_ID' | |
- wait | |
- label: ':cypress: :chromium: Run Cypress tests' | |
# Screenshots/videos will be accessible through the Artifacts tab in this build step | |
artifact_paths: | |
- './artifacts/**/*' | |
- './artifacts/*' | |
# Monitor the Dashboard Service's recommendations for number of machines to use to optimize | |
# P1 test runs | |
parallelism: $PARALLELISM | |
command: | |
# Pull the specific Buildkite version of the image in case multiple jobs are run at the same time | |
# and latest is overwritten | |
- docker pull <private_docker_registry_path>:${VERSION} | |
# Run Cypress tests and they will be recorded through the Dashboard Service; the exit code from the Cypress tests will be outputted for success or failure build | |
- docker-compose -f docker-compose.cypress.yml up --abort-on-container-exit --exit-code-from cypress | |
env: | |
VERSION: '$BUILDKITE_BUILD_ID' | |
# These values were set from our runCypress.sh -> triggerCypress.yml steps in our main CICD pipeline or directly in a Cypress pipeline's environment variable settings | |
CYPRESS_SPECS: '$CYPRESS_SPECS' | |
CYPRESS_TEST_ENV: '$CYPRESS_TEST_ENV' |
# Dockerfile | |
# Use Cypress's base image to help set up the environment/dependencies | |
FROM cypress/base:12.6.0 | |
# This helps to clean up the console output | |
ENV CI=1 | |
# Proceed with installing Node dependencies | |
RUN mkdir -p /opt/frontendapp/ | |
WORKDIR /opt/frontendapp/ | |
COPY package.json /opt/frontendapp/ | |
COPY package-lock.json /opt/frontendapp/ | |
RUN npm ci | |
# Copy over application code and installed node_modules | |
COPY . /opt/frontendapp | |
WORKDIR /opt/frontendapp | |
# Verify Cypress installation worked | |
RUN ./node_modules/.bin/cypress verify |
version: '3.2' | |
services: | |
cypress: | |
image: <private_docker_image_path>:${VERSION:-latest} | |
# To handle OOM issues when running Cypress headless electron in Docker | |
shm_size: '3gb' | |
build: | |
cache_from: | |
- <private_docker_image_path>:latest | |
context: . | |
dockerfile: Dockerfile.cypress | |
# This handles other weirdness with Docker and Electron | |
ipc: host | |
environment: | |
# For tagging the Docker image | |
- VERSION | |
# Selected Cypress tests to run and plug into the --spec option | |
- CYPRESS_SPECS | |
# "staging" | "testing" used to run the proper "npm run cypress:run:cicd:<test_env>" command | |
- CYPRESS_TEST_ENV | |
# For Cypress parallelization purposes, we need to pass in the Buildkite build ID into the --ci-build-id option | |
- BUILDKITE_BUILD_ID | |
# Upload Cypress screenshots/videos directly to the Buildkite Artifacts tab | |
# in case we can't access the recordings through the Dashboard Service anymore | |
volumes: | |
- ./artifacts/video/:/opt/frontendapp/cypress/videos/ | |
- ./artifacts/screenshots/:/opt/frontendapp/cypress/screenshots/ | |
command: "npm run cypress:run:cicd:${CYPRESS_TEST_ENV:-staging} -- --spec '${CYPRESS_SPECS:-cypress/integration/**/*}' --parallel --ci-build-id=${BUILDKITE_BUILD_ID}" |