How to single-source a package's version when publishing to Pypi using Github Action?

I found this guide which, of its own admission, is outdated.
Is there a canonical good-practice for how to originate a package’s version when using Github Actions ?
On a sidenote, if in my toml file I set the version to by dynamic, it seems to fall under the responsability of the build process, so how do I do that at all using github actions ?

I’m pretty sure that’s the same question twice - i.e., you’re intended to configure GitHub Actions so that it builds your code to upload it; so you set the version as dynamic in pyproject.toml and then make setup.py do whatever is appropriate to determine the version and call setup. Unfortunately I can’t help any further than that.

… so setup.py is still a thing ? I thought it was obsolete and never to be used again, in new projects at least ?

No, not at all. You are only not supposed to run it yourself. That’s the only thing that’s been deprecated (never mind forbidden). Instead, Setuptools runs it for you (adding yet another layer of backwards compatibility hacking to make that work). You are encouraged to specify as much as possible in pyproject.toml (and failing that, setup.cfg) instead of making setup.py do all the work, but that is only future-proofing.

Of course, if you use a different build system instead of Setuptools, it might have a different idea about how to do such configuration.

1 Like

I think Github Actions use twine, if that helps.

Edit: turns out my action installs the build package which in turn uses setuptools. Though the setuptools doc clearly and repeteadly says to opt out of the setup.py pattern as much as possible.

Sorry, I did a bit more research. You don’t need to run a setup.py to specify the version number with Setuptools - assuming that your version number is either stored in plain text in a file that you distribute with the code, or else can be found by importing some module and reading an attribute. You’ll still need setup.py if you want to use setuptools_scm to compute the version number based on the Git revision history - unless you use that separately first to create a version-number file, I guess.

1 Like

I like making pyproject.toml the single source of truth for project version numbers. Whenever I need to fetch the version number at runtime, I read it from the distribution’s metadata via importlib.metadata (basically approach #5 in that link). Usually I include something like the following in the top level __init__.py to make introspecting the installed version a bit easier:

DISTRIBUTION_NAME = "my-distribution-name"

__version__ = importlib.metadata.version(DISTRIBUTION_NAME)

For packages that I publish with Github Actions, I generally add a step to my release workflow that compares the release tag with the project version number to make sure they match. Other than that, Github Actions isn’t really involved in how the project version number is kept.

1 Like

Then couldn’t I just take the release tag and set it as the version ? That way, there would be one single source of version numbers : me when clicking the Release button on github and typing a tag number/name.
Supposing I only run the Pypi export when the action related to the Release action triggers, there shouldn’t need to be an absolute need for a version apart from that specific time, right ?

Yes, I also like having the repo state be the source for versions, and you can do that with setuptools_scm if your build backend is setuptools (no need for setup.py, it can still be declarative config). Other backends have similar methods to do the same.

1 Like

It is simpler to make the release version tag and the package version string the same - in this way, release tagging and package versioning can be done all in one step and from a single “source of truth”. For the “source of truth” of versions there are different options.

As has already been pointed out, a GitHub Action shouldn’t be relied to handle package versioning. What you’re publishing to PyPI is a set of versioned artifacts that have to be built, and this is something can and should be handled by a suitable build backend.

As to the “source of truth”, a simple approach which will work is to have a (version-controlled) package version file, say, src/__version__.py (so, an src layout where the src contains your package folder), containing the version string:

__version__ = "0.0.1"

and a project TOML with the dynamic field set to indicate dynamic versioning to build backends:

[project]
...
dynamic = ["version"]
...

and a section for your build backend to tell it where/how it should get the version from. In my case, I use PDM, so I have the following section in my project TOML:

[tool.pdm.version]
source = "file"
path = "src/__version__.py"

Builds will then automatically pick up the version from this source. For every release, you would increment the version file appropriately, prior to the build and release steps. The PDM backend also supports reading versions from SCM tags (Git, Mercurial), but I prefer the file-based approach as version changes can be reviewied in the PR.

If you want automated tagging of releases in CI this is where a GitHub Action may help, if one exists. But you could also write a shell one-liner that extracts the version string from version file (if you’re using that for releases also) and use that in your workflow release job.

The repo state is native to git, but the release number is managed by Github. Unless you mean accessing the tags applied to the commit, but is that accessible via git ?

But the build backend is invoked as part of the github action.

Github’s “releases” are a management layer on top of native git’s tags (specifically “lightweight” tags), when you create a release in github it creates a git tag or you can add a release based on an existing tag in the repo. That’s how setuptools_scm works, it checks if the repo’s current checked-out state matches a tag (or can describe it relative to a recent tag).

I’m not totally sure what SR Murthy’s comment was reacting to/describing, but I’ll add that you can have an Action invoke the build backend and a command for publishing to PyPI, if you want.

But the build backend is invoked as part of the github action.

Apologies if I wasn’t clear on this: obviously, the actual build step has to be done via a build backend - I don’t use setuptools but I believe you can use it. In your GitHub action you could automate this step by defining steps to run (1) the build command to build your artifacts, and then also (2) publish them to PyPI via twine.

For (1) if you use a static version file and specify that as the source for the version string in the project TOML, as you can do quite easily, then your artifacts will be versioned appropriately after the build step cmopletes. You could do something similar for (2) publication, assuming you are OK sharing the package version with the release tag.

For what it’s worth, this was strongly discouraged when I asked about it:

I think using versioneer or setuptools_scm (or versioningit, or …) should do what you want. Just make sure when using GitHub actions to checkout with full history, so that you get tags and enough history for git describe --tags to work properly.

2 Likes

How will/do you create your releases? Via a release PR targeting the main branch? The release number / tag [?] is something that you manage - you can create a custom tag as part of the release, and that tag would propagate to the branch targeted by the release, and could then be fetched.

Not even necessarily. There is a Release section on the right of the main repo page, you create a new Release, create a new tag for it or reuse an existing one as Reich (:eyes:) said, and it just uses the latest commit of the main branch (I’m sure you can target other branches as well). I don’t know that there is a way to automate it, and in any case I don’t need to. I would just select the new version number when creating the release’s tag, which would be caught by setuptools_scm (and the good news is that the action already uses the build module which uses the setuptools module, so I guess that’s one step towards interoperability) to integrate into the package and be caught by Pypi. That looks nice, I’ll head in that direction.

Yes, this is what I do as well :slight_smile:. So I am familiar with how GitHub Releases work, but my question was about whether you do this as part of a release PR. It seems from your answer that you don’t do this - I’m sure your workflow fits your requirements, but I think it’s nicer to create releases from a release PR. Especially if you take the view that the main branch should track releases.

From your GitHub actions file it looks as if you’re almost there - only need to script the steps for building, release tagging and publishing.

1 Like