Semantic Release to automate publishing to PyPI 🚀🐍

gui commited 4 months ago · 🐍 Python 🤖 Automation

Are you lazy? Well, I am. I hate repeated processes. Once I released Tryceratops 🦖 a few weeks ago I had to deal with a lot of repetition.

Yes, I mean releases.

Consistent Releases

Coding wasn't enough for an open-source project being used by other people, I had to be consistent with releases. My process was somewhat like this:

Don't be so repetitive!

It would be just a matter of time to make some mistakes. Until the day I forgot to run tests, or who knows, to publish on PyPI.

Well, what if I told you we can automate that? 🤖 Actually, we can automate everything. Even the boring parts that I didn't want to do.

Semantic Release with Configuration Examples

If you want some spoilers, you can check the final PR with the implementation, or just check an "automated commit" example.

As you read this article you're going to see several "commit links" pointing to GitHub, they're all small, atomic, and clear, so you can see how was the actual implementation.

After all,

Talk is cheap, show me the code!

- Linus Torvalds

Conventional Commits make versioning simple

Conventional commits is a simple standard compliant with Semantic Versioning, which means that I can automate decisions for either "is it relevant to be released?" and "which is the next version number?". Yes, I'm saving decision brain-power to focus on more important decisions just by committing in a proper way - That's neat!

Since my personal mission in this blog is to show real-life examples, I'd like to share the latest Tryceratops 🦖 commits, so you can see how it looks like.

Ok, you're lazy like me and didn't click. Here's some:

* 4cdb60a (HEAD -> main, tag: v0.3.0, origin/main) 0.3.0
* 0a2c1c5 feat!: rename 'notc' tokens to become 'noqa'
* 21a0394 chore: adjust CLI to always display tryceratops as the name
* e1cd625 chore: set up mypy as pre-commit linter
* fe8b7e6 style: resolve mypy issues for general
* ab0e48f style: resolve mypy issues for files
* 571696b style: resolve and ignore mypy issues in analyzers
* c74eb9d style: resolve mypy issues for some analyzers
* 15fe654 chore: add typing for toml
* 7f8f24f chore: add mypy
* 9dac24b docs: add changelog to PyPI and readme

Automate packaging with Poetry

See this small commit on GitHub

Just to get started, I decided to replace flit with Poetry to take care of the packaging, since Poetry had a few more features that I wanted other contributors to benefit like installing dependencies easily and generating the final package.

Now I could run: poetry add -D python-semantic-release and start setting up the pyproject.toml.

python-semantic-release Configuration

See this small commit on GitHub.

That's my favorite part. Let's start by looking at the semantic release part in pyproject.toml:

[tool.semantic_release]
version_variable = [
    "src/tryceratops/__init__.py:__version__"
]
version_toml = [
    "pyproject.toml:tool.poetry.version"
]
version_pattern = [
    "README.md:rev: v{version}",
    "docs/CONTRIBUTING.md:tryceratops, version {version}"
]
major_on_zero = false
branch = "main"
upload_to_PyPI = true
upload_to_release = true
build_command = "pip install poetry && poetry build"

I love that I don't need to care anymore about updating examples with the latest Tryceratops 🦖 version!

I used version_variable, version_toml, and version_pattern to define all the places either in Python, toml, or md that I wanted to be updated. It's just NEAT, see a generated commit example by yourself.

major_on_zero is suited for beta projects like Tryceratops 🦖, which keeps the major version at 0 until I feel confident we're stable.

branch as you might guess, determines which branch will be used for release.

build_command is what semantic release needs to do in order to generate the final package. Now that we're using poetry it would be poetry build.

Finally, generating releases to GitHub or PyPI is as easy as setting true to upload_*.

Yes, this easy

Furthermore, it started generating changelogs, which is way beyond expected 👏👏👏👏

Enable GitHub Actions

Now I don't want to trigger semantic release commands myself, it's boring.

Luckily GitHub has GitHub actions which is free CI/CD, and is good enough.

I started by enforcing unit tests and linting as a CI step: (See this small commit on GitHub).

And finally, I implemented the release step.





















⚠️ Be careful, you're about to see some code that is too beautiful ⚠️





















After creating the proper tokens, this should have worked:

See this small commit on GitHub

name: Semantic Release

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    concurrency: release

    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Python Semantic Release
        uses: relekang/python-semantic-release@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          PyPI_token: ${{ secrets.PyPI_TOKEN }}

Unfortunately, life is not always that pretty. By using an action from relekang/python-semantic-release I couldn't control the python version (Tryceratops 🦖 requires >= 3.8). See this small commit on GitHub

In the end, I had to run the command by myself what I didn't want to (I'm lazy, remember?)

name: Semantic Release

on:
  push:
    branches:
      - main

jobs:
  release:
    runs-on: ubuntu-latest
    concurrency: release
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
          token: ${{ secrets.TRYCERATOPS_GITHUB_TOKEN }}

      - name: Set up Python 3.8
        uses: actions/setup-python@v2
        with:
          python-version: 3.8

      - name: Install dependencies
        run: |
          python -m pip install poetry --upgrade pip
          poetry config virtualenvs.create false
          poetry install

      # Can't use: relekang/python-semantic-release@master because
      # it's running Python 3.7, and Tryceratops requires >=3.8
      - name: Python Semantic Release
        run: |
          git config --global user.name "github-actions"
          git config --global user.email "action@github.com"
          semantic-release publish -D commit_author="github-actions <action@github.com>"
        env:
          GH_TOKEN: ${{secrets.TRYCERATOPS_GITHUB_TOKEN}}
          PyPI_TOKEN: ${{secrets.PyPI_TOKEN}}

Meh, a bit longer than I expected. I also decided to replace the default GitHub token with a new one.

I did that to ensure semantic release will be able to commit even on protected branches.

So, tell me, are you still versioning and publishing manually?

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket