name: Publish Release
on:
workflow_dispatch:
inputs:
version:
description: "Version to release, without leading v (for example: 0.5.1)"
required: true
type: string
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
prepare-release:
runs-on: ubuntu-24.04
permissions:
contents: write
outputs:
version: ${{ steps.release.outputs.version }}
tag: ${{ steps.release.outputs.tag }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump versions, commit, and tag
id: release
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Version must be in X.Y.Z format, got: $VERSION" >&2
exit 1
fi
TAG="v${VERSION}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists" >&2
exit 1
fi
CURRENT_VERSION=$(sed -n '/^\[workspace\.package\]/,/^\[/{s/^version = "\(.*\)"/\1/p}' Cargo.toml)
sed -i '/^\[workspace\.package\]/,/^\[/{s/^version = ".*"/version = "'"$VERSION"'"/}' Cargo.toml
sed -i '/^\[workspace\.dependencies\]/,/^\[/{s/version = "'"$CURRENT_VERSION"'"/version = "'"$VERSION"'"/g}' Cargo.toml
sed -i '/^\[project\]/,/^\[/{s/^version = ".*"/version = "'"$VERSION"'"/}' pyproject.toml
declare -A CARGO_MANIFESTS=()
CARGO_MANIFESTS["./Cargo.toml"]=1
while IFS= read -r -d '' manifest; do
dir="$(dirname "$manifest")"
if grep -q '^[[:space:]]*\[workspace\]' "$manifest" || [ -f "$dir/Cargo.lock" ]; then
CARGO_MANIFESTS["$manifest"]=1
fi
done < <(
find . \
\( -path './.git' -o -path '*/target' \) -prune -o \
-name Cargo.toml -print0
)
for manifest in "${!CARGO_MANIFESTS[@]}"; do
echo "::group::cargo update --workspace --manifest-path ${manifest}"
cargo update --workspace --manifest-path "$manifest"
echo "::endgroup::"
done
docker run --rm -v "$PWD:/workspace" -w /workspace \
codeberg.org/msrd0/cargo-doc2readme:nightly \
cargo doc2readme --expand-macros
git add Cargo.toml pyproject.toml README.md
while IFS= read -r -d '' lockfile; do
git add "$lockfile"
done < <(
find . \
\( -path './.git' -o -path '*/target' \) -prune -o \
-name Cargo.lock -print0
)
if ! git diff --quiet; then
echo "Unstaged generated changes remain:" >&2
git status --short
exit 1
fi
if git diff --cached --quiet; then
echo "No version changes to commit" >&2
exit 1
fi
git commit -m "chore(release): prepare for ${TAG}"
git tag "$TAG"
git push origin "HEAD:${GITHUB_REF}"
git push origin "$TAG"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
build-python-wheels:
needs: prepare-release
name: Build Python wheels (${{ matrix.artifact }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- artifact: manylinux-x86_64
os: ubuntu-24.04
target: x86_64
manylinux: "2_28"
python_architecture: x64
- artifact: manylinux-x86
os: ubuntu-24.04
target: x86
manylinux: "2_28"
python_architecture: x64
- artifact: manylinux-aarch64
os: ubuntu-24.04
target: aarch64
manylinux: "2_28"
python_architecture: x64
- artifact: manylinux-armv7
os: ubuntu-24.04
target: armv7
manylinux: "2_28"
python_architecture: x64
- artifact: manylinux-s390x
os: ubuntu-24.04
target: s390x
manylinux: "2_28"
python_architecture: x64
- artifact: manylinux-ppc64le
os: ubuntu-24.04
target: ppc64le
manylinux: "2_28"
python_architecture: x64
- artifact: musllinux-x86_64
os: ubuntu-24.04
target: x86_64
manylinux: musllinux_1_2
python_architecture: x64
- artifact: musllinux-x86
os: ubuntu-24.04
target: x86
manylinux: musllinux_1_2
python_architecture: x64
- artifact: musllinux-aarch64
os: ubuntu-24.04
target: aarch64
manylinux: musllinux_1_2
python_architecture: x64
- artifact: musllinux-armv7
os: ubuntu-24.04
target: armv7
manylinux: musllinux_1_2
python_architecture: x64
- artifact: windows-x64
os: windows-latest
target: x64
python_architecture: x64
- artifact: windows-x86
os: windows-latest
target: x86
python_architecture: x86
- artifact: macos-aarch64
os: macos-14
target: aarch64
python_architecture: arm64
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare-release.outputs.tag }}
- uses: actions/setup-python@v6
with:
python-version: "3.x"
architecture: ${{ matrix.python_architecture }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: true
manylinux: ${{ matrix.manylinux }}
- name: Upload wheels
uses: actions/upload-artifact@v6
with:
name: wheels-${{ matrix.artifact }}
path: dist/*.whl
if-no-files-found: error
build-python-sdist:
needs: prepare-release
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare-release.outputs.tag }}
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v6
with:
name: wheels-sdist
path: dist/*.tar.gz
if-no-files-found: error
publish-rust:
needs: prepare-release
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare-release.outputs.tag }}
- name: Install cargo-workspaces
run: cargo install cargo-workspaces --locked
- name: Publish crates
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo workspaces publish --from-git --yes --allow-dirty
release:
runs-on: ubuntu-24.04
needs:
- prepare-release
- build-python-wheels
- build-python-sdist
- publish-rust
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.prepare-release.outputs.tag }}
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
path: dist
merge-multiple: true
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-path: "dist/*"
- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish --trusted-publishing automatic --check-url https://pypi.org/simple/ dist/*
- name: Generate changelog
id: changelog
uses: orhun/git-cliff-action@v4
with:
args: --latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare-release.outputs.tag }}
body: ${{ steps.changelog.outputs.content }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}