Repository navigation
Add PyPI release workflow - #12452
Conversation
crusaderky
left a comment
There was a problem hiding this comment.
Thanks for the PR! It mostly looks good. A few notes below.
| release_version="$RELEASE_VERSION" | ||
| if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "::error::Release versions must use x.x.x form, got $release_version" |
There was a problem hiding this comment.
| release_version="$RELEASE_VERSION" | |
| if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Release versions must use x.x.x form, got $release_version" | |
| if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Release versions must use x.x.x form, got $RELEASE_VERSION" |
| - name: Check built versions | ||
| run: | | ||
| python - <<'PY' | ||
| import email | ||
| import glob | ||
| import os | ||
| import tarfile | ||
| import zipfile | ||
|
|
||
| release_version = os.environ["RELEASE_VERSION"] | ||
| major, minor, patch = release_version.split(".") | ||
| next_patch_version = f"{major}.{minor}.{int(patch) + 1}" | ||
|
|
||
| wheel_paths = glob.glob("dist/*.whl") | ||
| sdist_paths = glob.glob("dist/*.tar.gz") | ||
| if len(wheel_paths) != 1 or len(sdist_paths) != 1: | ||
| raise SystemExit("Expected exactly one wheel and one sdist") | ||
|
|
||
| with zipfile.ZipFile(wheel_paths[0]) as wheel: | ||
| metadata_path = next(name for name in wheel.namelist() if name.endswith(".dist-info/METADATA")) | ||
| wheel_metadata = email.message_from_bytes(wheel.read(metadata_path)) | ||
| wheel_version = wheel_metadata["Version"] | ||
|
|
||
| with tarfile.open(sdist_paths[0], "r:gz") as sdist: | ||
| pkg_info_path = next(name for name in sdist.getnames() if name.endswith("/PKG-INFO")) | ||
| pkg_info = sdist.extractfile(pkg_info_path) | ||
| if pkg_info is None: | ||
| raise SystemExit("Could not read sdist PKG-INFO") | ||
| sdist_metadata = email.message_from_binary_file(pkg_info) | ||
| sdist_version = sdist_metadata["Version"] | ||
|
|
||
| for artifact, version in {"wheel": wheel_version, "sdist": sdist_version}.items(): | ||
| if version != release_version: | ||
| raise SystemExit(f"{artifact} version {version} does not match release {release_version}") | ||
|
|
||
| for artifact, metadata in {"wheel": wheel_metadata, "sdist": sdist_metadata}.items(): | ||
| requirements = metadata.get_all("Requires-Dist") or [] | ||
| distributed_requirements = [ | ||
| requirement | ||
| for requirement in requirements | ||
| if requirement.startswith("distributed") and 'extra == "distributed"' in requirement | ||
| ] | ||
| if len(distributed_requirements) != 1: | ||
| raise SystemExit( | ||
| f"{artifact} should declare exactly one distributed extra requirement, " | ||
| f"got {distributed_requirements}" | ||
| ) | ||
| requirement = distributed_requirements[0].replace(" ", "") | ||
| if f">={release_version}" not in requirement or f"<{next_patch_version}" not in requirement: | ||
| raise SystemExit( | ||
| f"{artifact} distributed extra should require distributed>={release_version}," | ||
| f"<{next_patch_version}, got {distributed_requirements[0]}" | ||
| ) | ||
| PY |
There was a problem hiding this comment.
IMHO this step is quite overcomplicated and redundant with smoke_test_pypi.
I would suggest replacing it with a simpler bash step at the end of the smoke_test_pypi workflow that tests that both of these commands return the expected version:
python -c import dask; print(dask.__version__)pip freeze | sed -n 's/^dask==//p'
| dry_run_publish_pypi: | ||
| name: Dry-run PyPI publish | ||
| needs: smoke_test_pypi | ||
| if: github.event_name == 'workflow_dispatch' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/download-artifact@v7 | ||
| with: | ||
| name: dist | ||
| path: dist | ||
| - name: Confirm publish skipped | ||
| run: | | ||
| ls -l dist | ||
| echo "Dry run only; not publishing dask==$RELEASE_VERSION to PyPI." | ||
|
|
There was a problem hiding this comment.
This whole block for the sake of an echo feels unnecessary?
| dry_run_publish_pypi: | |
| name: Dry-run PyPI publish | |
| needs: smoke_test_pypi | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/download-artifact@v7 | |
| with: | |
| name: dist | |
| path: dist | |
| - name: Confirm publish skipped | |
| run: | | |
| ls -l dist | |
| echo "Dry run only; not publishing dask==$RELEASE_VERSION to PyPI." |
| * dask version in distributed/pyproject.toml | ||
| * pins should be >= the current version but < the next version to allow | ||
| * Update dependency bounds in all pyproject.toml files | ||
| * `distributed` dependency in pyproject.toml of dask/dask |
There was a problem hiding this comment.
We should clarify that in dask/pyproject.toml, you need to set e.g.
[project.optional-dependencies]
distributed = ["distributed >=2026.6.0,<2026.6.1"]
...while in distributed/pyproject.toml you'll need to have e.g.
dependencies = [
"dask >=2026.6.0,<2026.6.1",
...
]Which means that you can do the dask release with the impossible distributed pin, thanks to the fact that distributed is an optional dependency, but vice versa, you can't release distributed 2026.6.0 before dask 2026.6.0 has been published, because for distributed dask is a hard dependency.
There was a problem hiding this comment.
Done. In the future I wouldn't mind finding a way to automate this as well. It feels error prone. Future work though.
| * In `dask/distributed`, run the `Release Publisher` workflow manually | ||
| with the Distributed version to rehearse and a Dask version that is | ||
| already available on PyPI. |
There was a problem hiding this comment.
import distributed against the previous dask version is at risk of failing to import.
I can't see an easy way out. I think it should be clarified here that it's not the end of the world if this fails.
What matters a lot more is the most recent Upstream/py314 test run in dask/distributed (it can also be triggered by hand), which automatically detects breakages in dask/distributed unit tests caused by recent changes in dask/dask.
| versions, publishes them to PyPI with Trusted Publishing, and publishes the | ||
| GitHub Release. | ||
|
|
||
| GitHub Actions pauses at the `pypi` environment for manual approval. Open |
There was a problem hiding this comment.
I can't find this pause in the publish_pypi job. Is it an implicit default in pypa/gh-action-pypi-publish@release? If so I'd rather make it explicit.
There was a problem hiding this comment.
This will be set in GitHub configuration under the pypi Environment settings. I do this in other projects and it works well.
| PyPI publishing skips files that already exist, so rerunning the workflow | ||
| can recover after a partial success such as a GitHub Release failure. |
There was a problem hiding this comment.
dask and distributed feature a single artifact each though; what's the benefit of this?
There was a problem hiding this comment.
The Github release is downstream of the pypi release in the workflow. There's a world where we succeed in publishing a PyPI release but fail somehow to publish a github release. We'd need to redo the pipeline for this and pass over the PyPI stage. Very open to other ideas here. I'll admit to not caring too much about the GitHub release.
|
Thanks for the review @crusaderky . Pushed changes. Re-review welcome. |
Unit Test ResultsSee test report for an extended history of previous test failures. This is useful for diagnosing flaky tests. 25 files ±0 25 suites ±0 7h 4m 25s ⏱️ - 4m 54s Results for commit ba7ee8e. ± Comparison against base commit d0666ee. |
|
Planning to merge this in tomorrow and do the release. Please speak up if anyone disagrees. |
crusaderky
left a comment
There was a problem hiding this comment.
I have not tested it, but otherwise it looks good. Thank you!
This moves the release procedure from a manual process done on a laptop to automated on GitHub Actions. This uses the Trusted Publisher functionality on PyPI that has, I think, become somewhat standard. There's still a manual approval process in the chain.
When reviewing I recommend looking at the release procedure doc.
For an example run see https://github.com/mrocklin/dask/actions/runs/27209613416
See dask/community#444
Sibling PR for distributed at dask/distributed#9297