From e21fd2f06648b66955c081d619a14dcb8bf932b5 Mon Sep 17 00:00:00 2001 From: wheheohu Date: Wed, 8 Jul 2026 13:13:26 +0800 Subject: [PATCH] Document release process, harden publish CI, fix stale docs badge Add a Release Process section to CLAUDE.md capturing the branch/version model we standardized on: feature branches touch only CHANGELOG's [Unreleased] section, and a dedicated short-lived release/X.Y.Z branch bumps the version and finalizes the changelog before a GitHub Release triggers publishing. Also replace the hardcoded "Current version" line with a pointer to pyproject (the single source of truth). Harden publish-to-pypi.yml: split into test -> publish (needs: test), running the suite and verifying the release tag matches pyproject before uploading. The workflow previously published untested and without any tag/version check. Fix docs/index.rst PyPI badge, which was a static image pinned to a stale 0.0.14; use the dynamic shields.io endpoint like README so it tracks PyPI automatically. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/publish-to-pypi.yml | 29 +++++++++++++++++++++++++ CLAUDE.md | 31 ++++++++++++++++++++++++--- docs/index.rst | 2 +- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index a936edc..2b394fb 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -5,7 +5,36 @@ on: types: [published] jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Verify release tag matches pyproject version + run: | + PYPROJECT_VERSION=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + TAG="${{ github.event.release.tag_name }}" + TAG_VERSION="${TAG#v}" + TAG_VERSION="${TAG_VERSION#V}" + echo "pyproject=$PYPROJECT_VERSION tag=$TAG normalized=$TAG_VERSION" + if [ "$PYPROJECT_VERSION" != "$TAG_VERSION" ]; then + echo "::error::Release tag '$TAG' does not match pyproject version '$PYPROJECT_VERSION'" + exit 1 + fi + + - name: Run tests + run: uv run pytest + publish: + needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/CLAUDE.md b/CLAUDE.md index d0b4c59..bfd47a2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Building and Publishing - Build package: `uv build` -- Publishing is automated via GitHub Actions on release creation +- Publishing is automated via GitHub Actions on GitHub Release creation, gated on the test suite and a tag↔version check. See [Release Process](#release-process) for the full runbook. ## Code Structure @@ -111,14 +111,39 @@ dftt_timecode/ ### CI/CD Workflows - **Documentation:** Builds and deploys to GitHub Pages on push to main (`.github/workflows/docs.yml`) -- **Publishing:** Automatically publishes to PyPI on GitHub release creation (`.github/workflows/publish-to-pypi.yml`) +- **Publishing:** Publishes to PyPI on GitHub Release creation, after running the test suite and verifying the release tag matches `pyproject.toml` (`.github/workflows/publish-to-pypi.yml`). See [Release Process](#release-process) ## Version Information -- Current version: 1.0.0 +- Current version: single-sourced from `pyproject.toml` (`[project].version`), read at runtime via `importlib.metadata` - Python requirement: >=3.11 - Main dependencies: All standard library (fractions, logging, math, functools, re, subprocess) - Dev dependencies: pytest, sphinx, pydata-sphinx-theme, myst-parser, sphinx-intl +## Release Process + +Versioning follows [Semantic Versioning](https://semver.org). The version number changes **only at release time**, on a dedicated short-lived branch — never on feature branches. + +### Branching and versioning model +- **Feature/bugfix branches** are short-lived and PR into `main`. Each PR adds its entry under the `## [Unreleased]` section of `CHANGELOG.md` and does **not** touch the version number. `main` therefore always accumulates changes under `Unreleased`. +- The **SemVer level is decided when cutting the release**, from what has accumulated under `Unreleased`: + - `### Added` (new, backwards-compatible) → **minor** bump + - `### Fixed` only → **patch** bump + - Any `### Removed` or breaking `### Changed` → **major** bump + +### Cutting a release +1. From an up-to-date `main`, create `release/X.Y.Z`. +2. Bump the version in the **single source of truth**: `pyproject.toml` → `[project].version`. Do not hardcode the version anywhere else — runtime `__version__` (`dftt_timecode/__init__.py`, via `importlib.metadata`) and the Sphinx docs (`docs/conf.py`, via `tomllib`) read it automatically. +3. In `CHANGELOG.md`, rename `## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD` and add a fresh empty `## [Unreleased]` above it. +4. Commit, push, and open a PR to `main` containing **only** release prep (version + changelog) — no feature code. +5. Merge the PR. +6. Create a **GitHub Release** with an annotated tag `vX.Y.Z` on the merge commit. This — not a bare tag push — is what triggers the PyPI publish (`.github/workflows/publish-to-pypi.yml`, `on: release: published`). The workflow runs the test suite and verifies the tag matches `pyproject.toml` before uploading. + +### Conventions and gotchas +- **Tag format:** `vX.Y.Z`, annotated. (Historical tags are inconsistent — standardize going forward.) +- **Order matters:** the version bump must be merged to `main` *before* the GitHub Release is created, or the tag↔version guard fails and nothing publishes. +- **PyPI is immutable:** a version can never be re-uploaded. Run `uv run pytest` and `uv build` locally before releasing. +- **Long-lived `release/*` branches** are only warranted for parallel maintenance lines (e.g. patching 1.x while developing a breaking 2.0). For single-line development, keep release branches short-lived. + ## Common Usage Patterns ### Basic Timecode Creation diff --git a/docs/index.rst b/docs/index.rst index 410515e..c5c5c2e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ dftt_timecode ============= -.. image:: https://img.shields.io/badge/pypi-0.0.14-brightgreen +.. image:: https://img.shields.io/pypi/v/dftt-timecode.svg :target: https://pypi.org/project/dftt-timecode/ :alt: PyPI