feat(python): add native distribution and release infrastructure
- add the Rust-backed Python API with PyStemmer compatibility - distribute standard compiled models as a separate Python package - generate model artifacts during builds instead of storing them in Git - add GitHub release and Pages-backed package index workflows - add Python tests, benchmarks, documentation, and Gradle integration - refresh the documentation site, branding, and language benchmarks
This commit is contained in:
52
.github/RELEASING-PYTHON.md
vendored
Normal file
52
.github/RELEASING-PYTHON.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# Releasing the Python distributions
|
||||
|
||||
This maintainer-only checklist is intentionally outside the public MkDocs site.
|
||||
|
||||
The repository descriptors use `0.0.0` as a non-release placeholder. Release
|
||||
workflows create isolated staging trees and inject the stable version selected
|
||||
by the tag. Never commit a release-number rewrite of the descriptors.
|
||||
Standard `.rxc` resources are also generated in that staging tree from the
|
||||
canonical model sources. The workflow compiles every model twice and rejects
|
||||
non-deterministic output; generated payload must never be added to Git.
|
||||
|
||||
## Validate without publishing
|
||||
|
||||
Run **Python Standard Models Release** manually with version `1.0.0`, then run
|
||||
**Python Native Release** with version `4.1.0`. `workflow_dispatch` validates
|
||||
artifacts but cannot publish. The native run must pass Linux x86-64, Linux
|
||||
ARM64, macOS universal2, and Windows x86-64.
|
||||
|
||||
For the Linux paths, maintainers can use `act` with rootless Podman and the
|
||||
event files under `.github/act/`. Do not pass production secrets to `act`.
|
||||
|
||||
## Publish in dependency order
|
||||
|
||||
Both tags must point to a commit already contained in `main`.
|
||||
|
||||
```bash
|
||||
git tag -a 'python-models-standard@1.0.0' \
|
||||
-m 'Python standard models 1.0.0'
|
||||
git push origin 'python-models-standard@1.0.0'
|
||||
```
|
||||
|
||||
Wait until the models workflow has published its GitHub Release and Pages
|
||||
index entry. Then publish the native distribution:
|
||||
|
||||
```bash
|
||||
git tag -a 'python@4.1.0' -m 'Python Radixor 4.1.0'
|
||||
git push origin 'python@4.1.0'
|
||||
```
|
||||
|
||||
Do not push both tags together: native publication requires the standard-model
|
||||
release to exist first.
|
||||
|
||||
## Artifact identity
|
||||
|
||||
Python releases use `SHA256SUMS` and GitHub keyless build-provenance
|
||||
attestations. They do not use the Java Maven OpenPGP key. Verify a downloaded
|
||||
artifact with:
|
||||
|
||||
```bash
|
||||
sha256sum --check SHA256SUMS
|
||||
gh attestation verify <artifact> --repo leogalambos/Radixor
|
||||
```
|
||||
5
.github/act/python-models-standard.json
vendored
Normal file
5
.github/act/python-models-standard.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"inputs": {
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
5
.github/act/python-native.json
vendored
Normal file
5
.github/act/python-native.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"inputs": {
|
||||
"version": "4.1.0"
|
||||
}
|
||||
}
|
||||
95
.github/workflows/pages.yml
vendored
95
.github/workflows/pages.yml
vendored
@@ -26,42 +26,43 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: pages-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: github-python-pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish-pages:
|
||||
name: Publish static reports
|
||||
build-pages:
|
||||
name: Build static reports
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out source repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@v4
|
||||
uses: gradle/actions/wrapper-validation@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d # v4
|
||||
|
||||
- name: Set up Temurin JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
|
||||
- name: Set up Gradle caching and instrumentation
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
uses: gradle/actions/setup-gradle@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d # v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: '3.14.6'
|
||||
|
||||
- name: Install MkDocs Material
|
||||
run: python -m pip install --upgrade pip mkdocs-material
|
||||
run: python -m pip install --disable-pip-version-check mkdocs-material==9.7.6
|
||||
|
||||
- name: Verify reproducibility inputs
|
||||
shell: bash
|
||||
@@ -326,18 +327,80 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdocs build --strict --config-file build/mkdocs/mkdocs.yml
|
||||
rsync -a --delete --exclude '.git' --exclude '.git/' --exclude 'builds/' build/mkdocs-site/ .gh-pages/
|
||||
rsync -a --delete --exclude '.git' --exclude '.git/' --exclude 'builds/' --exclude 'python/' build/mkdocs-site/ .gh-pages/
|
||||
mkdir -p .gh-pages/builds
|
||||
cp build/mkdocs-site/builds/index.html .gh-pages/builds/index.html
|
||||
cat > .gh-pages/.nojekyll <<EOF
|
||||
EOF
|
||||
|
||||
- name: Commit and push gh-pages
|
||||
- name: Prepare read-only publication candidate
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd .gh-pages
|
||||
rm -rf .gh-pages/python
|
||||
rm -f .gh-pages/.git
|
||||
if find .gh-pages -type l -print -quit | grep -q .; then
|
||||
echo 'Publication candidate contains a symbolic link.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload static-site candidate
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: pages-site-${{ github.run_id }}
|
||||
path: .gh-pages
|
||||
if-no-files-found: error
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
publish-pages:
|
||||
name: Publish static reports
|
||||
needs: build-pages
|
||||
runs-on: ubuntu-latest
|
||||
environment: python-github-pages
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Check out repository for publication
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: pages
|
||||
|
||||
- name: Select or initialize gh-pages
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd pages
|
||||
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 checkout -B gh-pages origin/gh-pages
|
||||
else
|
||||
git checkout --orphan gh-pages
|
||||
git rm -rf .
|
||||
fi
|
||||
|
||||
- name: Download static-site candidate
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: pages-site-${{ github.run_id }}
|
||||
path: candidate
|
||||
|
||||
- name: Validate and publish static site
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test ! -e candidate/python
|
||||
test ! -e candidate/.git
|
||||
if find candidate -type l -print -quit | grep -q .; then
|
||||
echo 'Publication candidate contains a symbolic link.' >&2
|
||||
exit 1
|
||||
fi
|
||||
rsync -a --delete \
|
||||
--exclude '.git' --exclude '.git/' \
|
||||
--exclude 'python' --exclude 'python/' candidate/ pages/
|
||||
cd pages
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
@@ -348,4 +411,4 @@ jobs:
|
||||
fi
|
||||
|
||||
git commit -m "Publish reports for run ${GITHUB_RUN_NUMBER}"
|
||||
git push origin gh-pages
|
||||
git push origin HEAD:gh-pages
|
||||
|
||||
235
.github/workflows/python-models-standard-release.yml
vendored
Normal file
235
.github/workflows/python-models-standard-release.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
name: Python Standard Models Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'python-models-standard@*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Stable distribution version to validate without publishing
|
||||
required: true
|
||||
default: '1.0.0'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: github-python-pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and verify standard models
|
||||
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.13'
|
||||
|
||||
- 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-models-standard@${REQUESTED_VERSION}"
|
||||
fi
|
||||
eval "$(./tools/parse-python-release-tag.sh "${tag}")"
|
||||
[[ "${PYTHON_DISTRIBUTION}" == 'radixor-models-standard' ]]
|
||||
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 build tools
|
||||
run: >-
|
||||
python -m pip install --disable-pip-version-check
|
||||
maturin==1.14.1 setuptools==80.9.0 wheel==0.45.1
|
||||
|
||||
- name: Compile models and build isolated release tree
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf build/python-release
|
||||
mkdir -p build/python-release/compiler-wheel build/python-release/compiler-runtime
|
||||
maturin build --release --locked --manifest-path python/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-standard \
|
||||
--distribution-version '${{ steps.release.outputs.version }}'
|
||||
python python/scripts/build_standard_distribution.py \
|
||||
--project build/python-release/models-standard \
|
||||
--outdir build/python-release/artifacts
|
||||
|
||||
- name: Verify archives and offline installation
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python python/scripts/verify_distributions.py \
|
||||
--standard-dir build/python-release/artifacts \
|
||||
--standard-version '${{ steps.release.outputs.version }}'
|
||||
python python/scripts/assemble_release.py \
|
||||
models-standard '${{ steps.release.outputs.version }}' \
|
||||
build/python-release/artifacts build/python-release/release
|
||||
python -m venv build/python-release/venv
|
||||
build/python-release/venv/bin/python -m pip install \
|
||||
--no-index --find-links build/python-release/release \
|
||||
radixor-models-standard
|
||||
build/python-release/venv/bin/python -c \
|
||||
"from importlib import resources; assert resources.files('radixor_models_standard').joinpath('manifest.json').is_file()"
|
||||
|
||||
- name: Prepare complete PEP 503 index candidate
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
index_root='build/python-release/index/python/simple'
|
||||
if [[ "${ACT:-false}" != 'true' ]] && 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-models-standard \
|
||||
--version '${{ steps.release.outputs.version }}' \
|
||||
--tag '${{ steps.release.outputs.tag }}' \
|
||||
--artifacts build/python-release/release
|
||||
|
||||
- name: Upload verified release candidate
|
||||
if: ${{ env.ACT != 'true' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: python-models-standard-release-${{ steps.release.outputs.version }}
|
||||
path: build/python-release/release/*
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload package-index candidate
|
||||
if: ${{ env.ACT != 'true' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: python-models-standard-index-${{ steps.release.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: build
|
||||
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-models-standard-release-${{ needs.build.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: 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.build.outputs.tag }}
|
||||
RELEASE_VERSION: ${{ needs.build.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if gh release view "${RELEASE_TAG}" >/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 \
|
||||
--verify-tag --draft --title "radixor-models-standard ${RELEASE_VERSION}" \
|
||||
--notes "Precompiled standard Radixor model distribution ${RELEASE_VERSION}."
|
||||
gh release edit "${RELEASE_TAG}" --draft=false
|
||||
|
||||
publish-index:
|
||||
name: Publish Python package index
|
||||
if: github.event_name == 'push'
|
||||
needs: [build, 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-models-standard-index-${{ needs.build.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-models-standard ${{ needs.build.outputs.version }}'
|
||||
git push origin HEAD:gh-pages
|
||||
452
.github/workflows/python-release.yml
vendored
Normal file
452
.github/workflows/python-release.yml
vendored
Normal file
@@ -0,0 +1,452 @@
|
||||
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.13'
|
||||
|
||||
- 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.13'
|
||||
|
||||
- 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.13'
|
||||
|
||||
- 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.13'
|
||||
|
||||
- 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' --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}" >/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 \
|
||||
--verify-tag --draft --title "radixor ${RELEASE_VERSION}" \
|
||||
--notes "Native Rust/Python Radixor distribution ${RELEASE_VERSION}."
|
||||
gh release edit "${RELEASE_TAG}" --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
|
||||
Reference in New Issue
Block a user