Skip to content

chore: release v0.10.0 #32

chore: release v0.10.0

chore: release v0.10.0 #32

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*'
# Default to read-only. Only the `release` job that creates the GitHub Release
# is granted `contents: write`, and only the two publish jobs are granted
# `id-token: write` for PyPI Trusted Publishing. The build jobs need neither.
permissions:
contents: read
jobs:
# ── Fail before anything is built if the tag and the declared versions
# disagree. PyPI uploads cannot be replaced, so this is cheap insurance:
# a wrong version reaching the index is permanent. ──────────────────
check-version:
name: Tag matches declared versions
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# Pinned rather than relying on the runner's default python: the parse
# below needs tomllib (3.11+), and the image's `python3` is not a
# guarantee. Reads the version from pyproject.toml's requires-python.
- uses: actions/setup-python@v7
with:
python-version-file: "pyproject.toml"
- name: Compare tag against pyproject.toml and rust/Cargo.toml
run: |
set -euo pipefail
tag_version="${GITHUB_REF_NAME#v}"
read -r leech core < <(python -c "
import tomllib
leech = tomllib.load(open('pyproject.toml', 'rb'))['project']['version']
core = tomllib.load(open('rust/Cargo.toml', 'rb'))['package']['version']
print(leech, core)
")
echo "tag=${tag_version} leech=${leech} leech_core=${core}"
if [ "$tag_version" != "$leech" ] || [ "$tag_version" != "$core" ]; then
echo "::error::version mismatch — tag ${tag_version}, pyproject.toml ${leech}, rust/Cargo.toml ${core}. All three must agree; see .claude/commands/release.md."
exit 1
fi
# ── Run the test suite at the tagged revision. `ci.yml` triggers on pushes
# to main and on PRs, so a tag push is otherwise unverified. Mirrors
# ci.yml's `test` job. ──────────────────────────────────────────────
test:
name: Test
needs: check-version
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v10.0.1
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version-file: "pyproject.toml"
- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- name: Install dependencies
env:
# GPU-less runner: CPU-only torch, as in ci.yml.
UV_TORCH_BACKEND: cpu
run: |
uv venv
uv pip install --torch-backend=cpu -e ".[test,rust,pod5]" \
|| uv pip install --torch-backend=cpu -e ".[test,pod5]"
- name: Run tests
env:
UV_TORCH_BACKEND: cpu
run: uv run --no-sync pytest
# ── Build leech-core (Rust extension) platform wheels ──────────────
build-rust-wheels:
name: Build leech-core (${{ matrix.target }})
needs: check-version
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.12'
# No --find-interpreter: the crate builds against the stable ABI
# (pyo3 `abi3-py312`), so maturin emits a single cp312-abi3 wheel per
# target that loads on CPython 3.12 and every later 3.x.
- name: Build wheels
uses: PyO3/maturin-action@v1.51.0
with:
target: ${{ matrix.target }}
args: --release --out dist
manylinux: auto
working-directory: rust
- uses: actions/upload-artifact@v7
with:
name: wheels-rust-${{ matrix.target }}
path: rust/dist/*.whl
# ── Build leech-core sdist ─────────────────────────────────────────
build-rust-sdist:
name: Build leech-core sdist
needs: check-version
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Build sdist
uses: PyO3/maturin-action@v1.51.0
with:
command: sdist
args: --out dist
working-directory: rust
- uses: actions/upload-artifact@v7
with:
name: wheels-rust-sdist
path: rust/dist/*.tar.gz
# ── Build leech (pure Python) sdist + wheel ────────────────────────
build-python:
name: Build leech Python package
needs: check-version
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v10.0.1
- name: Build sdist and wheel
run: uv build --out-dir dist
- uses: actions/upload-artifact@v7
with:
name: wheels-python
path: dist/*
# ── Create GitHub Release with all artifacts ───────────────────────
release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [build-rust-wheels, build-rust-sdist, build-python]
timeout-minutes: 15
# Only this job needs write — it creates the GitHub Release.
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: List release artifacts
run: ls -lh dist/
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Check if pre-release
id: check_prerelease
run: |
VERSION="${{ steps.get_version.outputs.version }}"
if [[ "$VERSION" == *"-alpha"* ]] || [[ "$VERSION" == *"-beta"* ]] || [[ "$VERSION" == *"-rc"* ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Extract changelog for this version
id: changelog
run: |
VERSION="${{ steps.get_version.outputs.version }}"
CHANGELOG_CONTENT=$(awk -v ver="$VERSION" '
/^## \[/ {
if (found) exit
if ($0 ~ "\\[" ver "\\]") found=1
next
}
found { print }
' CHANGELOG.md)
if [ -z "$CHANGELOG_CONTENT" ]; then
CHANGELOG_CONTENT=$(awk -v ver="$VERSION" '
/^## / {
if (found) exit
if ($0 ~ ver) found=1
next
}
found { print }
' CHANGELOG.md)
fi
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "$CHANGELOG_CONTENT" > changelog_extract.md
echo "found=true" >> $GITHUB_OUTPUT
else
echo "found=false" >> $GITHUB_OUTPUT
touch changelog_extract.md
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
name: Release ${{ steps.get_version.outputs.tag }}
body_path: ${{ steps.changelog.outputs.found == 'true' && 'changelog_extract.md' || '' }}
prerelease: ${{ steps.check_prerelease.outputs.prerelease }}
draft: false
generate_release_notes: true
files: dist/*
- name: Summary
run: |
echo "## Release Created! 🚀" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag:** ${{ steps.get_version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Pre-release:** ${{ steps.check_prerelease.outputs.prerelease }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Artifacts" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
ls -1 dist/ >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
# ---------------------------------------------------------------------------
# PyPI. Two jobs, one per distribution, so a leech-core upload failure never
# blocks `leech` (and vice versa) and each downloads an unambiguous artifact
# set. Both are independent of the `release` job above, so a PyPI outage does
# not cost the GitHub Release.
#
# Trusted Publishing (OIDC) — no API token, no repo secret. Each requires a
# PyPI publisher for repo rnabioco/leech, workflow release.yml. See
# .claude/commands/release.md.
#
# The two environments MUST differ, and that is not cosmetic. PyPI identifies
# a publisher by (owner, repo, workflow, environment) and enforces a unique
# constraint on exactly that tuple -- project name is not part of it. Two
# packages released from one workflow under one environment name therefore
# collide: registering the second pending publisher fails with "A pending
# trusted publisher matching this configuration has already been registered
# for a different project name". The environment is the only field left to
# tell them apart, so it carries the package name.
# ---------------------------------------------------------------------------
publish-pypi-core:
name: Publish leech-core to PyPI
needs: [test, build-rust-wheels, build-rust-sdist]
runs-on: ubuntu-latest
timeout-minutes: 15
environment: pypi-leech-core
permissions:
id-token: write
steps:
- name: Download leech-core wheels and sdist
uses: actions/download-artifact@v8
with:
pattern: wheels-rust-*
path: dist
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
publish-pypi-leech:
name: Publish leech to PyPI
needs: [test, build-python]
runs-on: ubuntu-latest
timeout-minutes: 15
environment: pypi-leech
permissions:
id-token: write
steps:
- name: Download leech wheel and sdist
uses: actions/download-artifact@v8
with:
pattern: wheels-python
path: dist
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist