I recently updated all JaxGaussianProcesses packages to use CircleCI for CI/CD. This post documents my experiences with this.

Why Run Continuous Integration and Continuous Deployment

Setting up CircleCI

Setting up CircleCI is straightforward. You simply create an account using your Github account and then add the repository you want to use. You can then create a .circleci folder in the root of your repository and add a config.yml file. This file contains the configuration for your CI/CD pipeline. To start with, my file took the following form:


version: 2.1
orbs:
  python: circleci/python@2.1.1
jobs:
  build-and-test:
    docker:
      - image: cimg/python:3.8.0
    steps:
      - checkout
      - python/install-packages:
          pkg-manager: pip
      - run:
          name: Run tests
          command: pytest
workflows:
  sample:
    jobs:
      - build-and-test

One complication came in that JaxKern stores its dependencies in the setup.py file, not the requirements.txt file. It seems most CircleCI documentation assumes the latter structure. To resolve this and install the dev requirements from your setup.py file, simply replace the python/install-packages step with the following:

      - python/install-packages:
          pkg-manager: pip-dist
          path-args: .[dev]

A further nuance is that to use JAX versions greater than 0.4.0, as we do in JaxKern, you need a pip version greater than the one given in cimg/python:3.8.0. To resolve this, I simply added the following step:

      - run:
          name: Update pip
          command: pip install --upgrade pip

Customising the CI/CD Pipeline

Continuous Deployment

Continuous deployment is wonderfully helpful. When a set of rules are met, the code is automatically deployed to PyPI. This means that you can be confident that the code on PyPI is always up to date. This is particularly useful for packages that are used by other packages. For example, JaxKern is used by GPJax. If JaxKern is not up to date on PyPI, then GPJax will not work.

To add this into your CircleCI config file, add the following job

  publish:
    docker:
      - image: cimg/python:3.9.0
    steps:
      - checkout
      - run:
          name: init .pypirc
          command: |
            echo -e "[distutils]" >> ~/.pypirc
            echo -e "index-servers = " >> ~/.pypirc
            echo -e "    pypi" >> ~/.pypirc
            echo -e "    jaxkern" >> ~/.pypirc
            echo -e "" >> ~/.pypirc
            echo -e "[pypi]" >> ~/.pypirc
            echo -e "    username = thomaspinder" >> ~/.pypirc
            echo -e "    password = $PYPI_TOKEN" >> ~/.pypirc
            echo -e "" >> ~/.pypirc
            echo -e "[jaxkern]" >> ~/.pypirc
            echo -e "    repository = https://upload.pypi.org/legacy/" >> ~/.pypirc
            echo -e "    username = __token__" >> ~/.pypirc
            echo -e "    password = $JAXKERN_PYPI" >> ~/.pypirc            
      - run:
          name: Build package
          command: |
            pip install -U twine
            python setup.py sdist bdist_wheel            
      - run:
          name: Upload to PyPI
          command: twine upload dist/* -r jaxkern --verbose

There’s a lot here, so let’s unpack it. We first create a .pypirc file. This file contains the credentials for uploading to PyPI. We then build the package and upload it to PyPI. The --verbose flag is useful for debugging. If you have multiple PyPI accounts, you can add them to the .pypirc file. For example, I have a PyPI account for JaxKern and a PyPI account for my personal projects. I can add both to the .pypirc file and then upload to the correct account by specifying the -r or --repository flag.

Note: The environment variables used above i.e., JAXKERN_PYPI and PYPI_TOKEN are set in the CircleCI environment variables, not Github. To set them, go to the project settings in CircleCI and then click on the Environment Variables tab. You can then add the variables there. The variables themselves are the API tokens for the PyPI accounts. You can create these tokens in your personal token setting.

The coverage report generated by pytest is also uploaded to CircleCI. This is achieved by adding the codecov orb to the config.yml file:

orbs:
  python: circleci/python@2.1.1
  codecov: codecov/codecov@3.2.2

Before adding the following chunk to the build-and-test job:

      - run:
          name: Run tests
          command: pytest --cov=./ --cov-report=xml
      - run:
          name: Upload tests to Codecov
          command: |
            curl -Os https://uploader.codecov.io/v0.1.0_4653/linux/codecov
            chmod +x codecov
            ./codecov -t ${CODECOV_TOKEN}            
      - codecov/upload:
          file: coverage.xml

Note - you’ll also need to make an environmental variable in CircleCI for the CODECOV_TOKEN. You can get this token from your Codecov settings.

To only build the package when a new tag is pushed, add the following to the workflows section of the config.yml file:

workflows:
  main:
    jobs:
      - build-and-test:
          filters:  # required since `deploy` has tag filters AND requires `build`
            tags:
              only: /.*/
      - publish:
          requires:
            - build-and-test
          filters:
            tags:
              only: /^v.*/ # Only run on tags starting with v
            branches:
              ignore: /.*/

In this chunk, the publish job is only run when a tag starting with v is pushed. This can be achieved by running

git tag -a v0.0.1 -m "Version 0.0.1"
git push origin v0.0.1

The build-and-test job is required for the publish job to run.

Initial Observations

User Interface

The interface in CircleCI is nice. Compared to Github Actions, I find it much cleaner and easier to navigate. Whilst not critical, this is a nice improvement.

Triggering Workflows

Once a repository is added to CircleCI, it will automatically trigger a workflow for every commit to a branch containing .circleci/config.yml. This is a nice feature, as it means that you can test your CI/CD pipeline before merging it into the main branch.

Builds can be manually triggered through the CircleCI web interface.

Local Testing

I always struggled to run Github actions locally. However, running CircleCI locally was a breeze. On my linux machine I installed CircleCI and Docker and connected the two through the following

sudo snap install docker circleci
sudo snap connect circleci:docker docker

You then must authenticate yourself with CircleCI by adding your API token. You can create an API token in your personal token setting. Once this is done, add the token to your CircleCI CLI by running

    circleci setup