Skip to content

Build & Publish

Build & Publish #67

Workflow file for this run

name: Build & Publish
env:
MAIN_TEST_PYTHON: '3.12'
on:
push:
branches: [dev, main]
pull_request:
workflow_dispatch:
release:
types: [published]
jobs:
build_wheels:
name: Build wheels (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux
runner: ubuntu-latest
- platform: macos
runner: macos-latest
- platform: windows
runner: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build wheels (Linux)
if: matrix.platform == 'linux'
uses: pypa/cibuildwheel@v2.21
env:
CIBW_BUILD: cp310-manylinux_x86_64 cp311-manylinux_x86_64 cp312-manylinux_x86_64
CIBW_BEFORE_ALL_LINUX: yum install -y eigen3-devel
with:
platform: linux
python-version: ${{ env.MAIN_TEST_PYTHON }}
output-dir: ./wheelhouse
- name: Build wheels (macOS)
if: matrix.platform == 'macos'
uses: pypa/cibuildwheel@v2.21
env:
CIBW_BUILD: cp310-macosx* cp311-macosx* cp312-macosx*
CIBW_ENVIRONMENT_MACOS: CMAKE_INCLUDE_PATH=/tmp/eigen3
CIBW_BEFORE_ALL_MACOS: python -c "import pathlib, shutil, tarfile, tempfile, urllib.request; url='https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz'; tmp=tempfile.mkdtemp(); archive=pathlib.Path(tmp)/'eigen.tar.gz'; archive.write_bytes(urllib.request.urlopen(url).read()); src_root=pathlib.Path(tmp)/'src'; src_root.mkdir(); tarfile.open(archive).extractall(src_root); src=next(src_root.glob('eigen-*')); dst=pathlib.Path('/tmp/eigen3'); shutil.rmtree(dst, ignore_errors=True); shutil.copytree(src, dst)"
with:
platform: macos
python-version: ${{ env.MAIN_TEST_PYTHON }}
macos-deployment-target: '11.0'
output-dir: ./wheelhouse
- name: Build wheels (Windows)
if: matrix.platform == 'windows'
uses: pypa/cibuildwheel@v2.21
env:
CIBW_BUILD: cp310-win* cp311-win* cp312-win*
CIBW_ENVIRONMENT_WINDOWS: CMAKE_INCLUDE_PATH=C:/eigen3
CIBW_BEFORE_ALL_WINDOWS: python -c "import io, pathlib, shutil, tempfile, urllib.request, zipfile; url='https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.zip'; data=urllib.request.urlopen(url).read(); tmp=tempfile.mkdtemp(); zipfile.ZipFile(io.BytesIO(data)).extractall(tmp); src=next(pathlib.Path(tmp).glob('eigen-*')); dst=pathlib.Path('C:/eigen3'); shutil.rmtree(dst, ignore_errors=True); shutil.copytree(src, dst)"
with:
platform: windows
python-version: ${{ env.MAIN_TEST_PYTHON }}
output-dir: ./wheelhouse
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.platform }}
path: ./wheelhouse/*.whl
retention-days: 30
build_conda:
name: Build conda (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux
runner: ubuntu-latest
- platform: macos
runner: macos-latest
- platform: windows
runner: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Miniconda (Linux)
if: matrix.platform == 'linux'
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ env.MAIN_TEST_PYTHON }}
miniforge-version: latest
channels: conda-forge
conda-remove-defaults: true
- name: Set up Miniforge (macOS)
if: matrix.platform == 'macos'
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ env.MAIN_TEST_PYTHON }}
miniforge-version: latest
channels: conda-forge
conda-remove-defaults: true
- name: Set up Miniforge (Windows)
if: matrix.platform == 'windows'
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ env.MAIN_TEST_PYTHON }}
miniforge-version: latest
channels: conda-forge
conda-remove-defaults: true
- name: Download conda-forge pinning config
shell: bash -l {0}
run: |
mkdir -p recipe
curl -sL https://raw.githubusercontent.com/conda-forge/conda-forge-pinning-feedstock/main/recipe/conda_build_config.yaml -o recipe/conda_build_config.yaml
- name: Build conda package (Linux)
if: matrix.platform == 'linux'
shell: bash -l {0}
run: |
conda install -y conda-build
conda build recipe/ --output-folder dist/conda 2>&1 | tee conda_build.log || (cat conda_build.log && exit 1)
- name: Build conda package (macOS)
if: matrix.platform == 'macos'
shell: bash -l {0}
run: |
conda install -y conda-build
conda build recipe/ --output-folder dist/conda 2>&1 | tee conda_build.log || (cat conda_build.log && exit 1)
- name: Build conda package (Windows)
if: matrix.platform == 'windows'
shell: bash -l {0}
run: |
conda install -y conda-build
conda build recipe/ --output-folder dist/conda 2>&1 | tee conda_build.log || (type conda_build.log && exit 1)
- name: Upload conda package
uses: actions/upload-artifact@v4
with:
name: conda-${{ matrix.platform }}
path: dist/conda/**/*
retention-days: 30
- name: Upload conda build log
if: always()
uses: actions/upload-artifact@v4
with:
name: conda-build-log-${{ matrix.platform }}
path: conda_build.log
retention-days: 30
test:
name: Test
needs: [build_wheels, build_conda]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.MAIN_TEST_PYTHON }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest numpy packaging
- name: Download wheels
uses: actions/download-artifact@v4
with:
path: wheelhouse
- name: Install and test
run: |
python - <<'PY'
import glob
import os
from packaging.tags import sys_tags
from packaging.utils import parse_wheel_filename
import subprocess
import sys
wheels = sorted(glob.glob("wheelhouse/**/*.whl", recursive=True))
print("Discovered wheels:")
for wheel in wheels:
print(f"- {wheel}")
candidate_wheels = [
wheel
for wheel in wheels
if os.path.basename(wheel).startswith("labellib-")
]
supported_tags = set(sys_tags())
compatible_wheels = []
for wheel in candidate_wheels:
_, _, _, tags = parse_wheel_filename(os.path.basename(wheel))
if tags & supported_tags:
compatible_wheels.append(wheel)
if not compatible_wheels:
raise SystemExit("No compatible labellib wheel found in downloaded artifacts")
wheel = compatible_wheels[0]
print(f"Installing wheel artifact: {wheel}")
subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-index", wheel])
PY
pytest
publish_conda:
name: Publish conda to tpeulen
needs: [build_conda, test]
if: (github.event_name == 'push' && github.ref == 'refs/heads/dev') || (github.event_name == 'release' && github.event.release.target_commitish == 'main')
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/download-artifact@v4
with:
pattern: conda-*
path: dist/conda
merge-multiple: true
- name: Validate Anaconda token is configured
env:
ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }}
run: |
if [ -z "$ANACONDA_API_TOKEN" ]; then
echo "Missing ANACONDA_TOKEN secret; cannot publish conda package."
exit 1
fi
- name: Publish conda package to tpeulen channel
env:
ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }}
run: |
python -m pip install --upgrade anaconda-client
shopt -s nullglob globstar
files=(dist/conda/**/labellib-*.tar.bz2 dist/conda/**/labellib-*.conda)
if [ ${#files[@]} -eq 0 ]; then
echo "No labellib conda packages found to upload."
exit 1
fi
if [ "$GITHUB_EVENT_NAME" = "push" ] && [ "$GITHUB_REF" = "refs/heads/dev" ]; then
echo "Uploading dev build with conda label: dev"
anaconda --site anaconda.org upload -u tpeulen --skip-existing --label dev "${files[@]}"
else
echo "Uploading release build without extra conda label"
anaconda --site anaconda.org upload -u tpeulen --skip-existing "${files[@]}"
fi
publish_testpypi:
name: Publish to TestPyPI
needs: [build_wheels, test]
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: Validate TestPyPI token is configured
env:
TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }}
run: |
if [ -z "$TEST_PYPI_TOKEN" ]; then
echo "Missing TEST_PYPI_TOKEN secret; cannot publish to TestPyPI."
exit 1
fi
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
packages-dir: dist
skip-existing: true
attestations: false
user: __token__
password: ${{ secrets.TEST_PYPI_TOKEN }}
publish_pypi:
name: Publish to PyPI
needs: [build_wheels, test]
if: github.event_name == 'release' && github.event.release.target_commitish == 'main'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: Validate PyPI token is configured
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
if [ -z "$PYPI_TOKEN" ]; then
echo "Missing PYPI_TOKEN secret; cannot publish to PyPI."
exit 1
fi
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true
attestations: false
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
attach_release_assets:
name: Attach wheel and conda artifacts to release
needs: [build_wheels, build_conda, test]
if: github.event_name == 'release' && github.event.release.target_commitish == 'main'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download wheel artifacts
uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: dist/wheels
merge-multiple: true
- name: Download conda artifacts
uses: actions/download-artifact@v4
with:
pattern: conda-*
path: dist/conda
merge-multiple: true
- name: Show collected release artifacts
shell: bash
run: |
shopt -s nullglob globstar
files=(dist/wheels/*.whl dist/conda/**/labellib-*.tar.bz2 dist/conda/**/labellib-*.conda)
if [ ${#files[@]} -eq 0 ]; then
echo "No wheel or conda artifacts found to attach to release."
exit 1
fi
printf '%s\n' "${files[@]}"
- name: Upload artifacts to GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.release.tag_name }}
files: |
dist/wheels/*.whl
dist/conda/**/labellib-*.conda
fail_on_unmatched_files: false