Repository navigation
chore(devenv): report installed packages below a declared floor, where a local pass is read (#15091) - #15125
Merged
Conversation
…e a local pass is read (#15091) Local verification runs under whatever interpreter is on PATH. When its packages are older than the versions this repo declares, every local gate still passes, and the pass says nothing about CI, which installs the declared set. Nothing said so. #14998 lost a diagnosis cycle to exactly that. Adds pipeline-scripts/check_dependency_floors.py: it reads the -r closure of requirements-ci.txt, requirements-ci-test.txt and the two backend requirement files, compares each lower bound against importlib.metadata, and names the installed and declared versions side by side. An absent distribution is not a shortfall; an empty enumeration is an error, never a clean report (#15087). Two callers, because a local pass is read in two places: - scripts/pr-preflight.sh, in the interpreter section that already reports the Python fallback but said nothing about packages - a pytest terminal-summary banner, so a bare pytest run that never touches preflight still says it Reported, never gated. A box below floor stays usable for ordinary work; the decision and the remedy (scripts/setup-ci-parity-env.sh) are recorded in docs/developer/CLAUDE_WORKFLOW.md.
4 tasks
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
…ts do (#15091) The no-print-console hook flagged the two print() calls in main(). Every other standalone gate in pipeline-scripts/ marks these with `# noqa: print` -- 55 occurrences across the directory -- and not one file there imports get_logger or logging.getLogger. These are CLI gates whose stdout IS their output, and CI reads it; routing it through a logger would be the wrong shape. Stream and exit-code split confirmed deliberate: a fatal empty enumeration goes to stderr with exit 2 and writes nothing to stdout, the report goes to stdout with exit 0, and --strict exits 1. Three distinct codes, so a caller can act on the difference.
Merged
7 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
The issue reads as "the dev box is below the declared floor, and installing is forbidden, so the
best available answer is a warning". Reading the tree contradicted the premise in a useful way.
scripts/setup-ci-parity-env.sh(#13573) already exists, already builds Python 3.14 plusrequirements-ci.txt+requirements-ci-test.txt, and installs nothing outside its own venv —so matching the declared set has never required install rights on the box. Better still, that venv
is already built here, and it resolves
fastapi 0.141.1— the exact floor #14998 tripped over.So the honest remedy is not "push and read CI"; it is "build (or activate) the parity venv", with
push-and-read-CI as the fallback. AC1 is therefore answered yes rather than no.
That relocates the defect. The environment is not incapable of meeting the floor — nothing
tells anyone which environment they are in at the moment a green result is read.
pr-preflight.shalready noticed the interpreter fallback and said so, but nothing anywhere in the repo compared an
installed package version against a declared floor; every existing check is either
interpreter-minor or requirements-text-vs-requirements-text.
Two placement findings shaped the fix:
language: pythonlocal hooks run in a pre-commit-managedisolated venv, so a hook there cannot observe the ambient environment at all.
conftest.pyis the only file that runs before every localpytest, andpytest_terminal_summaryputs the banner directly beneath the pass/fail counts — the line actuallyread. A
pytest_report_headerwould scroll away behind the run.#14998's false pass came from abare pytest run, not from preflight, so preflight alone would not have caught it.
Warn rather than fail, per the issue's own AC — and because 90 of 206 declarations are unsatisfied on
this box. A gate failing on that would be deleted within a day.
I did not use
packagingfor version comparison: it is not declared in any requirements file here,and making a plugin that loads on every pytest session depend on a transitively-present package is
the same class of fragility. The comparator is ~15 lines and unit-tested, including the pre-release
and unequal-segment-count cases a naive tuple compare gets wrong.
repo_tests/router_mount_parity_test.pyis owned by #15093 and untouched; it already cites thisissue at :30-40, and its deliberate choice to stay static (rather than assert a version) is why this
PR reports the environment instead of adding a runtime router guard.
What Changed
pipeline-scripts/check_dependency_floors.py(new). Walks the-rclosure ofrequirements-ci.txt,requirements-ci-test.txt,autobot-backend/requirements.txtandautobot-slm-backend/requirements.txt, compares every lower bound (==,>=,~=,>) againstimportlib.metadata, and prints installed and declared side by side. An absent distribution isnot a shortfall — it says nothing about what CI would resolve. An empty enumeration raises
(
EmptyEnumerationError, exit 2): "0 declarations, all satisfied" is indistinguishable from ahealthy environment, which is the defect bug(tests): api.terminal.router enumerates zero routes under python-suite shard 12/12 (unidentified sys.modules pollution) #15087 records.
--strictexits 1;--alldrops the cap.repo_tests/dependency_floor_banner.py(new) + registration inconftest.py. Prints the reportunder the pass/fail counts of every pytest run. Loads the checker by path, registering it in
sys.modulesfirst — required, not incidental:@dataclassresolves annotations throughsys.modules[cls.__module__], and a module executed without that key raisesAttributeErroronimport. A test caught this; the plugin had worked only because a sibling test had already imported
the module.
scripts/pr-preflight.sh: its existinginterpretersection now also reports package floors.Exit 2 from the checker is a
fail— the check breaking must not read as a clean environment.print()calls inmain()carry# noqa: print, the settled idiom for astandalone gate in
pipeline-scripts/— 55 occurrences across that directory, and no file thereimports
get_loggerorlogging.getLogger. These are CLI gates whose stdout is their outputand CI reads it, so a logger would be the wrong shape.
docs/developer/CLAUDE_WORKFLOW.md: records the decision (match the declared set via the parityvenv; mismatch is reported, never gated), both reporting points, and the
include_routerfinding.Verification
AC1 — decide and record. Recorded in
docs/developer/CLAUDE_WORKFLOW.md, beside the existingsetup-ci-parity-env.shsection where a reader already looks. (static)AC2 — loud rather than silent. (unit + forensic) Live run on a bare, unrelated test file, with
the banner landing beside the count:
AC3 — do the two versions genuinely differ? (forensic) Yes. Below the floor, the installed
fastapi 0.135.2include_router()copies eagerly — its body runsfor route in router.routes: ... self.add_api_route(...)— and_IncludedRouterdoes not existanywhere in the installed
starlette 1.0.0. At the declared 0.141.1 / starlette 1.6.0 it defers,appending an
_IncludedRouterwithpath=None; CI job98088603289on #15085 dumped exactly thatwhile the same code read 26 routes locally. #15093 owns the behaviour and the blast-radius sweep;
this PR records the finding in the workflow doc so it is not rediscovered.
AC4 — any other unsatisfied floor? Count reported. (unit) Yes, many. 206 declarations
enumerated. 90 unsatisfied under the system interpreter (Python 3.10.12); 44 unsatisfied under
the CI-parity venv (Python 3.14.6). fastapi is unsatisfied in the first and satisfied in the second.
Gates.
black --checkandflake8clean under Python 3.14 from the parity venv, so black'sAST safety check actually ran rather than being skipped as it is on 3.10.
check_python_file_size.pyclean; no
KNOWN_LARGEentry added; largest new file 246 lines.Stream and exit-code split (unit) — confirmed deliberate, three distinct codes so a caller can
act on the difference. No FATAL line is ever printed while the process exits 0:
FATAL: no version declarations found …--strictpr-preflight.shnever passes--strict, so it sees only 0 or 2, and treats 2 asfail.Tests. 52 pass. New files land in shard 7/12 (
pipeline-scripts/check_dependency_floors_test.py)and shard 8/12 (
repo_tests/dependency_floor_banner_test.py) perrepo_tests/stable_shard.py.Contrast mutations, all against this PR's final head:
audit()no longer raises on an empty enumeration3 failed, 49 passed—test_empty_tree_raises_instead_of_reporting_clean,test_requirement_files_that_declare_nothing_also_raise,test_empty_enumeration_exits_two_and_says_so>=compared strictly (have > want)5 failed, 38 passed— incl.test_ordering[0.141.1->=-0.141.1-True],test_ordering[1.6->=-1.6.0-True],test_at_or_above_floor_is_silent2 failed, 7 passed—test_names_both_versions_when_below_floor,test_points_at_the_remedyNot proven here. The deferred-inclusion behaviour itself is not reproduced locally — this box
cannot install 0.141.1, and the parity venv carries
starlette 1.3.1, below the declared>=1.6.0.The CI-side half rests on #15093's cited job. No i18n: this is developer tooling, and its neighbours
(
sys_modules_leak_guard.py,check_env_var_registry.py) carry plain English with zero i18n imports.Model Used
Claude Opus 5 (1M context)
Closes #15091