Safe, Simple, Automatic Releases

Sun 07 May 2023 by Moshe Zadka

Two words that strike fear in the heart of every software developer: "release process". Whether deploying a new version of a billion-person social network or a tiny little open source library, release processes are often manual, complicated, and easy to mess up.

Much has been said on the "new version of a billion-person social network" side. But what about those small, barely a person, open source libraries?

The focus here will be on

  • Open source
  • Python libraries


A new version? What is the number?

As already explored, CalVer works better than SemVer. So you already know something about the version.

With most standard CalVer schemes, the version starts with <Year>.<Month>. For example, something released in April 2023 would be 2023.04....

One option is to choose as the last digit a running number of releases in the month. So, 2023.4.3 would be the third release in April 2023. This means that when assigning the version, you have to look and see what previous releases, if any, happened this month.

More stateless is to choose the day. However, this means it is impossible to release more than once per day. This can be literally irresponsible: what happens if you need to quickly patch up a major goof?

There is another, sneakier, option. It is to take advantage that Python version standard does not mandate only three parts to the version.

For example, 2023.4.27.3 could be the third release in April 27th, 2023. Having to quickly release two versions sounds stressful, and I'm sorry someone had to experience that.

On top of all the stress, they had to have the presence of mind to check how many releases already happened today. This sounds like heaping misery on top of an already stressful situation.

Can we do better? Go completely stateless? Yes, we can.

Autocalver will assign versions to packages using <Year>.<Month>.<Day>.<Seconds from beginning of day>. The last number is only a little bit useful, but does serve as an increasing counter. The timestamp is taken from the commit log, not the build time, so builds are reproducible.


"Three may keep a Secret, if two of them are dead.", Benjamin Franklin, Poor Richard's Almanac, 1735.

Uploading your package to PyPI safely is not trivial. You can generate a package-specific token, then store it as an encrypted secret in GitHub actions, and then unpack it at the right stage, avoid leaking it, and push the package.

Sounds like being half a bad line away from leaking the API key. Luckily, there is a way to avoid it. The PyPI publish GitHub Action uses OpenID Connect to authenticate the runner against PyPI.

You will want to configure GitHub, with the appropriate parameters, as a trusted publisher.


When do you release? Are you checking releases manually? Why?

CI should make sure that, pre-merge, branches pass all automated checks. "Keep the main branch in a releasable state", as the kids used to say.

Wait. If main branch is releasable, why not...release it?

      - trunk

This will configure a GitHub action that releases on every push to trunk. (Trunk is the "main branch" of a tree, though yours might be named differently.)

See the entire action in autocalver's workflows


Configuring your project to use autocalver gives you automatic version numbers. Using PyPI/GitHub trusted publishing model eliminates complicated secret sharing schemes. GitHub upload actions on merge to main branch removes the need to make a decision about when to release.

Putting all of them together results in releases taking literally 0 work. More time for fun!