Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Build

# Builds the sdists and wheels for both packages. Runs on PRs and main to catch
# packaging problems early, and is called by release.yml

on:
# Not push+pull_request because then it runs twice for PRs from branches
# within the repo.
pull_request:
push:
branches: [main]
workflow_call:
outputs:
version:
description: The version both packages are built at.
value: ${{ jobs.build.outputs.version }}

# A new push to a PR cancels the previous run. Pushes to main fall back to the
# unique run_id and so never cancel each other.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.x"

# A mismatched pair cannot be replaced once uploaded.
- name: Check the packages are versioned in lockstep
id: version
shell: python
run: |
import os
import sys
import tomllib

def project(path):
with open(path, "rb") as f:
return tomllib.load(f)["project"]

kernels = project("pyproject.toml")
shim = project("hipequivariance_ops_torch/pyproject.toml")
errors = []
if shim["version"] != kernels["version"]:
errors.append(
f"shim version {shim['version']} != kernels version {kernels['version']}"
)
pin = f"{kernels['name']}=={kernels['version']}"
if pin not in shim["dependencies"]:
errors.append(f"shim dependencies {shim['dependencies']} lack {pin!r}")
if errors:
sys.exit("\n".join(errors))
print(f"version {kernels['version']}")
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
print(f"version={kernels['version']}", file=f)

# Each package goes in its own directory so release.yml can publish them
# separately.
- name: Build
run: |
python -m pip install build check-wheel-contents twine
python -m build --outdir dist/geometric-nn-ops-torch .
python -m build --outdir dist/hipequivariance-ops-torch hipequivariance_ops_torch
python -m twine check --strict dist/*/*

- name: Check wheel contents
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
check-wheel-contents --package geometric_nn_ops_torch \
"dist/geometric-nn-ops-torch/geometric_nn_ops_torch-$VERSION-py3-none-any.whl"
check-wheel-contents --package hipequivariance_ops_torch/cuequivariance_ops_torch \
"dist/hipequivariance-ops-torch/hipequivariance_ops_torch-$VERSION-py3-none-any.whl"

- name: Check the wheels carry the license
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
unzip -l "dist/geometric-nn-ops-torch/geometric_nn_ops_torch-$VERSION-py3-none-any.whl" \
"geometric_nn_ops_torch-$VERSION.dist-info/licenses/LICENSE"
unzip -l "dist/hipequivariance-ops-torch/hipequivariance_ops_torch-$VERSION-py3-none-any.whl" \
"hipequivariance_ops_torch-$VERSION.dist-info/licenses/LICENSE"

- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist-geometric-nn-ops-torch
path: dist/geometric-nn-ops-torch/
if-no-files-found: error

- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist-hipequivariance-ops-torch
path: dist/hipequivariance-ops-torch/
if-no-files-found: error
5 changes: 4 additions & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ jobs:
hipequivariance_ops_torch/pyproject.toml

- name: Install
# CPU torch does not pull in Triton and we don't declare it as a dep
# (see pyproject.toml). Without it, the Triton test modules skip at
# import and we want to at least test collection.
run: |
python -m pip install --only-binary=":all:" "torch==2.4.0+cpu" --index-url https://download.pytorch.org/whl/cpu
python -m pip install --only-binary=":all:" -e ".[test]" -e "./hipequivariance_ops_torch[test]"
python -m pip install --only-binary=":all:" "triton>=3.0" -e ".[test]" -e "./hipequivariance_ops_torch[test]"

# Runners have no GPU, so most tests skip and this is mainly a check that
# every module still imports and collects.
Expand Down
158 changes: 158 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: release

# Releases both packages at the version in their pyproject.toml files. Run it by
# dispatching it on a release tag, after pushing the tag:
#
# git tag v0.1.0rc1 <commit> && git push <remote> v0.1.0rc1
#
# The tag selects where to publish. vVERSION publishes to PyPI and test-vVERSION
# to TestPyPI, with VERSION X.Y.Z or X.Y.Z{a,b,rc}N. Both run the same steps
# except that a test-v tag publishes to TestPyPI, skips the GitHub release, and
# may be for a commit not on main. A v tag must be on main, and the workflow
# creates a GitHub release for it with the sdists and wheels attached before
# publishing to PyPI. a, b and rc versions are marked as GitHub pre-releases and
# not as the latest release.
#
# PyPI and TestPyPI Trusted Publishing for both projects are bound to this
# file's name and to the `pypi` and `testpypi` environments. Renaming the file
# requires updating all four publishers.

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
uses: ./.github/workflows/build.yml

check:
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
# The environment and package index to publish to: pypi or testpypi.
index: ${{ steps.check.outputs.index }}
prerelease: ${{ steps.check.outputs.prerelease }}
steps:
- name: Check the tag
id: check
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.build.outputs.version }}
run: |
if [[ "$GITHUB_REF_TYPE" != tag ]]; then
echo "::error::Run this workflow on a release tag, not $GITHUB_REF_TYPE $GITHUB_REF_NAME"
exit 1
fi
case "$GITHUB_REF_NAME" in
"v$VERSION") index=pypi ;;
"test-v$VERSION") index=testpypi ;;
*)
echo "::error::Tag $GITHUB_REF_NAME must be v$VERSION (PyPI) or test-v$VERSION (TestPyPI) for package version $VERSION"
exit 1
;;
esac
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]]; then
echo "::error::Release version $VERSION is not X.Y.Z or X.Y.Z{a,b,rc}N"
exit 1
fi
if [[ "$index" == pypi ]]; then
# "identical" or "ahead" means the tagged commit is an ancestor of main.
status=$(gh api "repos/$GITHUB_REPOSITORY/compare/$GITHUB_SHA...main" --jq .status)
if [[ "$status" != identical && "$status" != ahead ]]; then
echo "::error::Tag $GITHUB_REF_NAME is not on main (compare status: $status)"
exit 1
fi
fi
echo "index=$index" >> "$GITHUB_OUTPUT"
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi

github-release:
needs: check
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: dist-*
merge-multiple: true
path: dist/

- name: Create the GitHub release
# Skip actual release creation for tests. GitHub has no equivalent of
# TestPyPI. We skip at the step level so we exercise as much of the
# workflow as possible. It also allows us to avoid GitHub's weird
# behavior with jobs that depend on skipped jobs
# (https://github.com/orgs/community/discussions/45058)
if: needs.check.outputs.index == 'pypi'
env:
GH_TOKEN: ${{ github.token }}
PRERELEASE: ${{ needs.check.outputs.prerelease }}
run: |
options=()
if [[ "$PRERELEASE" == true ]]; then
options=(--prerelease --latest=false)
fi
gh release create "$GITHUB_REF_NAME" dist/* \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--title "geometric-nn-ops $GITHUB_REF_NAME" \
--generate-notes \
"${options[@]}"

# Each package publishes from its own job because a single PyPI token exchange
# can create at most one new project. The shim goes second because it pins the
# kernels' exact version and must not be published without them.
publish-kernels:
needs: [check, github-release]
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: ${{ needs.check.outputs.index }}
permissions:
id-token: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist-geometric-nn-ops-torch
path: dist/

- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
with:
repository-url: >-
${{ needs.check.outputs.index == 'pypi'
&& 'https://upload.pypi.org/legacy/'
|| 'https://test.pypi.org/legacy/' }}
print-hash: true

publish-shim:
needs: [check, publish-kernels]
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: ${{ needs.check.outputs.index }}
permissions:
id-token: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist-hipequivariance-ops-torch
path: dist/

- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
with:
repository-url: >-
${{ needs.check.outputs.index == 'pypi'
&& 'https://upload.pypi.org/legacy/'
|| 'https://test.pypi.org/legacy/' }}
print-hash: true
2 changes: 0 additions & 2 deletions geometric_nn_ops_torch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@

* ``geometric_nn_ops_torch.triangle`` — protein-folding (triangle) ops.
"""

__version__ = "0.1.0"
Loading