diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62a94cc..969306e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,13 +90,42 @@ jobs: - name: Build wheel + sdist run: | - rm -rf dist build *.egg-info + rm -rf dist build ./*.egg-info python -m build --wheel --sdist - name: Validate - run: twine check dist/* + run: twine check ./dist/* + + - name: Check whether this version already exists on PyPI + id: pypi-version + run: | + python - <<'PY' + import json + import os + import tomllib + import urllib.error + import urllib.request + + with open("pyproject.toml", "rb") as handle: + project = tomllib.load(handle)["project"] + name = project["name"] + version = project["version"] + url = f"https://pypi.org/pypi/{name}/{version}/json" + try: + with urllib.request.urlopen(url, timeout=15) as response: + payload = json.load(response) + exists = payload.get("info", {}).get("version") == version + except urllib.error.HTTPError as exc: + if exc.code != 404: + raise + exists = False + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"exists={'true' if exists else 'false'}\n") + print(f"PyPI {name} {version} already exists: {exists}") + PY - name: Publish to PyPI + if: steps.pypi-version.outputs.exists != 'true' uses: pypa/gh-action-pypi-publish@release/v1 github-release: @@ -126,8 +155,8 @@ jobs: python -m pip install --upgrade pip pip install build twine python -m build --wheel --sdist - twine check dist/* - sha256sum dist/* > SHA256SUMS + twine check ./dist/* + sha256sum ./dist/* > SHA256SUMS - name: Create GitHub release uses: softprops/action-gh-release@v2 diff --git a/docs/PUBLISH.md b/docs/PUBLISH.md index 458b1b4..96dd9d8 100644 --- a/docs/PUBLISH.md +++ b/docs/PUBLISH.md @@ -52,6 +52,9 @@ git push public v1.0.0 The tag workflow reruns qualification, validates tag/version parity, builds fresh artifacts, runs `twine check`, and publishes only after those gates pass. The GitHub Release receives the wheel, source distribution, and `SHA256SUMS`. +Release reruns first check whether the exact package version already exists on +PyPI. An existing version skips only the immutable upload; new versions still +require the configured trusted publisher. Verify the registry package from a new environment: diff --git a/tests/production/test_concurrency.py b/tests/production/test_concurrency.py index 39adbfe..83c066b 100644 --- a/tests/production/test_concurrency.py +++ b/tests/production/test_concurrency.py @@ -13,6 +13,7 @@ pytestmark = [pytest.mark.production, pytest.mark.load] +PROCESS_TIMEOUT_SECONDS = 60 class CountingEngine: @@ -54,7 +55,7 @@ def _embedded_stale_process_write(data_dir: str, ready, proceed) -> None: try: memory.search_memory("warm up the process-local engine") ready.set() - if not proceed.wait(timeout=30): + if not proceed.wait(timeout=PROCESS_TIMEOUT_SECONDS): raise RuntimeError("timed out waiting for the competing write") memory.add_memory("Worker fact uses marker-worker.") finally: @@ -184,7 +185,7 @@ def test_local_embedded_same_user_writes_survive_multiple_processes(tmp_path): process.start() try: for process in processes: - process.join(timeout=30) + process.join(timeout=PROCESS_TIMEOUT_SECONDS) assert process.exitcode == 0 finally: for process in processes: @@ -214,7 +215,7 @@ def test_local_embedded_refreshes_state_written_by_another_process(tmp_path): ) process.start() try: - assert ready.wait(timeout=30) + assert ready.wait(timeout=PROCESS_TIMEOUT_SECONDS) with SCM( user_id="shared-refresh-user", data_dir=tmp_path, @@ -222,7 +223,7 @@ def test_local_embedded_refreshes_state_written_by_another_process(tmp_path): ) as memory: memory.add_memory("Parent fact uses marker-parent.") proceed.set() - process.join(timeout=30) + process.join(timeout=PROCESS_TIMEOUT_SECONDS) assert process.exitcode == 0 finally: proceed.set()