Building, testing and publishing python packages using Github Actions

Building, testing and publishing python packages using Github Actions

  • Choosing When To Run an Action
  • Specifying Jobs
  • Build
  • Test
  • Publish
  • Wrapping up

Github Actions is a great product for continuous integration and delivery built directly into Github. I use it to run unit tests and publish python packages automatically in my open source projects (e.g., Summit).

However, getting actions setup properly has been a challenge. In this guide, I’ll show how to use Github Actions to run tests on PRs and publish a python package to pypi.

Choosing When To Run an Action

The first part of an action specifies what events cause the action to run. This is usually through the adverb on. Note that Github actions are specified in a YAML file in any repository under the path .github/workflows.

The following code is used to do a common pattern that is surprisingly difficult: Run the action on push or pull request but not both.

on:
  push:
  pull_request:
    branches:
      # Branches from forks have the form 'user:branch-name' so we only run
      # this job on pull_request events for branches that look like fork
      # branches. Without this we would end up running this job twice for non
      # forked PRs, once for the push and then once for opening the PR.
    - '**:**'

Specifying Jobs

Jobs are the meat of Github actions. Each job consists of keys such as runs-on (specifies the operating system) and steps (specifies the list of commands to run). A full list of keys can be found here. I like to break my actions into three jobs: build, test, and publish.

Build

The build job creates a dist for the package from the source code. I tend to use poetry as an all-in-one dependency and package manager, but you could also do this using build.

The code below checks out the repository, installs python and poetry, and builds the package. It also saves the built package as an artifact that can be used in subsequent steps. I set the retention time for the artifact to one day, which prevents a large number of unused artifacts building up.

Test

I tend to use pytest to run unit and integration tests. The code below runs unit tests on several python versions. Take a look and I’ll describe each part afterwards.

The code above does the following:

  • I use the needs: build line to specify that this job will only run after the build step is complete.
  • The strategy and matrix keys allow me to specify a list of python environments to run in parallel.
  • I install the correct version of python in each environment using curly brackets syntax for variables. Also, note that I use pip to install the package since this is what most people will use, and I want to test pip works.
  • Github actions allows me to cache dependencies to speed things up. I use the standard pip cache, but I specify the cache key as the lock file from poetry.
  • I download the artifact I created in the build step and install my package. Replace pura with the name of your package.
  • I run the tests; this code is specific to my use case, but adapt to use whatever command you use to run tests.

Publish

I want Github actions to publish to pypi whenever I bump the package versions in pyproject.toml. My strategy is to compare the version of the built package to the published version on pypi and push a release if the source code is ahead. Full description follows.

The code above does the following:

  • I first check out the repository, installing python and poetry, and downloading the built artifact. This is the same as in the test job.
  • I get the version of the package from the build using regex. Replace pura with the name of your package. Note the use of ::set-output to set a variable value to be used later
  • I use coveo pypi cli to check the current version of the published package.
  • I compare the version of the built package to the published version number, and publish the package if the built version is ahead of the published one. Some extra notes on this:
    • You will need to create an API token for your pypi account scoped at least to the package you want to publish and add the token as a Github action secret under PYPI_TOKEN.
    • The code only publishes the package on commits to the main branch and does a dry run for all other branches (e.g., on pull requests).I
  • I create and push a tag with the new version number. Again I only run this on the main branch and print out text otherwise. The text can be seen in the Github Actions UI:
  • image

Wrapping up

I hope this is helpful for your next python project! I’ve combined the full code below, but for an example in context, see Summit.