Files
Radixor/.github/workflows/python-release.yml

457 lines
17 KiB
YAML

name: Python Native Release
on:
push:
tags:
- 'python@*'
workflow_dispatch:
inputs:
version:
description: Stable distribution version to validate without publishing
required: true
default: '4.1.0'
type: string
permissions:
contents: read
concurrency:
group: github-python-pages
cancel-in-progress: false
jobs:
prepare:
name: Prepare versioned sources and sdist
runs-on: ubuntu-latest
outputs:
version: ${{ steps.release.outputs.version }}
tag: ${{ steps.release.outputs.tag }}
steps:
- name: Check out repository
if: ${{ env.ACT != 'true' }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.10'
- name: Install pinned Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
with:
toolchain: 1.88.0
- name: Select and validate release
id: release
shell: bash
env:
REQUESTED_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == 'push' ]]; then
tag="${GITHUB_REF_NAME}"
else
tag="python@${REQUESTED_VERSION}"
fi
eval "$(./tools/parse-python-release-tag.sh "${tag}")"
[[ "${PYTHON_DISTRIBUTION}" == 'radixor' ]]
if [[ "${GITHUB_EVENT_NAME}" == 'push' ]]; then
[[ "$(git rev-parse "${tag}^{commit}")" == "${GITHUB_SHA}" ]]
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
fi
printf 'version=%s\ntag=%s\n' "${PYTHON_VERSION}" "${tag}" >> "${GITHUB_OUTPUT}"
- name: Install pinned source-build tools
run: >-
python -m pip install --disable-pip-version-check
maturin==1.14.1 setuptools==80.9.0 wheel==0.45.1
- name: Materialize versioned native source
shell: bash
run: |
set -euo pipefail
rm -rf build/python-release
python python/scripts/prepare_release_tree.py \
native '${{ steps.release.outputs.version }}' \
build/python-release/native-source
- name: Build and verify source distributions
shell: bash
run: |
set -euo pipefail
mkdir -p build/python-release/compiler-wheel build/python-release/compiler-runtime \
build/python-release/native-sdist build/python-release/models
maturin build --release --locked \
--manifest-path build/python-release/native-source/Cargo.toml \
--out build/python-release/compiler-wheel
python -c "from pathlib import Path; import zipfile; wheels=list(Path('build/python-release/compiler-wheel').glob('*.whl')); assert len(wheels) == 1; zipfile.ZipFile(wheels[0]).extractall('build/python-release/compiler-runtime')"
PYTHONPATH=build/python-release/compiler-runtime \
python python/scripts/build_standard_models.py \
--project build/python-release/models-source \
--distribution-version 1.0.0
maturin sdist --manifest-path build/python-release/native-source/Cargo.toml \
--out build/python-release/native-sdist
python python/scripts/build_standard_distribution.py \
--project build/python-release/models-source \
--outdir build/python-release/models
python python/scripts/verify_distributions.py \
--standard-dir build/python-release/models \
--standard-version 1.0.0
PYTHONPATH=python/scripts python -c \
"from pathlib import Path; from verify_distributions import _verify_main_sdist; _verify_main_sdist(next(Path('build/python-release/native-sdist').glob('*.tar.gz')), '${{ steps.release.outputs.version }}')"
- name: Upload versioned native source
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-source-${{ steps.release.outputs.version }}
path: |
build/python-release/native-source
!build/python-release/native-source/target/**
if-no-files-found: error
retention-days: 1
- name: Upload native sdist
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-sdist-${{ steps.release.outputs.version }}
path: build/python-release/native-sdist/*.tar.gz
if-no-files-found: error
retention-days: 14
- name: Upload verified model fixture
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-model-fixture-${{ steps.release.outputs.version }}
path: build/python-release/models/*
if-no-files-found: error
retention-days: 1
build-linux-x86-64:
name: Build Linux x86-64 wheel
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.10'
- name: Install pinned Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
with:
toolchain: 1.88.0
- name: Download versioned native source
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: native-source-${{ needs.prepare.outputs.version }}
path: build/python-release/native-source
- name: Download model fixture
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: native-model-fixture-${{ needs.prepare.outputs.version }}
path: build/python-release/models
- name: Select Linux build isolation
id: linux-isolation
shell: bash
run: |
if [[ "${ACT:-}" == 'true' ]]; then
echo 'manylinux=off' >> "${GITHUB_OUTPUT}"
echo 'container=' >> "${GITHUB_OUTPUT}"
else
echo 'manylinux=auto' >> "${GITHUB_OUTPUT}"
echo 'container=quay.io/pypa/manylinux2014_x86_64@sha256:0a42cb7e5f4ba6bbfb8d0a86d1aab0c8876ba9c3be16bd99360ae42bf010ec77' >> "${GITHUB_OUTPUT}"
fi
- name: Build manylinux wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
with:
command: build
target: x86_64
manylinux: ${{ steps.linux-isolation.outputs.manylinux }}
container: ${{ steps.linux-isolation.outputs.container }}
maturin-version: v1.14.1
rust-toolchain: 1.88.0
working-directory: build/python-release/native-source
args: --release --locked --out ../wheel
- name: Smoke-test wheel with standard models
shell: bash
run: |
set -euo pipefail
python -m pip install --no-index \
--find-links build/python-release/wheel \
--find-links build/python-release/models radixor
python -c "from radixor import Stemmer; assert Stemmer('en').stem('running') == 'run'"
- name: Upload Linux x86-64 wheel
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-wheel-linux-x86-64-${{ needs.prepare.outputs.version }}
path: build/python-release/wheel/*.whl
if-no-files-found: error
retention-days: 14
build-platform-wheels:
name: Build ${{ matrix.name }} wheel
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- name: Linux aarch64
os: ubuntu-24.04-arm
target: aarch64
manylinux: auto
container: quay.io/pypa/manylinux2014_aarch64@sha256:63bfa74be47f0277e998cb7c1b571b27664ac848bb356b0f4588438f930285dd
artifact: linux-aarch64
- name: macOS universal2
os: macos-14
target: universal2-apple-darwin
manylinux: 'off'
container: ''
artifact: macos-universal2
- name: Windows x86-64
os: windows-2022
target: x86_64-pc-windows-msvc
manylinux: 'off'
container: ''
artifact: windows-x86-64
runs-on: ${{ matrix.os }}
steps:
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.10'
- name: Install pinned Rust toolchain
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
with:
toolchain: 1.88.0
- name: Download versioned native source
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: native-source-${{ needs.prepare.outputs.version }}
path: build/python-release/native-source
- name: Download model fixture
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: native-model-fixture-${{ needs.prepare.outputs.version }}
path: build/python-release/models
- name: Build platform wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
with:
command: build
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
container: ${{ matrix.container }}
maturin-version: v1.14.1
rust-toolchain: 1.88.0
working-directory: build/python-release/native-source
args: --release --locked --out ../wheel
- name: Smoke-test wheel with standard models
shell: bash
run: |
set -euo pipefail
python -m pip install --no-index \
--find-links build/python-release/wheel \
--find-links build/python-release/models radixor
python -c "from radixor import Stemmer; assert Stemmer('en').stem('running') == 'run'"
- name: Upload platform wheel
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: native-wheel-${{ matrix.artifact }}-${{ needs.prepare.outputs.version }}
path: build/python-release/wheel/*.whl
if-no-files-found: error
retention-days: 14
assemble:
name: Assemble verified release
needs: [prepare, build-linux-x86-64, build-platform-wheels]
runs-on: ubuntu-latest
steps:
- name: Check out repository
if: ${{ env.ACT != 'true' }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.10'
- name: Download native artifacts only
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: native-wheel-*
path: build/python-release/artifacts
merge-multiple: true
- name: Download native sdist
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: native-sdist-${{ needs.prepare.outputs.version }}
path: build/python-release/artifacts
- name: Enforce release allowlist and checksums
run: >-
python python/scripts/assemble_release.py native
'${{ needs.prepare.outputs.version }}'
build/python-release/artifacts build/python-release/release
- name: Prepare complete PEP 503 index candidate
shell: bash
run: |
set -euo pipefail
index_root='build/python-release/index/python/simple'
if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
git fetch origin gh-pages:refs/remotes/origin/gh-pages
git worktree add --detach build/python-release/pages origin/gh-pages
mkdir -p "$(dirname "${index_root}")"
if [[ -d build/python-release/pages/python/simple ]]; then
cp -R build/python-release/pages/python/simple "${index_root}"
fi
fi
python python/scripts/update_simple_index.py \
--root "${index_root}" \
--repository "${GITHUB_REPOSITORY}" \
--package radixor \
--version '${{ needs.prepare.outputs.version }}' \
--tag '${{ needs.prepare.outputs.tag }}' \
--artifacts build/python-release/release
- name: Upload verified release candidate
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: python-native-release-${{ needs.prepare.outputs.version }}
path: build/python-release/release/*
if-no-files-found: error
retention-days: 14
- name: Upload package-index candidate
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: python-native-index-${{ needs.prepare.outputs.version }}
path: build/python-release/index/python/simple
if-no-files-found: error
retention-days: 14
publish:
name: Publish immutable GitHub Release
if: github.event_name == 'push'
needs: [prepare, assemble]
runs-on: ubuntu-latest
environment: python-github-release
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Download verified release candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-native-release-${{ needs.prepare.outputs.version }}
path: release
- name: Verify release inventory
shell: bash
run: |
set -euo pipefail
cd release
sha256sum --check SHA256SUMS
awk '{print $2}' SHA256SUMS | LC_ALL=C sort > expected-files
find . -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' \) \
-printf '%f\n' | LC_ALL=C sort > actual-files
diff -u expected-files actual-files
rm expected-files actual-files
- name: Require published standard models
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
[[ "$(gh release view 'python-models-standard@1.0.0' \
--repo "${GITHUB_REPOSITORY}" --json isDraft --jq '.isDraft')" == 'false' ]]
- name: Attest package artifacts
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with:
subject-path: |
release/*.whl
release/*.tar.gz
- name: Create and publish draft release exactly once
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
if gh release view "${RELEASE_TAG}" \
--repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "Release already exists; refusing to replace its assets: ${RELEASE_TAG}" >&2
exit 1
fi
gh release create "${RELEASE_TAG}" \
release/*.whl release/*.tar.gz release/SHA256SUMS \
--repo "${GITHUB_REPOSITORY}" \
--verify-tag --draft --title "radixor ${RELEASE_VERSION}" \
--notes "Native Rust/Python Radixor distribution ${RELEASE_VERSION}."
gh release edit "${RELEASE_TAG}" \
--repo "${GITHUB_REPOSITORY}" --draft=false
publish-index:
name: Publish Python package index
if: github.event_name == 'push'
needs: [prepare, assemble, publish]
runs-on: ubuntu-latest
environment: python-github-pages
permissions:
contents: write
steps:
- name: Check out gh-pages only
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: gh-pages
path: pages
- name: Download validated index candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-native-index-${{ needs.prepare.outputs.version }}
path: candidate
- name: Commit package index
shell: bash
run: |
set -euo pipefail
mkdir -p pages/python/simple
rsync -a --delete candidate/ pages/python/simple/
cd pages
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add python/simple
git diff --cached --quiet && exit 0
git commit -m 'Index radixor ${{ needs.prepare.outputs.version }}'
git push origin HEAD:gh-pages