diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..43df589 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,88 @@ +name: Publish to PyPI + +on: + release: + types: [published] + workflow_dispatch: # Allow manual triggering + inputs: + target: + description: 'Target repository' + required: true + default: 'testpypi' + type: choice + options: + - testpypi + - pypi + +jobs: + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + environment: + name: ${{ github.event.inputs.target || 'pypi' }} + url: ${{ github.event.inputs.target == 'testpypi' && 'https://test.pypi.org/project/toady-cli/' || 'https://pypi.org/project/toady-cli/' }} + + permissions: + id-token: write # For trusted publishing + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: | + uv sync --all-extras + + - name: Run tests + run: | + make test-fast + + - name: Build package + run: | + make build + + - name: Check package + run: | + uv run twine check dist/* + + - name: Publish to TestPyPI + if: github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'testpypi' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + + - name: Publish to PyPI + if: github.event_name == 'release' && github.event.action == 'published' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + + - name: Create release comment + if: github.event_name == 'release' + uses: actions/github-script@v7 + with: + script: | + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: context.ref.replace('refs/tags/', '') + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: release.id, + body: `๐ŸŽ‰ Package published to PyPI!\n\n๐Ÿ“ฆ Install: \`pip install toady-cli\`\n๐Ÿ”— View: https://pypi.org/project/toady-cli/` + }); diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f8d9f79 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,42 @@ +# Include license and documentation +include LICENSE +include README.md +include CHANGELOG.md + +# Include configuration for packaging +include pyproject.toml + +# Include type hints marker +include src/toady/py.typed + +# Exclude development and configuration files +exclude .env.example +exclude .pre-commit-config.yaml +exclude Makefile +exclude mypy.ini +exclude pytest.ini +exclude requirements.txt +exclude uv.lock + +# Exclude development directories +recursive-exclude .cursor * +recursive-exclude .roo * +recursive-exclude .taskmaster * +recursive-exclude tests * +recursive-exclude scripts * +recursive-exclude docs * +recursive-exclude htmlcov * +recursive-exclude build * +recursive-exclude dist * + +# Exclude cache and temporary files +global-exclude *.pyc +global-exclude *.pyo +global-exclude *.orig +global-exclude *.rej +global-exclude *~ +global-exclude __pycache__ +global-exclude .pytest_cache +global-exclude .coverage +global-exclude coverage.xml +global-exclude coverage.json diff --git a/Makefile b/Makefile index d4d14b0..34474fd 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: help install install-dev test test-fast test-integration test-performance test-analysis .PHONY: lint format format-check type-check pre-commit check check-fast fix-check clean build .PHONY: sync lock update add remove deps-check shell run +.PHONY: publish-test publish check-publish setup-publish # Single source of truth: ALL commands use uv export PATH := $(HOME)/.local/bin:$(PATH) @@ -44,6 +45,12 @@ help: @echo " make shell Open shell with project dependencies loaded" @echo " make run ARGS='...' Run toady CLI in development mode" @echo "" + @echo "๐Ÿ“ฆ Publishing:" + @echo " make setup-publish Set up PyPI credentials (one-time setup)" + @echo " make check-publish Check package for PyPI compliance" + @echo " make publish-test Publish to TestPyPI for testing" + @echo " make publish Publish to production PyPI" + @echo "" @echo "๐Ÿงน Maintenance:" @echo " make clean Remove build artifacts and cache files" @echo " make build Build distribution packages" @@ -180,3 +187,58 @@ clean: build: clean @echo "๐Ÿ“ฆ Building distribution packages..." uv build + +## Publishing (PyPI Distribution) +setup-publish: + @echo "๐Ÿ” Setting up PyPI publishing credentials..." + @echo "" + @echo "1. Register accounts (if you haven't already):" + @echo " โ€ข TestPyPI: https://test.pypi.org/account/register/" + @echo " โ€ข PyPI: https://pypi.org/account/register/" + @echo "" + @echo "2. Create API tokens:" + @echo " โ€ข TestPyPI: https://test.pypi.org/manage/account/token/" + @echo " โ€ข PyPI: https://pypi.org/manage/account/token/" + @echo "" + @echo "3. Configure credentials in ~/.pypirc:" + @echo " [distutils]" + @echo " index-servers = pypi testpypi" + @echo "" + @echo " [pypi]" + @echo " username = __token__" + @echo " password = pypi-YOUR_PRODUCTION_TOKEN_HERE" + @echo "" + @echo " [testpypi]" + @echo " repository = https://test.pypi.org/legacy/" + @echo " username = __token__" + @echo " password = pypi-YOUR_TEST_TOKEN_HERE" + @echo "" + @echo "4. Alternatively, set environment variables:" + @echo " export TWINE_USERNAME=__token__" + @echo " export TWINE_PASSWORD=pypi-YOUR_TOKEN_HERE" + +check-publish: build + @echo "๐Ÿ” Checking package for PyPI compliance..." + uv run twine check dist/* + @echo "โœ… Package passed PyPI compliance checks!" + +publish-test: check-publish + @echo "๐Ÿงช Publishing to TestPyPI..." + @echo "โš ๏ธ This will upload version $(shell grep '^version = ' pyproject.toml | cut -d'"' -f2) to TestPyPI" + @read -p "Continue? (y/N) " confirm && [ "$$confirm" = "y" ] || exit 1 + uv run twine upload --repository testpypi dist/* + @echo "" + @echo "โœ… Published to TestPyPI!" + @echo "๐Ÿ”— View at: https://test.pypi.org/project/toady-cli/" + @echo "๐Ÿ“ฆ Test install: pip install --index-url https://test.pypi.org/simple/ toady-cli" + +publish: check-publish + @echo "๐Ÿš€ Publishing to production PyPI..." + @echo "โš ๏ธ WARNING: This will publish version $(shell grep '^version = ' pyproject.toml | cut -d'"' -f2) to PRODUCTION PyPI" + @echo "โš ๏ธ This action CANNOT be undone!" + @read -p "Are you absolutely sure? (y/N) " confirm && [ "$$confirm" = "y" ] || exit 1 + uv run twine upload dist/* + @echo "" + @echo "๐ŸŽ‰ Published to PyPI!" + @echo "๐Ÿ”— View at: https://pypi.org/project/toady-cli/" + @echo "๐Ÿ“ฆ Install: pip install toady-cli" diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..dc27eba --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,222 @@ +# Publishing Guide for Toady CLI + +This guide covers how to publish Toady CLI to PyPI using different methods. + +## ๐Ÿš€ Quick Start + +### Option 1: Using Make Commands (Recommended) + +```bash +# 1. Set up credentials (one-time) +make setup-publish + +# 2. Test on TestPyPI first +make publish-test + +# 3. Publish to production PyPI +make publish +``` + +### Option 2: Manual Commands + +```bash +# Build and check +make build +twine check dist/* + +# Upload to TestPyPI +twine upload --repository testpypi dist/* + +# Upload to PyPI +twine upload dist/* +``` + +## ๐Ÿ” Credential Setup + +### Method 1: API Tokens (Recommended) + +1. **Create accounts:** + - [TestPyPI](https://test.pypi.org/account/register/) + - [PyPI](https://pypi.org/account/register/) + +2. **Generate API tokens:** + - [TestPyPI tokens](https://test.pypi.org/manage/account/token/) + - [PyPI tokens](https://pypi.org/manage/account/token/) + +3. **Configure `~/.pypirc`:** + ```ini + [distutils] + index-servers = pypi testpypi + + [pypi] + username = __token__ + password = pypi-YOUR_PRODUCTION_TOKEN_HERE + + [testpypi] + repository = https://test.pypi.org/legacy/ + username = __token__ + password = pypi-YOUR_TEST_TOKEN_HERE + ``` + +### Method 2: Environment Variables + +```bash +export TWINE_USERNAME=__token__ +export TWINE_PASSWORD=pypi-YOUR_TOKEN_HERE +``` + +## ๐Ÿค– Automated Publishing (CI/CD) + +### GitHub Actions + +The project includes automated publishing via GitHub Actions: + +- **Automatic:** Publishes to PyPI when you create a GitHub release +- **Manual:** Use workflow dispatch to publish to TestPyPI + +#### Setup for GitHub Actions: + +1. **Add secrets to your repository:** + - Go to: Settings โ†’ Secrets and variables โ†’ Actions + - Add secrets: + - `PYPI_API_TOKEN`: Your PyPI API token + - `TEST_PYPI_API_TOKEN`: Your TestPyPI API token + +2. **Create a release:** + ```bash + git tag v0.1.0 + git push origin v0.1.0 + # Then create release on GitHub + ``` + +3. **Manual testing:** + - Go to Actions โ†’ Publish to PyPI โ†’ Run workflow + - Select "testpypi" target + +## ๐Ÿ“‹ Publishing Checklist + +### Before Publishing: + +- [ ] All tests pass: `make test` +- [ ] Version updated in `pyproject.toml` +- [ ] `CHANGELOG.md` updated +- [ ] Git tags created +- [ ] Package builds successfully: `make build` +- [ ] Package passes checks: `make check-publish` + +### Publishing Process: + +1. **Test on TestPyPI:** + ```bash + make publish-test + ``` + +2. **Verify TestPyPI installation:** + ```bash + pip install --index-url https://test.pypi.org/simple/ toady-cli + toady --version + ``` + +3. **Publish to production:** + ```bash + make publish + ``` + +4. **Verify production installation:** + ```bash + pip install toady-cli + toady --version + ``` + +## ๐Ÿ”„ Version Management + +### Semantic Versioning + +Follow [SemVer](https://semver.org/): +- `0.1.0` โ†’ `0.1.1` (patch: bug fixes) +- `0.1.0` โ†’ `0.2.0` (minor: new features) +- `0.1.0` โ†’ `1.0.0` (major: breaking changes) + +### Update Version + +1. **Update `pyproject.toml`:** + ```toml + version = "0.1.1" + ``` + +2. **Update `src/toady/__init__.py`:** + ```python + __version__ = "0.1.1" + ``` + +3. **Create git tag:** + ```bash + git add . + git commit -m "Bump version to 0.1.1" + git tag v0.1.1 + git push origin main --tags + ``` + +## ๐Ÿ“ฆ Package Maintenance + +### Regular Updates + +- Update dependencies: `make update` +- Run security audit: `uv pip audit` +- Update classifiers in `pyproject.toml` +- Keep README and documentation current + +### Yanking Releases + +If you need to remove a broken release: + +```bash +# Via web interface (recommended) +# Go to PyPI project page โ†’ Manage โ†’ Yank release + +# Via command line +twine upload --repository pypi --yank "Broken release" dist/old-version/* +``` + +## ๐Ÿ› ๏ธ Troubleshooting + +### Common Issues + +1. **"File already exists"** + - You can't re-upload the same version + - Increment version number + +2. **"Invalid authentication"** + - Check API token format: `pypi-...` + - Verify token has upload permissions + +3. **"Package name taken"** + - Choose a different name in `pyproject.toml` + - Check availability on PyPI first + +4. **"Metadata validation failed"** + - Run `make check-publish` to see issues + - Fix `pyproject.toml` metadata + +### Getting Help + +- [PyPI Help](https://pypi.org/help/) +- [Packaging User Guide](https://packaging.python.org/) +- [Twine Documentation](https://twine.readthedocs.io/) + +## ๐ŸŒŸ Best Practices + +1. **Always test on TestPyPI first** +2. **Use API tokens, not passwords** +3. **Keep credentials secure and rotated** +4. **Tag releases in git** +5. **Update changelog and documentation** +6. **Run full test suite before publishing** +7. **Use automated publishing for consistency** + +## ๐Ÿ“š Additional Resources + +- [Python Packaging Authority](https://www.pypa.io/) +- [Python Packaging User Guide](https://packaging.python.org/) +- [PEP 517 - Build System Interface](https://peps.python.org/pep-0517/) +- [PEP 518 - Build System Requirements](https://peps.python.org/pep-0518/) diff --git a/pyproject.toml b/pyproject.toml index 6af5f4a..ac7307d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,8 @@ name = "toady-cli" version = "0.1.0" description = "A CLI tool for managing GitHub PR code reviews efficiently" readme = "README.md" -license = {file = "LICENSE"} +license = "MIT" +license-files = ["LICENSE"] authors = [ {name = "Tony Blank", email = "guillotine@lawnfucker.com"}, ] @@ -18,7 +19,6 @@ keywords = ["github", "cli", "code-review", "pull-request", "automation"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", @@ -97,6 +97,7 @@ toady = "toady.cli:main" [tool.setuptools.packages.find] where = ["src"] +include = ["toady*"] [tool.setuptools.package-data] toady = ["py.typed"]