diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..da10e4c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +# Build the sdist + wheel on every tag, validate them, and publish to PyPI via +# Trusted Publishing (OIDC — no long-lived API token stored in the repo). The +# publish job is gated on the ``pypi`` GitHub Environment, so it does nothing +# until a maintainer configures that environment and the matching Trusted +# Publisher on PyPI; until then tagging still produces validated, downloadable +# artifacts without pushing anything to the index. + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: build + validate distributions + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install build tooling + run: python -m pip install -e .[release] + - name: Build sdist and wheel + run: python -m build + - name: Verify tag matches package version + if: startsWith(github.ref, 'refs/tags/') + run: | + version="$(python -c 'import importlib.metadata as m; print(m.version("pyisolate"))')" + tag="${GITHUB_REF_NAME#v}" + if [ "$version" != "$tag" ]; then + echo "tag $tag does not match package version $version" >&2 + exit 1 + fi + - name: Check distribution metadata + run: python -m twine check dist/* + - name: Capture release provenance + run: pyisolate-doctor --json > dist/provenance.json || true + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: publish to PyPI (Trusted Publishing) + needs: build + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-24.04 + environment: + name: pypi + url: https://pypi.org/project/pyisolate/ + permissions: + id-token: write # required for OIDC Trusted Publishing + steps: + - name: Download built distributions + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Remove non-distribution artifacts + run: rm -f dist/provenance.json + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 1c33521..098d87c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__/ *.bpf.o *.egg-info/ .env/ +/dist/ +/build/ diff --git a/docs/packaging-reproducibility.md b/docs/packaging-reproducibility.md index 85dafd6..4d73d3b 100644 --- a/docs/packaging-reproducibility.md +++ b/docs/packaging-reproducibility.md @@ -78,3 +78,29 @@ Suggested release flow: Persist `pyisolate-doctor` output next to signatures to tie artifacts back to an exact interpreter + kernel environment. + +## 7) Automated PyPI publishing + +`.github/workflows/release.yml` builds, validates, and (optionally) publishes the +package. It runs on any `v*` tag and on manual `workflow_dispatch`: + +- **build job** — installs `.[release]`, runs `python -m build`, asserts the tag + matches the package version, runs `twine check dist/*`, captures + `pyisolate-doctor --json` provenance, and uploads `dist/` as a workflow + artifact. This job needs no secrets, so tagging always yields validated, + downloadable distributions. +- **publish job** — runs only for tag pushes, is gated on the `pypi` GitHub + Environment, and uploads via **PyPI Trusted Publishing** (OIDC). No API token + is stored in the repository. + +To enable publishing: + +1. On PyPI, add a *Trusted Publisher* for the project pointing at this repo, + the `Release` workflow, and the `pypi` environment. +2. In GitHub, create an Environment named `pypi` (add reviewers/branch + protection if you want a manual approval gate before upload). +3. Bump `version` in `pyproject.toml`, commit, then push a matching `vX.Y.Z` + tag. The version-match check fails the build if the tag and metadata differ. + +Until the Trusted Publisher and environment exist, the publish job is a no-op +gate and only the build/validation job runs. diff --git a/pyproject.toml b/pyproject.toml index 1331f1c..80f4dcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,12 +2,45 @@ name = "pyisolate" version = "0.0.1" description = "Prototype PyIsolate sandbox; kernel enforcement and no-GIL support are experimental roadmap items" +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] authors = [{ name = "PyIsolate" }] requires-python = ">=3.11" +keywords = [ + "sandbox", + "isolation", + "security", + "seccomp", + "landlock", + "ebpf", + "sub-interpreter", + "no-gil", +] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Security", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Systems Administration", +] dependencies = ["pyyaml", "cryptography", "platformdirs"] +[project.urls] +Homepage = "https://github.com/seanwevans/pyisolate" +Repository = "https://github.com/seanwevans/pyisolate" +Issues = "https://github.com/seanwevans/pyisolate/issues" +Changelog = "https://github.com/seanwevans/pyisolate/blob/main/ROADMAP.md" + [build-system] -requires = ["setuptools>=64"] +# setuptools >= 77 emits PEP 639 license metadata (License-Expression / +# License-File) that twine check and modern PyPI validate cleanly. +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find]