diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7d66b8e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + tags: ["v*"] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + with: + python-version: "3.13" + - run: uv sync + - run: uv run pytest -q -m "slow or not slow" + - run: uv build + - run: uvx twine check dist/* + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/simloop/ + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index 2703c2a..5a2da3c 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ dead-lettering) written in plain asyncio — tested end to end with simloop. Its suite runs hundreds of seeds of partitions, crashes, and poison jobs, and shows that removing any load-bearing safeguard produces a violation the explorer finds and replays from a seed. The bug table -lives in [examples/jobqueue/README.md](examples/jobqueue/README.md). +lives in [examples/jobqueue/README.md](https://github.com/dhruvl/simloop/blob/main/examples/jobqueue/README.md). ## Honest limits @@ -109,7 +109,7 @@ Code that goes through the event-loop API is supported; code that bypasses it is fenced: threads and executors, raw sockets, subprocesses, signals, TLS, and `getaddrinfo` raise `SimulationFenceError` rather than silently breaking determinism. Write-side flow control is not simulated. -The full contract is in [docs/supported-api.md](docs/supported-api.md). +The full contract is in [docs/supported-api.md](https://github.com/dhruvl/simloop/blob/main/docs/supported-api.md). ## License diff --git a/pyproject.toml b/pyproject.toml index c029d11..73c0f3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.12" license = "MIT" license-files = ["LICENSE"] -authors = [{ name = "Dhruv Kumar Singh", email = "dsingh32@binghamton.edu" }] +authors = [{ name = "Dhruv Kumar Singh", email = "singhdhruvk@gmail.com" }] keywords = ["asyncio", "testing", "deterministic", "simulation", "dst"] classifiers = [ "Development Status :: 3 - Alpha", @@ -19,6 +19,11 @@ classifiers = [ ] dependencies = [] +[project.urls] +Homepage = "https://github.com/dhruvl/simloop" +Repository = "https://github.com/dhruvl/simloop" +Issues = "https://github.com/dhruvl/simloop/issues" + [dependency-groups] dev = ["pytest>=8", "mypy>=1.11"] @@ -38,3 +43,9 @@ markers = ["slow: long-running seed sweeps and replay-stability checks"] strict = true files = ["src", "tests", "benchmarks", "examples/jobqueue"] mypy_path = "examples/jobqueue" + +[tool.hatch.build.targets.sdist] +only-include = ["src/simloop"] + +[tool.hatch.build.targets.wheel] +packages = ["src/simloop"] diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..8a6e9ed --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,58 @@ +"""The published artifacts must contain the package and nothing else.""" + +import subprocess +import tarfile +import zipfile +from pathlib import Path + +import pytest + +from simloop import __version__ + +REPO_ROOT = Path(__file__).resolve().parent.parent + +SDIST_ALLOWED_FILES = {"PKG-INFO", "pyproject.toml", "README.md", "LICENSE", ".gitignore"} +PACKAGE_PREFIX = "src/simloop/" + + +def _build(tmp_path: Path, kind: str) -> Path: + subprocess.run( + ["uv", "build", f"--{kind}", "--out-dir", str(tmp_path)], + cwd=REPO_ROOT, + check=True, + capture_output=True, + ) + pattern = "*.tar.gz" if kind == "sdist" else "*.whl" + (artifact,) = tmp_path.glob(pattern) + return artifact + + +@pytest.mark.slow +def test_sdist_ships_only_the_package(tmp_path: Path) -> None: + sdist = _build(tmp_path, "sdist") + with tarfile.open(sdist) as tar: + names = [m.name for m in tar.getmembers() if not m.isdir()] + # every member is "-/" + relpaths = [name.split("/", 1)[1] for name in names] + unexpected = [ + p + for p in relpaths + if p not in SDIST_ALLOWED_FILES and not p.startswith(PACKAGE_PREFIX) + ] + assert unexpected == [] + assert "src/simloop/__init__.py" in relpaths + + +@pytest.mark.slow +def test_wheel_ships_only_the_package(tmp_path: Path) -> None: + wheel = _build(tmp_path, "wheel") + with zipfile.ZipFile(wheel) as zf: + names = [n for n in zf.namelist() if not n.endswith("/")] + dist_info_prefix = f"simloop-{__version__}.dist-info/" + unexpected = [ + n + for n in names + if not n.startswith("simloop/") and not n.startswith(dist_info_prefix) + ] + assert unexpected == [] + assert "simloop/__init__.py" in names