dependabot / dependabot/dependabot-core

[uv] git dependencies are tracked as unconstrained PyPI packages, so a pinned tag is never updated

Open
#16,129 0 comments 1 reaction 1 assignee Claimed by @AbhishekBhaskar View on GitHub
L: go:modules L: javascript L: python L: python:uv L: ruby:bundler
Dominant language
Ruby
Stars
5.8k
Forks
1.5k
Avg merge
2d 18h
Merged PRs (30d)
149

Description

### Is there an existing issue for this?

Searched. The closest ones are not this:

- #14644 asks for the opposite — a way to *ignore* `[tool.uv.sources]` `path` entries.
- #14728 / #14729 (merged) fixed the same class of bug one layer down: `pin_pep508_entry` was
rewriting `"pkg @ git+https://…"` into `pkg==`, swapping a fork for the PyPI package. The
fix now returns the entry untouched, with the comment *"already pinned to a URL"*.

This issue is about the **parsing** layer, which still resolves those dependencies against PyPI.

### Package ecosystem

uv

### Package manager version

uv 0.12.x

### Language version

Python 3.13

### Manifest location and content before the Dependabot update

`pyproject.toml`:

```toml
[project]
name = "repro"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["click"]

[tool.uv.sources]
click = { git = "https://github.com/pallets/click.git", tag = "8.1.7" }
```

The PEP 508 direct-reference form behaves the same way:

```toml
dependencies = ["click @ git+https://github.com/pallets/click.git@8.1.7"]
```

### What you expected to see, versus what you actually saw

**Expected**: Dependabot notices the pinned tag is behind and opens a PR moving `tag = "8.1.7"` to
the latest tag of that repository, re-running `uv lock` with it — the way it already handles git
dependencies for `bundler`, `npm_and_yarn` and `gomod` through `Dependabot::GitCommitChecker`.

**Actual**: the dependency is emitted as an unconstrained PyPI package. The git URL and the tag never
reach the update checker, so nothing about the pinned tag is ever checked. Two outcomes, depending on
the name:

- **A name that also exists on PyPI** — Dependabot resolves against PyPI and can propose a version of
a package the project does not use. This is the same failure #14728 described, one layer up.
- **A name that does not exist on PyPI** (the common case for a private library) — nothing is found,
no PR is ever opened, and no error is logged. On a private repository of ours, six upstream releases
went by over seventeen days with daily runs and `patterns: ["*"]`, and not one was proposed. The
silence is what makes this hard to notice: the manifest simply stops being maintained.

### Root cause

Three places, all on `main`:

1. **The Python helper keeps only four fields and drops the URL.**
`uv/helpers/lib/parser.py` builds each dependency from `packaging.requirements.Requirement`, which
does expose `req.url` for a direct reference — but only `name`, `version`, `markers`, `requirement`
(the specifier) and `extras` are read. `grep -c 'req.url' uv/helpers/lib/parser.py` → `0`.

2. **A missing specifier then becomes "any version".**
`uv/lib/dependabot/uv/file_parser/pyproject_files_parser.rb`:

```ruby
# In uv no constraint means any version is acceptable
requirement_value = dep.requirement == "" ? "*" : dep.requirement

requirements: [{ requirement: requirement_value, file: …, source: nil, groups: … }]
```

A git-sourced dependency is listed bare in `[project.dependencies]`, so it arrives with no
specifier and leaves with `"*"` and `source: nil`.

3. **`[tool.uv.sources]` is only ever read for `path` entries.**
`uv/lib/dependabot/uv/file_fetcher.rb`:

```ruby
uv_sources.filter_map do |name, source_config|
if source_config.is_a?(Hash) && source_config["path"]
{ name: name, path: source_config["path"], file: … }
end
end
```

`git`, `url` and `index` entries are dropped. The parser's own comment says as much: *"Editable
`[tool.uv.sources]` path dependencies are separate packages that are fetched as support files for
resolution but must not be modified."*

There is no git handling anywhere in the uv update checker either — `grep -rn 'GitCommitChecker'
uv/lib/` returns nothing, while `bundler` and `npm_and_yarn` both use it.

### Smallest manifest that reproduces the issue

The `pyproject.toml` above, with the `uv.lock` `uv lock` generates from it. In the lock the
dependency is recorded as `source = { git = "https://github.com/pallets/click.git?tag=8.1.7#" }`,
so the intended source is unambiguous in the files Dependabot already fetches.

### Notes

Renovate's `pep621` manager handles this shape natively, and its implementation is a useful reference
for the direction: `[tool.uv.sources]` entries are switched on by source kind, `path` / `url` /
`workspace` each get an explicit skip reason, and a `git` entry with a `tag` is routed to the
`github-tags` / `gitlab-tags` / `git-tags` datasource with the tag as the current value.

Happy to work on a PR if the direction is welcome — including the smaller intermediate step of
marking these dependencies unsupported rather than resolving them against PyPI, which would stop the
wrong-source updates described above without needing the full git-tag support.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.