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
209 changes: 209 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,215 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

# ------------------------------------------ Dependency Bounds ------------------------------------------
# `unit-tests` installs what uv.lock pins, so it never exercises the ranges in pyproject.toml.
# Upper bounds are tracked separately: an upstream release could turn such a job red on its own.

# Deterministic enough to gate on, since floors only move when pyproject.toml does. Development
# dependencies mask some floors; `dependency-lower-bounds-no-harness` below reaches those.
dependency-lower-bounds:
env:
# workaround for Rich table column width
COLUMNS: 140
if: |
always() && !cancelled() &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled') &&
needs.files-changed.outputs.python == 'true'
needs: ["prepare-environment", "files-changed", "yaml-lint", "python-lint"]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: "Check out repository code"
uses: "actions/checkout@v7"
# Oldest supported interpreter: pyarrow 14, pyyaml 6.0 and ujson 5.0 have no wheels past
# cp312, so a newer one would build them from source. Bump when requires-python changes.
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.10"
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
version: "${{ needs.prepare-environment.outputs.UV_VERSION }}"
python-version: "3.10"
# Rewrites uv.lock in the checkout, which is discarded with the runner.
- name: "Re-resolve at the declared lower bounds"
run: uv lock --resolution lowest-direct
- name: Install dependencies
run: uv sync --all-groups --all-extras
- name: "Report the resolved versions"
run: uv pip list
- name: Unit Tests
run: uv run pytest --cov infrahub_sdk tests/unit/

# Proves a plain install works on every supported interpreter, which the uv.lock-based jobs cannot.
install-matrix:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
if: |
always() && !cancelled() &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled') &&
needs.files-changed.outputs.python == 'true'
needs: ["prepare-environment", "files-changed", "yaml-lint", "python-lint"]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: "Check out repository code"
uses: "actions/checkout@v7"
with:
# hatch-vcs derives the version from git tags, and a shallow clone has none.
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
version: "${{ needs.prepare-environment.outputs.UV_VERSION }}"
python-version: ${{ matrix.python-version }}
- name: "Build the wheel"
run: uv build --wheel --out-dir dist/
# Not `uv sync`: a fresh resolution of the built wheel is what a user actually gets.
- name: "Install the wheel with every extra"
run: |
uv venv --python ${{ matrix.python-version }} .venv-install
uv pip install --python .venv-install "$(echo dist/*.whl)[all]"
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
- name: "Report the resolved versions"
run: uv pip list --python .venv-install
# Run from elsewhere: `python -` puts the working directory on sys.path, so from the checkout
# this would import the source tree rather than the wheel.
- name: "Import every shipped module"
working-directory: ${{ runner.temp }}
run: |
${{ github.workspace }}/.venv-install/bin/python - <<'PY'
import importlib, pkgutil, sys
import infrahub_sdk

failures = []
for module in pkgutil.walk_packages(infrahub_sdk.__path__, "infrahub_sdk."):
try:
importlib.import_module(module.name)
except BaseException as exc: # noqa: BLE001
failures.append(f"{module.name}: {type(exc).__name__}: {exc}")
for failure in failures:
print(f"FAIL {failure}")
print(f"{len(failures)} module(s) failed to import")
sys.exit(1 if failures else 0)
PY
- name: "Run the CLI"
run: |
.venv-install/bin/infrahubctl --help
.venv-install/bin/infrahubctl schema --help
.venv-install/bin/infrahubctl branch --help

# With no dependency groups, nothing pulls pytest, pydantic, anyio or typing-extensions above
# their declared floors, so this is the only job that validates those four. httpx and
# graphql-core stay out of reach: ariadne-codegen requires httpx>=0.28 and graphql-core>=3.2.
# It cannot run the unit suite without the harness, so it checks the surface a user touches.
dependency-lower-bounds-no-harness:
if: |
always() && !cancelled() &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled') &&
needs.files-changed.outputs.python == 'true'
needs: ["prepare-environment", "files-changed", "yaml-lint", "python-lint"]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: "Check out repository code"
uses: "actions/checkout@v7"
with:
# hatch-vcs derives the version from git tags, and a shallow clone has none.
fetch-depth: 0
# The oldest supported interpreter, for the same wheel-availability reason as the job above.
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.10"
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
version: "${{ needs.prepare-environment.outputs.UV_VERSION }}"
python-version: "3.10"
- name: "Build the wheel"
run: uv build --wheel --out-dir dist/
# From pyproject.toml, not the wheel: `--resolution lowest-direct` only lowers what is named
# on the command line, so asking for the wheel leaves its dependencies at their newest.
- name: "Resolve the extras at their declared lower bounds"
run: |
uv pip compile pyproject.toml --extra ctl --extra testing \
--resolution lowest-direct --python-version 3.10 -o floors.txt
- name: "Install the wheel against those floors, with no dependency groups"
run: |
uv venv --python 3.10 .venv-floors
uv pip install --python .venv-floors -r floors.txt
uv pip install --python .venv-floors --no-deps "$(echo dist/*.whl)"
- name: "Report the resolved versions"
run: uv pip list --python .venv-floors
# From outside the checkout, so this tests the wheel in site-packages and not the source tree.
- name: "Import every shipped module"
working-directory: ${{ runner.temp }}
run: |
${{ github.workspace }}/.venv-floors/bin/python - <<'PY'
import importlib, pkgutil, sys
import infrahub_sdk

# `testing.docker` needs the testcontainers extra, which is deliberately absent here.
skip = {"infrahub_sdk.testing.docker"}
failures = []
for module in pkgutil.walk_packages(infrahub_sdk.__path__, "infrahub_sdk."):
if module.name in skip:
continue
try:
importlib.import_module(module.name)
except BaseException as exc: # noqa: BLE001
failures.append(f"{module.name}: {type(exc).__name__}: {exc}")
for failure in failures:
print(f"FAIL {failure}")
print(f"{len(failures)} module(s) failed to import")
sys.exit(1 if failures else 0)
PY
- name: "Run the CLI"
run: |
.venv-floors/bin/infrahubctl --help
.venv-floors/bin/infrahubctl schema --help
.venv-floors/bin/infrahubctl branch --help
# Collection is where the plugin does its work; importing it would prove far less.
- name: "Collect a test through the bundled pytest plugin"
working-directory: ${{ runner.temp }}
run: |
rm -rf plugin-check && mkdir -p plugin-check/templates && cd plugin-check
cat > infrahub_config.yml <<'YAML'
---
jinja2_transforms:
- name: bgp_config
description: "Template for BGP config base"
query: "bgp_sessions"
template_path: "templates/bgp_config.j2"
YAML
cat > test_bgp.yml <<'YAML'
---
infrahub_tests:
- resource: "Jinja2Transform"
resource_name: "bgp_config"
tests:
- name: smoke
spec:
kind: "jinja2-transform-smoke"
YAML
printf 'router bgp {{ asn }}\n' > templates/bgp_config.j2
# The default shell is `bash -e` without pipefail, so a pytest failure would otherwise
# be reported as a grep failure.
set -o pipefail
${{ github.workspace }}/.venv-floors/bin/python -m pytest \
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
--infrahub-repo-config=infrahub_config.yml --strict-markers --collect-only -q \
| tee collected.txt
grep -q "infrahub_jinja2_transform__bgp_config__smoke" collected.txt

# ------------------------------------------ Integration Tests ------------------------------------------
integration-tests-latest-infrahub:
if: |
Expand Down
1 change: 1 addition & 0 deletions changelog/+dependency-bound-ci.housekeeping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added two CI jobs covering the declared dependency ranges, which the existing test matrix never exercised because it installs exactly what `uv.lock` pins. One re-resolves at the declared lower bounds and runs the unit tests; the other builds the wheel and confirms it installs, imports and runs the CLI on every supported Python with a fresh resolution. Upper bounds are tracked separately, since any upstream release could otherwise turn an unrelated pull request red.
7 changes: 7 additions & 0 deletions changelog/+dependency-floor-corrections.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Two declared lower bounds were wrong and have been corrected.

`typer` now requires `>=0.16.0`, up from `>=0.15.0`. Combined with the `click>=8.3` the SDK already required, typer 0.15 crashed on any `infrahubctl --help` with `TypeError: Parameter.make_metavar() missing 1 required positional argument`, because click 8.3 changed that signature and typer only adapted in 0.16. The old floor advertised a combination that could not work.

`Jinja2` now requires `>=3.1.5`, up from `>=3`. On 3.1.4 and earlier, template error reporting points at the wrong template when a nested template uses an undefined variable, and omits the source path when an imported template is missing.

If you pin either package below its new floor, installing the SDK now fails while resolving instead of breaking once you run it.
19 changes: 12 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ dependencies = [
"httpx>=0.20",
"anyio>=3.3.0", # 3.3.0 introduced `anyio.Path`, used by the async file handler
"typing-extensions>=4.4.0", # 4.4.0 introduced PEP 696 `TypeVar(default=...)`
"Jinja2>=3", # template rendering in `template/` and `protocols_generator/`
# 3.1.5 for the error positions the template error reporting relies on; 3.1.4 reports
# the wrong template for a nested undefined and omits the source path on a missing file.
"Jinja2>=3.1.5", # template rendering in `template/` and `protocols_generator/`
"pyyaml>=6", # YAML parsing in `yaml.py`, `spec/`, `template/infrahub_filters.py` and the pytest plugin
"rich>=12", # `Traceback`/`Frame`/`Syntax` in the Jinja error model, and transfer progress bars
"ujson>=5",
Expand All @@ -51,7 +53,10 @@ infrahubctl = "infrahub_sdk.ctl.cli:app"
ctl = [
"pyarrow>=14",
"ruamel.yaml>=0.18", # round-trip mode preserves comments when `schema format` rewrites a file
"typer>=0.15.0",
# 0.16 is the first release that calls click's `Parameter.make_metavar()` with the `ctx`
# argument click 8.3 made mandatory. Below it, any `--help` raises TypeError against the
# `click` floor below. Widening this means lowering that floor, not this one.
"typer>=0.16.0",
# Not imported directly; constrains the `click` that `typer` resolves to.
"click>=8.3,<9",
"ariadne-codegen==0.18.0",
Expand Down Expand Up @@ -89,16 +94,16 @@ tests = [
"pytest-xdist>=3.3.1",
]
lint = [
"yamllint",
"yamllint>=1.35",
"mypy==2.3.1",
"ruff==0.15.12",
"astroid>=3.1,<4.0",
"ty==0.0.14",
"rumdl==0.2.28",
]
types = [
"types-ujson",
"types-pyyaml",
"types-ujson>=5.10",
"types-pyyaml>=6.0.12",
"types-python-slugify>=8.0.0.3",
]
docs = [
Expand All @@ -109,10 +114,10 @@ dev = [
{include-group = "lint"},
{include-group = "types"},
{include-group = "docs"},
"ipython",
"ipython>=8.18",
"requests>=2.33.0",
"prek>=0.3.0",
"codecov",
"codecov>=2.1",
"invoke>=2.2.1",
"towncrier>=24.8.0",
]
Expand Down
22 changes: 11 additions & 11 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.