Skip to content

Commit ea3da47

Browse files
committed
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.
1 parent 52b756a commit ea3da47

4 files changed

Lines changed: 56 additions & 59 deletions

File tree

.github/workflows/publish-to-pypi.yml

Lines changed: 16 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,10 @@ name: Publish to PyPI
33
on:
44
release:
55
types: [published]
6-
workflow_dispatch:
7-
inputs:
8-
version:
9-
description: "Version to build (e.g., 0.2.1)"
10-
required: true
11-
type: string
12-
publish_to_pypi:
13-
description: "Actually publish to PyPI?"
14-
required: true
15-
default: false
16-
type: boolean
6+
7+
# Least privilege by default: jobs get no GITHUB_TOKEN scopes unless they
8+
# opt in below.
9+
permissions: {}
1710

1811
jobs:
1912
build:
@@ -24,48 +17,23 @@ jobs:
2417

2518
steps:
2619
- uses: actions/checkout@v4
27-
28-
- name: Set up Python
29-
uses: actions/setup-python@v5
3020
with:
31-
python-version: "3.11"
32-
33-
- name: Extract version
34-
id: get_version
35-
run: |
36-
if [ "${{ github.event_name }}" = "release" ]; then
37-
# Strip 'v' prefix from tag (v0.2.1 -> 0.2.1)
38-
VERSION=${GITHUB_REF_NAME#v}
39-
else
40-
# Use manual input
41-
VERSION="${{ github.event.inputs.version }}"
42-
fi
43-
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
44-
echo "Extracted version: $VERSION"
45-
46-
- name: Update version in pyproject.toml
47-
run: |
48-
sed -i "s/^version = .*/version = \"${{ steps.get_version.outputs.VERSION }}\"/" pyproject.toml
49-
echo "Updated pyproject.toml:"
50-
grep "^version" pyproject.toml
51-
52-
- name: Commit version update to repo
53-
if: github.event_name == 'release'
54-
run: |
55-
git config user.name "github-actions[bot]"
56-
git config user.email "github-actions[bot]@users.noreply.github.com"
57-
git add pyproject.toml
58-
git commit -m "Bump version to ${{ steps.get_version.outputs.VERSION }}"
59-
git push origin HEAD:main
21+
# hatch-vcs derives the version from git tags, so we need full history.
22+
fetch-depth: 0
6023

6124
- name: Install uv
25+
# uv provisions its own Python (honoring requires-python), so no
26+
# separate actions/setup-python step is needed.
6227
run: |
6328
curl -LsSf https://astral.sh/uv/install.sh | sh
6429
echo "$HOME/.local/bin" >> $GITHUB_PATH
6530
6631
- name: Build package
32+
# hatch-vcs reads the version straight from the release tag.
6733
run: |
6834
uv build
35+
echo "Built distributions:"
36+
ls -l dist/
6937
7038
- name: Store the distribution packages
7139
uses: actions/upload-artifact@v4
@@ -74,7 +42,6 @@ jobs:
7442
path: dist/
7543

7644
- name: Upload assets to GitHub Release
77-
if: github.event_name == 'release'
7845
env:
7946
GH_TOKEN: ${{ github.token }}
8047
run: |
@@ -83,11 +50,14 @@ jobs:
8350
publish-to-pypi:
8451
name: Publish to PyPI
8552
needs: build
86-
if: github.event_name == 'release' || github.event.inputs.publish_to_pypi == 'true'
8753
runs-on: ubuntu-latest
8854
environment:
8955
name: pypi
9056
url: https://pypi.org/p/codeplain
57+
permissions:
58+
# Mint a short-lived OIDC token so PyPI Trusted Publishing can
59+
# authenticate this run — no long-lived API token needed.
60+
id-token: write
9161

9262
steps:
9363
- name: Download distribution packages
@@ -96,14 +66,5 @@ jobs:
9666
name: python-package-distributions
9767
path: dist/
9868

99-
- name: Install uv
100-
run: |
101-
curl -LsSf https://astral.sh/uv/install.sh | sh
102-
echo "$HOME/.local/bin" >> $GITHUB_PATH
103-
10469
- name: Publish to PyPI
105-
env:
106-
TWINE_USERNAME: __token__
107-
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
108-
run: |
109-
uv tool run twine upload dist/*
70+
uses: pypa/gh-action-pypi-publish@release/v1

_version.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["hatchling"]
2+
requires = ["hatchling", "hatch-vcs"]
33
build-backend = "hatchling.build"
44

55
[project]
@@ -41,8 +41,11 @@ dev = [
4141
[project.scripts]
4242
codeplain = "plain2code:main"
4343

44+
# Derive the version from the git tag (e.g. v0.3.8 -> 0.3.8). The version is
45+
# baked into the package metadata at build time; system_config.py reads it back
46+
# from that metadata (with a git-tag fallback for uninstalled source checkouts).
4447
[tool.hatch.version]
45-
path = "_version.py"
48+
source = "vcs"
4649

4750
[tool.hatch.build.targets.wheel]
4851
include = [

system_config.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,46 @@
11
import importlib.resources
2+
import os
23
import sys
34

45
import yaml
56

6-
from _version import __version__
77
from plain2code_console import console
88

99

10+
def _resolve_version() -> str:
11+
"""Resolve the client version.
12+
13+
For an installed package the version is read from its metadata (hatch-vcs
14+
bakes it in from the git tag at build time). When running from a source
15+
checkout that hasn't been installed, fall back to the nearest git tag.
16+
"""
17+
from importlib.metadata import PackageNotFoundError, version
18+
19+
try:
20+
return version("codeplain")
21+
except PackageNotFoundError:
22+
pass
23+
24+
try:
25+
import git
26+
27+
# Anchor to this source file's own location so we inspect the
28+
# codeplain checkout's repo, not the caller's working directory
29+
# (codeplain may be run from anywhere).
30+
source_dir = os.path.dirname(os.path.abspath(__file__))
31+
repo = git.Repo(source_dir, search_parent_directories=True)
32+
33+
# Highest version tag, regardless of branch ancestry (a dev run may sit
34+
# on a feature branch that doesn't descend from the latest release tag).
35+
latest_tag = repo.git.tag("--list", "--sort=-v:refname").splitlines()[0]
36+
return latest_tag.lstrip("v")
37+
except Exception:
38+
return "0.0.0.dev0"
39+
40+
41+
__version__ = _resolve_version()
42+
43+
1044
class SystemConfig:
1145
"""Manages system-level configuration including requirements and error messages."""
1246

0 commit comments

Comments
 (0)