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.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-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}" \ --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-models-standard ${RELEASE_VERSION}" \ --notes "Precompiled standard Radixor model 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: [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