From ecbd65dd89ac7768f262805510568f0e5be08e69 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Mon, 22 Jun 2026 14:37:45 +1200 Subject: [PATCH 01/11] ci: Publish releases from git tags with auto-generated notes --- .github/release.yml | 20 ++++++++++++++++++++ .github/workflows/release-testpypi.yml | 8 ++------ .github/workflows/release.yml | 13 ++----------- 3 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..eabcfe9 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,20 @@ +changelog: + exclude: + authors: + - dependabot + - dependabot[bot] + categories: + - title: Features + labels: + - feature + - enhancement + - title: Bug Fixes + labels: + - bug + - fix + - title: Documentation + labels: + - documentation + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/release-testpypi.yml b/.github/workflows/release-testpypi.yml index 6357c42..f710fc2 100644 --- a/.github/workflows/release-testpypi.yml +++ b/.github/workflows/release-testpypi.yml @@ -9,6 +9,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up uv uses: astral-sh/setup-uv@v5 @@ -18,12 +20,6 @@ jobs: - name: Set up Python run: uv python install 3.12 - - name: Show package version - run: | - VERSION="$(uv run python -c 'import tomllib; print(tomllib.loads(open("pyproject.toml","rb").read().decode())["project"]["version"])')" - echo "Publishing version: ${VERSION}" - echo "::notice title=TestPyPI version::${VERSION}" - - name: Build run: uv build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 947e861..c3fdb3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up uv uses: astral-sh/setup-uv@v5 @@ -20,17 +22,6 @@ jobs: - name: Set up Python run: uv python install 3.12 - - name: Verify tag matches package version - run: | - TAG_VERSION="${GITHUB_REF_NAME#v}" - PKG_VERSION="$(uv run python -c 'import tomllib,sys; print(tomllib.loads(open("pyproject.toml","rb").read().decode())["project"]["version"])')" - echo "Tag version: ${TAG_VERSION}" - echo "Package version: ${PKG_VERSION}" - if [ "${TAG_VERSION}" != "${PKG_VERSION}" ]; then - echo "::error::Tag ${GITHUB_REF_NAME} does not match pyproject.toml version ${PKG_VERSION}" - exit 1 - fi - - name: Build run: uv build From 6a092cf282ea124d8b966aa1e069e029c2d138a4 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Mon, 22 Jun 2026 14:37:45 +1200 Subject: [PATCH 02/11] build: Derive version from git tags via hatch-vcs --- Makefile | 33 +-------------------------------- pyproject.toml | 10 ++++++++-- scripts/bump_version.py | 40 ---------------------------------------- uv.lock | 1 - 4 files changed, 9 insertions(+), 75 deletions(-) delete mode 100644 scripts/bump_version.py diff --git a/Makefile b/Makefile index 8664726..1a280cf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install lint format mypy test test-integration test-integration-local check build release-patch release-minor release-major +.PHONY: install lint format mypy test test-integration test-integration-local check build install: uv sync @@ -32,34 +32,3 @@ format-check: build: uv build - -# Bump version, commit, tag, push — CI publishes automatically. -# Usage: make release-patch (0.1.0 → 0.1.1) -# make release-minor (0.1.0 → 0.2.0) -# make release-major (0.1.0 → 1.0.0) -release-patch: check - $(eval VERSION := $(shell python3 scripts/bump_version.py patch)) - uv lock - git add pyproject.toml uv.lock - git commit -m "Release v$(VERSION)" - git tag "v$(VERSION)" - git push && git push --tags - @echo "Released v$(VERSION) — CI will publish to PyPI (https://pypi.org/p/datamasque-cli)" - -release-minor: check - $(eval VERSION := $(shell python3 scripts/bump_version.py minor)) - uv lock - git add pyproject.toml uv.lock - git commit -m "Release v$(VERSION)" - git tag "v$(VERSION)" - git push && git push --tags - @echo "Released v$(VERSION) — CI will publish to PyPI (https://pypi.org/p/datamasque-cli)" - -release-major: check - $(eval VERSION := $(shell python3 scripts/bump_version.py major)) - uv lock - git add pyproject.toml uv.lock - git commit -m "Release v$(VERSION)" - git tag "v$(VERSION)" - git push && git push --tags - @echo "Released v$(VERSION) — CI will publish to PyPI (https://pypi.org/p/datamasque-cli)" diff --git a/pyproject.toml b/pyproject.toml index b4dfa47..827067e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "datamasque-cli" -version = "1.4.0" +dynamic = ["version"] description = "Official command-line interface for the DataMasque data-masking platform." authors = [ { name = "DataMasque Ltd" }, @@ -127,8 +127,14 @@ markers = [ addopts = "-m 'not integration'" [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" +[tool.hatch.version] +source = "vcs" + +[tool.hatch.version.raw-options] +local_scheme = "no-local-version" + [tool.hatch.build.targets.wheel] packages = ["src/datamasque_cli"] diff --git a/scripts/bump_version.py b/scripts/bump_version.py deleted file mode 100644 index b76fce4..0000000 --- a/scripts/bump_version.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -"""Bump the version in pyproject.toml and print the new version.""" - -from __future__ import annotations - -import re -import sys - -LEVEL = sys.argv[1] if len(sys.argv) > 1 else "patch" - -with open("pyproject.toml") as f: - content = f.read() - -match = re.search(r'version = "(\d+)\.(\d+)\.(\d+)"', content) -if not match: - print("Could not find version in pyproject.toml", file=sys.stderr) - sys.exit(1) - -major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) - -if LEVEL == "patch": - patch += 1 -elif LEVEL == "minor": - minor += 1 - patch = 0 -elif LEVEL == "major": - major += 1 - minor = 0 - patch = 0 -else: - print(f"Unknown level: {LEVEL}. Use patch, minor, or major.", file=sys.stderr) - sys.exit(1) - -new_version = f"{major}.{minor}.{patch}" -new_content = content.replace(match.group(0), f'version = "{new_version}"') - -with open("pyproject.toml", "w") as f: - f.write(new_content) - -print(new_version) diff --git a/uv.lock b/uv.lock index 13b056f..938a4a7 100644 --- a/uv.lock +++ b/uv.lock @@ -141,7 +141,6 @@ wheels = [ [[package]] name = "datamasque-cli" -version = "1.4.0" source = { editable = "." } dependencies = [ { name = "datamasque-python" }, From 1e36794de71f77f87b3d52ba828c3cae728c3f1d Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Mon, 22 Jun 2026 14:37:45 +1200 Subject: [PATCH 03/11] docs(contributing): Document the tag-based release flow --- CONTRIBUTING.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83b3fa1..a270e0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -217,22 +217,30 @@ and so on. ## Releasing -Releases are published automatically by CI when a version tag is pushed. +The version is derived from the Git tag at build time (`hatch-vcs`), +so there is nothing to bump by hand. +To cut a release: + +1. On GitHub, open **Releases**, then **Draft a new release**. +2. Choose a new tag in `vX.Y.Z` form (semver, `v`-prefixed), for example `v1.4.1`. +3. Click **Generate release notes**, then **Publish release**. + +From the terminal this is: ```console -make release-patch # 0.1.0 → 0.1.1 — bug fixes -make release-minor # 0.1.0 → 0.2.0 — new features -make release-major # 0.1.0 → 1.0.0 — breaking changes +gh release create v1.4.1 --generate-notes ``` -Each target runs `make check`, -bumps the version in `pyproject.toml`, -refreshes `uv.lock`, -commits, tags, and pushes. -CI handles the publish to PyPI. +Publishing the tag triggers the `Release` workflow, +which builds the sdist and wheel +and uploads them to PyPI via trusted publishing (OIDC). + +Release notes are grouped from the pull requests merged since the previous +release, by label (see `.github/release.yml`), +so give pull requests clear titles and labels. -To smoke-test a release against TestPyPI without tagging, -trigger the `Release (TestPyPI)` workflow manually from the GitHub Actions tab. +To smoke-test a build against TestPyPI without releasing, +trigger the `Release (TestPyPI)` workflow manually from the Actions tab. ## Toolchain From bc13a422e6c80d4db69204bae837172d6b7ac593 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Mon, 22 Jun 2026 14:44:48 +1200 Subject: [PATCH 04/11] docs(contributing): Use the dm version subcommand --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a270e0a..d955500 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ File an issue on the [GitHub issue tracker](https://github.com/datamasque/datamasque-cli/issues). Please include: -- the version of `datamasque-cli` you're using (`dm --version` or `pip show datamasque-cli`); +- the version of `datamasque-cli` you're using (`dm version` or `pip show datamasque-cli`); - the Python version and operating system; - the command you ran (with credentials and other sensitive arguments redacted); - the full output, including any traceback. @@ -48,8 +48,8 @@ so the `dm` entry point on the venv reflects your working tree — no reinstall after each edit. ```console -uv run dm --version # one-shot, no venv activation needed -source .venv/bin/activate && dm --version # or activate once per shell +uv run dm version # one-shot, no venv activation needed +source .venv/bin/activate && dm version # or activate once per shell ``` Point it at a DataMasque instance. From 1d9a6827b82c026219d28d1986efd6f0988edd1e Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Mon, 22 Jun 2026 14:44:48 +1200 Subject: [PATCH 05/11] docs: Remove CHANGELOG.md in favour of GitHub release notes --- CHANGELOG.md | 82 ---------------------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3205275..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,82 +0,0 @@ -# Changelog - -## v1.4.0 - -### Added -- `dm connections create --file` now supports Databricks SQL Warehouse - (`"type": "databricks"`) and MongoDB (`"type": "mongodb"`) connections. - Both list, get, create, and delete like the existing connection types. - -## v1.3.0 - -### Added -- `dm system ai-engine show` and `dm system ai-engine set ` — view and - configure the AI Engine URL. - -## v1.2.0 - -### Added -- `dm ifm` command group - for managing in-flight masking ruleset plans - and running mask operations against the IFM service: - - `dm ifm list` — - list all IFM ruleset plans. - - `dm ifm get ` — - show plan metadata, - or the ruleset YAML with `--yaml`. - - `dm ifm create --name --file ` — - create a plan from a YAML ruleset, - with optional `--enabled/--disabled` and `--log-level`. - - `dm ifm update ` — - update a plan; - pass any of `--file`, `--enabled/--disabled`, `--log-level` - and only those fields are sent. - - `dm ifm delete ` — - delete a plan - (interactive confirm, - or `--yes` to skip). - - `dm ifm mask --data ` — - mask a JSON list of records against a plan, - with `--disable-instance-secret`, - `--run-secret`, - `--log-level`, - `--request-id`, - and `--json/--no-json` (NDJSON) output. - - `dm ifm verify-token` — - verify the current IFM token and list its scopes. - - Authentication reuses your existing `dm` profile credentials - via the SDK's `DataMasqueIfmClient`, - which transparently exchanges admin-server credentials for an IFM JWT. - -## v1.1.0 - -### Added -- `dm catalog` command — emits the full subcommand tree as JSON for agent - introspection. `--compact` for `{path, help}` only (~1.4kB), default for - full options/arguments. -- Auto-detection of agent context: output flips to JSON automatically when - stdout is not a TTY, when `DM_OUTPUT=json` is set, or when the - vendor-neutral `AI_AGENT` env var is present. `DM_OUTPUT=table` forces - human output. -- Structured error envelope on stderr in agent mode: - `{"error": {"code": "...", "message": "...", "hint": "..."}}` — stdout - stays empty on failure so downstream pipes don't trip. - -### Changed -- Exit codes are now differentiated by error category. Previously every - error returned 1; now: `not_found`=3, `invalid_input`=4, `ambiguous`=5, - `auth_required`=6, `auth_failed`=7, `conflict`=8, `transport_error`=9. - `error` (unclassified) remains 1; 2 is reserved for typer/click usage - errors. Stable across minor versions. -- Long values (UUIDs especially) now fold across lines in table output - rather than being silently truncated with `…` in narrow terminals. - -### Internal -- `ErrorCode` and `ConnectionType` are now `StrEnum`s; the abort code arg - is type-checked at edit time and the connection-type "Valid: ..." hint - is generated from the enum. - -## v1.0.0 - -Initial release. From fb5cd655fad48f996c3394d75d79811b2223459d Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Tue, 23 Jun 2026 10:01:41 +1200 Subject: [PATCH 06/11] ci: guard release tags are well-formed and newer than the latest --- .github/workflows/release.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3fdb3a..7fe1ba7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,19 @@ jobs: with: fetch-depth: 0 + - name: Validate release tag + run: | + tag="${GITHUB_REF_NAME}" + if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Tag $tag is not in vMAJOR.MINOR.PATCH form" + exit 1 + fi + newest="$(git tag --list 'v*.*.*' --sort=-v:refname | head -n1)" + if [ "$tag" != "$newest" ]; then + echo "::error::Tag $tag is not the newest release tag ($newest); refusing to publish an out-of-order version" + exit 1 + fi + - name: Set up uv uses: astral-sh/setup-uv@v5 with: From 7e2a1fafeced61947556b3c46588413a00f9b307 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Tue, 23 Jun 2026 10:01:41 +1200 Subject: [PATCH 07/11] build: add make release-* targets over gh release create --- Makefile | 11 ++++++++++- scripts/release.sh | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 scripts/release.sh diff --git a/Makefile b/Makefile index 1a280cf..368ab6e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: install lint format mypy test test-integration test-integration-local check build +.PHONY: install lint format mypy test test-integration test-integration-local check build release-patch release-minor release-major install: uv sync @@ -32,3 +32,12 @@ format-check: build: uv build + +release-patch: + @sh scripts/release.sh patch + +release-minor: + @sh scripts/release.sh minor + +release-major: + @sh scripts/release.sh major diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100644 index 0000000..d640c91 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +# Cut a release by tagging only: hatch-vcs derives the version from the tag and +# the Release workflow publishes to PyPI, so nothing is committed to main. +set -eu + +level=${1:-patch} +git fetch --tags --quiet +latest=$(git describe --tags --abbrev=0 --match 'v*.*.*') + +IFS=. read -r major minor patch <&2; exit 1 ;; +esac + +next="v$major.$minor.$patch" +printf 'Release %s (from %s)? This publishes to PyPI and cannot be undone. [y/N] ' "$next" "$latest" +read -r reply +case "$reply" in + [yY]*) gh release create "$next" --generate-notes --target main ;; + *) echo "Aborted." >&2; exit 1 ;; +esac From 0a4a57f7789b4022639bf305de4685d83fe0f91a Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Tue, 23 Jun 2026 10:01:41 +1200 Subject: [PATCH 08/11] docs(contributing): document the make release-* fast path --- CONTRIBUTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d955500..a976621 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -231,6 +231,18 @@ From the terminal this is: gh release create v1.4.1 --generate-notes ``` +Or, to bump from the latest tag without picking the number yourself: + +```console +make release-patch # latest tag + 0.0.1 — bug fixes +make release-minor # latest tag + 0.1.0 — new features +make release-major # latest tag + 1.0.0 — breaking changes +``` + +These read the latest `vX.Y.Z` tag, confirm the next one, and create the release +through `gh`. Nothing is committed to `main`, so the branch ruleset does not +block them. + Publishing the tag triggers the `Release` workflow, which builds the sdist and wheel and uploads them to PyPI via trusted publishing (OIDC). From 73493a4e9967c6a7773577598e87493dba002cf4 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Tue, 23 Jun 2026 10:07:17 +1200 Subject: [PATCH 09/11] docs(contributing): make the release workflow fully explicit --- CONTRIBUTING.md | 79 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a976621..81212c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -217,42 +217,75 @@ and so on. ## Releasing -The version is derived from the Git tag at build time (`hatch-vcs`), -so there is nothing to bump by hand. -To cut a release: +A release is a Git tag. +The version is derived from that tag at build time by `hatch-vcs`, +so there is no version field to edit +and nothing is committed to `main` to cut a release. -1. On GitHub, open **Releases**, then **Draft a new release**. -2. Choose a new tag in `vX.Y.Z` form (semver, `v`-prefixed), for example `v1.4.1`. -3. Click **Generate release notes**, then **Publish release**. +Tags are semver, `v`-prefixed, in `vMAJOR.MINOR.PATCH` form, +for example `v1.4.1`. -From the terminal this is: +### Cut a release + +Pick the change level and run one of: ```console -gh release create v1.4.1 --generate-notes +make release-patch # latest tag + 0.0.1: bug fixes only +make release-minor # latest tag + 0.1.0: backwards-compatible features +make release-major # latest tag + 1.0.0: breaking changes ``` -Or, to bump from the latest tag without picking the number yourself: +Each target reads the latest `vX.Y.Z` tag, +works out the next version, +asks you to confirm, +then creates the GitHub release through `gh`. +Because it only creates a tag, +the `main` branch ruleset does not block it. + +To pick the number yourself, the same thing from the terminal is: ```console -make release-patch # latest tag + 0.0.1 — bug fixes -make release-minor # latest tag + 0.1.0 — new features -make release-major # latest tag + 1.0.0 — breaking changes +gh release create v1.4.1 --generate-notes ``` -These read the latest `vX.Y.Z` tag, confirm the next one, and create the release -through `gh`. Nothing is committed to `main`, so the branch ruleset does not -block them. +Or from the browser: +open **Releases**, then **Draft a new release**, +choose the new `vX.Y.Z` tag, +click **Generate release notes**, +then **Publish release**. + +### What happens after you tag + +Publishing the tag triggers the `Release` workflow +(`.github/workflows/release.yml`), which: + +1. validates the tag is well-formed and is the newest release tag, + refusing to publish a malformed or out-of-order version; +2. builds the sdist and wheel with `uv build`, + stamped with the tag version; +3. publishes them to PyPI via trusted publishing (OIDC), + so no API token is stored in the repo. + +A published version is immutable: +PyPI will not let you re-upload or reuse a version number, +so a wrong tag means abandoning that number and tagging the next one. + +### Release notes -Publishing the tag triggers the `Release` workflow, -which builds the sdist and wheel -and uploads them to PyPI via trusted publishing (OIDC). +GitHub generates the notes from the pull requests +merged since the previous release, +grouped by label (see `.github/release.yml`): +Features, Bug Fixes, Documentation, then everything else. +Give each pull request a clear title and an appropriate label +so the notes read well. -Release notes are grouped from the pull requests merged since the previous -release, by label (see `.github/release.yml`), -so give pull requests clear titles and labels. +### Smoke-testing a build -To smoke-test a build against TestPyPI without releasing, -trigger the `Release (TestPyPI)` workflow manually from the Actions tab. +To exercise a build without releasing, +trigger the `Release (TestPyPI)` workflow by hand from the **Actions** tab. +An untagged build is versioned as a development release, +for example `1.4.1.dev3`, +which carries no local version segment and so uploads cleanly. ## Toolchain From 4992dc0f6ff3c526e5392b07082464243bbd97f7 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Tue, 23 Jun 2026 12:26:27 +1200 Subject: [PATCH 10/11] docs: tighten the release section and release.sh comment --- CONTRIBUTING.md | 78 +++++++++++++--------------------------------- scripts/release.sh | 4 +-- 2 files changed, 24 insertions(+), 58 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81212c5..a5a0eb6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -218,74 +218,40 @@ and so on. ## Releasing A release is a Git tag. -The version is derived from that tag at build time by `hatch-vcs`, -so there is no version field to edit -and nothing is committed to `main` to cut a release. +The version comes from the tag rather than a file, +so there is nothing to bump by hand and nothing is committed to `main`. +The notes are generated by GitHub from the pull requests merged since the last +release, so there is no changelog to maintain either. -Tags are semver, `v`-prefixed, in `vMAJOR.MINOR.PATCH` form, -for example `v1.4.1`. +Tags are semver, `v`-prefixed: `vMAJOR.MINOR.PATCH`, for example `v1.4.1`. -### Cut a release - -Pick the change level and run one of: +To cut a release, pick the change level: ```console -make release-patch # latest tag + 0.0.1: bug fixes only -make release-minor # latest tag + 0.1.0: backwards-compatible features -make release-major # latest tag + 1.0.0: breaking changes +make release-patch # bug fixes only +make release-minor # backwards-compatible features +make release-major # breaking changes ``` -Each target reads the latest `vX.Y.Z` tag, +Each reads the latest tag, works out the next version, asks you to confirm, -then creates the GitHub release through `gh`. -Because it only creates a tag, -the `main` branch ruleset does not block it. - -To pick the number yourself, the same thing from the terminal is: - -```console -gh release create v1.4.1 --generate-notes -``` - -Or from the browser: -open **Releases**, then **Draft a new release**, -choose the new `vX.Y.Z` tag, -click **Generate release notes**, -then **Publish release**. - -### What happens after you tag - -Publishing the tag triggers the `Release` workflow -(`.github/workflows/release.yml`), which: - -1. validates the tag is well-formed and is the newest release tag, - refusing to publish a malformed or out-of-order version; -2. builds the sdist and wheel with `uv build`, - stamped with the tag version; -3. publishes them to PyPI via trusted publishing (OIDC), - so no API token is stored in the repo. +and creates the release. +Creating the tag is what triggers CI to build and publish to PyPI, +so give your pull requests clear titles and labels +for the generated notes to read well. +The same thing by hand is `gh release create v1.4.1 --generate-notes`, +or the GitHub Releases UI. A published version is immutable: -PyPI will not let you re-upload or reuse a version number, -so a wrong tag means abandoning that number and tagging the next one. - -### Release notes - -GitHub generates the notes from the pull requests -merged since the previous release, -grouped by label (see `.github/release.yml`): -Features, Bug Fixes, Documentation, then everything else. -Give each pull request a clear title and an appropriate label -so the notes read well. - -### Smoke-testing a build +PyPI will not let you reuse a number, +so a wrong tag means moving on to the next one. To exercise a build without releasing, -trigger the `Release (TestPyPI)` workflow by hand from the **Actions** tab. -An untagged build is versioned as a development release, -for example `1.4.1.dev3`, -which carries no local version segment and so uploads cleanly. +run the `Release (TestPyPI)` workflow from the **Actions** tab. +Between tags the version is a `.devN` pre-release, +which is also how an unreleased change can be published +for others to install and test. ## Toolchain diff --git a/scripts/release.sh b/scripts/release.sh index d640c91..3a406e6 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -# Cut a release by tagging only: hatch-vcs derives the version from the tag and -# the Release workflow publishes to PyPI, so nothing is committed to main. +# Cut a release by creating a tag only: +# the version is derived from the tag, so nothing is committed to main. set -eu level=${1:-patch} From 9bc4624e7aab57bd17d5b633b8b612f049936924 Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Tue, 23 Jun 2026 12:41:40 +1200 Subject: [PATCH 11/11] ci: flatten release notes to a single list, keep the dependabot filter --- .github/release.yml | 15 --------------- CONTRIBUTING.md | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/release.yml b/.github/release.yml index eabcfe9..02e6a9b 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -3,18 +3,3 @@ changelog: authors: - dependabot - dependabot[bot] - categories: - - title: Features - labels: - - feature - - enhancement - - title: Bug Fixes - labels: - - bug - - fix - - title: Documentation - labels: - - documentation - - title: Other Changes - labels: - - "*" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5a0eb6..13924a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -238,7 +238,7 @@ works out the next version, asks you to confirm, and creates the release. Creating the tag is what triggers CI to build and publish to PyPI, -so give your pull requests clear titles and labels +so give your pull requests clear titles for the generated notes to read well. The same thing by hand is `gh release create v1.4.1 --generate-notes`,