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: ubuntu-latest outputs: # Run checks/tests if no-test label is not present and code changed (or on main) test-code: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (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, 'no-test') && steps.changed.outputs.release_build_changed == 'true' }} # Run format/lint checks (always unless no-test label) run-checks: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }} # 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, 'no-test') && (steps.changed.outputs.trampoline_any_changed == 'true' || 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 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 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}" 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 }} 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-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 }} test: needs: plan if: ${{ needs.plan.outputs.test-code == 'true' }} uses: ./.github/workflows/test.yml 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 test-smoke: needs: build-dev-binaries uses: ./.github/workflows/test-smoke.yml with: sha: ${{ github.sha }} test-integration: needs: build-dev-binaries uses: ./.github/workflows/test-integration.yml secrets: inherit permissions: id-token: write with: sha: ${{ github.sha }} test-system: needs: build-dev-binaries uses: ./.github/workflows/test-system.yml with: sha: ${{ github.sha }} test-ecosystem: needs: build-dev-binaries 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.test-code == 'true' }} uses: ./.github/workflows/bench.yml secrets: inherit # 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: echo '${{ toJSON(needs) }}' | jq -e 'all(.[]; .result == "success" or .result == "skipped")' > /dev/null