From e83b48aa71a4da5bbd4a94036df7518f591b64d4 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Sat, 22 Aug 2026 05:32:11 +0500 Subject: [PATCH] feat(python): support uv and pip contracts Signed-off-by: rldyourmnd --- .github/workflows/python-ci.yml | 104 ++++++++++++------ .../workflows/runtime-fixtures-languages.yml | 16 ++- CHANGELOG.md | 4 + catalog/capabilities.yml | 10 +- catalog/tools.yml | 1 + examples/languages/python.yml | 1 + 6 files changed, 99 insertions(+), 37 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 6237b70..481e0fe 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,8 +1,7 @@ name: python-ci -# Reusable Python CI for BOTH tiers: pinned astral-sh/setup-uv with dependency -# caching, a uv-managed Python toolchain, and caller-provided install/test -# commands passed via env (no `${{ inputs.* }}` inline in run:). +# Reusable Python CI for uv and pip. Hosted callers receive exact setup actions; +# immutable ephemeral callers may verify and reuse a baked interpreter/toolchain. on: workflow_call: @@ -10,20 +9,29 @@ on: runner: type: string default: 'ubuntu-latest' + package_manager: + description: 'One of uv or pip.' + type: string + default: 'uv' + use_preinstalled_toolchain: + description: 'Verify baked commands instead of setup actions. Use only on immutable ephemeral runners.' + type: boolean + default: false python_version: + description: 'Exact major.minor or major.minor.patch Python version.' type: string default: '3.13' - working_directory: - type: string - default: '.' uv_version: - description: 'Exact astral-sh/uv version to pin.' + description: 'Exact uv version.' type: string default: '0.11.30' + working_directory: + type: string + default: '.' install_command: - description: 'Dependency install command (bash). Empty to skip.' + description: 'Dependency install command (bash). Empty selects uv sync --frozen; pip callers must be explicit.' type: string - default: 'uv sync --frozen' + default: '' test_command: description: 'Test/verify command (bash).' type: string @@ -40,18 +48,13 @@ defaults: jobs: python: - name: python (${{ inputs.python_version }}) + name: python (${{ inputs.package_manager }}, ${{ inputs.python_version }}) runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout_minutes }} permissions: contents: read defaults: run: - # Explicit, and not redundant with the workflow-level default above: - # a job-level `defaults.run` replaces the workflow-level one rather than - # merging with it, so declaring working-directory here silently drops the - # shell. On Linux nothing changes because bash is the default anyway; on - # Windows every run step becomes PowerShell and bash syntax stops working. shell: bash working-directory: ${{ inputs.working_directory }} steps: @@ -60,36 +63,73 @@ jobs: with: persist-credentials: false - - name: Set up uv + - name: Validate toolchain contract + env: + PACKAGE_MANAGER: ${{ inputs.package_manager }} + PREINSTALLED: ${{ inputs.use_preinstalled_toolchain }} + PYTHON_VERSION: ${{ inputs.python_version }} + UV_VERSION: ${{ inputs.uv_version }} + INSTALL_COMMAND: ${{ inputs.install_command }} + run: | + set -euo pipefail + case "$PACKAGE_MANAGER" in uv|pip) ;; *) + echo "unsupported package_manager: $PACKAGE_MANAGER" >&2 + exit 2 + esac + [[ "$PYTHON_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] + [[ "$UV_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "$PREINSTALLED" == true || "$PREINSTALLED" == false ]] + if [[ "$PACKAGE_MANAGER" == pip && -z "$INSTALL_COMMAND" ]]; then + echo 'pip requires an explicit install_command; dependency locking is project-owned' >&2 + exit 2 + fi + + - name: Set up uv and Python + if: ${{ inputs.package_manager == 'uv' && !inputs.use_preinstalled_toolchain }} uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: version: ${{ inputs.uv_version }} enable-cache: true - # python-version sets UV_PYTHON; activate-environment creates a venv - # on that interpreter and puts it on PATH for every later step. Both - # are needed. A previous version ran `uv python install` on its own, - # which downloaded the requested Python and then left it unused: the - # caller's install and test commands resolved `python` from PATH and - # got the runner's system interpreter instead. The first fixture run - # caught it — the job announced "Python 3.13 tests passed" while - # pytest had run on the image's 3.12. python-version: ${{ inputs.python_version }} activate-environment: true + - name: Set up Python for pip + if: ${{ inputs.package_manager == 'pip' && !inputs.use_preinstalled_toolchain }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ inputs.python_version }} + + - name: Verify resolved toolchain + env: + PACKAGE_MANAGER: ${{ inputs.package_manager }} + PYTHON_VERSION: ${{ inputs.python_version }} + UV_VERSION: ${{ inputs.uv_version }} + run: | + set -euo pipefail + actual_python=$(python -c 'import platform; print(platform.python_version())') + [[ "$actual_python" == "$PYTHON_VERSION" || "$actual_python" == "$PYTHON_VERSION".* ]] + case "$PACKAGE_MANAGER" in + uv) [[ "$(uv --version)" == "uv $UV_VERSION"* ]] ;; + pip) python -m pip --version >/dev/null ;; + esac + - name: Install dependencies - if: ${{ inputs.install_command != '' }} env: + PACKAGE_MANAGER: ${{ inputs.package_manager }} INSTALL_COMMAND: ${{ inputs.install_command }} - run: bash -euo pipefail -c "$INSTALL_COMMAND" + run: | + set -euo pipefail + if [[ -n "$INSTALL_COMMAND" ]]; then + bash -euo pipefail -c "$INSTALL_COMMAND" + else + uv sync --frozen + fi - name: Run tests env: + PACKAGE_MANAGER: ${{ inputs.package_manager }} TEST_COMMAND: ${{ inputs.test_command }} - PY_VERSION: ${{ inputs.python_version }} run: | bash -euo pipefail -c "$TEST_COMMAND" - # Report the interpreter that actually ran, not the one that was - # requested. Printing the input was how the inert python_version - # stayed invisible for so long. - echo "Tests passed on $(python -V 2>&1) (requested ${PY_VERSION})." \ - >> "$GITHUB_STEP_SUMMARY" + printf 'Python CI passed with %s on %s.\n' \ + "$PACKAGE_MANAGER" "$(python -V 2>&1)" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/runtime-fixtures-languages.yml b/.github/workflows/runtime-fixtures-languages.yml index f0e86d6..2543727 100644 --- a/.github/workflows/runtime-fixtures-languages.yml +++ b/.github/workflows/runtime-fixtures-languages.yml @@ -68,6 +68,19 @@ jobs: install_command: "uv pip install -e . pytest" test_command: "python -m pytest -q" + fixture-python-pip: + name: fixture / python-ci / pip + permissions: + contents: read + uses: ./.github/workflows/python-ci.yml + with: + runner: ubuntu-latest + package_manager: pip + python_version: '3.13' + working_directory: tests/fixtures/python + install_command: 'python -m pip install -e . pytest' + test_command: 'python -m pytest -q' + # supporting — Node lane under bun. fixture-node-ci: name: fixture / node-ci @@ -685,6 +698,7 @@ jobs: - observe-qt-ci - fixture-go-ci - fixture-python-ci + - fixture-python-pip - fixture-node-ci - fixture-node-package-managers - fixture-rust-ci @@ -737,7 +751,7 @@ jobs: RESULTS: ${{ toJSON(needs) }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} PROVES: >- - {"fixture-go-ci":"go-ci.yml","fixture-python-ci":"python-ci.yml","fixture-node-ci":"node-ci.yml","fixture-node-package-managers":"node-ci.yml","fixture-rust-ci":"rust-ci.yml","fixture-cpp-ci":"cpp-ci.yml","fixture-terraform-ci":"terraform-ci.yml","fixture-sql-ci":"sql-ci.yml","fixture-docs-quality":"docs-quality.yml","fixture-hadolint":"hadolint-ci.yml","fixture-web-ci":"web-ci.yml","fixture-container-ci":"container-ci.yml","fixture-cross-platform-smoke":"cross-platform-smoke.yml","fixture-java-ci":"java-ci.yml","fixture-dotnet-ci":"dotnet-ci.yml","fixture-swift-ci":"swift-ci.yml","fixture-mutation-testing":"mutation-testing.yml","fixture-r-ci":"r-ci.yml","fixture-benchmark-compare":"benchmark-compare.yml","fixture-dart-flutter-ci":"dart-flutter-ci.yml","fixture-kotlin-android-ci":"kotlin-android-ci.yml","fixture-qt-ci":"qt-ci.yml","fixture-go-ci-os":"go-ci.yml","fixture-python-ci-os":"python-ci.yml","fixture-rust-ci-os":"rust-ci.yml","fixture-dotnet-ci-os":"dotnet-ci.yml","fixture-java-ci-os":"java-ci.yml","fixture-node-ci-os":"node-ci.yml","fixture-terraform-ci-os":"terraform-ci.yml","fixture-sql-ci-os":"sql-ci.yml","fixture-web-ci-os":"web-ci.yml"} + {"fixture-go-ci":"go-ci.yml","fixture-python-ci":"python-ci.yml","fixture-python-pip":"python-ci.yml","fixture-node-ci":"node-ci.yml","fixture-node-package-managers":"node-ci.yml","fixture-rust-ci":"rust-ci.yml","fixture-cpp-ci":"cpp-ci.yml","fixture-terraform-ci":"terraform-ci.yml","fixture-sql-ci":"sql-ci.yml","fixture-docs-quality":"docs-quality.yml","fixture-hadolint":"hadolint-ci.yml","fixture-web-ci":"web-ci.yml","fixture-container-ci":"container-ci.yml","fixture-cross-platform-smoke":"cross-platform-smoke.yml","fixture-java-ci":"java-ci.yml","fixture-dotnet-ci":"dotnet-ci.yml","fixture-swift-ci":"swift-ci.yml","fixture-mutation-testing":"mutation-testing.yml","fixture-r-ci":"r-ci.yml","fixture-benchmark-compare":"benchmark-compare.yml","fixture-dart-flutter-ci":"dart-flutter-ci.yml","fixture-kotlin-android-ci":"kotlin-android-ci.yml","fixture-qt-ci":"qt-ci.yml","fixture-go-ci-os":"go-ci.yml","fixture-python-ci-os":"python-ci.yml","fixture-rust-ci-os":"rust-ci.yml","fixture-dotnet-ci-os":"dotnet-ci.yml","fixture-java-ci-os":"java-ci.yml","fixture-node-ci-os":"node-ci.yml","fixture-terraform-ci-os":"terraform-ci.yml","fixture-sql-ci-os":"sql-ci.yml","fixture-web-ci-os":"web-ci.yml"} GUARDS: >- {"fixture-dart-flutter-ci":["observe-dart-flutter-ci"],"fixture-kotlin-android-ci":["observe-kotlin-android-ci"],"fixture-qt-ci":["observe-qt-ci"]} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 72fe673..9fd52aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The project follows Semantic Versioning. ### Changed +- Expanded `python-ci.yml` to an explicit uv-or-pip contract. Hosted callers + receive the appropriate pinned setup action; immutable ephemeral callers may + verify baked commands. pip fails closed without a project-owned install + command instead of guessing dependency or lockfile policy. - Expanded `node-ci.yml` from a Bun-only lane to a fail-closed npm, pnpm, Yarn and Bun contract. Hosted callers receive exact setup; immutable ephemeral callers can verify and reuse baked toolchains, avoiding repeated downloads. diff --git a/catalog/capabilities.yml b/catalog/capabilities.yml index 11c6762..3f209e4 100644 --- a/catalog/capabilities.yml +++ b/catalog/capabilities.yml @@ -1006,11 +1006,11 @@ capabilities: - "contents: read" required_settings: [] risks: - - "Unknown package managers fail before dependency installation; supported values are npm, pnpm, Yarn and Bun" - - "Preinstalled mode is safe only on immutable ephemeral runners whose baked versions match the requested contract" + - "pip callers must provide an explicit install command because dependency locking is project-owned" + - "Preinstalled mode is safe only on immutable ephemeral runners whose Python and uv versions match" - "Private-repo runner minutes are metered beyond the included free allotment" deprecations: null - last_verified: "2026-07-04" + last_verified: "2026-08-22" sources: - "https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python" @@ -1027,7 +1027,9 @@ capabilities: - "contents: read" required_settings: [] risks: - - "Workflow is present on disk and validated by generated workflow inventory" + - "Unknown package managers fail before dependency installation; supported values are npm, pnpm, Yarn and Bun" + - "Preinstalled mode is safe only on immutable ephemeral runners whose baked versions match the requested contract" + - "Private-repo runner minutes are metered beyond the included free allotment" deprecations: null last_verified: "2026-08-22" sources: diff --git a/catalog/tools.yml b/catalog/tools.yml index 71ad724..c56e9ad 100644 --- a/catalog/tools.yml +++ b/catalog/tools.yml @@ -240,6 +240,7 @@ tools: - .github/workflows/maintenance.yml - .github/workflows/mutation-testing.yml - .github/workflows/private-static.yml + - .github/workflows/python-ci.yml - .github/workflows/release.yml - .github/workflows/runtime-fixtures.yml - .github/workflows/runtime-fixtures-event-write.yml diff --git a/examples/languages/python.yml b/examples/languages/python.yml index d700ba1..fbef245 100644 --- a/examples/languages/python.yml +++ b/examples/languages/python.yml @@ -13,6 +13,7 @@ jobs: uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/python-ci.yml@ with: runner: ubuntu-latest + package_manager: uv python_version: '3.13' install_command: 'uv sync --frozen' test_command: 'uv run pytest'