diff --git a/.cursor/rules/diataxis-docs.mdc b/.cursor/rules/diataxis-docs.mdc index fa5bdd7..2864596 100644 --- a/.cursor/rules/diataxis-docs.mdc +++ b/.cursor/rules/diataxis-docs.mdc @@ -37,4 +37,6 @@ This repository's docs follow [Diátaxis](https://diataxis.fr/). Keep them consi ## Published site -Configured by `mkdocs.yml` (Material). GitHub Pages workflow: `.github/workflows/docs.yml`. +Configured by `mkdocs.yml` (Material + mike version picker). Changelog: `docs/release-notes.md`, which keeps an `## Unreleased` section maintained per PR (renamed to the version at release time by `scripts/prepare_release.py`). + +GitHub Pages is the `gh-pages` branch (mike). The Deploy docs workflow builds on docs PRs and deploys a versioned site (from the tagged release commit) only after a successful Publish to PyPI run (`workflow_run`), not on every `master` docs push. diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..c28e7d7 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,62 @@ +name: Changelog + +# Every PR must add a user-facing entry under the "## Unreleased" section of +# docs/release-notes.md. Trivial PRs (CI, refactors, typo fixes) can bypass this +# by adding the "skip-changelog" label. + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] + +permissions: + contents: read + +concurrency: + group: changelog-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + verify: + runs-on: ubuntu-latest + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changelog') }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Require a new Unreleased entry + env: + BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + FILE="docs/release-notes.md" + git fetch origin "$BASE_REF" --depth=1 + + count_unreleased_bullets() { + # Reads a file on stdin, prints the number of bullet lines that live + # under the first "## Unreleased" heading (until the next "## "). + awk ' + /^## / { + in_block = ($0 ~ /^## Unreleased([[:space:]]|$)/) ? 1 : 0 + next + } + in_block && /^[[:space:]]*[*-][[:space:]]+/ { n++ } + END { print n + 0 } + ' + } + + HEAD_BULLETS=$(count_unreleased_bullets < "$FILE") + if git show "origin/${BASE_REF}:${FILE}" > /tmp/base-release-notes.md 2>/dev/null; then + BASE_BULLETS=$(count_unreleased_bullets < /tmp/base-release-notes.md) + else + BASE_BULLETS=0 + fi + + echo "Unreleased bullets: base=${BASE_BULLETS} head=${HEAD_BULLETS}" + + if [ "$HEAD_BULLETS" -le "$BASE_BULLETS" ]; then + echo "::error file=${FILE}::Add a bullet under the '## Unreleased' section in ${FILE}, or label this PR 'skip-changelog'." + exit 1 + fi + echo "Changelog entry found." diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ff610fc..d1d4701 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,43 +1,43 @@ name: Deploy docs -# Test on this PR: the build job runs automatically on pull_request. -# Test full Pages deploy before merge: Actions → Deploy docs → Run workflow -# (select this branch). Requires Pages source = GitHub Actions in repo settings. +# PR / path changes: build-only (mkdocs --strict). +# Versioned deploy (mike → gh-pages) runs after a successful "Publish to PyPI" +# workflow, or via workflow_dispatch for manual recovery. +# +# One-time repo setting: Pages source = Deploy from a branch → gh-pages / (root). + on: - push: - branches: [master] - paths: - - "docs/**" - - "docs/assets/**" - - "mkdocs.yml" - - "requirements-docs.txt" - - ".github/workflows/docs.yml" pull_request: types: [opened, synchronize, reopened, ready_for_review] paths: - "docs/**" - - "docs/assets/**" - "mkdocs.yml" - "requirements-docs.txt" - ".github/workflows/docs.yml" + workflow_run: + workflows: ["Publish to PyPI"] + types: [completed] workflow_dispatch: inputs: - deploy: - description: "Upload artifact and deploy to GitHub Pages" + version: + description: "Docs version to deploy (e.g. 0.7.0, without v prefix)" + required: true + type: string + update_latest: + description: "Also update the latest alias and set it as default" type: boolean default: true permissions: contents: read - pages: write - id-token: write concurrency: - group: pages + group: docs-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: false jobs: build: + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -60,24 +60,78 @@ jobs: - name: Build site run: mkdocs build --strict --clean - - name: Upload Pages artifact - if: > - github.event_name == 'push' || - (github.event_name == 'workflow_dispatch' && inputs.deploy) - uses: actions/upload-pages-artifact@v3 - with: - path: site - deploy: if: > - github.event_name == 'push' || - (github.event_name == 'workflow_dispatch' && inputs.deploy) - needs: build + (github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success') || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + permissions: + contents: write steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + - name: Checkout the tagged release commit + uses: actions/checkout@v4 + with: + # workflow_run: the commit Publish ran on (the tagged release commit). + # workflow_dispatch: the vX.Y.Z tag for the requested version. + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || format('v{0}', inputs.version) }} + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install docs dependencies + run: pip install -r requirements-docs.txt + + - name: Resolve version + id: ver + env: + EVENT_NAME: ${{ github.event_name }} + DISPATCH_VERSION: ${{ inputs.version }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + UPDATE_LATEST_INPUT: ${{ inputs.update_latest }} + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + VERSION="${DISPATCH_VERSION#v}" + UPDATE_LATEST="${UPDATE_LATEST_INPUT:-true}" + else + git fetch --tags origin + VERSION="" + if [[ "${HEAD_BRANCH:-}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + VERSION="${HEAD_BRANCH#v}" + elif [ -n "${HEAD_SHA:-}" ]; then + TAG=$(git tag --points-at "$HEAD_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1 || true) + if [ -n "$TAG" ]; then + VERSION="${TAG#v}" + fi + fi + if [ -z "$VERSION" ]; then + echo "Could not resolve release version from workflow_run (branch=$HEAD_BRANCH sha=$HEAD_SHA)" + exit 1 + fi + UPDATE_LATEST=true + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "update_latest=$UPDATE_LATEST" >> "$GITHUB_OUTPUT" + echo "Deploying docs version $VERSION (update_latest=$UPDATE_LATEST)" + + - name: Configure git for mike + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Deploy with mike + env: + VERSION: ${{ steps.ver.outputs.version }} + UPDATE_LATEST: ${{ steps.ver.outputs.update_latest }} + run: | + set -euo pipefail + if [ "$UPDATE_LATEST" = "true" ]; then + mike deploy --push --update-aliases "$VERSION" latest + mike set-default --push latest + else + mike deploy --push "$VERSION" + fi diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0893a46..b8e9ae7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,5 +1,26 @@ name: Publish to PyPI +# Releasing (prep-commit-then-tag; this pipeline only verifies, never mutates) +# --------------------------------------------------------------------------- +# 1. Land PRs on master. Each PR adds its entry under "## Unreleased" in +# docs/release-notes.md (enforced by the Changelog workflow). +# 2. Cut the release locally: +# +# python scripts/prepare_release.py X.Y.Z +# # review the diff, then: +# git add tapsdk/__version__.py docs/release-notes.md +# git commit -m "Release X.Y.Z" +# git tag -a vX.Y.Z -m "Release X.Y.Z" +# git push origin HEAD vX.Y.Z +# +# This workflow re-runs the reusable test matrix on the tagged commit, verifies +# the tag matches tapsdk.__version__ and that release notes were prepared, then +# builds and uploads to PyPI via Trusted Publishing. Versioned docs deploy +# separately in "Deploy docs" after this workflow succeeds. +# +# Maintainers must configure PyPI Trusted Publishing for project tap-python-sdk, +# repository TapWithUs/tap-python-sdk, and a GitHub `pypi` environment. + on: push: tags: @@ -11,38 +32,22 @@ permissions: jobs: test: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" - - name: Lint with flake8 - run: | - pip install flake8 - flake8 examples tapsdk tests - - name: Run tests - run: pytest -v + uses: ./.github/workflows/tests.yml publish: needs: test runs-on: ubuntu-latest environment: pypi steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" + - name: Validate tag version matches package version run: | TAG="${GITHUB_REF_NAME#v}" @@ -52,16 +57,48 @@ jobs: exit 1 fi echo "Version validated: $PACKAGE_VERSION" + + - name: Verify release notes were prepared + env: + VERSION: ${{ github.ref_name }} + run: | + set -euo pipefail + FILE="docs/release-notes.md" + VER="${VERSION#v}" + + if ! grep -qE "^## ${VER//./\\.} \(" "$FILE"; then + echo "::error file=${FILE}::No '## ${VER} ()' section found. Run scripts/prepare_release.py ${VER} before tagging." + exit 1 + fi + + # The Unreleased section must be empty (entries moved into the version). + UNRELEASED_BULLETS=$(awk ' + /^## / { + in_block = ($0 ~ /^## Unreleased([[:space:]]|$)/) ? 1 : 0 + next + } + in_block && /^[[:space:]]*[*-][[:space:]]+/ { n++ } + END { print n + 0 } + ' "$FILE") + if [ "$UNRELEASED_BULLETS" -ne 0 ]; then + echo "::error file=${FILE}::'## Unreleased' still has ${UNRELEASED_BULLETS} entrie(s); run scripts/prepare_release.py to move them into ${VER}." + exit 1 + fi + echo "Release notes verified for ${VER}." + - name: Install build dependencies run: | python -m pip install --upgrade pip pip install build + - name: Build package run: python -m build + - name: Check package metadata run: | pip install twine twine check dist/* + # Import package/version only — avoid TapSDK/bleak, which needs bluetoothctl on Linux. - name: Smoke test built wheel run: | @@ -69,11 +106,13 @@ jobs: /tmp/wheel-venv/bin/pip install --upgrade pip /tmp/wheel-venv/bin/pip install dist/*.whl /tmp/wheel-venv/bin/python -c "import tapsdk; from tapsdk.__version__ import __version__; print(__version__)" + - name: Smoke test built sdist run: | python -m venv /tmp/sdist-venv /tmp/sdist-venv/bin/pip install --upgrade pip /tmp/sdist-venv/bin/pip install dist/*.tar.gz /tmp/sdist-venv/bin/python -c "import tapsdk; from tapsdk.__version__ import __version__; print(__version__)" + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e50c48a..73f0739 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,25 +6,5 @@ on: branches: [master] jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" - - name: Lint with flake8 - run: | - pip install flake8 - flake8 examples tapsdk tests - - name: Run tests - run: pytest -v + test: + uses: ./.github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..594efcc --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: Tests + +# Reusable lint + test matrix. Single source of truth called by the CI workflow +# (on PRs and pushes to master) and by the Publish workflow (on tags), so the +# matrix is defined once and publish is gated on the same tests (#39). + +on: + workflow_call: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + - name: Lint with flake8 + run: | + pip install flake8 + flake8 examples tapsdk tests + - name: Run tests + run: pytest -v diff --git a/MANIFEST.in b/MANIFEST.in index 219957b..0ecf124 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ -include History.md include LICENSE include Readme.md +include docs/release-notes.md recursive-include tests * recursive-exclude * __pycache__ diff --git a/Readme.md b/Readme.md index 8561afe..dd31508 100644 --- a/Readme.md +++ b/Readme.md @@ -8,7 +8,7 @@ BLE SDK for building Python apps that connect to **Tap Strap** and **TapXR**, se ### Documentation -Published docs (MkDocs Material): [https://tapwithus.github.io/tap-python-sdk/](https://tapwithus.github.io/tap-python-sdk/) +Published docs (MkDocs Material, versioned with mike): [https://tapwithus.github.io/tap-python-sdk/](https://tapwithus.github.io/tap-python-sdk/) Pick the path that matches your goal: @@ -18,6 +18,7 @@ Pick the path that matches your goal: | Solve a specific task | [How-to guides](docs/how-to/index.md) | | Look up APIs and types | [Reference](docs/reference/index.md) | | Understand modes and sensors | [Explanation](docs/explanation/index.md) | +| Read the changelog | [Release notes](docs/release-notes.md) | Full index: [docs/index.md](docs/index.md). Local preview: `pip install -r requirements-docs.txt && mkdocs serve`. @@ -56,7 +57,31 @@ Pair the Tap with the OS first. Update firmware with Tap Manager. More complete ### Migrating from 0.6.x -Breaking API changes are listed in [Migrate from 0.6](docs/how-to/migrate-from-0.6.md) and [History.md](History.md). +Breaking API changes are listed in [Migrate from 0.6](docs/how-to/migrate-from-0.6.md) and [Release notes](docs/release-notes.md). + +### Contributing + +Every pull request should add a user-facing entry under the **Unreleased** +heading in [Release notes](docs/release-notes.md). PRs with no user-facing change +(CI, refactors, typo fixes) can skip this by adding the `skip-changelog` label. + +### Releasing + +Releases use a prep-commit-then-tag flow so the tag, PyPI artifact, and docs all +match: + +```bash +python scripts/prepare_release.py X.Y.Z # bumps version, cuts Unreleased -> X.Y.Z +git add tapsdk/__version__.py docs/release-notes.md +git commit -m "Release X.Y.Z" +git tag -a vX.Y.Z -m "Release X.Y.Z" +git push origin HEAD vX.Y.Z +``` + +Pushing the tag runs [`.github/workflows/publish.yml`](.github/workflows/publish.yml), +which re-runs tests, verifies the version and release notes, and publishes to +PyPI. Versioned docs deploy separately after a successful publish. See the header +comments in that workflow for details. ### Testing diff --git a/docs/index.md b/docs/index.md index 9dc8248..d17ce80 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,6 +8,7 @@ Pick the section that matches what you need: | Solve a specific task | [How-to guides](how-to/index.md) | | Look up an API, type, or event | [Reference](reference/index.md) | | Understand how modes and sensors work | [Explanation](explanation/index.md) | +| Read the changelog for a PyPI release | [Release notes](release-notes.md) | ## Package diff --git a/History.md b/docs/release-notes.md similarity index 71% rename from History.md rename to docs/release-notes.md index c42fee8..20d59db 100644 --- a/History.md +++ b/docs/release-notes.md @@ -1,21 +1,20 @@ -# History +# Release notes -## Releasing +Changelog for published `tap-python-sdk` releases on PyPI. -PyPI releases are published automatically when a version tag is pushed to GitHub. +Add user-facing changes for the next release under **Unreleased** in your pull +request. At release time `scripts/prepare_release.py` renames this section to the +new version and opens a fresh empty one. -1. Bump `__version__` in `tapsdk/__version__.py` and update this file. -2. Merge the release changes into `develop`, then into `master` as needed. -3. Create and push an annotated tag whose name matches the package version (for example `v0.7.0`): - - ```bash - git tag -a v0.7.0 -m "Release 0.7.0" - git push origin v0.7.0 - ``` +## Unreleased +______________________ +### Main features -The `Publish to PyPI` workflow runs the same lint and test matrix as CI, verifies that the tag (without the `v` prefix) matches `tapsdk.__version__`, builds the package with `python -m build`, and uploads it to PyPI using Trusted Publishing. +* Versioned docs site (mike) deployed after successful PyPI publish, with a release notes page derived from `docs/release-notes.md` (#47) (#48) +* Prep-commit-then-tag release flow: author-written `Unreleased` entries, `scripts/prepare_release.py`, and a verify-only publish pipeline (#47) (#48) +* Shared reusable test workflow used by CI and Publish (#39) (#48) -Maintainers must configure PyPI Trusted Publishing for the `tap-python-sdk` project name, the `TapWithUs/tap-python-sdk` repository, and a GitHub `pypi` environment before the first automated release. +### Bug fixes ## 0.7.0 (2026-06-09) ______________________ @@ -84,4 +83,3 @@ ______________________ ### Main features * SDK created. - diff --git a/mkdocs.yml b/mkdocs.yml index 4f758ff..eb0a76c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,6 +41,15 @@ theme: plugins: - search +extra: + version: + provider: mike + social: + - icon: fontawesome/brands/github + link: https://github.com/TapWithUs/tap-python-sdk + - icon: fontawesome/brands/python + link: https://pypi.org/project/tap-python-sdk/ + markdown_extensions: - admonition - attr_list @@ -82,10 +91,4 @@ nav: - Connection model: explanation/connection-model.md - Input modes: explanation/input-modes.md - Raw sensors: explanation/raw-sensors.md - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/TapWithUs/tap-python-sdk - - icon: fontawesome/brands/python - link: https://pypi.org/project/tap-python-sdk/ + - Release notes: release-notes.md diff --git a/requirements-docs.txt b/requirements-docs.txt index d752c76..ff4cec4 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,2 +1,3 @@ mkdocs>=1.6,<2 mkdocs-material>=9.5,<10 +mike>=2.1,<3 diff --git a/scripts/prepare_release.py b/scripts/prepare_release.py new file mode 100644 index 0000000..74c8160 --- /dev/null +++ b/scripts/prepare_release.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Prepare a release commit: bump the version and cut the changelog. + +This keeps the release pipeline read-only. Run it locally, review the diff, +then commit and tag the result (the script prints the exact commands). + +What it does: + 1. Validates the Unreleased section and builds the cut changelog in memory. + 2. Sets tapsdk/__version__.py to X.Y.Z. + 3. Writes docs/release-notes.md with Unreleased renamed to + "## X.Y.Z (YYYY-MM-DD)" and a fresh empty "## Unreleased". + +It refuses to run if the current Unreleased section has no bullet entries. + +Usage: + python scripts/prepare_release.py X.Y.Z [--date YYYY-MM-DD] +""" + +from __future__ import annotations + +import argparse +import re +import sys +from datetime import date +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +VERSION_FILE = ROOT / "tapsdk" / "__version__.py" +RELEASE_NOTES = ROOT / "docs" / "release-notes.md" + +VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") +BULLET_RE = re.compile(r"^\s*[*-]\s+\S") +UNRELEASED_HEADING = "## Unreleased" + +EMPTY_UNRELEASED = ( + "## Unreleased\n" + "______________________\n" + "### Main features\n" + "\n" + "### Bug fixes\n" +) + + +def fail(message: str) -> None: + raise SystemExit(f"error: {message}") + + +def split_unreleased(text: str) -> tuple[str, list[str], str]: + """Return (before, unreleased_block_lines, after). + + `unreleased_block_lines` covers the "## Unreleased" heading through the line + before the next "## " heading. + """ + lines = text.splitlines(keepends=True) + start = None + for i, line in enumerate(lines): + if line.rstrip("\n") == UNRELEASED_HEADING or line.startswith( + UNRELEASED_HEADING + " " + ): + start = i + break + if start is None: + fail(f"no '{UNRELEASED_HEADING}' heading found in {RELEASE_NOTES}") + + end = len(lines) + for j in range(start + 1, len(lines)): + if lines[j].startswith("## "): + end = j + break + + return "".join(lines[:start]), lines[start:end], "".join(lines[end:]) + + +def bump_version(version: str) -> None: + VERSION_FILE.write_text(f'__version__ = "{version}"\n', encoding="utf-8") + print(f"Set {VERSION_FILE.relative_to(ROOT)} to {version}") + + +def prepare_changelog(version: str, release_date: str) -> str: + """Validate and return the updated release-notes text (does not write).""" + text = RELEASE_NOTES.read_text(encoding="utf-8") + + if re.search(rf"^## {re.escape(version)}(?:\s|\(|$)", text, re.MULTILINE): + fail(f"docs/release-notes.md already has a '## {version}' section") + + before, block, after = split_unreleased(text) + + if not any(BULLET_RE.match(line) for line in block[1:]): + fail( + "the '## Unreleased' section has no entries; nothing to release. " + "Add bullets before preparing a release." + ) + + # Replace the heading line with the versioned heading; keep the entries. + versioned = [f"## {version} ({release_date})\n"] + block[1:] + released_block = "".join(versioned).rstrip("\n") + "\n" + + return ( + before + + EMPTY_UNRELEASED + + "\n" + + released_block + + ("\n" + after.lstrip("\n") if after.strip() else "") + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", help="Release version, e.g. 0.8.0") + parser.add_argument( + "--date", + default=date.today().isoformat(), + help="Release date (YYYY-MM-DD); defaults to today", + ) + args = parser.parse_args(argv) + + version = args.version.lstrip("v") + if not VERSION_RE.match(version): + fail(f"invalid version {args.version!r}; expected X.Y.Z") + + # Validate and build the new changelog before writing anything, so a failed + # check cannot leave __version__.py bumped with notes uncut. + new_notes = prepare_changelog(version, args.date) + bump_version(version) + RELEASE_NOTES.write_text(new_notes, encoding="utf-8") + print( + f"Renamed Unreleased -> {version} ({args.date}) in " + f"{RELEASE_NOTES.relative_to(ROOT)} and opened a fresh Unreleased section" + ) + + tag = f"v{version}" + print() + print("Review the changes, then commit and tag:") + print(" git add tapsdk/__version__.py docs/release-notes.md") + print(f' git commit -m "Release {version}"') + print(f' git tag -a {tag} -m "Release {version}"') + print(" git push origin HEAD") + print(f" git push origin {tag}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/setup.py b/setup.py index 226063a..c904e5c 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ here = os.path.abspath(os.path.dirname(__file__)) with io.open(os.path.join(here, "Readme.md"), encoding="utf-8") as f: long_description = "\n" + f.read() -with io.open(os.path.join(here, "History.md"), encoding="utf-8") as f: +with io.open(os.path.join(here, "docs", "release-notes.md"), encoding="utf-8") as f: long_description += "\n\n" + f.read() # Load the package's __version__.py module as a dictionary.