Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 72 additions & 32 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
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:
inputs:
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
Expand All @@ -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:
Expand All @@ -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"
16 changes: 15 additions & 1 deletion .github/workflows/runtime-fixtures-languages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions catalog/capabilities.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions catalog/tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions examples/languages/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
uses: NDDev-OpenNetwork/ci-workflows/.github/workflows/python-ci.yml@<sha>
with:
runner: ubuntu-latest
package_manager: uv
python_version: '3.13'
install_command: 'uv sync --frozen'
test_command: 'uv run pytest'
Loading