From 8e3f39256d773e1bfd510eeae6df0c93fb36cf92 Mon Sep 17 00:00:00 2001 From: jake1025 Date: Fri, 31 Jul 2026 16:39:31 -0400 Subject: [PATCH] Add manually-triggered release workflow, remove local upload command New workflow_dispatch workflow (bump_type: patch/minor/major) runs the test suite as a gate, bumps the version via bumpversion, pushes the resulting commit and tag, builds sdist+wheel, publishes to PyPI via Trusted Publishing (OIDC, no stored API token), and creates a GitHub Release for the new tag. This fully replaces the local `python setup.py upload` path (twine + manual bumpversion + manual git push), which is removed from setup.py along with its now-unused imports and the read_current_version() helper that only it called. Refs #18 Co-Authored-By: Claude Sonnet 5 --- .github/workflows/release.yml | 62 ++++++++++++++++ setup.py | 133 +--------------------------------- 2 files changed, 63 insertions(+), 132 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..94f461c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Release + +on: + workflow_dispatch: + inputs: + bump_type: + description: 'Version part to bump' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +jobs: + release: + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + id-token: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install build and release tooling + run: pip install -e ".[test]" bumpversion build + + - name: Run tests + run: pytest + + - name: Bump version + run: bumpversion "${{ github.event.inputs.bump_type }}" + + - name: Push version bump commit and tag + run: git push --follow-tags + + - name: Build distributions + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tag=$(git describe --tags --abbrev=0) + gh release create "$tag" dist/* --title "$tag" --generate-notes diff --git a/setup.py b/setup.py index e2f13e9..1ce8446 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,7 @@ # See setup.py for humans: https://github.com/navdeep-G/setup.py import io -import os -import sys -import shutil -from setuptools import setup, find_packages, Command - -# Python 2/3 compat. -try: - from configparser import RawConfigParser -except ImportError: - from ConfigParser import RawConfigParser +from setuptools import setup, find_packages import versioneer @@ -24,9 +15,6 @@ VERSION = versioneer.get_version() -here = os.path.abspath(os.path.dirname(__file__)) - - def read_requirements(extension=None): ext = '' if extension is None else '-{}'.format(extension) filename = 'requirements{}.txt'.format(ext) @@ -35,126 +23,7 @@ def read_requirements(extension=None): return requirements -def read_current_version(): - """Read the current_version string in .bumpversion.cfg""" - config = RawConfigParser() - config.add_section('bumpversion') - config.read_file(io.open('.bumpversion.cfg', 'rt', encoding='utf-8')) - items = dict(config.items('bumpversion')) - current_version = items.get('current_version') - return current_version - - -class UploadCommand(Command): - """Support setup.py upload.""" - - description = 'Build and publish the package.' - user_options = [ - ('release', 'r', 'Upload to PyPI release instance. (Default: test instance)'), - ('skip-tests', None, 'Upload without running test first. (Default: run tests)') - ] - - def initialize_options(self): - self.release = False - self.skip_tests = False - self.upload_cmd = 'twine upload --repository-url https://test.pypi.org/legacy/ dist/*' - self.install_cmd = 'pip install --index-url https://test.pypi.org/simple/ {0}'.format(NAME) - - def finalize_options(self): - if self.release: - self.upload_cmd = 'twine upload dist/*' - self.install_cmd = 'pip install {0}'.format(NAME) - - @staticmethod - def status(s): - """Prints things in bold.""" - print('\033[1m{0}\033[0m'.format(s)) - - def abort(self): - self.status('Upload aborted.') - sys.exit(1) - - def validate_deps(self): - """Validates required packages are installed.""" - _error = False - - try: - import twine - except ImportError: - self.status('Please `pip install twine` to use upload command.') - _error = True - - try: - import bumpversion - except ImportError: - self.status('Please `pip install bumpversion` to use upload command.') - _error = True - - if _error: - self.abort() - - def validate_dirty(self): - """Aborts upload if there are uncommitted changes.""" - if 'dirty' in VERSION: - self.status('Uncommitted changes detected in branch.') - self.abort() - - - def run(self): - self.validate_deps() - self.validate_dirty() - - if not self.skip_tests: - self.status('Testing build...') - res = os.system('{0} -m pytest'.format(sys.executable)) - - if res != 0: - self.abort() - - current_version = read_current_version() - if VERSION != current_version: - self.status('Existing version: {0}'.format(current_version)) - self.status('New patch detected: {0}'.format(VERSION)) - - self.status('Bumping version...') - res = os.system('bumpversion patch') - - if res != 0: - self.abort() - - new_version = read_current_version() - self.status('New version: {0}'.format(new_version)) - os.system('git push --tags') - - self.status('Cleaning build...') - os.system('{0} setup.py clean --all'.format(sys.executable)) - - try: - self.status('Removing previous builds...') - shutil.rmtree(os.path.join(here, 'dist')) - except OSError: - pass - - self.status('Building Source and Wheel (universal) distribution...') - res = os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) - - if res != 0: - self.abort() - - self.status('Uploading the package to PyPI via Twine...') - res = os.system(self.upload_cmd) - - if res != 0: - self.abort() - - self.status('Upload success!') - self.status('Installation command: {0}'.format(self.install_cmd)) - - sys.exit(0) - - cmdclass = versioneer.get_cmdclass() -cmdclass.update({'upload': UploadCommand}) setup(