name: CI on: pull_request: branches: - main push: branches: - main jobs: quality: name: Quality Checks runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install Python from .python-version run: $HOME/.local/bin/uv python install - name: Sync development dependencies run: $HOME/.local/bin/uv sync --group dev - name: Run quality checks run: PATH="$HOME/.local/bin:$PATH" make ci-check tests: name: Test Suite runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install Python from .python-version run: $HOME/.local/bin/uv python install - name: Sync development dependencies run: $HOME/.local/bin/uv sync --group dev - name: Run tests run: PATH="$HOME/.local/bin:$PATH" make test publish: name: Publish to Gitea Packages runs-on: ubuntu-latest needs: [quality, tests] if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@v4 - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install Python from .python-version run: $HOME/.local/bin/uv python install - name: Sync development dependencies run: $HOME/.local/bin/uv sync --group dev - name: Read package version from pyproject.toml id: package_meta run: | $HOME/.local/bin/uv run python - <<'PY' >> "$GITHUB_OUTPUT" import tomllib from pathlib import Path project = tomllib.loads(Path('pyproject.toml').read_text(encoding='utf-8'))['project'] print(f"name={project['name']}") print(f"version={project['version']}") PY - name: Show package version run: echo "Publishing ${{ steps.package_meta.outputs.name }} version ${{ steps.package_meta.outputs.version }}" - name: Validate package registry configuration env: PYPI_REPOSITORY_URL: ${{ secrets.PYPI_REPOSITORY_URL }} PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} run: | test -n "$PYPI_REPOSITORY_URL" || (echo "PYPI_REPOSITORY_URL secret is required" && exit 1) test -n "$PACKAGE_USERNAME" || (echo "PACKAGE_USERNAME secret is required" && exit 1) test -n "$PACKAGE_TOKEN" || (echo "PACKAGE_TOKEN secret is required" && exit 1) - name: Check whether this package version already exists id: package_registry env: PYPI_REPOSITORY_URL: ${{ secrets.PYPI_REPOSITORY_URL }} PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} PACKAGE_NAME: ${{ steps.package_meta.outputs.name }} PACKAGE_VERSION: ${{ steps.package_meta.outputs.version }} run: | $HOME/.local/bin/uv run python - <<'PY' >> "$GITHUB_OUTPUT" import base64 import os import re from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen repository_url = os.environ['PYPI_REPOSITORY_URL'].rstrip('/') package_name = os.environ['PACKAGE_NAME'] package_version = os.environ['PACKAGE_VERSION'] normalized_name = re.sub(r'[-_.]+', '-', package_name).lower() filename_candidates = { f"{re.sub(r'[-_.]+', '_', package_name)}-{package_version}", f'{normalized_name}-{package_version}', } index_url = f'{repository_url}/simple/{normalized_name}/' credentials = f"{os.environ['PACKAGE_USERNAME']}:{os.environ['PACKAGE_TOKEN']}".encode() authorization = base64.b64encode(credentials).decode() request = Request(index_url, headers={'Authorization': f'Basic {authorization}'}) try: with urlopen(request, timeout=15) as response: page = response.read().decode('utf-8', 'replace') except HTTPError as exc: if exc.code == 404: print('exists=false') print(f'index_url={index_url}') else: raise except URLError as exc: raise SystemExit(f'Unable to query package registry: {exc}') from exc else: exists = any(candidate in page for candidate in filename_candidates) print(f"exists={'true' if exists else 'false'}") print(f'index_url={index_url}') PY - name: Show package publish decision run: | echo "Registry index: ${{ steps.package_registry.outputs.index_url }}" echo "Version exists: ${{ steps.package_registry.outputs.exists }}" - name: Build distribution if: steps.package_registry.outputs.exists != 'true' run: PATH="$HOME/.local/bin:$PATH" make build - name: Check built artifacts if: steps.package_registry.outputs.exists != 'true' run: PATH="$HOME/.local/bin:$PATH" uv run --with twine twine check dist/* - name: Skip upload for existing package version if: steps.package_registry.outputs.exists == 'true' run: | echo "Version ${{ steps.package_meta.outputs.version }} already exists in Gitea Packages." echo "Bump [project].version in pyproject.toml to publish a new release." - name: Publish to Gitea PyPI registry if: steps.package_registry.outputs.exists != 'true' env: TWINE_REPOSITORY_URL: ${{ secrets.PYPI_REPOSITORY_URL }} TWINE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} TWINE_PASSWORD: ${{ secrets.PACKAGE_TOKEN }} run: PATH="$HOME/.local/bin:$PATH" uv run --with twine twine upload dist/*