From ea3da47c834a90a873866372fba0db4faee4e382 Mon Sep 17 00:00:00 2001 From: zanjonke Date: Tue, 21 Jul 2026 15:00:33 +0200 Subject: [PATCH] Derive version from git tag and publish to PyPI via OIDC Replace the broken sed/commit-back release flow with tag-driven versioning: - pyproject.toml: build with hatch-vcs, version sourced from the git tag (source = "vcs"); drop the _version.py path source. - Remove _version.py; system_config resolves the version from package metadata, falling back to the highest git tag for source checkouts. - publish-to-pypi.yml: release-triggered only, least-privilege permissions, and publish via PyPI Trusted Publishing (OIDC) using pypa/gh-action-pypi-publish instead of a long-lived API token. --- .github/workflows/publish-to-pypi.yml | 71 ++++++--------------------- _version.py | 1 - pyproject.toml | 7 ++- system_config.py | 36 +++++++++++++- 4 files changed, 56 insertions(+), 59 deletions(-) delete mode 100644 _version.py diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 9d5dc3d8..72148d87 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -3,17 +3,10 @@ name: Publish to PyPI on: release: types: [published] - workflow_dispatch: - inputs: - version: - description: "Version to build (e.g., 0.2.1)" - required: true - type: string - publish_to_pypi: - description: "Actually publish to PyPI?" - required: true - default: false - type: boolean + +# Least privilege by default: jobs get no GITHUB_TOKEN scopes unless they +# opt in below. +permissions: {} jobs: build: @@ -24,48 +17,23 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 with: - python-version: "3.11" - - - name: Extract version - id: get_version - run: | - if [ "${{ github.event_name }}" = "release" ]; then - # Strip 'v' prefix from tag (v0.2.1 -> 0.2.1) - VERSION=${GITHUB_REF_NAME#v} - else - # Use manual input - VERSION="${{ github.event.inputs.version }}" - fi - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION" - - - name: Update version in pyproject.toml - run: | - sed -i "s/^version = .*/version = \"${{ steps.get_version.outputs.VERSION }}\"/" pyproject.toml - echo "Updated pyproject.toml:" - grep "^version" pyproject.toml - - - name: Commit version update to repo - if: github.event_name == 'release' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add pyproject.toml - git commit -m "Bump version to ${{ steps.get_version.outputs.VERSION }}" - git push origin HEAD:main + # hatch-vcs derives the version from git tags, so we need full history. + fetch-depth: 0 - name: Install uv + # uv provisions its own Python (honoring requires-python), so no + # separate actions/setup-python step is needed. run: | curl -LsSf https://astral.sh/uv/install.sh | sh echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Build package + # hatch-vcs reads the version straight from the release tag. run: | uv build + echo "Built distributions:" + ls -l dist/ - name: Store the distribution packages uses: actions/upload-artifact@v4 @@ -74,7 +42,6 @@ jobs: path: dist/ - name: Upload assets to GitHub Release - if: github.event_name == 'release' env: GH_TOKEN: ${{ github.token }} run: | @@ -83,11 +50,14 @@ jobs: publish-to-pypi: name: Publish to PyPI needs: build - if: github.event_name == 'release' || github.event.inputs.publish_to_pypi == 'true' runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/codeplain + permissions: + # Mint a short-lived OIDC token so PyPI Trusted Publishing can + # authenticate this run — no long-lived API token needed. + id-token: write steps: - name: Download distribution packages @@ -96,14 +66,5 @@ jobs: name: python-package-distributions path: dist/ - - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - uv tool run twine upload dist/* + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/_version.py b/_version.py deleted file mode 100644 index 8879c6c7..00000000 --- a/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.3.7" diff --git a/pyproject.toml b/pyproject.toml index 7c421d1e..5b58f8e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] @@ -41,8 +41,11 @@ dev = [ [project.scripts] codeplain = "plain2code:main" +# Derive the version from the git tag (e.g. v0.3.8 -> 0.3.8). The version is +# baked into the package metadata at build time; system_config.py reads it back +# from that metadata (with a git-tag fallback for uninstalled source checkouts). [tool.hatch.version] -path = "_version.py" +source = "vcs" [tool.hatch.build.targets.wheel] include = [ diff --git a/system_config.py b/system_config.py index bd295d4c..49f81942 100644 --- a/system_config.py +++ b/system_config.py @@ -1,12 +1,46 @@ import importlib.resources +import os import sys import yaml -from _version import __version__ from plain2code_console import console +def _resolve_version() -> str: + """Resolve the client version. + + For an installed package the version is read from its metadata (hatch-vcs + bakes it in from the git tag at build time). When running from a source + checkout that hasn't been installed, fall back to the nearest git tag. + """ + from importlib.metadata import PackageNotFoundError, version + + try: + return version("codeplain") + except PackageNotFoundError: + pass + + try: + import git + + # Anchor to this source file's own location so we inspect the + # codeplain checkout's repo, not the caller's working directory + # (codeplain may be run from anywhere). + source_dir = os.path.dirname(os.path.abspath(__file__)) + repo = git.Repo(source_dir, search_parent_directories=True) + + # Highest version tag, regardless of branch ancestry (a dev run may sit + # on a feature branch that doesn't descend from the latest release tag). + latest_tag = repo.git.tag("--list", "--sort=-v:refname").splitlines()[0] + return latest_tag.lstrip("v") + except Exception: + return "0.0.0.dev0" + + +__version__ = _resolve_version() + + class SystemConfig: """Manages system-level configuration including requirements and error messages."""