mirror of
https://github.com/astral-sh/uv
synced 2026-01-22 14:00:11 -05:00
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.
352 lines
14 KiB
YAML
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) }}
|