From 56ecfe0f39082764b365907ad0d3a83a8a8e9ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Brunat?= Date: Tue, 18 Aug 2026 15:23:01 +0200 Subject: [PATCH] docs: document the release procedure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publishing is manual from a maintainer's machine, so the steps only existed as knowledge. RELEASING.md writes them down, ordered so the irreversible PyPI upload comes last, and states plainly that a version number can never be reused — only yanked. Covers the two places the version must be bumped (they are not derived from one another), the pre-publication checks, tagging the merge commit so the tag points at reviewed code, verifying the wheel actually ships py.typed, and the recovery path when a release is wrong. Also ships CHANGELOG.md in the sdist, so the release notes travel with the source distribution. --- README.md | 3 +- RELEASING.md | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 RELEASING.md diff --git a/README.md b/README.md index ad0b4c4..eb0b6f9 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,8 @@ uv run ruff check . # lint uv run mypy # strict type checking ``` -See [CHANGELOG.md](CHANGELOG.md) for release notes, including breaking changes. +See [CHANGELOG.md](CHANGELOG.md) for release notes, including breaking changes, +and [RELEASING.md](RELEASING.md) for how a new version is published. ## License diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..a981879 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,142 @@ +# Releasing + +How a new version of `clevercloud-sdk` reaches PyPI. Publishing is manual and +run from a maintainer's machine: there is no publishing workflow and no PyPI +token stored in the repository. + +A published version is **permanent**. PyPI does not allow reusing a version +number, even after deleting a release — a mistake can only be *yanked*, which +hides it from resolvers without freeing the number. Everything below is +ordered so the irreversible step comes last. + +## Prerequisites + +- Maintainer rights on the [`clevercloud-sdk`](https://pypi.org/project/clevercloud-sdk/) + PyPI project. +- A PyPI API token, created under **Account settings → API tokens**, scoped to + this project rather than to the whole account. +- [`uv`](https://docs.astral.sh/uv/) installed locally. + +## 1. Prepare the version + +Pick the number according to [semantic versioning](https://semver.org/): a +breaking change to the public API means a new major (or minor while `0.x`). + +Update it in **both** places — they are not derived from one another, and a +mismatch is only visible after publication: + +- `version` in `pyproject.toml` +- `__version__` in `src/clever_cloud/__init__.py` + +Then add the release section to `CHANGELOG.md`, keeping the existing headings: +Security, Fixed, Added, Breaking changes. Anything that forces users to touch +their code belongs under **Breaking changes**, with the migration in one line. + +## 2. Check the working tree + +```bash +uv sync --extra dev +uv run ruff check . +uv run mypy +uv run pytest --cov +``` + +All three must pass. CI runs the same on Python 3.11, 3.12 and 3.13; run it +locally against the oldest supported version if you touched anything typing- or +syntax-related: + +```bash +uv run --python 3.11 --extra dev --isolated pytest +``` + +## 3. Merge, then tag + +Land the changes on `main` through a pull request, then tag the **merge commit** +so the tag points at reviewed code: + +```bash +git checkout main && git pull +git tag -a v0.2.0 -m "v0.2.0 + +" +git push origin v0.2.0 +``` + +Tags are named `vMAJOR.MINOR.PATCH`. Annotated (`-a`), not lightweight, so the +tag carries an author, a date and a message. + +## 4. Build and verify + +```bash +rm -rf dist +uv build +uvx twine check dist/* +``` + +`twine check` validates the metadata PyPI will reject at upload time. Also +confirm the wheel ships the typing marker — the package advertises +`Typing :: Typed`, and without this file type checkers ignore the annotations +entirely: + +```bash +python -m zipfile -l dist/*.whl | grep clever_cloud/py.typed +``` + +Optionally, install the built artifact in a throwaway environment and import it, +which catches a broken package that still builds fine: + +```bash +uv run --isolated --no-project --with dist/*.whl python -c " +import clever_cloud; print(clever_cloud.__version__)" +``` + +## 5. Publish to PyPI + +Test it against TestPyPI first if anything about the packaging changed: + +```bash +uv publish --publish-url https://test.pypi.org/legacy/ --token +``` + +Then publish for real. This is the irreversible step: + +```bash +uv publish --token +# or: UV_PUBLISH_TOKEN= uv publish +``` + +Pass the token on the command line or through `UV_PUBLISH_TOKEN`; do not write +it into a file in the repository. + +Verify the result: + +```bash +uv run --isolated --no-project --with clevercloud-sdk==0.2.0 python -c " +import clever_cloud; print(clever_cloud.__version__)" +``` + +## 6. Create the GitHub release + +Publish the release notes from the CHANGELOG section for this version, and +attach the artifacts you just uploaded so both distributions are archived +outside PyPI: + +```bash +gh release create v0.2.0 \ + --title "v0.2.0 — " \ + --notes-file \ + dist/clevercloud_sdk-0.2.0-py3-none-any.whl \ + dist/clevercloud_sdk-0.2.0.tar.gz +``` + +## If something went wrong + +A bad version cannot be replaced. Yank it, then publish a fixed one: + +```bash +# On pypi.org: project → Manage → Releases → Options → Yank +``` + +Yanking keeps the files available for anyone who pinned that exact version, but +stops resolvers from picking it up. Then bump to the next patch version and go +through this document again — never try to reupload the same number. diff --git a/pyproject.toml b/pyproject.toml index fdc0e66..7b1a118 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ Issues = "https://github.com/CleverCloud/clevercloud-sdk-python/issues" include = [ "/src", "/tests", + "/CHANGELOG.md", ] [tool.hatch.build.targets.wheel]