Skip to content

ci: test the declared dependency lower bounds and the install surface - #1319

Open
ogenstad wants to merge 2 commits into
infrahub-developfrom
pog-dependency-bounds-ci-IHS-224
Open

ci: test the declared dependency lower bounds and the install surface#1319
ogenstad wants to merge 2 commits into
infrahub-developfrom
pog-dependency-bounds-ci-IHS-224

Conversation

@ogenstad

@ogenstad ogenstad commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Why

The unit-tests matrix runs five Python versions but every job installs exactly what uv.lock pins, so the version ranges declared in pyproject.toml have never been exercised by anything. This adds the jobs that hold a pull request to them.

Goal: the declared lower bounds are correct, and the published package installs on every supported interpreter.

Non-goals: upper bounds. Any upstream release could turn such a job red without a line of this code changing, and blocking an unrelated pull request on that is worse than not knowing. Tracked separately.

Ref IHS-224.

What changed

Two jobs:

  • dependency-lower-bounds re-resolves at the declared floors and runs the unit tests. Gates pull requests, because a floor only moves when pyproject.toml does.
  • install-matrix builds the wheel and confirms it installs, imports every shipped module and runs the CLI on all five supported interpreters, resolving fresh rather than from the lock. This is the half the existing matrix cannot see: it proves the pinned set works, not what a user actually gets from a plain install.

Writing the first job immediately found two wrong floors:

  • typer>=0.15.0 was impossible. Combined with the click>=8.3 this package already required, typer 0.15 crashes on any infrahubctl --help with TypeError: Parameter.make_metavar() missing 1 required positional argument: 'ctx'. click 8.3 made that argument mandatory and typer only followed in 0.16. Verified by holding typer at 0.15.0 and moving click:

    typer click --help
    0.15.0 8.3.0 crashes
    0.15.0 8.1.8 exit 0
    0.16.0 8.3.0 exit 0

    So >=0.16.0 is not a narrowing, it states a constraint that already existed implicitly. Note that widening typer support means lowering the click floor, not this one; typer 0.15.4 additionally caps click<8.2, so the compatible set is not a simple range.

  • Jinja2>=3 was wrong. Before 3.1.5, template error reporting attributes a nested undefined variable to the wrong template and drops the source path from a missing-import error. Bisected: 3.1.4 fails, 3.1.5 passes.

Five development requirements had no lower bound at all, which made the floor resolution fail outright rather than merely resolve oddly: yamllint reached back to 0.1.0, whose sdist does not build. Bounds on development dependencies carry no promise to users; they only need to keep the resolution sane, so they are set near what the lock already holds.

How to review

Start with the two jobs in .github/workflows/ci.yml, then the floor changes in pyproject.toml. The reasoning that most deserves scrutiny is in the comments on dependency-lower-bounds, covering why it is a single job on 3.10 rather than a matrix, and what it cannot reach.

Two limits stated there and worth repeating:

  • Where a development dependency constrains a package more tightly than we do, it wins. pytest, httpx, graphql-core, pydantic, anyio and typing-extensions all resolve above their declared floors in this job. Reaching those needs an environment with no development dependencies in it, which this does not attempt.
  • 9 floors are genuinely exercised: dulwich 0.24.7, jinja2 3.1.5, netutils 1.0.0, pyarrow 14.0.0, pyyaml 6.0, rich 12.0.0, typer 0.16.0, ujson 5.0.0, whenever 0.9.3.

How to test

uv lock --resolution lowest-direct && uv sync --all-groups --all-extras && uv run pytest tests/unit/
git checkout uv.lock && uv lock   # restore afterwards

Verified locally before and after the floor corrections:

Environment Result
floors, before the corrections 18 failed, 1843 passed
lock versions on 3.10 (control) 8 failed, 1853 passed
floors, after the corrections 7 failed, 1854 passed

The failures common to the floor and control runs are artifacts of a scratch virtualenv, not of the floors. After the corrections, the floor run's failure set is a strict subset of the control's, so no floor-specific failure remains. The 11 that were floor-specific split cleanly into the two clusters above, 7 typer and 4 Jinja2.

Also verified: a fresh default-resolution install of [all] succeeds and imports every shipped module on 3.10, 3.12 and 3.14; ruff, ty and mypy clean; yamllint -s . clean; unit suite 1859 passed at lock versions with the 2 known macOS temp-path failures; lock consistent.

Impact & rollout

  • Backward compatibility: raising the two floors is resolver-visible. Nothing that worked stops working, since typer 0.15 could not produce a working CLI against the required click, and the Jinja2 versions dropped misreport template errors.
  • Performance: adds one job plus a five-way matrix of install-only jobs. The matrix does no test running, so it is cheap.
  • Config/env changes: none.
  • Deployment notes: dependency-lower-bounds is intended as a required check. It is deterministic, so it will not flap.

Checklist

  • Tests added/updated
  • Changelog entry added
  • External docs updated (if user-facing or ops-facing change)
  • Internal .md docs updated (internal knowledge and AI code tools knowledge)

Summary by cubic

Adds three CI jobs that exercise the declared dependency ranges in pyproject.toml, which the unit-test matrix never covered because it installs exactly what uv.lock pins. The lower-bound jobs immediately exposed two wrong floors, now corrected: typer>=0.16.0 (0.15 crashes on any infrahubctl --help with the click>=8.3 the SDK already required) and Jinja2>=3.1.5 (earlier versions misattribute template errors). Addresses IHS-224.

CI jobs

  • dependency-lower-bounds re-resolves at the declared floors and runs the unit suite on Python 3.10, the only supported interpreter with floor-era wheels for pyarrow, pyyaml and ujson.
  • dependency-lower-bounds-no-harness reaches the floors the first job cannot, since the test harness pulls pytest, pydantic, anyio and typing-extensions above their declared floors. It imports every shipped module, runs the CLI, and collects a test through the bundled pytest plugin.
  • install-matrix builds the wheel and installs it with a fresh resolution on all five supported Python versions, importing every shipped module and running the CLI.
  • All wheel checks run from outside the checkout so they test the installed wheel, not the source tree.
  • Upper bounds are deliberately not tested; any upstream release could turn such a job red with no code change, so that work is tracked separately.

Dependency metadata

  • Five development dependencies with no lower bound now have one near what the lock holds; without them the floor resolution fails outright.
  • Pinning either corrected package below its new floor now fails at resolution time instead of breaking at runtime.

Written for commit bdfca3d. Summary will update on new commits.

Review in cubic

The unit-test matrix installs exactly what uv.lock pins, so the version ranges
in pyproject.toml have never been exercised. Two jobs now cover the parts a
pull request can be held to.

dependency-lower-bounds re-resolves at the declared floors and runs the unit
tests. It runs on 3.10 alone: the resolver picks the same floors on every
supported version, but pyarrow 14, pyyaml 6.0 and ujson 5.0 ship no wheels past
cp312, so a newer interpreter would compile them from source and fail for
reasons unrelated to this code. A floor states the oldest version validated,
not one installable everywhere.

install-matrix builds the wheel and confirms it installs, imports every shipped
module and runs the CLI on all five supported interpreters, resolving fresh
rather than from the lock. That is the half the existing matrix cannot see,
since it proves the pinned set works rather than what a user actually gets.

Writing the first job found two floors that were wrong. typer 0.15 crashes on
any `--help` with the click 8.3 this package already required, because click
made `Parameter.make_metavar()` take a ctx and typer only followed in 0.16, so
the old floor advertised a combination that cannot work. Jinja2 before 3.1.5
attributes a nested undefined variable to the wrong template and drops the
source path from a missing-import error.

Five development requirements had no lower bound at all, which made the floor
resolution fail outright: yamllint reached back to 0.1.0, whose sdist does not
build. Their bounds carry no promise to users, they only need to keep the
resolution sane, so they are set near what the lock already holds.

Upper bounds are deliberately absent. Any upstream release could turn such a
job red without a line here changing, and blocking an unrelated pull request on
that is worse than not knowing.
@ogenstad
ogenstad requested a review from a team as a code owner September 3, 2026 09:50
@ogenstad ogenstad added group/ci Issue related to the CI pipeline type/tech-debt Item we know we need to improve way it is implemented labels Sep 3, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: bdfca3d
Status: ✅  Deploy successful!
Preview URL: https://a96fb884.infrahub-sdk-python.pages.dev
Branch Preview URL: https://pog-dependency-bounds-ci-ihs.infrahub-sdk-python.pages.dev

View logs

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

❗ There is a different number of reports uploaded between BASE (546dc82) and HEAD (bdfca3d). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (546dc82) HEAD (bdfca3d)
integration-tests 1 0
@@                 Coverage Diff                  @@
##           infrahub-develop    #1319      +/-   ##
====================================================
- Coverage             85.33%   79.69%   -5.65%     
====================================================
  Files                   148      148              
  Lines                 14085    14085              
  Branches               1936     1936              
====================================================
- Hits                  12020    11225     -795     
- Misses                 1499     2306     +807     
+ Partials                566      554      -12     
Flag Coverage Δ
integration-tests ?
python-3.10 60.17% <ø> (ø)
python-3.11 60.17% <ø> (+0.01%) ⬆️
python-3.12 60.16% <ø> (ø)
python-3.13 60.16% <ø> (-0.02%) ⬇️
python-3.14 60.16% <ø> (-0.02%) ⬇️
python-filler-3.12 21.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 34 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/ci.yml">

<violation number="1" location=".github/workflows/ci.yml:316">
P2: According to linked Jira issue IHS-224, this support matrix also requires a scheduled upper-bound check. Add the non-gating Python 3.14 re-resolution job and failure tracking instead of omitting upper-bound coverage entirely.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
# ------------------------------------------ Dependency Bounds ------------------------------------------
# `unit-tests` above installs exactly what uv.lock pins, so it never exercises the version ranges
# declared in pyproject.toml. The two jobs below cover the parts of that gap that a pull request
# can be held to. Upper bounds are deliberately absent: any upstream release could turn such a job

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: According to linked Jira issue IHS-224, this support matrix also requires a scheduled upper-bound check. Add the non-gating Python 3.14 re-resolution job and failure tracking instead of omitting upper-bound coverage entirely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 316:

<comment>According to linked Jira issue IHS-224, this support matrix also requires a scheduled upper-bound check. Add the non-gating Python 3.14 re-resolution job and failure tracking instead of omitting upper-bound coverage entirely.</comment>

<file context>
@@ -310,6 +310,127 @@ jobs:
+  # ------------------------------------------ Dependency Bounds  ------------------------------------------
+  # `unit-tests` above installs exactly what uv.lock pins, so it never exercises the version ranges
+  # declared in pyproject.toml. The two jobs below cover the parts of that gap that a pull request
+  # can be held to. Upper bounds are deliberately absent: any upstream release could turn such a job
+  # red without a line of our code changing, which is not something a PR should be blocked on.
+
</file context>

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml
Comment on lines +340 to +348
# Pinned to the oldest interpreter we support, because that is where floor-era wheels exist.
# The resolver selects the same floors on every supported version, but installing them does
# not work everywhere: pyarrow 14, pyyaml 6.0 and ujson 5.0 ship no wheels past cp312, so on
# 3.13 or 3.14 this job would compile them from source and fail for reasons unrelated to our
# code. A matrix would therefore add nothing. A floor states the oldest version we have
# validated, not one installable on every interpreter; a newer interpreter simply resolves
# higher, which `install-matrix` below confirms still works.
# Bump this when the oldest supported version changes. If it goes stale the job fails loudly,
# because `requires-python` will reject the interpreter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bot's probably overdone that comment

…d floors

The import check ran from the checkout root, where `python -` puts the working
directory on sys.path, so it imported the source tree and never touched the
wheel it was meant to be testing. It now runs from the runner temp directory.
Confirmed: from the checkout it resolves infrahub_sdk to the repository, from
elsewhere to site-packages.

Adds dependency-lower-bounds-no-harness, the only place the floors masked by
the all-groups job are reachable. With no dependency groups installed, pytest
resolves to 7.0.0 rather than 9.0.0, pydantic to 2.0.3 rather than 2.10.6,
anyio to 3.3.0 rather than 4.4.0, and typing-extensions to 4.6.1 rather than
4.12.2. That validates the pydantic floor corrected two releases ago, which
nothing has exercised until now. httpx and graphql-core stay out of reach even
here, because ariadne-codegen requires httpx>=0.28 and graphql-core>=3.2.

It resolves from pyproject.toml rather than from the built wheel because
`--resolution lowest-direct` only lowers what is named on the command line;
asking for the wheel makes its dependencies transitive and leaves them newest.
That was measured, not assumed.

Without the harness it cannot run the unit suite, so it exercises the surface a
user touches: every module imports, the CLI runs, and the bundled plugin
collects a test from a generated repository config. The plugin check invokes
pytest rather than importing the plugin, since pytest loads it through its
entry point.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/ci.yml">

<violation number="1" location=".github/workflows/ci.yml:542">
P3: GitHub Actions runs `run:` with `bash -e` but not `-o pipefail`, so in `python -m pytest ... | tee collected.txt` the pipeline's exit status is `tee`'s (0). A pytest collection failure therefore doesn't abort the step; it is only caught indirectly when the following `grep -q` finds no collected nodeid and fails, hiding the real pytest traceback behind an unhelpful grep error. Add a `PIPESTATUS` check (or `set -o pipefail`) so genuine collection failures surface directly.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/workflows/ci.yml
kind: "jinja2-transform-smoke"
YAML
printf 'router bgp {{ asn }}\n' > templates/bgp_config.j2
${{ github.workspace }}/.venv-floors/bin/python -m pytest \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: GitHub Actions runs run: with bash -e but not -o pipefail, so in python -m pytest ... | tee collected.txt the pipeline's exit status is tee's (0). A pytest collection failure therefore doesn't abort the step; it is only caught indirectly when the following grep -q finds no collected nodeid and fails, hiding the real pytest traceback behind an unhelpful grep error. Add a PIPESTATUS check (or set -o pipefail) so genuine collection failures surface directly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 542:

<comment>GitHub Actions runs `run:` with `bash -e` but not `-o pipefail`, so in `python -m pytest ... | tee collected.txt` the pipeline's exit status is `tee`'s (0). A pytest collection failure therefore doesn't abort the step; it is only caught indirectly when the following `grep -q` finds no collected nodeid and fails, hiding the real pytest traceback behind an unhelpful grep error. Add a `PIPESTATUS` check (or `set -o pipefail`) so genuine collection failures surface directly.</comment>

<file context>
@@ -431,6 +436,114 @@ jobs:
+                    kind: "jinja2-transform-smoke"
+          YAML
+          printf 'router bgp {{ asn }}\n' > templates/bgp_config.j2
+          ${{ github.workspace }}/.venv-floors/bin/python -m pytest \
+            --infrahub-repo-config=infrahub_config.yml --strict-markers --collect-only -q \
+            | tee collected.txt
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

group/ci Issue related to the CI pipeline type/tech-debt Item we know we need to improve way it is implemented

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants