Files
uv/.github/workflows/ci.yml
Zanie Blue 41c96d43c9 Reduce the number of CI checks run on pull requests (#17471)
We're hitting GitHub concurrency limits (organization wide limit of 60
jobs), and while we could move to paid runners with high concurrency
limits, I'd prefer to stay on the free runners and some of these jobs,
e.g., `test-system`, require GitHub runners.

This moves a bunch of our extended testing behind a label, e.g.,
`test:extended` or `test:system`, and only runs them on `main` by
default.
2026-01-14 13:12:11 -06:00

352 lines
14 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
plan:
runs-on: depot-ubuntu-24.04
outputs:
# Run checks/tests if test:skip label is not present and code changed (or on main)
test-code: ${{ !contains(github.event.pull_request.labels.*.name, 'test:skip') && (steps.changed.outputs.code_any_changed == 'true' || github.ref == 'refs/heads/main') }}
# Run schema check if schema file changed
check-schema: ${{ steps.changed.outputs.schema_changed == 'true' }}
# Run release build test if release files changed
build-release-binaries: ${{ !contains(github.event.pull_request.labels.*.name, 'test:skip') && steps.changed.outputs.release_build_changed == 'true' }}
# Run format/lint checks (always unless test:skip label)
run-checks: ${{ !contains(github.event.pull_request.labels.*.name, 'test:skip') }}
# Run publish test if publish-related files changed
test-publish: ${{ steps.changed.outputs.publish_changed == 'true' || github.ref == 'refs/heads/main' }}
# Run trampoline checks if trampoline-related code changed
test-windows-trampoline: ${{ !contains(github.event.pull_request.labels.*.name, 'test:skip') && (steps.changed.outputs.trampoline_any_changed == 'true' || github.ref == 'refs/heads/main') }}
# Save Rust cache if on main or if cache-relevant files changed (Cargo files, toolchain, workflows)
save-rust-cache: ${{ github.ref == 'refs/heads/main' || steps.changed.outputs.cache_changed == 'true' }}
# Run benchmarks only if Rust code changed
run-bench: ${{ !contains(github.event.pull_request.labels.*.name, 'test:skip') && (steps.changed.outputs.rust_code_changed == 'true' || github.ref == 'refs/heads/main') }}
# Smoke and ecosystem tests - run unless test:skip label
test-smoke: ${{ !contains(github.event.pull_request.labels.*.name, 'test:skip') }}
test-ecosystem: ${{ !contains(github.event.pull_request.labels.*.name, 'test:skip') }}
# Extended test suites - only run on main or with opt-in labels
test-integration: ${{ contains(github.event.pull_request.labels.*.name, 'test:integration') || contains(github.event.pull_request.labels.*.name, 'test:extended') || github.ref == 'refs/heads/main' }}
test-system: ${{ contains(github.event.pull_request.labels.*.name, 'test:system') || contains(github.event.pull_request.labels.*.name, 'test:extended') || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: "Determine changed files"
id: changed
shell: bash
run: |
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha || 'origin/main' }}...HEAD)
CODE_CHANGED=false
SCHEMA_CHANGED=false
RELEASE_BUILD_CHANGED=false
PUBLISH_CHANGED=false
TRAMPOLINE_CHANGED=false
CACHE_CHANGED=false
RUST_CODE_CHANGED=false
while IFS= read -r file; do
# Check if the schema file changed (e.g., in a release PR)
if [[ "${file}" == "uv.schema.json" ]]; then
echo "Detected schema change: ${file}"
SCHEMA_CHANGED=true
fi
# Check if release build files changed (pyproject.toml, Cargo.toml, etc.)
if [[ "${file}" == "pyproject.toml" || "${file}" == "Cargo.toml" || "${file}" == "Cargo.lock" || "${file}" == "rust-toolchain.toml" || "${file}" == ".cargo/config.toml" || "${file}" == "crates/uv-build/Cargo.toml" || "${file}" == "crates/uv-build/pyproject.toml" || "${file}" == ".github/workflows/build-release-binaries.yml" ]]; then
echo "Detected release build change: ${file}"
RELEASE_BUILD_CHANGED=true
fi
# Check if publish-related files changed
if [[ "${file}" =~ ^crates/uv-publish/ || "${file}" =~ ^scripts/publish/ || "${file}" == ".github/workflows/ci.yml" ]]; then
echo "Detected publish change: ${file}"
PUBLISH_CHANGED=true
fi
# Check if trampoline-related files changed
if [[ "${file}" =~ ^crates/uv-trampoline/ ]] || [[ "${file}" =~ ^crates/uv-trampoline-builder/ ]]; then
echo "Detected trampoline change: ${file}"
TRAMPOLINE_CHANGED=true
fi
# Check if cache-relevant files changed (Cargo files, toolchain, workflows)
if [[ "${file}" == "Cargo.lock" || "${file}" == "Cargo.toml" || "${file}" == "rust-toolchain.toml" || "${file}" == ".cargo/config.toml" || "${file}" =~ ^crates/.*/Cargo\.toml$ || "${file}" =~ ^\.github/workflows/.*\.yml$ ]]; then
echo "Detected cache-relevant change: ${file}"
CACHE_CHANGED=true
fi
# Check if Rust code changed (for benchmarks)
if [[ "${file}" =~ \.rs$ ]] || [[ "${file}" =~ Cargo\.toml$ ]] || [[ "${file}" == "Cargo.lock" ]] || [[ "${file}" == "rust-toolchain.toml" ]] || [[ "${file}" =~ ^\.cargo/ ]]; then
echo "Detected Rust code change: ${file}"
RUST_CODE_CHANGED=true
fi
if [[ "${file}" =~ ^docs/ ]]; then
echo "Skipping ${file} (matches docs/ pattern)"
continue
fi
if [[ "${file}" =~ ^mkdocs.*\.yml$ ]]; then
echo "Skipping ${file} (matches mkdocs*.yml pattern)"
continue
fi
if [[ "${file}" =~ \.md$ ]]; then
echo "Skipping ${file} (matches *.md pattern)"
continue
fi
if [[ "${file}" =~ ^bin/ ]]; then
echo "Skipping ${file} (matches bin/ pattern)"
continue
fi
if [[ "${file}" =~ ^assets/ ]]; then
echo "Skipping ${file} (matches assets/ pattern)"
continue
fi
echo "Detected code change in: ${file}"
CODE_CHANGED=true
done <<< "${CHANGED_FILES}"
echo "code_any_changed=${CODE_CHANGED}" >> "${GITHUB_OUTPUT}"
echo "schema_changed=${SCHEMA_CHANGED}" >> "${GITHUB_OUTPUT}"
echo "release_build_changed=${RELEASE_BUILD_CHANGED}" >> "${GITHUB_OUTPUT}"
echo "publish_changed=${PUBLISH_CHANGED}" >> "${GITHUB_OUTPUT}"
echo "trampoline_any_changed=${TRAMPOLINE_CHANGED}" >> "${GITHUB_OUTPUT}"
echo "cache_changed=${CACHE_CHANGED}" >> "${GITHUB_OUTPUT}"
echo "rust_code_changed=${RUST_CODE_CHANGED}" >> "${GITHUB_OUTPUT}"
check-fmt:
uses: ./.github/workflows/check-fmt.yml
check-lint:
needs: plan
uses: ./.github/workflows/check-lint.yml
with:
code-changed: ${{ needs.plan.outputs.test-code }}
save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }}
check-docs:
needs: plan
if: ${{ needs.plan.outputs.run-checks == 'true' }}
uses: ./.github/workflows/check-docs.yml
secrets: inherit
check-zizmor:
needs: plan
if: ${{ needs.plan.outputs.run-checks == 'true' }}
uses: ./.github/workflows/check-zizmor.yml
permissions:
contents: read
security-events: write
check-publish:
needs: plan
if: ${{ needs.plan.outputs.test-code == 'true' }}
uses: ./.github/workflows/check-publish.yml
check-release:
needs: plan
if: ${{ needs.plan.outputs.run-checks == 'true' }}
uses: ./.github/workflows/check-release.yml
check-generated-files:
needs: plan
if: ${{ needs.plan.outputs.test-code == 'true' }}
uses: ./.github/workflows/check-generated-files.yml
with:
schema-changed: ${{ needs.plan.outputs.check-schema }}
save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }}
test:
needs: plan
if: ${{ needs.plan.outputs.test-code == 'true' }}
uses: ./.github/workflows/test.yml
with:
save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }}
test-windows-trampolines:
needs: plan
if: ${{ needs.plan.outputs.test-windows-trampoline == 'true' }}
uses: ./.github/workflows/test-windows-trampolines.yml
build-dev-binaries:
needs: plan
if: ${{ needs.plan.outputs.test-code == 'true' }}
uses: ./.github/workflows/build-dev-binaries.yml
with:
save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }}
test-smoke:
needs:
- plan
- build-dev-binaries
if: ${{ needs.plan.outputs.test-smoke == 'true' }}
uses: ./.github/workflows/test-smoke.yml
with:
sha: ${{ github.sha }}
test-integration:
needs:
- plan
- build-dev-binaries
if: ${{ needs.plan.outputs.test-integration == 'true' }}
uses: ./.github/workflows/test-integration.yml
secrets: inherit
permissions:
id-token: write
with:
sha: ${{ github.sha }}
test-system:
needs:
- plan
- build-dev-binaries
if: ${{ needs.plan.outputs.test-system == 'true' }}
uses: ./.github/workflows/test-system.yml
with:
sha: ${{ github.sha }}
test-ecosystem:
needs:
- plan
- build-dev-binaries
if: ${{ needs.plan.outputs.test-ecosystem == 'true' }}
uses: ./.github/workflows/test-ecosystem.yml
with:
sha: ${{ github.sha }}
build-release-binaries:
needs: plan
if: ${{ needs.plan.outputs.build-release-binaries == 'true' }}
uses: ./.github/workflows/build-release-binaries.yml
secrets: inherit
bench:
needs: plan
if: ${{ needs.plan.outputs.run-bench == 'true' }}
uses: ./.github/workflows/bench.yml
secrets: inherit
with:
save-rust-cache: ${{ needs.plan.outputs.save-rust-cache }}
# This job cannot be moved into a reusable workflow because it includes coverage for uploading
# attestations and PyPI does not support attestations in reusable workflows.
test-publish:
name: "test uv publish"
timeout-minutes: 20
needs:
- plan
- build-dev-binaries
runs-on: ubuntu-latest
# Only the main repository is a trusted publisher
if: ${{ github.repository == 'astral-sh/uv' && github.event.pull_request.head.repo.fork != true && needs.plan.outputs.test-publish == 'true' }}
environment: uv-test-publish
env:
# No dbus in GitHub Actions
PYTHON_KEYRING_BACKEND: keyrings.alt.file.PlaintextKeyring
PYTHON_VERSION: 3.12
permissions:
# For trusted publishing
id-token: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: "Download binary"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: uv-linux-libc-${{ github.sha }}
- name: "Prepare binary"
run: chmod +x ./uv
- name: "Build astral-test-pypa-gh-action"
run: |
# Build a yet unused version of `astral-test-pypa-gh-action`
mkdir astral-test-pypa-gh-action
cd astral-test-pypa-gh-action
../uv init --package
# Get the latest patch version
patch_version=$(curl https://test.pypi.org/simple/astral-test-pypa-gh-action/?format=application/vnd.pypi.simple.v1+json | jq --raw-output '.files[-1].filename' | sed 's/astral_test_pypa_gh_action-0\.1\.\([0-9]\+\)\.tar\.gz/\1/')
# Set the current version to one higher (which should be unused)
sed -i "s/0.1.0/0.1.$((patch_version + 1))/g" pyproject.toml
../uv build
- name: "Publish astral-test-pypa-gh-action"
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
# With this GitHub action, we can't do as rigid checks as with our custom Python script, so we publish more
# leniently
skip-existing: "true"
verbose: "true"
repository-url: "https://test.pypi.org/legacy/"
packages-dir: "astral-test-pypa-gh-action/dist"
- name: "Add password to keyring"
run: |
# `keyrings.alt` contains the plaintext keyring
./uv tool install --with keyrings.alt keyring
echo $UV_TEST_PUBLISH_KEYRING | keyring set https://test.pypi.org/legacy/?astral-test-keyring __token__
env:
UV_TEST_PUBLISH_KEYRING: ${{ secrets.UV_TEST_PUBLISH_KEYRING }}
- name: "Add password to uv text store"
run: |
./uv auth login https://test.pypi.org/legacy/?astral-test-text-store --token ${UV_TEST_PUBLISH_TEXT_STORE}
env:
UV_TEST_PUBLISH_TEXT_STORE: ${{ secrets.UV_TEST_PUBLISH_TEXT_STORE }}
- name: "Publish test packages"
# `-p 3.12` prefers the python we just installed over the one locked in `.python_version`.
run: ./uv run -p "${PYTHON_VERSION}" scripts/publish/test_publish.py --uv ./uv all
env:
RUST_LOG: uv=debug,uv_publish=trace
UV_TEST_PUBLISH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_TOKEN }}
UV_TEST_PUBLISH_PASSWORD: ${{ secrets.UV_TEST_PUBLISH_PASSWORD }}
UV_TEST_PUBLISH_GITLAB_PAT: ${{ secrets.UV_TEST_PUBLISH_GITLAB_PAT }}
UV_TEST_PUBLISH_CODEBERG_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CODEBERG_TOKEN }}
UV_TEST_PUBLISH_CLOUDSMITH_TOKEN: ${{ secrets.UV_TEST_PUBLISH_CLOUDSMITH_TOKEN }}
UV_TEST_PUBLISH_PYX_TOKEN: ${{ secrets.UV_TEST_PUBLISH_PYX_TOKEN }}
UV_TEST_PUBLISH_PYTHON_VERSION: ${{ env.PYTHON_VERSION }}
required-checks-passed:
name: "all required jobs passed"
if: always()
needs:
- check-fmt
- check-lint
- check-docs
- check-generated-files
- test
- build-dev-binaries
runs-on: ubuntu-latest
steps:
- name: "Check required jobs passed"
run: |
failing=$(echo "$NEEDS_JSON" | jq -r 'to_entries[] | select(.value.result != "success" and .value.result != "skipped") | "\(.key): \(.value.result)"')
if [ -n "$failing" ]; then
echo "$failing"
exit 1
fi
env:
NEEDS_JSON: ${{ toJSON(needs) }}