From 9439cd1b0e3930520b6b491b3f72e843a4258e05 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sun, 8 Nov 2026 10:27:04 +0000 Subject: [PATCH] ci: install uv and maturin from PyPI when GitHub release downloads fail A local setup-uv action installs the pinned uv from PyPI instead of GitHub releases. Each maturin-action step retries once, and macOS, Windows and source distribution builds then fall back to the same maturin from PyPI. --- .github/actions/setup-uv/action.yml | 58 ++++++++++++ .github/workflows/benchmark-dbt.yml | 2 +- .github/workflows/build-distributions.yml | 99 ++++++++++++++++++++- .github/workflows/compiler-differential.yml | 2 +- .github/workflows/release-performance.yml | 2 +- .github/workflows/release-please.yml | 2 +- .github/workflows/rust-cache.yml | 6 +- .github/workflows/scope-performance.yml | 2 +- .github/workflows/verify.yml | 34 +++---- scripts/maturin_from_pypi.sh | 29 ++++++ 10 files changed, 207 insertions(+), 29 deletions(-) create mode 100644 .github/actions/setup-uv/action.yml create mode 100755 scripts/maturin_from_pypi.sh diff --git a/.github/actions/setup-uv/action.yml b/.github/actions/setup-uv/action.yml new file mode 100644 index 0000000000..04c2391499 --- /dev/null +++ b/.github/actions/setup-uv/action.yml @@ -0,0 +1,58 @@ +name: Set up uv +description: >- + Install a pinned uv from PyPI and optionally cache its package cache. Installing from PyPI + keeps setup working when GitHub release downloads fail. +inputs: + version: + description: Exact uv version to install + required: true + enable-cache: + description: Restore and save the uv cache, keyed on the dependency files + required: false + default: "false" + cache-dependency-glob: + description: Files whose contents key the cache, one path per line + required: false + default: uv.lock +runs: + using: composite + steps: + # The runner's cached Python, left off PATH so jobs keep the Python they set up themselves. + - id: python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + update-environment: false + - name: Install uv from PyPI + shell: bash + env: + PYTHON: ${{ steps.python.outputs.python-path }} + UV_VERSION: ${{ inputs.version }} + run: | + package="${RUNNER_TEMP}/uv-package" + "${PYTHON}" -m pip install --disable-pip-version-check --no-warn-script-location \ + --target "${package}" "uv==${UV_VERSION}" + mkdir -p "${RUNNER_TEMP}/uv-bin" + for executable in "${package}"/bin/uv* "${package}"/Scripts/uv*; do + if [[ -f "${executable}" ]]; then cp "${executable}" "${RUNNER_TEMP}/uv-bin/"; fi + done + echo "${RUNNER_TEMP}/uv-bin" >> "${GITHUB_PATH}" + "${RUNNER_TEMP}/uv-bin/uv" --version + - id: cache-key + if: inputs.enable-cache == 'true' + name: Key the uv cache on the dependency files + shell: bash + env: + FILES: ${{ inputs.cache-dependency-glob }} + UV_VERSION: ${{ inputs.version }} + run: | + echo "UV_CACHE_DIR=${RUNNER_TEMP}/setup-uv-cache" >> "${GITHUB_ENV}" + digest=$(while IFS= read -r file; do + if [[ -n "${file// /}" ]]; then sha256sum "${file// /}"; fi + done <<< "${FILES}" | sha256sum | cut -c1-64) + echo "key=uv-pypi-${RUNNER_OS}-${RUNNER_ARCH}-${UV_VERSION}-${digest}" >> "${GITHUB_OUTPUT}" + - if: inputs.enable-cache == 'true' + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/setup-uv-cache + key: ${{ steps.cache-key.outputs.key }} diff --git a/.github/workflows/benchmark-dbt.yml b/.github/workflows/benchmark-dbt.yml index fde3196e6b..e188995614 100644 --- a/.github/workflows/benchmark-dbt.yml +++ b/.github/workflows/benchmark-dbt.yml @@ -70,7 +70,7 @@ jobs: shell: bash steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" - name: Validate inputs diff --git a/.github/workflows/build-distributions.yml b/.github/workflows/build-distributions.yml index 99770a02e4..e3a6871318 100644 --- a/.github/workflows/build-distributions.yml +++ b/.github/workflows/build-distributions.yml @@ -141,8 +141,13 @@ jobs: run: | npm ci npm run build - - uses: PyO3/maturin-action@v1 + # maturin-action downloads maturin from GitHub releases, which can fail on its own. Each + # build retries once; macOS and Windows then fall back to the same maturin from PyPI. Linux + # builds need the action's manylinux container, so they have no fallback. + - id: extension + uses: PyO3/maturin-action@v1 if: inputs.prebuilt-run-id == '' + continue-on-error: true with: command: build # One platform records a cargo build timing report for build time work. @@ -151,8 +156,33 @@ jobs: ${{ matrix.platform.name == 'linux-x86_64' && '--timings' || '' }} target: ${{ matrix.platform.target }} manylinux: auto - - uses: PyO3/maturin-action@v1 + - id: extension-retry + name: Retry the extension build + uses: PyO3/maturin-action@v1 + if: inputs.prebuilt-run-id == '' && steps.extension.outcome == 'failure' + continue-on-error: ${{ runner.os != 'Linux' }} + with: + command: build + # One platform records a cargo build timing report for build time work. + args: >- + --profile ${{ inputs.cargo-profile }} --out dist --features extension-module + ${{ matrix.platform.name == 'linux-x86_64' && '--timings' || '' }} + target: ${{ matrix.platform.target }} + manylinux: auto + - name: Build the extension with maturin from PyPI + if: >- + inputs.prebuilt-run-id == '' && runner.os != 'Linux' + && steps.extension.outcome == 'failure' && steps.extension-retry.outcome == 'failure' + shell: bash + env: + MATURIN_ARGS: >- + --profile ${{ inputs.cargo-profile }} --out dist --features extension-module + TARGET: ${{ matrix.platform.target }} + run: scripts/maturin_from_pypi.sh build ${MATURIN_ARGS} + - id: cli + uses: PyO3/maturin-action@v1 if: inputs.prebuilt-run-id == '' + continue-on-error: true # On macOS maturin links the extension with these flags for every crate, which changes # their build hash. Passing the same flags here lets sqb reuse the crates the extension # build just compiled instead of compiling them again; sqb has no undefined symbols, and @@ -169,6 +199,37 @@ jobs: ${{ matrix.platform.name == 'linux-x86_64' && '--timings' || '' }} target: ${{ matrix.platform.target }} manylinux: auto + - id: cli-retry + name: Retry the sqb build + uses: PyO3/maturin-action@v1 + if: inputs.prebuilt-run-id == '' && steps.cli.outcome == 'failure' + continue-on-error: ${{ runner.os != 'Linux' }} + env: + RUSTFLAGS: >- + ${{ runner.os == 'macOS' && '-C link-arg=-undefined -C link-arg=dynamic_lookup' || '' }} + # A wheel must never ship a sqb server without its UI. + SQLBUILD_UI_REQUIRED: "1" + with: + command: build + args: >- + --profile ${{ inputs.cargo-profile }} --out dist --manifest-path crates/sqlbuild-cli/Cargo.toml --features ui + ${{ matrix.platform.name == 'linux-x86_64' && '--timings' || '' }} + target: ${{ matrix.platform.target }} + manylinux: auto + - name: Build sqb with maturin from PyPI + if: >- + inputs.prebuilt-run-id == '' && runner.os != 'Linux' + && steps.cli.outcome == 'failure' && steps.cli-retry.outcome == 'failure' + shell: bash + env: + RUSTFLAGS: >- + ${{ runner.os == 'macOS' && '-C link-arg=-undefined -C link-arg=dynamic_lookup' || '' }} + # A wheel must never ship a sqb server without its UI. + SQLBUILD_UI_REQUIRED: "1" + MATURIN_ARGS: >- + --profile ${{ inputs.cargo-profile }} --out dist --manifest-path crates/sqlbuild-cli/Cargo.toml --features ui + TARGET: ${{ matrix.platform.target }} + run: scripts/maturin_from_pypi.sh build ${MATURIN_ARGS} - name: Upload the cargo build timing reports if: inputs.prebuilt-run-id == '' && matrix.platform.name == 'linux-x86_64' uses: actions/upload-artifact@v4 @@ -286,16 +347,46 @@ jobs: if: inputs.prebuilt-run-id == '' with: toolchain: stable - - uses: PyO3/maturin-action@v1 + - id: sdist + uses: PyO3/maturin-action@v1 if: inputs.prebuilt-run-id == '' + continue-on-error: true with: command: sdist args: --out dist - - uses: PyO3/maturin-action@v1 + - id: sdist-retry + name: Retry the source distribution + uses: PyO3/maturin-action@v1 + if: inputs.prebuilt-run-id == '' && steps.sdist.outcome == 'failure' + continue-on-error: true + with: + command: sdist + args: --out dist + - name: Build the source distribution with maturin from PyPI + if: >- + inputs.prebuilt-run-id == '' && steps.sdist.outcome == 'failure' + && steps.sdist-retry.outcome == 'failure' + run: scripts/maturin_from_pypi.sh sdist --out dist + - id: sdist-cli + uses: PyO3/maturin-action@v1 if: inputs.prebuilt-run-id == '' + continue-on-error: true + with: + command: sdist + args: --out dist --manifest-path crates/sqlbuild-cli/Cargo.toml + - id: sdist-cli-retry + name: Retry the sqb source distribution + uses: PyO3/maturin-action@v1 + if: inputs.prebuilt-run-id == '' && steps.sdist-cli.outcome == 'failure' + continue-on-error: true with: command: sdist args: --out dist --manifest-path crates/sqlbuild-cli/Cargo.toml + - name: Build the sqb source distribution with maturin from PyPI + if: >- + inputs.prebuilt-run-id == '' && steps.sdist-cli.outcome == 'failure' + && steps.sdist-cli-retry.outcome == 'failure' + run: scripts/maturin_from_pypi.sh sdist --out dist --manifest-path crates/sqlbuild-cli/Cargo.toml - name: Download prebuilt source distributions if: inputs.prebuilt-run-id != '' uses: actions/download-artifact@v4 diff --git a/.github/workflows/compiler-differential.yml b/.github/workflows/compiler-differential.yml index 62b3e6f28e..80ddac1aeb 100644 --- a/.github/workflows/compiler-differential.yml +++ b/.github/workflows/compiler-differential.yml @@ -23,7 +23,7 @@ jobs: models: [3000, 10000] steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true diff --git a/.github/workflows/release-performance.yml b/.github/workflows/release-performance.yml index 00d27f1f14..cfd805f9d7 100644 --- a/.github/workflows/release-performance.yml +++ b/.github/workflows/release-performance.yml @@ -52,7 +52,7 @@ jobs: with: # Release tags identify the previous release before its wheels reach PyPI. fetch-depth: 0 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 64cd17f919..2ebe6f0620 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -74,7 +74,7 @@ jobs: ref: ${{ steps.release_branch.outputs.name }} token: ${{ steps.delivery-token.outputs.token }} - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv if: steps.release.outputs.prs_created == 'true' with: version: "0.12.3" diff --git a/.github/workflows/rust-cache.yml b/.github/workflows/rust-cache.yml index edc1862c43..625671d7cf 100644 --- a/.github/workflows/rust-cache.yml +++ b/.github/workflows/rust-cache.yml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" - uses: dtolnay/rust-toolchain@master @@ -58,7 +58,7 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" - uses: dtolnay/rust-toolchain@master @@ -97,7 +97,7 @@ jobs: shell: bash steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" - uses: dtolnay/rust-toolchain@master diff --git a/.github/workflows/scope-performance.yml b/.github/workflows/scope-performance.yml index 7344ac2654..38410b9d11 100644 --- a/.github/workflows/scope-performance.yml +++ b/.github/workflows/scope-performance.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 114ea51c4b..2dab9d96db 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -261,7 +261,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" - uses: dtolnay/rust-toolchain@master @@ -282,7 +282,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -338,7 +338,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -388,7 +388,7 @@ jobs: PYTEST_XDIST_AUTO_NUM_WORKERS: "4" steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -447,7 +447,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -494,7 +494,7 @@ jobs: UV_PYTHON: ${{ matrix.python }} steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -531,7 +531,7 @@ jobs: shell: bash steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -637,7 +637,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -672,7 +672,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -705,7 +705,7 @@ jobs: models: [3000, 5000, 10000] steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -757,7 +757,7 @@ jobs: models: [1000, 3000, 5000, 10000] steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -826,7 +826,7 @@ jobs: with: ref: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || 'main' }} path: base - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -903,7 +903,7 @@ jobs: models: [3000, 5000, 10000] steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -974,7 +974,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -1024,7 +1024,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -1061,7 +1061,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true @@ -1110,7 +1110,7 @@ jobs: timeout-minutes: 20 steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: ./.github/actions/setup-uv with: version: "0.12.3" enable-cache: true diff --git a/scripts/maturin_from_pypi.sh b/scripts/maturin_from_pypi.sh new file mode 100755 index 0000000000..8bf631a7f9 --- /dev/null +++ b/scripts/maturin_from_pypi.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Builds with maturin installed from PyPI, for when PyO3/maturin-action cannot download maturin +# from GitHub releases. The version is the one the action resolves for this repository. This runs +# on the host, so it never replaces a manylinux container build. +# Usage: scripts/maturin_from_pypi.sh build|sdist [maturin arguments...]; builds read TARGET. +set -euo pipefail + +MATURIN_VERSION=1.16.0 +command=$1 +shift + +python=python +command -v python > /dev/null || python=python3 +"${python}" -m venv "${RUNNER_TEMP}/maturin-pypi" +venv_python="${RUNNER_TEMP}/maturin-pypi/bin/python" +[[ -x "${venv_python}" ]] || venv_python="${RUNNER_TEMP}/maturin-pypi/Scripts/python.exe" +"${venv_python}" -m pip install --disable-pip-version-check "maturin==${MATURIN_VERSION}" + +if [[ "${command}" == sdist ]]; then + exec "${venv_python}" -m maturin sdist "$@" +fi +case "${RUNNER_OS}-${TARGET}" in + macOS-x86_64) triple=x86_64-apple-darwin ;; + macOS-aarch64) triple=aarch64-apple-darwin ;; + Windows-x86_64) triple=x86_64-pc-windows-msvc ;; + *) echo "no host fallback for ${RUNNER_OS} ${TARGET}" >&2; exit 1 ;; +esac +rustup target add "${triple}" +exec "${venv_python}" -m maturin build --target "${triple}" "$@"