Skip to content

chore(ci): stop Dependabot bumping the deliberate setuptools<81 sidecar pin #447

Description

@bioedca

Maintenance kind

CI

Work requested

Dependabot's weekly pip group proposes a setuptools bump against
packaging/setuptools-compatibility.txt, and every such bump is wrong by construction.
That file exists only to hold setuptools below the release that removed
pkg_resources — its own header states the bound and the reason:

tMAVEN's maven_class.__init__ does import pkg_resources, which it never declares.
setuptools DEPRECATED pkg_resources by 80.9.0 (still shipped, warns on import), and
REMOVED it in 82.0.0 — while sidecar/conda-lock.yml resolves 82.0.1 on every platform,
i.e. past the removal. Without this wheel an installed app builds its sidecar env fine
and then fails at the FIRST idealization (#212).

The pin is therefore a deliberate downgrade held against the ecosystem, not a stale
dependency. Letting Dependabot advance it defeats the file's entire purpose.

#444 (setuptools 80.9.0 → 83.0.0) is the first instance and has been closed. It touched
that one file and nothing else, which settles the mechanism: the pip entry at
directory: "/" does reach this file.

What is actually unguarded, and what is not

An earlier version of this issue said, in one compound sentence, that "nothing in the repo
checks the version against the <81 bound, so a future bump that happens to carry a single
hash would go green and ship a sidecar that dies at the first idealization", and that #444
was stopped by luck. Both halves need work, and in opposite directions — which is why
the sentence is replaced rather than trimmed.

The inference is false and is deleted.
tests/test_setup_sidecar.py::test_the_requirement_is_pinned_and_hashed asserts the exact
version 80.9.0 (:96) and the exact sha256 digest (:99), so any bump fails the
required test matrix on all three OSes whether it carries one hash or two. #444
additionally tripped the parser — scripts/setup_sidecar.py::setuptools_requirement uses
re.fullmatch against exactly one setuptools==<version> --hash=sha256:<64 hex>
(:138-139), and #444's diff carried two --hash= lines — but the version and digest
assertions would have caught a single-hash bump on their own. No bump can go green today,
and #444 was not luck.

The bound claim is over-broad rather than false, and the correction runs the other way:
the <81 bound is asserted — just nowhere a pull request reaches.

.github/workflows/packaging.yml:241 (Unix) and :293 (Windows) run, against the
installed sidecar env:

import setuptools, pkg_resources; assert tuple(int(p) for p in setuptools.__version__.split('.')[:1]) < (81,), setuptools.__version__

and tests/test_marker_contract.py:389 pins that assertion's presence by requiring the
literal import setuptools, pkg_resources in the workflow text, with the message
"packaging.yml install-smoke must assert the bundled setuptools<81 pin was applied to the
sidecar env, so a regression names its cause (issue #212)". But packaging.yml is
advisory and non-required by construction — its own header says it is triggered "ONLY by
workflow_dispatch (never pull_request/push/merge_group)" — so it never reports on a
Dependabot pull request.

So the accurate statement is narrower and still load-bearing: no assertion that runs on a
pull request derives from the bound.
On #444's diff — that one file — what stops it in
required CI is the parser plus the exact-version and exact-digest assertions, and none of
their failure messages names pkg_resources, 82.0.0, or the bound.

Two real defects survive that correction.

  1. Recurring dead-end pull requests. The proposal repeats on the weekly schedule.
    Each one costs a three-OS CI run and a maintainer close, forever, for a change that can
    never be correct. setuptools appears in no other pip manifest in the repository —
    not pyproject.toml, not requirements-docs.txt — so an ignore entry for it
    suppresses nothing else. Note separately that the pip entry groups with
    patterns: ["*"], so a future weekly pull request can bundle the doomed setuptools bump
    with legitimate dev-tool bumps and closing it discards both: a reason to stop the
    proposal at the source rather than keep closing it.

  2. The failure names the format, never the reason — and the check that does name the reason
    is heavy, and reached only by a coincidence of paths.
    The two ways a
    compatibility-file-only bump fails in required CI are SetupError: setuptools-compatibility.txt must be exactly one pinned, sha256-hashed setuptools requirement; got … (what build(deps): Bump setuptools from 80.9.0 to 83.0.0 in the pip group #444 hit, on its two hashes) and, for a single-hash bump, a bare
    assert '83.0.0' == '80.9.0'. Neither mentions pkg_resources, 82.0.0, or the <81
    bound. test_the_removal_trigger_is_recorded (tests/test_setup_sidecar.py:205-208)
    checks that REMOVAL TRIGGER, pkg_resources and 82.0.0 are present in the file;
    nothing puts them in front of whoever is looking at red CI. So the obvious way to make the
    suite green is to update the assertions to match the new pin.

    That follow-on edit does not ship — but it is caught late and by accident. It touches
    tests/test_setup_sidecar.py, which matches the relevance pattern
    tests/test_.*sidecar.*\.py at .github/workflows/sidecar.yml:134, so sidecar / parity
    — triggered on pull_request (:47) and present in the main-baseline ruleset's required
    status checks — stops answering "not applicable", provisions the isolated sidecar env, and
    runs python scripts/setup_sidecar.py --python … --with-pytest (:171-172). That installs
    the bumped requirement and then probes maven_class, whose undeclared import pkg_resources is the entire reason for the pin; run_probe raises SetupError and main
    returns 1 (scripts/setup_sidecar.py:269-272, :414-416). So the reason is named in
    required CI — by a heavy live job (timeout-minutes: 30) whose env and probe steps run
    only because the file being relaxed happens to match a filter written for the idealization
    tests, and which on the Dependabot pull request itself takes the other branch and reports
    "not applicable (pass)" in seconds (:139). Fix 2 replaces it with a fast local failure
    that says why.

Two independent fixes, both wanted:

  1. Stop Dependabot proposing it — an ignore entry for setuptools in the pip block of
    .github/dependabot.yml, with a comment pointing at the compatibility file and its
    removal trigger. The @dependabot ignore <dependency name> pull-request comment — offered
    in build(deps): Bump setuptools from 80.9.0 to 83.0.0 in the pip group #444's own body, where it is described as closing the pull request and stopping
    Dependabot creating any more for that dependency — would also work, but it stores the
    condition on GitHub's side where the repository cannot see it; a config entry is the
    auditable form.
  2. Make the bound load-bearing in the pure test matrix that runs on every pull
    request
    — assert in tests/test_setup_sidecar.py that the parsed major version is
    below 81, with a message that names the pkg_resources removal, so the "update the
    assertion to match" path fails fast and locally instead of only inside the live sidecar
    job.

Note the config-validation hazard already recorded at the top of .github/dependabot.yml:
the file validates all-or-nothing, so a rejected block stops every ecosystem — including
the two github-actions entries that keep the SHA pins of the required checks current — and
does not fall back to the previous behaviour.

Acceptance criteria

Superseded by the grooming block below.

Scope and non-goals

In scope: the Dependabot ignore entry; the version-bound assertion; the pointer comment in
the compatibility file; and adding .github/dependabot.yml to
test_no_consumer_restates_the_version_or_the_bound's file mapping.

Out of scope: changing the pinned setuptools version or its hash; touching
sidecar/conda-lock.yml; anything about the tMAVEN vendored code itself; removing the
compatibility wheel (that is the separate removal trigger already documented in the file,
and it fires only when tMAVEN stops importing pkg_resources). Also out of scope: adding
ignore entries for anything other than setuptools; any change to the github-actions
entries; and rewording the stale # FIVE consumers, not the three #218 names and not the four the first version of this test found comment above that mapping, or the compatibility
file header's parallel "a review found a fifth" sentence — both are pre-existing and
correcting them here would be scope breach.

Size estimate

S — up to 150 added lines. Raised from XS during grooming: two new tests written to this
repository's standard (each carries the rationale docstring the surrounding module uses),
plus the config entry with its explanatory comments and the compatibility-file header edit,
does not credibly fit in 50.

Risk level

high — the lane, and a Greptile credit is worth spending if the seat has budget

Does this need an architecture decision record?

no

Dependencies / blockers

none

Related work / file overlap

#444 (closed, the instance that prompted this), #212 (the idealization failure the pin
prevents), #217 (extracts the packaging.yml install-smoke into one checked-in script that
release.yml also calls; release.yml triggers only on a v* tag push and dispatch
(.github/workflows/release.yml:22-26), so the pull-request picture above is unchanged
either way — independent of this issue and not a dependency of it), #452 (the
setup-micromamba v3.2.1 failures; it names this issue as a parallel case, and pinning the
action back — with the github-actions ignore entries that would require in both
entries — is one of its stated non-goals, so it does not edit .github/dependabot.yml and
there is no file overlap). Files: .github/dependabot.yml, tests/test_setup_sidecar.py,
packaging/setuptools-compatibility.txt.

Target area

CI / repository governance

Milestone

none / cross-cutting

Required safety attestations

  • I searched existing issues and this is not a duplicate.
  • I am not reporting a vulnerability publicly; security reports use the private advisory flow.
  • I included no secrets or private, raw, unlicensed, user, or lab data.

Grooming status — 2026-08-13

  • Status: unblocked.
  • Open dependencies: none.
  • Autonomy: agent-can-do-alone.
  • Chosen design — the ignore shape: ignore setuptools entirely in the pip entry —
    no versions: and no update-types: narrowing. The pin is to one artifact by digest, so
    any automated edit to that line is wrong whether or not it lands inside the bound: 80.9.0
    is already the newest release below 81, and even a hypothetical in-bound 80.9.1 would need
    a fresh pip hash and a deliberate two-value edit. Verified safe because setuptools
    appears in no other pip manifest in the repository.
  • Chosen design — where the 81 lives: as a literal in the new guard in
    tests/test_setup_sidecar.py. This has to be settled here rather than argued on a
    risk:high pull request, because packaging/setuptools-compatibility.txt:4-6 declares
    itself "THE single source of truth … nothing may re-state the version or the bound
    anywhere else", scripts/setup_sidecar.py:127-128 repeats it ("Parsed rather than
    restated"), and test_no_consumer_restates_the_version_or_the_bound's docstring (:141)
    names hard-coding <81 as "the third defect in build(packaging): pin and hash the bundled setuptools wheel, with one source of truth #218". It is nonetheless already the
    repository's own practice: .github/workflows/packaging.yml:241 and :293 hold
    < (81,) as a literal on a non-comment line and pass that test, because the test
    forbids the requirement spelling — its regex is
    setuptools\s*(==|<=|>=|<|>|~=)\s*[0-9] (tests/test_setup_sidecar.py:170) — and not a
    numeric comparison. int(version.split(".")[0]) < 81 introduces no such spelling. The
    assertion message spells the bound as prose ("below 81; setuptools 82.0.0 removed
    pkg_resources") rather than as setuptools<81 — not because that guard would catch it,
    since its mapping (tests/test_setup_sidecar.py:152-157) does not include
    tests/test_setup_sidecar.py and a literal there is invisible to it today, but because of
    the header's rule quoted above and because a message is quotable: a mapped consumer that
    later pastes it would fail the guard for a reason nobody would look for here. The version
    number is still read from the file and never restated.
  • Chosen test home: the new tests go in tests/test_setup_sidecar.py, not
    tests/test_marker_contract.py. The latter already parses this config and holds the yaml
    loader, but whether it counts as one of ADR-0064's frozen "agent test modules" is
    unsettled — the phrase is enumerated nowhere — and this issue must not be the place that
    decides it. test_setup_sidecar.py is unambiguously outside the agent layer and already
    owns the compatibility-file contract; import yaml is already used by six other test
    modules, so it adds no dependency.
  • Review risk: high. .github/dependabot.yml validates all-or-nothing, so a malformed
    edit silently disarms every ecosystem rather than failing loudly — including the action-SHA
    updates that feed the required checks.
  • Terminal condition: PR-ready handoff. The config-error banner can only be read after
    the change is on the default branch, and the repository owner merges knowing that.

Acceptance criteria

  • .github/dependabot.yml's pip entry carries an ignore list holding exactly one
    entry, dependency-name: setuptools, with no versions: and no update-types: key.
    The explanation — why the pin is deliberate, and the removal trigger — is written as
    YAML comments: #-leading lines are the only ones
    test_no_consumer_restates_the_version_or_the_bound skips (:176), so the literal
    setuptools<81 written on a data line would fail that test once this file is inside
    its mapping. A bound written as YAML dataversions: ["<81"] — would slip past
    that regex unnoticed, since the regex needs setuptools adjacent to a comparator on
    the same line, which is a second reason the entry must carry no versions: key at all.

  • A new test in tests/test_setup_sidecar.py loads .github/dependabot.yml with
    yaml.safe_load and fails if the pip entry's ignore list does not name
    setuptools, or names it with a versions/update-types narrowing.
    Currently-failing demonstration, recorded in the PR: run the new test against the
    tree before the config edit — it fails, because the pip entry has no ignore key
    at all — then apply the config edit and it passes. Quote both runs.

  • tests/test_setup_sidecar.py asserts the parsed major version is below 81 —
    int(version.split(".")[0]) < 81 on the version setuptools_requirement() returns,
    which adds no dependency — and the assertion message names the pkg_resources
    removal in setuptools 82.0.0 and the bound as prose, per the second Chosen-design
    clause above, rather than only the file format. Currently-failing demonstration,
    recorded in the PR:
    temporarily change only the final requirement line of
    packaging/setuptools-compatibility.txt to setuptools==83.0.0 with a syntactically
    valid 64-hex digest — leave the header intact, or test_the_removal_trigger_is_recorded
    goes red for an unrelated reason — and relax
    test_the_requirement_is_pinned_and_hashed's exact-version (:96) and exact-digest
    (:99) assertions to the new values, which is the "update the assertion to match the
    bump" fix this criterion exists to stop. Then run pytest tests/test_setup_sidecar.py:
    the new bound assertion must be the only red, and its message must contain
    pkg_resources. No other test in that module reads the concrete version — the two
    parser tests write their own file into tmp_path and redirect the module constant with
    monkeypatch. Revert all three edits; git status clean before review is requested.

  • packaging/setuptools-compatibility.txt's comment block names .github/dependabot.yml
    and contains the literal phrase not a consumer, on a line outside the
    "THE CONSUMERS, named rather than counted" list — that list keeps its existing four
    entries (:37-40) unchanged, so the new path cannot be read as a fifth file that reads
    the pin. The same block records that the bound guard this issue adds holds 81 as a
    literal inside an assertion, in the same form .github/workflows/packaging.yml:241 and
    :293 already use. It must not claim that is the only such literal in the tree:
    .github/workflows/sidecar-measure.yml:86 installs "setuptools<81" outright, on a
    non-comment line, in a manual-dispatch workflow the guard does not scan.

  • .github/dependabot.yml is added to
    test_no_consumer_restates_the_version_or_the_bound's file mapping (whose function is
    "may not restate the pin", not "reads the pin"), so the one file whose job is to
    suppress automated edits to the pin is itself scanned for a copy of it — the
    versions: ["<81"] hazard in AC1 is exactly the edit that would put one there.
    Currently-failing demonstration with its negative control, both recorded in the PR:
    add an uncommented setuptools<81 line to .github/dependabot.yml; with the mapping
    entry present the test fails naming that file, and with the same line present but the
    mapping entry removed the test passes — quote both runs, so the mapping addition is
    evidenced as the thing that binds. Revert the injected line. The stale
    # FIVE consumers … comment above the mapping is left exactly as it is (see Scope and
    non-goals): the entry is added because that file may not restate the pin, not as a
    fifth consumer of it.

  • Config validity: the committed reach stops at the parse and the ignore shape above.
    The config-error banner is not asserted and no criterion here pretends to —
    gh api repos/bioedca/tether/dependabot/config returns 404 (checked 2026-08-13), so
    there is nothing for a test to read. Instead the PR body states in one sentence that the
    banner under Insights → Dependency graph → Dependabot is to be read after merge, and the remedy
    if it appears is to revert the entry, because the previous behaviour does not resume on
    its own.

  • Existing coverage stays green, including the two modules that already read the files
    being edited: pytest tests/test_setup_sidecar.py tests/test_marker_contract.py.
    test_marker_contract.py's Dependabot assertions read only the github-actions
    entries (via _dependabot_github_actions_directories, :1028-1046), so an ignore
    key on the pip entry is invisible to them.

  • Full local gates: pre-commit run --all-files and the lane's matrix line from
    AGENTS.md. No docs page and no schema change, so mkdocs build --strict and
    scripts/dump_schema.py --check are not implicated.

  • Ready condition: met; awaiting a fresh exact-scope marker.

Metadata

Metadata

Assignees

No one assigned

    Labels

    dependenciesPull requests that update a dependency (Dependabot)risk:highDeclared risk: high. Routes nothing - every PR walks the same lane. May only increase.size:SDiff budget <=150 changed lines (excl. lockfiles/generated)status:readyReadytype:choreMaintenance

    Projects

    Status
    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions