Making a release ================ zabgrab's version comes from git tags, through `setuptools_scm `_: there's no version number to edit. Building at the commit tagged ``1.2.0`` gives version ``1.2.0``, and builds between releases get development versions such as ``1.2.1.dev3+g1234abc``. Publishing a release on GitHub uploads it to PyPI (the ``Publish to PyPI`` workflow) and publishes its documentation to GitHub Pages (the ``Publish documentation`` workflow). Version numbers --------------- Releases follow `semantic versioning `_, ``MAJOR.MINOR.PATCH``: - **PATCH** (``1.0.1``) for bug fixes only. - **MINOR** (``1.1.0``) for new features that don't break anything: existing commands, options, config files and data files work as before. - **MAJOR** (``2.0.0``) for breaking changes, such as removing or renaming options or config keys, or changing the layout of ``.mat`` and ``.h5`` files. Tags are the bare version number, without a ``v`` prefix. Preparing the release --------------------- Each release is prepared on its own branch, which is tagged and then merged into ``main``: 1. Make sure ``main`` has everything for the release, and that CI passed on its latest commit (lint, tests on every platform, the documentation and the build smoke test). 2. If the ``.mat``/``.h5`` layout changed, check the new files in MATLAB (``tests/matlab/check_files.m``) before releasing; see :doc:`contributing`. 3. Create a release branch from ``main``, named after the version: .. code-block:: bash git switch main git pull git switch -c release/1.2.0 4. Update ``CHANGELOG.rst``: replace ``(unreleased)`` in the heading with the release date, e.g. ``1.2.0 (2027-03-01)`` (keeping the underline as long as the heading), and check the list of changes is complete. Make any other last changes for the release here too, such as documentation fixes. 5. Commit, push the branch and open a pull request into ``main``: .. code-block:: bash git commit -am "Release 1.2.0" git push -u origin release/1.2.0 gh pr create --base main --title "Release 1.2.0" --body "Release 1.2.0." Wait for CI to pass on it. Fix anything that comes up on the same branch. 6. Optionally, try the package out first: run the ``Publish to PyPI`` workflow by hand on the release branch (*Actions* → *Publish to PyPI* → *Run workflow*, choosing ``release/1.2.0``). It uploads a development build to TestPyPI, which you can install and try (see :ref:`pypi-setup`). Tagging and publishing ---------------------- Tag the release branch's last commit, once CI passed on it, then merge the pull request into ``main``. Use an annotated tag (``-a``), so the tag records who made it and when: .. code-block:: bash git switch release/1.2.0 git pull git tag -a 1.2.0 -m "zabgrab 1.2.0" git push origin 1.2.0 gh release create 1.2.0 --title 1.2.0 --notes "See CHANGELOG.rst for the changes." gh pr merge release/1.2.0 --merge --delete-branch Merge with a merge commit (``--merge``, or *Create a merge commit* on GitHub), not *Squash and merge* or *Rebase and merge*: those copy the changes into new commits, leaving the tagged commit outside ``main``'s history. With a merge commit, ``main`` contains the tag, so later development builds are numbered after it (e.g. ``1.2.1.dev3+g1234abc``). To publish the release on GitHub instead of with ``gh release create``: push the tag, then open *Releases* → *Draft a new release*, choose the tag, use the version as the title, paste the version's changelog entries as the description (*Generate release notes* adds the merged pull requests too), and click *Publish release*. Publishing the release (not saving it as a draft) starts two workflows; check both ran under the repository's *Actions* tab: ``Publish to PyPI`` Builds the release from its tag and uploads it to PyPI, at once: this can't be undone, since PyPI never accepts the same version twice. If the ``pypi`` environment has required reviewers, the upload waits for one of them to approve it on the run's page. See :ref:`pypi`. ``Publish documentation`` Builds the documentation for the tagged version and deploys it to https://seands.github.io/zabgrab/ (the release's own run only starts another on ``main``, which does the work). A release marked as a *pre-release* on GitHub, tagged e.g. ``1.2.0rc1``, is uploaded to PyPI too, where ``pip install zabgrab`` skips it unless given ``--pre`` or the exact version. The documentation site's root keeps showing the latest full release. The documentation site ---------------------- GitHub Pages serves two versions of the documentation: https://seands.github.io/zabgrab/ The latest release, built from its tag. Until the first release, this redirects to the development documentation. https://seands.github.io/zabgrab/dev/ The development version, built from ``main`` after every push, with a banner saying it's unreleased. Use it to check documentation changes before releasing them. Each deployment replaces the whole site, so the ``Publish documentation`` workflow always builds both. It runs on pushes to ``main``, when a release is published, and by hand (*Actions* → *Publish documentation* → *Run workflow*), e.g. to republish the site after a failed run. Pages must be set to deploy from GitHub Actions (the repository's *Settings* → *Pages* → *Source*). .. _pypi: Publishing to PyPI ------------------ Publishing the release also starts the ``Publish to PyPI`` workflow, which builds the release from its tag and uploads it to https://pypi.org/project/zabgrab/. It checks first that the files are named for the tag's version (e.g. ``zabgrab-1.2.0-py3-none-any.whl``), and stops if they aren't, since PyPI never accepts the same version twice. Check it ran under *Actions*, then check the new version installs: .. code-block:: bash pip install --upgrade zabgrab==1.2.0 The workflow uploads with PyPI's `trusted publishing `_: PyPI trusts uploads from this workflow in this repository, so no API token is stored anywhere. .. _pypi-setup: Setting it up ~~~~~~~~~~~~~ This is done once, before the first automatic release: 1. On PyPI, open the zabgrab project's *Manage* → *Publishing* page (for a project not on PyPI yet, use *Your account* → *Publishing* to add a pending publisher), and add a GitHub trusted publisher: - Owner: ``SeanDS`` - Repository name: ``zabgrab`` - Workflow name: ``publish.yml`` - Environment name: ``pypi`` 2. On GitHub, open the repository's *Settings* → *Environments*, and create an environment named ``pypi``. Optionally: - add yourself under *Required reviewers*, so each upload waits for you to approve it, in the workflow run's page; - under *Deployment branches and tags*, choose *Selected branches and tags* and add a tag rule ``*.*.*``, so only runs from a version tag can upload. The environments need no secrets or variables: the workflow proves where it runs to PyPI itself. 3. To try the setup without a release, do the same on `TestPyPI `_ (a separate account), with the environment name ``testpypi``, and create a ``testpypi`` environment on GitHub. Then run the workflow by hand (*Actions* → *Publish to PyPI* → *Run workflow*): it uploads a development build of the chosen branch (e.g. ``1.2.1.dev3``) to TestPyPI. Install it from there to check it: .. code-block:: bash pip install --index-url https://test.pypi.org/simple/ \ --extra-index-url https://pypi.org/simple/ zabgrab==1.2.1.dev3 Publishing by hand ~~~~~~~~~~~~~~~~~~ If the workflow can't be used, build the release from a clean checkout of the tag, check it, and upload it with `twine `_ and a PyPI API token: .. code-block:: bash git switch --detach 1.2.0 git status # must be clean, or the version gets a local suffix python -m pip install --upgrade build twine rm -rf dist python -m build python -m twine check dist/* python -m twine upload dist/* The files in ``dist/`` must be named after the release version (e.g. ``zabgrab-1.2.0-py3-none-any.whl``); a ``dev`` or ``+g...`` version means the build wasn't run at the tag or the checkout wasn't clean. If something goes wrong ----------------------- If the ``Publish to PyPI`` workflow failed before uploading (e.g. the trusted publisher wasn't set up), fix the problem, then open the failed run under *Actions* and click *Re-run failed jobs*. A tag that hasn't been uploaded to PyPI (its release isn't published yet, or the upload failed), before anyone could have installed it, can be deleted and made again: .. code-block:: bash gh release delete 1.2.0 --yes git push --delete origin 1.2.0 git tag -d 1.2.0 Never move or reuse a version that has been uploaded to PyPI (PyPI refuses to take the same version twice anyway): fix the problem and release the next patch version instead.