Skip to content

Release

Release #58

name: Release
# Triggers on version tags: v9.0.0, v9.1.0, etc.
#
# Release checklist (run before pushing the tag):
# 1. All tests green on main.
# 2. Version bumped in setup.py and metbit/__init__.py.
# 3. CHANGELOG.md updated.
# 4. git tag v9.0.0 && git push origin v9.0.0
#
# PyPI trusted publishing must be configured at:
# https://pypi.org/manage/project/metbit/settings/publishing/
# with workflow name "python-publish.yml" and environment "pypi".
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g. v9.0.0)"
required: true
type: string
# Normalize the release tag across both trigger types.
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
permissions:
contents: write # create GitHub Release + upload assets
id-token: write # OIDC trusted publishing to PyPI
jobs:
# ---------------------------------------------------------------------------
# 1. Gate: full functional test suite must be green on all supported Pythons
# ---------------------------------------------------------------------------
test:
name: "Test – Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install build tools + dev dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements-dev.txt
- name: Build and install (including C extension)
run: pip install -e . --no-build-isolation
- name: Run test suite (excluding slow/perf)
run: |
pytest \
-m "not slow and not perf" \
--junit-xml=test-results.xml \
--cov-report=xml:coverage.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-py${{ matrix.python-version }}
path: test-results.xml
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-py${{ matrix.python-version }}
path: coverage.xml
# ---------------------------------------------------------------------------
# 2. Source distribution
# ---------------------------------------------------------------------------
build-sdist:
name: Build sdist
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build source distribution
run: |
pip install build
python -m build --sdist
- uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz
# ---------------------------------------------------------------------------
# 3. Binary wheels via cibuildwheel
# Linux: manylinux_2_28 x86_64 + aarch64 (QEMU), musllinux x86_64
# macOS: x86_64 + arm64
# Windows: AMD64
# ---------------------------------------------------------------------------
build-wheels:
name: "Wheels – ${{ matrix.os }}"
needs: test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Set up QEMU (Linux aarch64 cross-compilation)
if: matrix.os == 'ubuntu-latest'
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install cibuildwheel
run: pip install cibuildwheel==2.21.3
- name: Build wheels
env:
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*"
CIBW_SKIP: "*-win32 *-manylinux_i686 pp*"
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
CIBW_ARCHS_LINUX: "x86_64 aarch64"
CIBW_ARCHS_MACOS: "x86_64 arm64"
CIBW_ARCHS_WINDOWS: "AMD64"
CIBW_ENVIRONMENT_LINUX: "CFLAGS='-fopenmp' LDFLAGS='-lgomp'"
CIBW_TEST_COMMAND: >
python -c "
import metbit;
assert metbit.__version__, 'version missing';
from metbit._native import pearson_columns, backend_info;
import numpy as np;
r = pearson_columns(np.eye(5), anchor_index=0);
assert abs(r[0] - 1.0) < 1e-12, r[0];
print('OK - backend:', backend_info())
"
CIBW_TEST_REQUIRES: "numpy scipy scikit-learn"
CIBW_BUILD_VERBOSITY: 1
run: cibuildwheel --output-dir wheelhouse
- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}
path: wheelhouse/*.whl
# ---------------------------------------------------------------------------
# 4. Verify all artifacts: integrity check + security scan
# ---------------------------------------------------------------------------
verify:
name: Verify artifacts
needs: [build-sdist, build-wheels]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all built artifacts
uses: actions/download-artifact@v4
with:
pattern: "sdist wheels-*"
path: dist/
merge-multiple: true
- name: List artifacts
run: ls -lh dist/
- name: Integrity check (twine)
run: |
pip install twine
twine check dist/*
- name: Security scan (Trivy, warn only)
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: fs
scan-ref: .
severity: "CRITICAL,HIGH"
exit-code: "0"
# ---------------------------------------------------------------------------
# 5. Publish to PyPI (OIDC trusted publishing - no API token needed)
# ---------------------------------------------------------------------------
publish-pypi:
name: Publish to PyPI
needs: verify
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/metbit/${{ env.RELEASE_TAG }}
steps:
- name: Download all built artifacts
uses: actions/download-artifact@v4
with:
pattern: "sdist wheels-*"
path: dist/
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
skip-existing: false
# ---------------------------------------------------------------------------
# 6. Create GitHub Release and attach all artifacts
# ---------------------------------------------------------------------------
github-release:
name: GitHub Release
needs: publish-pypi
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract changelog section for this version
id: changelog
shell: bash
run: |
VERSION="${RELEASE_TAG#v}"
NOTES=$(awk "/^## \[${VERSION}\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md)
{
echo "notes<<EOF"
echo "$NOTES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
env:
RELEASE_TAG: ${{ env.RELEASE_TAG }}
- name: Download all built artifacts
uses: actions/download-artifact@v4
with:
pattern: "sdist wheels-*"
path: release-assets/
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
name: "metbit ${{ env.RELEASE_TAG }}"
body: ${{ steps.changelog.outputs.notes }}
draft: false
prerelease: false
files: release-assets/*
generate_release_notes: true