Fix CI dependency drift - #16
Open
JeyKip wants to merge 4 commits into
Open
Conversation
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.
Summary
CI was green on
mainbut would have failed on the next run — on both thetestandlintjobs — without a single line of our code changing. Two upstream releases landed after our last commit and silently changed the behaviour ofuv sync --dev, which CI runs with no lockfile.This PR fixes both failures, commits
uv.lockso that dependency changes can only arrive deliberately, and makes CI enforce the lockfile withuv sync --dev --locked.Nothing here is a behaviour change for users of the CLI. The one user-visible path that was broken — the "Did you mean …?" suggestion on an unknown command — is restored to its intended behaviour.
Why this appeared now
Our last commit to
mainwas 2026-04-22. Becauseuv.lockwas gitignored, every CI run re-resolved dependencies from scratch against whatever PyPI served that day. Two releases after 2026-04-22 changed what that resolution produces:main(#15) — CI greenclickdisappears from the environment →testjob breaksselect = ["ALL"]→lintjob breaksBoth are time-triggered, not code-triggered. The green checkmarks on
mainare real; they just describe a dependency set that no longer resolves.Changes
Problem 1 —
ModuleNotFoundError: No module named 'click'Symptom
uv run pytestaborts during collection:Root cause
src/dualentry_cli/cli.pyimportedclickdirectly, butclickwas never declared in[project.dependencies]. It was only ever present as a transitive dependency of typer.typer 0.26.0 (released 2026-05-26) vendored Click into
typer/_clickand dropped it as an external dependency, in PR #1774, merged 2026-05-26. The PR vendors Click 8.3.1 into atyper/_clicksubdirectory.Our constraint was
typer>=0.12,<1.0, so a fresh resolve picks the newest — today 0.27.1 — andclickis simply never installed.The second-order bug
Declaring
clickas a dependency would have fixed the import but left a subtler defect in place.HelpfulGroup.resolve_commandcaughtclick.UsageError, but since 0.26.0TyperGroupraises the vendored exception, which is an unrelated class:typer._click.exceptions.UsageError is click.exceptions.UsageError → False.The
exceptclause would never match, silently turning our custom suggestion handler into dead code and falling back to typer's built-in message. So the fix is to dropclickentirely rather than declare it.Fix
cli.pynow importsUsageErrorfromtyper._click.exceptionsand usestyper.echo(publicly re-exported by typer) instead ofclick.echo.clickis not added as a dependency — it is no longer imported anywhere.>=0.26, becausetyper._clickdoes not exist below it. The previous>=0.12floor was already a false claim: the code could not have run on 0.12–0.25 as written once click was removed from the environment.Why keep the override at all
typerprovides its own suggestions, but at difflib's defaultcutoff=0.6, while ours usescutoff=0.4. Here is a real difference between these two values:bankbank-transfersfixedfixed-assetsprepayvendor-prepaymentsDeleting the override in favour of typer's built-in would have silently regressed all of these.
Regression coverage
This path had no test coverage, which is why the breakage went unnoticed. Added
TestUnknownCommandSuggestionscovering a typo, the short-prefix case that pins the 0.4 cutoff specifically, and an unmatchable input. The tests assert on our wording (Unknown command '…'), so they fail if the handler is ever bypassed and typer'sNo such commandmessage appears instead.Problem 2 — 26 new lint errors
Symptom
uv run ruff check .reports 26 errors — 21 ×CPY001, 5 ×PLR0917— against code nobody had touched.Root cause
ruff 0.16.0 (released 2026-07-23) stabilized both rules out of preview:
CPY001/missing-copyright-notice— tracking issue astral-sh/ruff/issues/19487PLR0917/too-many-positional-arguments— tracking issue astral-sh/ruff/issues/16867Our config uses
select = ["ALL"], which opts in to every rule ruff has and every rule it will ever add. A stabilized preview rule therefore becomes a CI gate with no action on our side. Verified by pinning ruff:ruff check src/ tests/Fix
CPY001→ ignored. The rule requires a copyright header on every file. This project ships noLICENSEfile and declares nolicenseinpyproject.toml, so there is no copyright statement to assert. Adding 21 headers for an undefined license would be worse than not having them.PLR0917→ fixed properly. All five sites were genuine long signatures, and every call site already passed the extra arguments by keyword, so marking them keyword-only with*codifies the convention already in use rather than changing any behaviour:make_resource_app_do_listlist_cmd_transaction_list_transaction_detailTwo
_do_listcall sites were updated to pass keywords.Problem 3 (root cause) —
uv.lockwas gitignoredBoth failures above share one cause: CI and contributors re-resolved dependencies on every run.
uv.lockhad been in.gitignoresince the initial commit and was never tracked. That means:release.ymlrunsuv sync --devand then freezes the resolved dependencies into a PyInstaller binary. Rebuilding a given tag today produces a binary with different dependency versions than the original build — the tag did not determine the artifact.pyinstaller>=6.0is itself unpinned, so the packaging tool drifted too.uv.lockrecords a sha256 for each of the 56 packages. Without it, release builds installed whatever PyPI served.uv sync.Committing the lockfile is the standard practice for an application (as opposed to a library), which this is.
Enforcing it in CI
Committing the lockfile is only half of it — nothing yet stops the lockfile from drifting out of sync with
pyproject.toml. Bothci.ymljobs now use:--lockedverifies the lockfile is up to date withpyproject.tomland fails instead of silently re-resolving. So changing a dependency without runninguv lockis now a CI failure rather than an invisible divergence between what the lockfile claims and what CI actually installs. The lockfile is resolved universally, so a single lock serves the whole 3.11 / 3.12 / 3.13 test matrix.release.ymldeliberately keeps plainuv sync --dev. It rewrites the version inpyproject.tomlfrom the git tag before syncing, and the lockfile records the root package's own version (uv.lock→version = "0.1.17"), so the stamp makes the lockfile stale by construction.Test plan
uv run pytest)uv run ruff check .)dualentry <command>