diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..07cdb0c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,159 @@ +# Release automation: build the source distribution, create a GitHub release +# with it and publish it to PyPI. +# +# This runs when a release tag is pushed. There are no wheels: borghash is a +# Cython extension, a wheel built on a GH runner would be platform specific and +# only the sdist has ever been published. +# +# The GitHub release is created as a *draft* on purpose: the release notes want +# a human. Publishing the draft is a single click in the GitHub UI. +# +# The upload to PyPI is a separate job only so that the "pypi" environment gate +# applies to the upload alone - that is the last chance to stop a release before +# the irreversible step. +# +# One-time setup, so that no API token has to be stored anywhere: +# - on pypi.org, add a trusted publisher to the "borghash" project: +# owner "borgbackup", repository "borghash", workflow "release.yml", +# environment "pypi". +# - create the "pypi" environment in the repository settings. Configuring +# required reviewers for it makes the upload wait for an approval. + +name: Release + +on: + push: + # borghash tags have no "v" prefix: 0.2.0, and 0.3.0b1 for a pre-release. + tags: + - '*.*.*' + +permissions: + contents: read + +jobs: + release: + name: Build the sdist and draft the GitHub release + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + permissions: + contents: write # to create the release + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Just fetching one commit is not enough for setuptools-scm, so we fetch all. + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.13' + + - name: Build the sdist + # The .c files are not in git and Cython is not in build-system.requires, + # so "python -m build" can not cythonize in its isolated environment: the + # extensions have to be cythonized first, like the CI workflow does it. + # This is also what "cythonize with the latest Cython release" in + # CHANGES.rst has always meant. + run: | + set -euxo pipefail + python -m pip install --upgrade pip setuptools build twine Cython + python setup.py build_ext --inplace + python -m build --sdist + twine check dist/* + ls -l dist/ + + - name: Check that the sdist is the one for this tag + # A missing or unfetched tag makes setuptools-scm silently produce a dev + # version - much cheaper to notice here than on PyPI. + env: + TAG: ${{ github.ref_name }} + run: | + set -euxo pipefail + test -f "dist/borghash-$TAG.tar.gz" + + - name: Check that the sdist is complete and installable + # A release that can not be installed from PyPI is the worst kind of + # release. Nothing else installs borghash from a sdist, and no other + # workflow runs on a tag push, so this is the release gate: the sdist + # must build without Cython (only the generated .c files are in it) and + # the test suite must pass against what it installs. + run: | + set -euxo pipefail + python -m venv "$RUNNER_TEMP/venv-sdist" + "$RUNNER_TEMP/venv-sdist/bin/pip" install --upgrade pip + "$RUNNER_TEMP/venv-sdist/bin/pip" install dist/borghash-*.tar.gz + "$RUNNER_TEMP/venv-sdist/bin/pip" install pytest pytest-benchmark + # tests/ is not in the sdist, it comes from the checkout - but borghash + # is imported from the venv, the checkout only has it below src/. + "$RUNNER_TEMP/venv-sdist/bin/pytest" -v -rs tests/ + "$RUNNER_TEMP/venv-sdist/bin/borghash-demo" + + - name: Create the draft release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + set -euxo pipefail + # 0.3.0b1 and friends are pre-releases, 0.3.0 is not. + prerelease="" + case "$TAG" in *a*|*b*|*rc*) prerelease="--prerelease" ;; esac + cat > release-notes.md < /dev/null 2>&1; then + # a re-run of this job: keep the (possibly already edited) release and + # just replace its assets. + gh release upload "$TAG" --clobber dist/*.tar.gz + else + gh release create "$TAG" \ + --draft $prerelease \ + --title "borghash $TAG" \ + --notes-file release-notes.md \ + dist/*.tar.gz + fi + gh release view "$TAG" --json isDraft,isPrerelease,assets + + - name: Keep the sdist for the PyPI upload + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sdist + path: dist/*.tar.gz + if-no-files-found: error + + pypi: + name: Upload the sdist to PyPI + needs: [release] + + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + environment: + name: pypi + url: https://pypi.org/project/borghash/ + + permissions: + contents: read + id-token: write # trusted publishing + + steps: + - name: Get the sdist built by the release job + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: sdist + path: dist + + - name: What we are about to upload + run: ls -l dist/ + + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 diff --git a/README.rst b/README.rst index fb9b7d0..20342d6 100644 --- a/README.rst +++ b/README.rst @@ -174,6 +174,26 @@ The generated C files are included in the sdist, thus installing the built package does not require Cython. +Making a release +---------------- + +Update ``CHANGES.rst`` (the heading of the new section belongs onto the last +commit that goes into the release) and merge that via a pull request. Then put +an annotated, signed tag named like the version (no ``v`` prefix) onto the +"update CHANGES" commit and push it:: + + git tag -s -m "tagged/signed release 0.3.0" 0.3.0 + git push origin 0.3.0 + +Pushing the tag runs ``.github/workflows/release.yml``, which builds the sdist, +checks that it is complete and installable, and creates a *draft* GitHub +release with it. The upload to PyPI happens in the ``pypi`` job, which uses +trusted publishing (no API token) and waits for an approval if the ``pypi`` +environment has required reviewers configured. + +Finally, write the release notes and publish the draft release. + + Want a demo? ------------