Skip to content

ci: keep pins.json in sync with pyproject.toml - #9351

Merged
lstein merged 17 commits into
invoke-ai:mainfrom
lstein:ci/pins-consistency-check
Aug 5, 2026
Merged

ci: keep pins.json in sync with pyproject.toml#9351
lstein merged 17 commits into
invoke-ai:mainfrom
lstein:ci/pins-consistency-check

Conversation

@lstein

@lstein lstein commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

The launcher installs InvokeAI's dependencies from two sources that nothing keeps in sync: pins.json (fetched at the release tag, used for the torch index URL in legacy installs) and pyproject.toml/uv.lock. pins.json drifted — its Linux ROCm index stayed at rocm6.3 after pyproject.toml moved to rocm7.1 — so launcher installs on AMD got ROCm 6.3 wheels, which cannot work at all on RDNA4 cards (#9328). #9337 fixed the immediate URL; this PR adds the guardrail so it can't silently drift again.

Changes

  • New scripts/check_pins.py: validates pins.json against an explicit required platform/backend matrix (linux: cpu/cuda/rocm, win32: cpu/cuda, darwin: none — MPS has no separate torch index). Missing entries, missing platform sections, unknown platforms, unsupported backends, and URLs that disagree with the matching torch-<backend> [[tool.uv.index]] in pyproject.toml all fail the check. A missing entry matters as much as a stale one: the launcher's schema makes every backend key optional and it only passes --index when the entry exists, so an absent entry silently falls back to the default PyPI index.
  • New tests/test_check_pins.py: removes each required entry in turn (and each platform section) and asserts a nonzero exit, plus stale-URL, unknown-platform and unsupported-backend cases. The parametrized cases are generated from the matrix itself so coverage can't silently shrink.
  • pins.json: adds the win32.cpu entry the new matrix exposes as missing. Without it a Windows "No dedicated GPU" legacy install gets no --index and pulls the CUDA-bundled PyPI torch wheel rather than the +cpu build; linux.cpu has always been there.
  • uv-lock-checks workflow: runs the new check, triggered by changes to pyproject.toml, uv.lock, pins.json, or the check script.
  • python-tests workflow: path filter extended to scripts/** and pins.json, so editing the checker runs its own tests.
  • Manual install docs: ROCm-specific caution on the --torch-backend=rocm7.1 tab, with the "torch<2.12" workaround for [bug]: Release 6.13.5rc1 not installing ROCm 7.1 as expected #9328.

On the torch cap (removed)

Earlier revisions of this PR capped the base requirement at torch<2.12. That has been dropped — it was applied to sys_platform != 'darwin', so it would have landed in published metadata for every Linux and Windows install (CUDA, CPU-only, Linux ARM64) on the strength of a ROCm-only report, and it protected none of the paths I claimed:

  • Legacy (pre-6.14) launcher installs resolve against the released version's frozen PyPI metadata, not this pyproject.toml.
  • Bootstrap (6.14+) launcher installs pass invokeai[cpu|cuda|rocm], and every one of those extras pins torch exactly, so the base range is never consulted.
  • Manual installs (--torch-backend=<x>, no extra) are what actually resolve the base range — on all platforms, not just ROCm.

There is no environment marker for "resolving against the rocm7.1 index", so the constraint can't be scoped in metadata; it now lives in the manual install docs instead. pyproject.toml's specifier is unchanged from main, so uv.lock is not touched by this PR.

Caveat

Because the launcher fetches pins.json at the release tag, none of this retroactively fixes already-tagged releases (e.g. v6.13.5.rc1 carries rocm6.3 forever). It takes effect from the next tag onward.

QA

  • python3 scripts/check_pins.py passes on the fixed files.
  • pytest tests/test_check_pins.py — 19 passed.
  • uv lock --locked passes; uv.lock is byte-identical to main.
  • Workflow YAML validated.

Related: #9328, #9337, invoke-ai/launcher#131

🤖 Generated with Claude Code

lstein and others added 2 commits July 11, 2026 11:54
pins.json is consumed only by the Invoke Launcher (fetched at the release
tag) to pick the torch wheel index for legacy installs. Nothing in-repo
references it, so it silently drifted from pyproject.toml: its Linux ROCm
index stayed at rocm6.3 after the torch-rocm index moved to rocm7.1,
causing the launcher to install ROCm 6.3 wheels (invoke-ai#9328, launcher#131).

- Add scripts/check_pins.py, which fails if any torchIndexUrl entry in
  pins.json differs from the matching [[tool.uv.index]] URL in
  pyproject.toml.
- Run it from the uv-lock-checks workflow, triggered by changes to any of
  pyproject.toml, uv.lock, pins.json, or the check script.
- Bump pins.json rocm index to rocm7.1 (matches invoke-ai#9337) so the check
  passes.
- Bump the workflow's uv from 0.6.10 to 0.11.28: uv.lock is already lock
  format revision 3, which 0.6.x cannot parse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
torch 2.12.x+rocm7.1 breaks generation (invoke-ai#9328; 2.11 works, and 2.10 is
what the rocm extra pins). The cap only matters for legacy (pre-6.14)
launcher installs, which resolve the base range live against the pytorch
wheel indexes instead of using uv.lock — with the range open to <3.0,
AMD users could land on the broken 2.12.x. Lockfile-based installs are
unaffected: uv.lock stays at 2.7.1 / 2.10.0+rocm7.1 (no locked versions
changed, only the recorded specifier).

Remove the cap once the 2.12 ROCm incompatibility is diagnosed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lstein
lstein requested a review from blessedcoolant as a code owner July 11, 2026 15:56
@github-actions github-actions Bot added CI-CD Continuous integration / Continuous delivery Root python-deps PRs that change python dependencies labels Jul 11, 2026
@lstein lstein added the 6.14.0 label Jul 11, 2026
@lstein lstein moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap Jul 11, 2026

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • invokeai/scripts/check_pins.py:32-43: The consistency check only validates entries that already exist in pins.json; it does not require every launcher-supported platform/backend entry to be present. Removing torchIndexUrl.linux.rocm still prints pins.json is consistent with pyproject.toml and exits 0. The launcher's schema (main over in the launcher repo) permits backend keys to be absent, and its legacy install path only adds --index when the selected entry exists. An omitted ROCm entry therefore silently falls back from the ROCm wheel index, causing AMD installations to resolve an unsuitable build or fail. This recreates the exact installation failure the new guardrail is intended to prevent. To expose this issue, add a test that removes each required CPU, CUDA, or ROCm entry in turn and asserts that the checker exits nonzero; the implementation should validate an explicit required platform/backend matrix, not merely iterate existing entries.

JPPhoto and others added 3 commits July 15, 2026 13:52
Resolved uv.lock conflict by regenerating from main's lock with `uv lock`
rather than merging hunks. The only substantive change is the recorded
`torch` specifier (>=2.7.0,<2.12); no package versions moved.
Addresses JPPhoto's review. The checker only iterated the entries that
already existed in pins.json, so deleting torchIndexUrl.linux.rocm still
exited 0 — recreating the exact silent-fallback failure the guardrail was
meant to prevent (the launcher only passes --index when the entry exists,
and its schema makes every backend key optional).

- Validate pins.json against REQUIRED_BACKENDS, an explicit matrix of the
  platform/backend combinations the launcher can select and Invoke supports.
  Missing entries, missing platform sections, unknown platforms and
  unsupported backends are now all errors, alongside the existing URL check.
- Split the logic into check_pins(), with main() taking a repo root, so the
  exit code is directly testable.
- Add tests/test_check_pins.py: removes each required entry in turn and
  asserts a nonzero exit, plus stale-URL, unknown-platform and
  unsupported-backend cases.
- pins.json: add the win32.cpu entry the matrix exposes as missing. Without
  it, a Windows "no dedicated GPU" legacy install gets no --index and pulls
  the CUDA-bundled PyPI torch wheel instead of the +cpu build. Linux already
  had this entry.
- python-tests: trigger on scripts/** and pins.json so these tests actually
  run when the checker or the pins change.
- Revert the uv 0.6.10 -> 0.11.28 bump in uv-lock-checks: 0.6.10 parses the
  revision-3 lock fine (verified), so the stated justification was wrong and
  the bump left this workflow out of step with python-tests.
@github-actions github-actions Bot added the python-tests PRs that change python tests label Jul 24, 2026
@lstein

lstein commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch — you're right, and the failure mode was exactly as you described: del pins["torchIndexUrl"]["linux"]["rocm"] printed "consistent" and exited 0, because the loop only walked entries that were already there. Fixed at 7f8809b.

Explicit required matrix. scripts/check_pins.py now validates against a declared matrix rather than iterating what happens to exist:

REQUIRED_BACKENDS: dict[str, set[str]] = {
    "win32": {"cpu", "cuda"},
    "linux": {"cpu", "cuda", "rocm"},
    "darwin": set(),
}

It's an exact match in both directions, so four classes of drift now fail:

  • missing entrytorchIndexUrl.linux.rocm removed → exit 1, with the message spelling out the consequence ("the launcher would omit --index for rocm installs on linux and resolve torch from the default index")
  • missing platform section — including darwin, whose empty-but-present section is still required
  • unsupported backend — e.g. a win32.rocm entry, which PyTorch publishes no wheels for and which pyproject.toml marks sys_platform == 'linux'
  • stale URL — the original [bug]: Release 6.13.5rc1 not installing ROCm 7.1 as expected #9328 case, unchanged

I derived the matrix from the launcher rather than guessing: getTorchPlatform() in src/main/util.ts maps all four GPU choices to cuda/rocm/cpu on every OS, and InstallFlowStepConfigureGpuPicker.tsx offers all four regardless of platform — so every cell the launcher can actually reach and that Invoke supports is required. darwin is empty on purpose (MPS has no separate torch index) and that intent is now asserted, not implied by absence.

Tests. New tests/test_check_pins.py (19 cases). The parametrized ones are generated from check_pins.REQUIRED_BACKENDS itself, so they can't silently stop covering a backend if the matrix is edited, and there's a separate assertion that the matrix still contains linux cpu/cuda/rocm and win32 cpu/cuda — narrowing the matrix to make a failure go away trips that. To make exit codes directly testable, main() now takes a repo root and the logic moved into a pure check_pins(pins, pyproject).

Also added scripts/** and pins.json to the python tests workflow's path filter — without that, editing check_pins.py wouldn't have run its own tests. (This incidentally also fixes the same gap for the existing test_docs_json_export.py.)

One consequence worth flagging: the new matrix immediately caught a second missing cell, win32.cpu, so this PR now adds it to pins.json. Today a Windows "No dedicated GPU" legacy install gets no --index and resolves torch from the default PyPI index — which on Windows is the CUDA-bundled wheel, not the +cpu build. Linux has had linux.cpu all along; this just brings Windows in line. If that was a deliberate omission rather than an oversight, say the word and I'll drop cpu from the win32 row instead.

Unrelated to your review, two other changes in this push:

  • uv.lock conflict resolved by regenerating from main's lock with uv lock rather than merging hunks. uv lock --locked passes; the only substantive change is the recorded torch specifier — no package versions moved.
  • Reverted the uv 0.6.100.11.28 bump in uv-lock-checks.yml. I'd justified it in the PR description by claiming uv 0.6.x can't parse a revision-3 lockfile. I actually tested it this time — 0.6.10 resolves this lock fine — so the premise was wrong and the bump only left this workflow out of step with python-tests. The workflow diff is now just the pins check.

@lstein
lstein requested a review from JPPhoto July 24, 2026 23:07

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One more thing found:

  • invokeai/pyproject.toml:52-54: The reported incompatibility is specifically torch 2.12.x+rocm7.1, but the new sys_platform != 'darwin' upper bound is emitted into package metadata for every Linux and Windows installation, including CUDA, CPU-only, and Linux ARM64 environments that do not use ROCm. Direct/manual installs in those environments can therefore reject an otherwise required torch>=2.12, downgrade an existing Torch installation, or fail dependency resolution; this also contradicts the PR's claim that the cap only affects legacy launcher installs. Test: resolve and smoke-test InvokeAI with Torch 2.12 on representative Windows CUDA, Windows CPU, Linux CUDA, Linux CPU, and Linux ARM64 environments, and either scope the cap to the affected legacy ROCm resolution path or document evidence that Torch 2.12 is incompatible on every capped platform.

… docs

Addresses JPPhoto's second review. The cap was applied to the base
`sys_platform != 'darwin'` requirement, so it landed in published package
metadata for every Linux and Windows install — CUDA, CPU-only and Linux
ARM64 included — despite invoke-ai#9328 only ever reporting a ROCm 7.1 failure.

My original justification ("only affects legacy pre-6.14 launcher installs")
was wrong in both directions:

- It never protected legacy launcher installs. Those install an
  already-released version, resolving against that release's frozen PyPI
  metadata, not this pyproject.
- It never protected bootstrap (6.14+) launcher installs either. Those pass
  `invokeai[cpu|cuda|rocm]`, and every one of those extras pins torch
  exactly, so the base range is never consulted.
- What it did affect is manual installs, which per docs/start-here/manual
  use `--torch-backend=<x>` with no extra and therefore resolve the base
  range directly — on every platform, not just ROCm.

There is no environment marker for "resolving against the rocm7.1 index", so
the cap cannot be scoped in metadata. Constrain it where it can actually be
targeted instead: the `rocm` extra already pins torch==2.10.0+rocm7.1, and
the manual install docs now carry a ROCm-specific caution with the
`"torch<2.12"` workaround for users following the `--torch-backend=rocm7.1`
path.

pyproject.toml returns to `torch>=2.7.0,<3.0`, so uv.lock is untouched by
this PR again.
@lstein

lstein commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

You're right, and it's worse than a too-broad marker — the cap wasn't protecting anything. Removed at 3a072fe.

I went and checked the launcher source rather than defending my own description, and the justification I wrote ("the cap only affects legacy pre-6.14 launcher installs") was wrong in both directions:

  • It never protected legacy launcher installs. Those install an already-released version, so uv pip install invokeai==6.13.x resolves against that release's frozen PyPI metadata — not the pyproject.toml on main. Nothing I put here can reach them.
  • It never protected bootstrap (6.14+) installs either. getInvokeExtras() always appends the torch platform on non-darwin, so the launcher installs invokeai[cpu|cuda|rocm], and each of those extras pins torch exactly (==2.7.1+cpu, ==2.7.1+cu128, ==2.10.0+rocm7.1). The base range is never consulted on that path.
  • What it actually hit is manual installs — exactly the environments you listed. docs/start-here/manual step 7 has users pass --torch-backend=cu128 / =cpu / =rocm7.1 with no extra, so the base range is the resolution path for Windows CUDA, Windows CPU, Linux CUDA, Linux CPU and Linux ARM64 alike. A blanket <2.12 would reject torch 2.12 for all of them on the strength of a ROCm-only bug report.

So the cap imposed the cost you identified and bought nothing in return. pyproject.toml is back to torch>=2.7.0,<3.0, and since that's byte-identical to main, uv.lock is out of this PR's diff entirely — the lockfile merge conflict goes away with it.

On scoping rather than removing: there's no environment marker that expresses "resolving against the rocm7.1 index", so the constraint genuinely can't be scoped in package metadata. I put it where it can be targeted — a caution in the manual install docs, on the --torch-backend=rocm7.1 tab, with the "torch<2.12" workaround, noting it's ROCm-only. The supported AMD path (launcher → invokeai[rocm]) was already safe via the exact pin, so that's the only reachable gap left.

I didn't run the torch 2.12 smoke-test matrix you suggested — with the cap gone there's nothing left to justify, since we're back to main's existing constraint rather than adding a new one. If you'd rather have the ROCm caveat tracked as an issue against the rocm extra instead of living in the docs, happy to move it.

The PR is now purely the pins.json guardrail plus that docs note; title and description updated to match.

@github-actions github-actions Bot added the docs PRs that change docs label Jul 27, 2026
@lstein lstein changed the title ci: keep pins.json in sync with pyproject.toml; cap torch <2.12 ci: keep pins.json in sync with pyproject.toml Jul 27, 2026
@lstein
lstein requested a review from JPPhoto July 27, 2026 20:34

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A few housekeeping things:

  • docs/src/content/docs/start-here/manual.mdx:135-140: The caution says to retain the workaround "until that is resolved" but links to issue #9328, which is already closed as completed and primarily tracked the stale ROCm 6.3 launcher index. The separate Torch 2.12 generation failure therefore has no open tracking or removal condition, so readers may interpret the closed issue as evidence that the workaround is obsolete. Test: follow the rendered documentation link and verify it targets an open issue dedicated to the Torch 2.12 plus ROCm 7.1 failure, with reproduction and resolution criteria; otherwise create that issue and update the link.

  • docs/src/content/docs/start-here/manual.mdx:142: The statement that every other backend "work[s] fine with torch 2.12" is stronger than the available evidence. The linked report establishes only a ROCm failure, and the PR discussion explicitly says the proposed CUDA, CPU, Windows, and ARM64 smoke-test matrix was not run. This leaves a user-facing cross-platform compatibility guarantee unsupported. Test: perform generation smoke tests with Torch 2.12 on Windows CUDA, Windows CPU, Linux CUDA, Linux CPU, Linux ARM64, and macOS MPS, or narrow the wording to say only that the documented workaround applies to the reported ROCm case.

invoke-ai#9328 was closed as completed once pins.json was corrected to the rocm7.1
index; the separate torch 2.12.x generation failure reported in that thread
had no open tracking issue, so the caveat had no removal condition. Filed
invoke-ai#9410 for it and link there instead.

Also narrow the cross-platform claim: we have no torch 2.12 smoke tests on
CUDA/CPU/Windows/ARM64/MPS, so say only that the workaround applies to the
reported ROCm case rather than asserting other backends are unaffected.
@lstein

lstein commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Both housekeeping items addressed in 1520cd1.

1. Stale/closed issue link. You're right — #9328 was closed as completed once pins.json was corrected to the rocm7.1 index, and the torch 2.12 generation failure reported further down that thread never got its own issue. Filed #9410 for it and pointed the caution there instead. That issue carries the evidence we actually have (fishd72's report: 2.12.1 fails, 2.11.0 works, reproduced by a second AMD user; ComfyUI runs 2.12.1 on the same ROCm 7.1 stack), an explicit "what is not established" section, a repro, and a resolution criterion that names this docs block as the thing to remove when it closes. The pyproject.toml comment now points at #9410 too, for the same reason.

2. Overreaching compatibility claim. Agreed, and I didn't run that matrix. The line now reads:

This workaround applies to the reported ROCm case only; it isn't needed for the other torch backends.

That states the workaround's scope without asserting anything about torch 2.12's behaviour on backends we haven't tested.

@keturn

keturn commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Could we just… not have pins.json anymore?

All it contains is PyPI index URLs, but pyproject.toml's tool.uv.sources is managing those itself these days, isn't it?

@JPPhoto
JPPhoto self-requested a review July 31, 2026 01:55

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • invokeai/scripts/check_pins.py:50-95: Checker ignores pins.json's launcher-critical python field. Changing project.requires-python to >=3.13,<3.14 while keeping "python": "3.12" returns no errors. Launcher creates Python 3.12 before installing incompatible package metadata, causing install failure despite green consistency check. Test: remove python, then set it outside project.requires-python; both cases must return nonzero.

  • docs/src/content/docs/start-here/manual.mdx:142-143: The claim that it "isn't needed" for other backends isn't verified, so I'd leave that clause off. Supporting evidence: linked issue #9410 explicitly says CUDA, CPU, Windows, ARM64, and MPS were not tested and makes no claim beyond ROCm. Of course, you could always test on every platform combination out there...

JPPhoto and others added 3 commits August 2, 2026 00:28
The launcher's zod schema declares `python: z.string()` (required) and passes
pins.python to `uv venv --python` on both the bootstrap and legacy install
paths, before anything is installed. Bumping project.requires-python without
bumping pins.json therefore builds a venv the package metadata then rejects -
and the check passed, because it only looked at torchIndexUrl.

check_pins.py now also checks that pins.json's python satisfies
project.requires-python. It stays stdlib-only (CI runs it with a bare python3),
so the specifier handling is hand-rolled and deliberately narrow: ==, !=, >=,
<=, > and < against dotted numeric versions, zero-padded so 3.12 == 3.12.0.
Anything it cannot evaluate is reported rather than guessed at. Differentially
tested against packaging over 1014 version/operator/bound combinations with no
divergences.

Hardening found while attacking the above:

- check_python never raises. requires-python is optional in PEP 621 and can be
  mistyped as a bare TOML float; either used to throw out of check_pins(),
  which runs before the torchIndexUrl loop and so suppressed the ROCm drift
  check this script exists for.
- A present-but-null platform section ("linux": null) passed silently, because
  .get() cannot tell it from an absent one.
- Version components are bounded, so a pin like 3.12.99999999 - which satisfies
  every specifier but which uv cannot resolve to an interpreter - is rejected.
- A trailing comma in requires-python is no longer read as an empty clause.

Also drop the unverified 'isn't needed for the other torch backends' clause
from the ROCm caveat, per review: nothing on CUDA/CPU/Windows/ARM64/MPS was
smoke-tested with torch 2.12.
@lstein

lstein commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Both addressed in 402e4ba.

1. python was unchecked. Confirmed, and it bites harder than the torchIndexUrl gap did. In the launcher, zPins declares python: z.string() — required, not optional — and pins.python is what gets passed to uv venv --python <version>, on both the bootstrap and the legacy path. The venv is built before anything is installed, so a mismatch doesn't fail fast: it fails at the very last step of a long install, after python has been downloaded and the venv created.

check_pins.py now validates it, and both cases you named exit nonzero:

  • python removed:

    pins.json is missing the 'python' field; the launcher uses it to decide which python version to build the venv with, and it must satisfy pyproject.toml's requires-python

  • requires-python bumped to >=3.13,<3.14 with python left at 3.12 (your exact scenario):

    pins.json python is '3.12' but pyproject.toml requires-python is '>=3.13, <3.14' (fails '>=3.13'); the launcher would create a python 3.12 venv and then install package metadata that rejects that interpreter

The script stays stdlib-only, since CI runs it with a bare python3 — so the specifier handling is hand-rolled and deliberately narrow. It evaluates ==, !=, >=, <=, >, < against dotted numeric versions with zero-padding (3.12 == 3.12.0), and anything it can't evaluate — ~=, .* wildcards, epochs, pre-releases — is reported as an error rather than guessed at, so an unparseable specifier fails loudly instead of quietly passing. I differential-tested the comparison against packaging over 1014 version × operator × bound combinations: no divergences.

Things I found while attacking my own patch, all fixed in the same commit — worth calling out since two of them were regressions the new check introduced:

  • check_python now never raises. requires-python is optional in PEP 621, and it's easy to typo as a bare TOML float (requires-python = 3.12). Either one threw out of check_pins() — which runs before the torchIndexUrl loop, so the new check could suppress the ROCm drift check the script exists for. There's now a regression test asserting both errors appear together.
  • A present-but-null platform section ("linux": null) passed silently — .get() can't distinguish it from an absent key. That's the same silent-degradation class as the original bug, so it's fixed and tested for every platform.
  • Version components are bounded, so "python": "3.12.99999999" is rejected. It satisfies every specifier, but uv venv --python 3.12.99999999 finds no interpreter — green CI, broken install.
  • A trailing comma in requires-python (">=3.11, <3.13,") is legal and was being read as an empty, unevaluatable clause. Now skipped.
  • _satisfies raises on an unknown operator instead of falling through to <. I mutation-tested the suite: dropping the != or <= branch used to leave all tests green, because the fallthrough happened to give the right answer for the cases I'd written. Both are pinned now, along with the other four.

New tests cover: missing python; requires-python moved off the pin; absent, non-string and vacuous requires-python; python set to 3.9 / 3.10 / 3.13 / 4.0; ten non-version values; an unevaluatable specifier; null/non-object sections at both levels; and a comparison table with a guard asserting every supported operator appears in it.

2. Docs clause. Dropped. The sentence is now just:

This workaround applies to the reported ROCm case only.

And no, I'm not going to test every platform combination out there. 🙂

@lstein

lstein commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

@keturn I looked at this while working out what the checker needed to cover, and the short answer is "not yet, and not unilaterally from this side."

Two things stand in the way:

python has no equivalent in pyproject.toml. The launcher needs one concrete interpreter version to hand to uv venv --python <version>, and it needs it before it installs anything. requires-python is a range (>=3.11, <3.13), not a choice — it can't answer "which python do I build the venv with". That field is doing real work on both the bootstrap and legacy install paths.

torchIndexUrl is still live on the legacy path. You're right that it's no longer the resolution authority for bootstrap installs (>= 6.14) — those uv sync against the release's pyproject.toml + uv.lock, and the launcher's own schema comment says as much. But install-manager.ts still reads pins.torchIndexUrl[systemPlatform][torchPlatform] for the pre-6.14 path and passes it as --index. tool.uv.sources doesn't help there: the legacy path installs the published release's wheel metadata, and [tool.uv.index] / [tool.uv.sources] are uv workspace configuration that isn't carried in wheel metadata at all.

There's also a sequencing constraint. zPins marks both fields required and the launcher fetches pins.json from the release tag, so any release missing the file fails to install on every launcher version that predates its removal — not just old launchers being stubborn, but every user who hasn't updated. Retiring it means: drop the legacy install path (or stop reading pins on it), ship that launcher release, wait for adoption, and only then remove the file from Invoke releases.

So I'd say yes, it should eventually go — but it's a launcher-side change first, and until then this guardrail is guarding something that's still load-bearing. Happy to file that as a follow-up if it's worth tracking.

@keturn

keturn commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

I was questioning whether we were at the point where updating launcher's approach would be a better investment than adding this layer to keep the moving parts in sync. Now I see that launcher has been making steps in this direction, which is encouraging.

Frankly, I wasn't prepared for this part:

[tool.uv.index] / [tool.uv.sources] are uv workspace configuration that isn't carried in wheel metadata at all.

Installing something off of PyPI is entirely blind to not only the pyproject.toml's uv.index but also the uv.lock as well? So much for my hopes that we could just write uv tool install invokeai and have it take care of everything!

@JPPhoto
JPPhoto self-requested a review August 3, 2026 11:32

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • invokeai/scripts/check_pins.py:57-64,140-145: Accepts bogus versions such as 3.12.9999; checker passes, but launcher cannot create that interpreter and installation fails. Test: set pins.json python to 3.12.9999; python3 scripts/check_pins.py returns 0 while UV_CACHE_DIR=/tmp/uvcheck uv python find 3.12.9999 fails.

  • invokeai/scripts/check_pins.py:24: Claims python scripts/check_pins.py works from anywhere, but the relative path only works from repository root. Test: run cd /tmp && python scripts/check_pins.py; Python reports the file does not exist. Either update the claim or make this work from anywhere.

The python pin was validated with a bounded regex, which made a bogus
version merely unlikely rather than impossible: "3.12.9999" passed, and
`uv python find 3.12.9999` finds no interpreter, so CI stayed green
while the launcher's install broke at the last step.

Replace the heuristic with two checks that make the bad values
unrepresentable:

- The pin must be exactly major.minor. `uv venv --python 3.12` already
  resolves to the newest 3.12.x, `--python 3.12.7` demands one exact
  build that can leave uv's index, and the launcher's reinstall check
  compares only major()/minor() of this field anyway. That removes the
  whole 3.12.<anything> family by shape.
- Its major.minor must appear in project.classifiers. requires-python
  says which versions the metadata allows; the classifiers say which
  ones we ship for. Without it, an unreal version inside an open-ended
  requires-python (">=3.11" and a "3.99" pin) satisfies every clause and
  no digit bound catches it.

Also fix the module docstring: only the path handed to python is
location-independent, not the working directory it is typed in.

Hardening found by attacking the above:

- Bound the classifier pattern's digits like its two siblings. int()
  refuses a string of more than 4300 digits, so an unbounded pattern let
  a classifier raise straight out of check_python - which runs first and
  would have taken the torchIndexUrl checks with it.
- Guard the pins.json top level. A non-object raised AttributeError from
  pins.get(), with the same suppressing effect (pre-existing).
- Reject leading zeros, so the pin has exactly one spelling.
- Report dynamic classifiers as dynamic rather than as absent.
- De-duplicate classifier versions so a repeat is not listed twice.

Tests assert the reason a patch pin is rejected, not just the exit code:
the classifier check rejects those too, so without that a loosened
version pattern would leave the suite green while advising the user to
add a classifier for "3.12.7".
@lstein

lstein commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Both addressed in bf690d1.

1. Bogus versions. Confirmed — 3.12.9999 sailed through, and bounding the digit count the way I did was a heuristic dressed up as a check, so it was only ever going to move the goalposts (you'd have found 3.99 next). Replaced with two checks that make the bad values unrepresentable rather than unlikely:

  • The pin must be exactly major.minor. No patch component at all. uv venv --python 3.12 resolves to the newest 3.12.x; --python 3.12.7 demands one exact build, which freezes users on an unpatched interpreter and breaks outright once that build leaves uv's index. And the launcher itself only compares major()/minor() of pins.python against the existing venv when deciding whether to reinstall python (install-manager.ts:300-301), so a patch component is inert there anyway. That kills the whole 3.12.<anything> family by shape, including your 3.12.9999.
  • The major.minor must appear in project.classifiers. requires-python says which versions the metadata allows; the classifier list says which ones this project actually ships for. That closes the case a digit bound can't: 3.99 satisfies an open-ended >=3.11 and no digit count will ever catch it, but nothing declares support for it.

Your repro now:

$ python3 scripts/check_pins.py
pins.json is out of sync with pyproject.toml:
  - pins.json python is '3.12.9999'; expected a major.minor version the launcher can hand to
    `uv venv --python`, like '3.12' (a patch component is deliberately not accepted - uv already
    picks the newest patch for a major.minor version)

and with requires-python = ">=3.11" and python = "3.99":

  - pins.json python is '3.99' but pyproject.toml's classifiers declare support only for 3.12;
    add the classifier if that version is really supported, otherwise the launcher builds the
    venv on an interpreter this project does not ship for

One consequence worth flagging since it's a policy call, not just a bug fix: bumping the pin now requires the matching classifier. Today classifiers lists only 3.12 while requires-python allows 3.11, so pinning 3.11 would fail until someone adds Programming Language :: Python :: 3.11. I think that's the right failure — the classifiers are what we publish to PyPI, so they should be true — but say the word if you'd rather it be advisory.

Missing classifiers are an error rather than a skip, for the same reason a missing requires-python is: silently degrading to a weaker check is the failure mode this whole script exists to prevent.

2. Docstring. You're right, only the python3 argument is location-independent, not the shell you type it in. The claim now reads:

The repo root is derived from this file's own location, so the working directory
does not matter — only the path you hand to python does::

python3 scripts/check_pins.py                     # from the repo root
python3 /path/to/InvokeAI/scripts/check_pins.py   # from anywhere else

and there's a test that runs the script as a subprocess from a tmp_path cwd, so the "cwd is irrelevant" half of that claim is now pinned rather than asserted.

New tests: 3.12.9999 / 3.12.0 / 3.12.7 / 03.12 rejected; a pin outside the classifier list; an unreal version inside an open-ended requires-python; classifiers deleted, malformed, or dynamic; classifiers that merely contain a version (:: 3 :: Only, :: 3.12 :: Only, :: 3.1.4) not read as declaring it; and the cwd-independence check above. 110 tests, all green.

Things I found attacking the above, all in the same commit:

  • The classifier pattern's digits are bounded like its two siblings now. int() refuses a string of more than 4300 digits, so an unbounded pattern let a classifier of 4400 nines raise straight out of check_python — which runs first, so it would have taken the torchIndexUrl checks down with it. Same class as the round-4 regression, so there's a test asserting the ROCm drift is still reported with that classifier present.
  • A pins.json whose top level isn't an object (["3.12"]) raised AttributeError from pins.get(), with the same suppressing effect. Pre-existing rather than new, but same contract, so fixed here.
  • Leading zeros: "03.12" passed end-to-end. uv venv --python 03.12 does work, so it wasn't a break, but the pin now has exactly one spelling.
  • dynamic = ["classifiers"] is legal PEP 621 and would have failed with "declares no classifier", which is misleading. It now says the classifiers are dynamic and that the script needs teaching.
  • The patch-pin tests asserted only the exit code, and the classifier check rejects those values too — so reverting the version pattern left all of them green. They assert the reason now.

Mutation-tested: 12 mutants across the new checks, all killed.

@lstein
lstein requested a review from JPPhoto August 5, 2026 13:24

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is approved!

For a follow-up PR:

  • invokeai/scripts/check_pins.py:200-220: Treats optional, non-normative Python classifiers as an authoritative allowlist. A valid requires-python = ">=3.11, <3.13" with pins.json set to 3.11 fails unless a classifier is also added, even though package metadata permits 3.11. Test: set pins.json python to 3.11 without changing pyproject.toml; checker exits 1 solely because classifier 3.11 is absent.

…heck

# Conflicts:
#	.github/workflows/uv-lock-checks.yml
@lstein
lstein merged commit 8486d47 into invoke-ai:main Aug 5, 2026
16 checks passed
@lstein
lstein deleted the ci/pins-consistency-check branch August 5, 2026 16:36
@lstein
lstein restored the ci/pins-consistency-check branch August 5, 2026 16:37
@lstein
lstein deleted the ci/pins-consistency-check branch August 5, 2026 16:38
@lstein

lstein commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks! The conflict in uv-lock-checks.yml (against #9357, which landed in the meantime) is resolved in 5c5161c — both checks kept, each with its own path filter.

Follow-up opened as #9466. You're right, and the framing helped: I was using a documentation field to answer an installability question. requires-python is the only authority the script fails on again; a pin the classifiers don't mention is now a warning that names the classifier to add, and their absence, malformation or deferral to project.dynamic isn't a finding at all.

Your exact repro now exits 0:

warning: pins.json python is '3.11', which pyproject.toml's classifiers do not mention
(they list 3.12). That is allowed - classifiers are informational, and requires-python is
what gates installation - but consider adding 'Programming Language :: Python :: 3.11' so
the published metadata matches.
pins.json is consistent with pyproject.toml

That gives up one thing, and the docstring says so rather than leaving it implicit: a version satisfying an open-ended requires-python but that no interpreter has (>=3.11 with a 3.99 pin) is no longer caught. That's the case the allowlist was closing, but the price was blocking legal pins — and with the repo's >=3.11, <3.13 there's no reachable gap anyway.

Two things I found attacking that change, both fixed in #9466: Programming Language :: Python :: 3.012 counted as declaring 3.12, since the comparison normalized both sides — not a trove classifier, so it silenced the advisory in exactly the case it exists to name; and the warning loop ran before the errors were printed, so a raise while computing advice would have discarded the error list.

#9466 is stacked on this branch (it needs scripts/check_pins.py), so its diff currently includes this PR's commits. Once this merges I'll rebase it down to its own two.

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

Labels

6.14.0 CI-CD Continuous integration / Continuous delivery docs PRs that change docs python-deps PRs that change python dependencies python-tests PRs that change python tests Root

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

3 participants