diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..489d7be --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,75 @@ +name: Release + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + name: Build release artifacts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Verify release tag matches package version + shell: bash + run: | + set -euo pipefail + tag="${GITHUB_REF_NAME#v}" + version="$(python - <<'PY' + import tomllib + from pathlib import Path + data = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) + print(data["project"]["version"]) + PY + )" + echo "tag=$tag package=$version" + test "$tag" = "$version" + + - name: Build and inspect package + run: | + python -m pip install --upgrade build twine + python -m build + python -m twine check dist/* + + - name: Smoke-test built wheel + shell: bash + run: | + set -euo pipefail + python -m venv /tmp/counterproof-release + /tmp/counterproof-release/bin/pip install dist/*.whl + /tmp/counterproof-release/bin/counterproof --help >/tmp/counterproof-help.txt + grep -q 'CounterProof\|Counterproof' /tmp/counterproof-help.txt + + - uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + if-no-files-found: error + retention-days: 7 + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/counterproof + permissions: + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..f7914c0 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,51 @@ +# Releasing CounterProof + +CounterProof publishes Python distributions through PyPI Trusted Publishing. The release workflow uses GitHub OIDC and does not store a long-lived PyPI API token. + +## One-time setup + +Before the first release: + +1. In GitHub, create an environment named `pypi`. +2. In PyPI account settings, add a **pending GitHub publisher** with: + - PyPI project name: `counterproof` + - Owner: `hippoley` + - Repository: `CounterProof` + - Workflow filename: `release.yml` + - Environment: `pypi` +3. Keep the GitHub environment restricted to maintainers you trust. Requiring approval for the environment is recommended when available. + +A pending publisher does not reserve the package name until the first successful publish, so do not treat setup alone as ownership of `counterproof`. + +## Release procedure + +1. Update `project.version` in `pyproject.toml`. +2. Merge all intended release changes to `main`. +3. Confirm main CI is green. +4. Create a GitHub Release whose tag is exactly `v`, for example `v0.2.0`. +5. Publishing the GitHub Release triggers `.github/workflows/release.yml`. + +The workflow refuses to publish if the release tag and `pyproject.toml` version disagree. + +## What the workflow verifies + +Before uploading anything, it: + +- builds both source and wheel distributions; +- runs `twine check`; +- installs the built wheel into a clean virtual environment; +- verifies the installed `counterproof` CLI is present. + +Only the publish job receives `id-token: write`, and that job is bound to the `pypi` GitHub environment. + +## After publishing + +Verify: + +```bash +python -m pip install --upgrade counterproof +counterproof --help +counterproof doctor +``` + +Then update README installation examples from the GitHub URL to `pip install counterproof` only after the PyPI release is actually available. diff --git a/pyproject.toml b/pyproject.toml index 39506ea..7b8c681 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "counterproof" version = "0.2.0" -description = "Prove agent PR fixes by replaying changed tests against pre-change code." +description = "Prove AI-assisted PR claims with replayable BASE-to-HEAD evidence and scoped receipts." readme = "README.md" license = "Apache-2.0" authors = [{ name = "hippoley" }]