Semantic Release to automate publishing to PyPI 🚀🐍
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:
- Do something
- Ensure tests are passing
- Decide if it's meaningful
- Pick my favorite number of the day, that's going to be the next version!
- Create a new tag in git
- Create a new release on GitHub (should I add details to it? Nah, it's cool, after all, who reads release notes?)
- Build and publish to PyPI (so people can
pip install
)
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?