From 35881654889077ae2ca5bd8374ef0be545ccf52f Mon Sep 17 00:00:00 2001 From: Lilith Date: Wed, 21 Jan 2026 12:36:40 -0800 Subject: [PATCH] ci: add Forgejo Actions workflow for PyPI publishing --- .forgejo/workflows/publish.yml | 156 +++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 .forgejo/workflows/publish.yml diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml new file mode 100644 index 0000000..628d0bd --- /dev/null +++ b/.forgejo/workflows/publish.yml @@ -0,0 +1,156 @@ +# ============================================================================= +# Forgejo Actions Workflow - Python Package Publishing +# ============================================================================= +# Standardized template for Python packages published to Forgejo PyPI registry +# +# Features: +# - Pre-publish version check (avoids wasteful builds) +# - Optional testing (non-blocking) +# - Path-based triggers (pyproject.toml, src/** changes only) +# - Graceful duplicate version handling (409 Conflict) +# - Python 3.12 standard environment +# +# Usage: +# 1. Copy to: /.forgejo/workflows/pypi-publish.yml +# 2. Ensure pyproject.toml has [project] metadata (name, version) +# 3. Commit and push to main/master +# +# Secrets required: +# - PYPI_TOKEN: Forgejo PyPI registry token +# ============================================================================= + +name: Publish to PyPI + +on: + push: + branches: [main, master] + paths: + - 'pyproject.toml' # Trigger on version bumps + - 'src/**' # Or source code changes + - 'setup.py' # Legacy setup files + workflow_dispatch: # Manual trigger for first-time publish + +env: + PYTHON_VERSION: '3.12' + REGISTRY_URL: 'https://forge.nasty.sh/api/packages/lilith/pypi/' + +jobs: + publish: + name: Build and Publish + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install build tools + run: | + echo "=== Installing build tools ===" + python -m pip install --upgrade pip + pip install build twine + echo "✓ Build tools installed" + + - name: Check if version already published + id: check_version + run: | + echo "=== Reading package metadata ===" + + # Read package name and version from pyproject.toml + if [ -f "pyproject.toml" ]; then + pkg_name=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])") + pkg_version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") + else + echo "✗ Error: pyproject.toml not found" + exit 1 + fi + + echo "package=$pkg_name" >> $GITHUB_OUTPUT + echo "version=$pkg_version" >> $GITHUB_OUTPUT + + echo "" + echo "Package: $pkg_name" + echo "Version: $pkg_version" + echo "Registry: ${{ env.REGISTRY_URL }}" + echo "" + + # Check if version exists in Forgejo PyPI registry + echo "=== Checking if version already published ===" + if pip index versions "$pkg_name" --index-url ${{ env.REGISTRY_URL }}/simple 2>/dev/null | grep -q "$pkg_version"; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "✓ Version $pkg_version already published to registry" + echo " No action needed" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "→ Version $pkg_version not found in registry" + echo " Will proceed with build and publish" + fi + + - name: Run tests (if available) + if: steps.check_version.outputs.exists == 'false' + continue-on-error: true + run: | + echo "=== Running tests ===" + + # Check if tests directory exists and pytest is configured + if [ -d "tests" ] && grep -q "pytest" pyproject.toml 2>/dev/null; then + echo "Installing dev dependencies..." + pip install -e ".[dev]" 2>/dev/null || pip install pytest pytest-asyncio + + echo "Running pytest..." + if pytest tests/ -v 2>&1; then + echo "✓ All tests passed" + else + echo "⚠ Tests failed but continuing (non-blocking)" + fi + else + echo "⊘ No tests found or pytest not configured, skipping" + fi + + - name: Build package + if: steps.check_version.outputs.exists == 'false' + run: | + echo "=== Building package ===" + python -m build + echo "" + echo "✓ Build complete" + echo "Built artifacts:" + ls -lh dist/ + + - name: Publish to Forgejo PyPI + if: steps.check_version.outputs.exists == 'false' + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + TWINE_REPOSITORY_URL: ${{ env.REGISTRY_URL }} + run: | + echo "=== Publishing to Forgejo PyPI ===" + echo "Package: ${{ steps.check_version.outputs.package }}@${{ steps.check_version.outputs.version }}" + echo "Registry: ${{ env.REGISTRY_URL }}" + echo "" + + # Attempt upload with graceful conflict handling + if twine_output=$(twine upload dist/* 2>&1); then + echo "$twine_output" + echo "" + echo "✓ Successfully published to Forgejo PyPI" + echo " Package: ${{ steps.check_version.outputs.package }}" + echo " Version: ${{ steps.check_version.outputs.version }}" + echo " Install: pip install ${{ steps.check_version.outputs.package }} --index-url ${{ env.REGISTRY_URL }}/simple" + else + # Check if failure was due to version already existing (409 Conflict) + if echo "$twine_output" | grep -qi "already exists\|conflict\|409"; then + echo "$twine_output" + echo "" + echo "✓ Version already published (expected race condition)" + echo " This can happen if multiple workflows run simultaneously" + exit 0 + else + echo "✗ Upload failed:" + echo "$twine_output" + exit 1 + fi + fi