Tests in CircleCI
Last Updated: 2022-01-19
Overview
The FxA team works mostly within a monolithic git repository. This is broken up into many packages, where each package tends to be a microservice or library shared between other packages.
CircleCI is a service used by FxA to automatically run tests across all packages for every pull request, branch push, and commit tag on GitHub. Scripts and configuration for CircleCI mostly live in the .circleci
directory.
Though there are exceptions on a per-package basis, the following things tend to be true:
-
We use Docker to build containers within which tests are run, as well as build artifacts for each package.
-
The
test
workflow runs for every commit to a pull request, branch, or tag. This workflow performs build, test, and deploy steps across all packages. -
The
deploy-tag
workflow runs for every tagged commit. This workflow skips tests, performing build & deploy steps across all packages. -
Each package can provide a
Dockerfile-test
file. From this, a container will be built in whichnpm run test
will be executed. A successful exit status is considered a passing test run. -
Each package can provide a
Dockerfile
orDockerfile-build
file. This will be used to build a Docker image for deployment to Docker Hub. It should be very similar toDockerfile-test
- but it can omit steps & dependencies used only for testing. -
Commits to
main
branch result in image deployments to Docker Hub for each package, using thelatest
tag. -
Commits to branches prefixed with
feature
ordockerpush
also result in images pushed to Docker Hub - in this case, the branch name is used as the Docker tag. -
Commits tagged with
git tag
or via GitHub trigger deployments of images to Docker Hub. Here, the git tag is used as the Docker tag.
Again, there are exceptions to all the above. The rest of this document will go into detail on the workflows, jobs, scripts, and important files that make up CircleCI testing - and more importantly attempt to highlight the exceptions and per-package customizations.
Workflows
A workflow in CircleCI is described like so:
a set of rules for defining a collection of jobs and their run order. Workflows support complex job orchestration using a simple set of configuration keys
test
This workflow gets run on all commits for all pull requests, branches, and tags.
All steps in the workflow depends on the install
job, so that gets run first.
After install
, the build-module
job is run in parallel for most packages - check .circleci/config.yml
for an up-to-date list. This shared job handles the work of building Docker containers with Dockerfile-test
& Dockerfile-build
, running tests, and deploying images.
Some packages do not use build-module
- these each use customized jobs:
deploy-tag
The jobs specified in this workflow only run for tags.
All steps in the workflow depends on the install
job, so that gets run first.
Then, the shared deploy-module
job is run in parallel for most packages - check .circleci/config.yml
for an up-to-date list.
Jobs
Jobs are the individual build tasks performed by CircleCI. They're orchestrated in workflows, where they can be run in parallel and/or made dependent on each other. The results of one job can also feed into another.
install
Common installation and setup for all other jobs.
Checks out the project code into the default working directory ~/project
. Runs npm ci
in the root of the project.
Creates packages/version.json
based on CircleCI env vars to describe the hash, version, source, and build URL of the current run. This version.json
will also be stored as a build artifact with the test run.
Also runs .circleci/modules-to-test.js
and outputs to packages/test.list
as a selection of which packages' tests should be run.
Finally, the install
job uses persist_to_workspace
to copy the ~/project
directory to a shared workspace that will be copied into the filesystem for each following job via attach_workspace
.
build-module
Common task used by many modules in the test
workflow to build, test, and deploy. It takes three parameters:
module
- string, name of the package to buildtest
- string, default:test
, can be used to run a different NPM script rather thannpm run test
db
- boolean, default: false
Working directory is set to ~/project/packages/{module}
. Attaches to the shared workspace. Sets up remote Docker environment for building containers.
If the db
parameter is true, it starts up containers for mysql, memcached, redis, and firestore.
Finally, it runs .circleci/build-test-deploy.sh {test}
. This runs the build, test, and deploy scripts for the module in the current working directory.
deploy-module
Common task used by many modules in the deploy-tag
workflow to build & deploy images without running tests. Takes one parameter:
module
- string
Working directory is set to ~/project/packages/{module}
. Attaches to the shared workspace. Sets up remote Docker environment for building containers.
Runs .circleci/build.sh {module}
to build a Docker container. Then, runs .circleci/deploy.sh {module}
to deploy the container to Docker Hub.
fxa-content-server
Testing fxa-content-server is very complex and heavy in resource usage. Though this is a single job, it is spread across several parallel container nodes in CircleCI (currently parallelism: 6
).
Each container is prepared with .circleci/install-content-server.sh
and then the tests are run with .circleci/test-content-server.sh
. These scripts split up which tests are run depending on the $CIRCLE_NODE_INDEX
env variable, which varies depending the parallel container node running the script.
build-and-deploy-content-server
Since the fxa-content-server
job uses a custom parallel scheme to run tests, building a Docker container image is not useful for many runs like pull requests.
So, we split those tasks into the build-and-deploy-content-server
job, which is only executed for main
branch and branch names prefixed by feature.
and dockerpush.
This is done by running .circleci/install-content-server.sh
, followed by .circleci/build.sh fxa-content-server
and .circleci/deploy.sh fxa-content-server
.
(Unlike other modules, the test.sh
script is not run here, since the fxa-content-server
job already took care of it.)
fxa-shared
Custom task for fxa-shared package. It runs lint and test directly on the CircleCI host node - rather than building a Docker container for running tests.
docs
Custom task for building and publishing documentation. Runs _scripts/gh-pages.sh
- but only on main branch in the main project repo.
This requires SSH keys that enable CircleCI to commit to the gh-page
on the main repo, so the script will be skipped if those keys are unavailable.
Important scripts & files
packages/fxa-circleci/Dockerfile
This is the base Dockerfile for the container used by most jobs. It includes most of the dependencies commonly used by all other packages. This includes a version of Firefox for Linux for integration tests - check the Dockerfile for the current version (68.0 as of this writing). Rust and Cargo are also included.
Note: If you commit an update to this Dockerfile, try not to include changes to other parts of the project in the same Pull Request. Other jobs in the same test run are likely to lag behind and use the previous version of the fxa-circleci container, because the current test run will not have had a chance to finish deploying the new image.
.circleci/config.yml
Contains general configuration, job definitions, and workflows orchestrating all the jobs.
.circleci/modules-to-test.js
Running all tests across all modules / packages in FxA can be time-comsuming. Time can be saved by skipping tests that we know are unrelated to changes in some given commit. Applying that logic, this script outputs a name of individual packages that should be tested - or all
if it decides that everything needs to be run.
For main branch and other non-PR commits, the output is always all
.
For pull requests, the script fetches and parses the diff for the current PR. The URL for the diff is constructed from CircleCI env vars.
If there's an error while fetching or parsing the diff, all
is output. Otherwise, the script checks the path of each file changed to build a list of modules for testing.
Some modules depend on other modules. Dependencies that resolve to workspace:*
are added recursively to the list of modules to test.
.circleci/build-test-deploy.sh
Runs build.sh
, test.sh
, and deploy.sh
for a given package - deriving the package name from the current working directory. Passes along the first parameter as the second parameter in test.sh
- which specifies a different NPM script to run besides the default npm run test
.
.circleci/build.sh
Common build script for most modules.
Logic for running or skipping build for a given module is implemented here: packages/test.list
is expected to contain a list output from modules-to-test.js
- if the current module's name is missing and the list isn't all
, this script will exit immediately.
Parameters are just $1
for the name of the module being tested.
The file packages/version.json
is expected to have been generated earlier - i.e. from the install
job in CircleCI. This file is copied into the package.
For modules where Dockerfile
is present, a container tagged {MODULE}:build
is built from Dockerfile
.
For modules where Dockerfile-build
is present, the container is built from that file instead.
There are exceptions for fxa-auth-server
, fxa-content-server
, and fxa-payments-server
: For each of these modules, the Docker build is performed in the packages
parent directory. This is because each of these modules depends on files from other packages (e.g. fxa-shared
) and Docker will not allow ../
path references outside the base directory for the container build.
Finally, fxa-auth-server
executes _scripts/clone-authdb.sh
in order to set up the correct database interface for the server. (TKTK?)
.circleci/test.sh
Common script that runs tests for most modules.
Parameters are $1
the name of the module being tested and $2
the name of an npm script that executes tests (defaults to test
if omitted).
Logic for running or skipping tests for a given module is implemented here: packages/test.list
is expected to contain a list output from modules-to-test.js
- if the current module's name is missing and the list isn't all
, this script will exit immediately.
For most modules: if Dockerfile-test
is present, it's used to build a Docker container.
The container is tagged ${MODULE}:test
, where MODULE
is named in the first parameter of the script. Then, npm run test
is run within the container - the name of the npm script can be customized with the second parameter of test.sh
.
If eslint
or tslint
is present in the module's package.json
, then npm run lint
is also run in the container. If there's a Gruntfile.js
that contains eslint
, then grunt eslint
will be run in the container.
.circleci/deploy.sh
Common script for deploying the Docker container built from an FxA package to Docker Hub.
The basic gist of this script is that it takes a {MODULE}:build
container - e.g. as created via build.sh
- and re-tags it as appropriate for the current CI run before pushing it to Docker Hub.
These images are deployed to Mozilla's Docker Hub account. So, for example, many modules can be found with a search for mozilla/fxa-
. Also look for images such as mozilla/123done
and mozilla/browserid-verifier
, which are listed under packages
but do not follow the fxa-*
naming convention.
The rest of the tag name depends on a few other conditions:
If the build is on main
branch, then the tag will be {MODULE}:latest
.
If a build is tagged in Git, then that Git tag is used - i.e. {MODULE}:{GIT_TAG_NAME}
.
If a build is on a branch with a name prefixed with feature
or dockerpush
, that branch name will be used in the tag - i.e. {MODULE}:{GIT_BRANCH_NAME}
.
It's also possible to supply $MODULE_SUFFIX
env var tweak the Docker Hub repo name - i.e. mozilla/$MODULE-$MODULE_SUFFIX
.
Untagged commits or commits on branches that do fall into one of the above cases do not result in a container deployed to Docker Hub.
Logic for running or skipping deployment for a given module is also implemented here: packages/test.list
is expected to contain a list output from modules-to-test.js
- if the current module's name is missing and the list isn't all
, this script will exit immediately.
TBD:
- describe how content-server tests are run
- how tests are split up between parallel CircleCI nodes
- how integration tests work with intern
- pairing tests are run separately