From 1ac6459503b12b684f16d56717914d6eced59898 Mon Sep 17 00:00:00 2001 From: Gokul Krishna Date: Tue, 25 Aug 2026 03:27:14 +0530 Subject: [PATCH] build: prepare trusted PyPI release workflow --- .github/workflows/ci.yml | 10 +++-- .github/workflows/publish.yml | 84 +++++++++++++++++++++++++++++++++++ README.md | 13 +++--- docs/RELEASING.md | 31 +++++++++++++ pyproject.toml | 14 +++++- src/contribcheck/__init__.py | 3 +- src/contribcheck/github.py | 3 +- tests/test_github.py | 2 + 8 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 docs/RELEASING.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 535b819..e1aaf70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,13 @@ jobs: quality: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v6 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 with: enable-cache: true - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - run: uv sync --all-extras --dev --locked @@ -25,3 +27,5 @@ jobs: - run: uv run ruff format --check . - run: uv run mypy src tests - run: uv run pytest --cov=contribcheck --cov-report=term-missing --cov-report=xml + - run: uv build + - run: uvx --from twine==7.0.0 twine check dist/* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..45a4c39 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,84 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +concurrency: + group: pypi-${{ github.event.release.tag_name }} + cancel-in-progress: false + +permissions: {} + +jobs: + build: + name: Build and verify distributions + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out release tag + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + + - name: Set up uv + uses: astral-sh/setup-uv@d0d8abe699bfb85fec6de9f7adb5ae17292296ff # v6 + with: + enable-cache: false + + - name: Verify tag matches package version + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + tag_version="${RELEASE_TAG#v}" + package_version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" + if [[ "$tag_version" != "$package_version" ]]; then + echo "Release tag $RELEASE_TAG does not match package version $package_version" >&2 + exit 1 + fi + + - name: Build wheel and source distribution + run: uv build + + - name: Verify package metadata + run: uvx --from twine==7.0.0 twine check dist/* + + - name: Smoke-test the wheel + run: | + python -m venv /tmp/contribcheck-release + /tmp/contribcheck-release/bin/python -m pip install dist/*.whl + /tmp/contribcheck-release/bin/contribcheck --version + + - name: Upload distributions + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: python-package-distributions + path: dist/ + if-no-files-found: error + retention-days: 7 + + publish: + name: Publish distributions to PyPI + if: github.repository == 'gokul-debugger/contribcheck' + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/contribcheck/ + permissions: + id-token: write + steps: + - name: Download distributions + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distributions + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 diff --git a/README.md b/README.md index 7902352..775ace7 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ An evidence-based CLI and API that checks whether a GitHub issue is genuinely ready for an open-source contribution. [![CI](https://github.com/gokul-debugger/contribcheck/actions/workflows/ci.yml/badge.svg)](https://github.com/gokul-debugger/contribcheck/actions/workflows/ci.yml) +[![PyPI](https://img.shields.io/pypi/v/contribcheck?logo=pypi&logoColor=white)](https://pypi.org/project/contribcheck/) [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![FastAPI](https://img.shields.io/badge/API-FastAPI-009688?logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/gokul-debugger/contribcheck/blob/main/LICENSE) ContribCheck identifies blockers that issue-discovery lists often miss: dependency issues, missing base branches, existing assignments, comment claims, competing pull requests, unhealthy default-branch CI, and missing contribution guidance. @@ -98,11 +99,11 @@ The overall result is one of: ContribCheck requires Python 3.11 or newer. ```bash -python3.12 -m venv .venv -source .venv/bin/activate -pip install -e . +pipx install contribcheck ``` +Alternatively, install it into an existing Python environment with `pip install contribcheck`. The optional web service is available with `pip install "contribcheck[server]"`. + Public GitHub requests work without authentication, but GitHub applies a much smaller anonymous rate limit. Set a token for regular use: ```bash @@ -203,7 +204,7 @@ contribcheck serve Open `http://127.0.0.1:8000/` for the browser interface or `http://127.0.0.1:8000/docs` for the interactive API documentation. The browser interface sends only the issue reference and optional actor to the local API. It does not accept, store, or log GitHub tokens. -![ContribCheck web interface](docs/contribcheck-ui.png) +![ContribCheck web interface](https://raw.githubusercontent.com/gokul-debugger/contribcheck/main/docs/contribcheck-ui.png) Call the API directly: @@ -243,7 +244,7 @@ Tests use a mocked GitHub transport and do not consume API quota. Contributions are welcome. Start with the [good first issues](https://github.com/gokul-debugger/contribcheck/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) or browse tasks marked [help wanted](https://github.com/gokul-debugger/contribcheck/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). -Read [CONTRIBUTING.md](CONTRIBUTING.md), comment with a short implementation plan, and wait for confirmation before starting work that changes a public contract. +Read [CONTRIBUTING.md](https://github.com/gokul-debugger/contribcheck/blob/main/CONTRIBUTING.md), comment with a short implementation plan, and wait for confirmation before starting work that changes a public contract. ## Current Limitations diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..2bf4688 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,31 @@ +# Release Process + +ContribCheck publishes signed distributions to PyPI through GitHub Trusted Publishing. No PyPI password or long-lived API token is stored in the repository. + +## One-time PyPI configuration + +Create a pending Trusted Publisher on PyPI with these exact values: + +| Field | Value | +|---|---| +| PyPI project name | `contribcheck` | +| GitHub owner | `gokul-debugger` | +| Repository | `contribcheck` | +| Workflow | `publish.yml` | +| Environment | `pypi` | + +Create a GitHub environment named `pypi`. Add required-reviewer protection before publishing if the repository plan supports it. + +## Release checklist + +1. Confirm `main` is clean and CI is passing. +2. Set the same release version in `pyproject.toml` and `src/contribcheck/__init__.py`. +3. Update `uv.lock` when the project version or dependencies change. +4. Run the full local quality suite. +5. Build both distributions and inspect their contents. +6. Create an annotated `vX.Y.Z` tag from the intended commit. +7. Publish a GitHub release for that tag. +8. Wait for the `Publish to PyPI` workflow to finish. +9. Verify installation in a clean environment with `pipx install contribcheck`. + +The workflow refuses to publish when the release tag and package metadata versions differ. PyPI does not permit replacing an uploaded version, so publish only from a reviewed commit. diff --git a/pyproject.toml b/pyproject.toml index 4134e29..dba9b74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,9 @@ keywords = [ classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", + "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -57,13 +59,23 @@ contribcheck = "contribcheck.cli:app" [project.urls] Homepage = "https://github.com/gokul-debugger/contribcheck" +Documentation = "https://github.com/gokul-debugger/contribcheck#readme" Issues = "https://github.com/gokul-debugger/contribcheck/issues" +Releases = "https://github.com/gokul-debugger/contribcheck/releases" +Source = "https://github.com/gokul-debugger/contribcheck" [tool.hatch.build.targets.wheel] packages = ["src/contribcheck"] [tool.hatch.build.targets.sdist] -include = ["src/contribcheck", "tests", "README.md", "LICENSE", "CONTRIBUTING.md"] +include = [ + "src/contribcheck", + "tests", + "docs/RELEASING.md", + "README.md", + "LICENSE", + "CONTRIBUTING.md", +] [tool.ruff] line-length = 100 diff --git a/src/contribcheck/__init__.py b/src/contribcheck/__init__.py index 7ca903e..71f031b 100644 --- a/src/contribcheck/__init__.py +++ b/src/contribcheck/__init__.py @@ -1,8 +1,9 @@ """ContribCheck public package interface.""" +__version__ = "0.1.0" + from contribcheck.analyzer import IssueAnalyzer from contribcheck.github import GitHubClient from contribcheck.models import InspectionReport, OverallStatus __all__ = ["GitHubClient", "InspectionReport", "IssueAnalyzer", "OverallStatus"] -__version__ = "0.1.0" diff --git a/src/contribcheck/github.py b/src/contribcheck/github.py index a9abceb..bc0b1eb 100644 --- a/src/contribcheck/github.py +++ b/src/contribcheck/github.py @@ -10,6 +10,7 @@ import httpx +from contribcheck import __version__ from contribcheck.exceptions import GitHubAPIError, GitHubRateLimitError from contribcheck.models import IssueTarget, JsonObject @@ -36,7 +37,7 @@ def __init__( ) headers = { "Accept": "application/vnd.github+json", - "User-Agent": "contribcheck/0.1.0", + "User-Agent": f"contribcheck/{__version__}", "X-GitHub-Api-Version": API_VERSION, } if resolved_token: diff --git a/tests/test_github.py b/tests/test_github.py index 69e557f..3e51d64 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -5,6 +5,7 @@ import httpx import pytest +from contribcheck import __version__ from contribcheck.exceptions import GitHubRateLimitError from contribcheck.github import API_VERSION, GitHubClient, normalize_api_base_url from contribcheck.models import IssueTarget @@ -17,6 +18,7 @@ async def test_client_sends_version_and_authentication_headers() -> None: async def handler(request: httpx.Request) -> httpx.Response: assert request.headers["X-GitHub-Api-Version"] == API_VERSION assert request.headers["Authorization"] == "Bearer secret" + assert request.headers["User-Agent"] == f"contribcheck/{__version__}" return httpx.Response(200, json={"default_branch": "main"}) async with GitHubClient(token="secret", transport=httpx.MockTransport(handler)) as client: