feat(pep621): use exclude-newer during uv lock when minimumReleaseAge is set - #41913
feat(pep621): use exclude-newer during uv lock when minimumReleaseAge is set#41913thejoeejoee wants to merge 16 commits into
Conversation
5f43c9b to
4bc8168
Compare
RahulGautamSingh
left a comment
There was a problem hiding this comment.
Please, use the PR template
There was a problem hiding this comment.
I think this could benefit from some refinement:
- With uv,
--exclude-newercan also be defined inpyproject.tomloruv.toml(see here). A CLI value always takes precedence but this creates a problem if thepyproject.tomlsetting is not the same value asminimumReleaseAge:- If it is more restrictive (older date), a less restrictive renovate
minimumReleaseAgewould override it and potentially allow packages the user explicitly wanted to exclude. - If it is less restrictive (newer date or absent), renovate correctly tightens the constraint.
- Suggestion: Read
exclude-newerfrompyproject.tomland, if present, use the more restrictive (older) date of the two, i.e.,min(renovate_minimum_release_age, pyproject_exclude_newer_date).
- If it is more restrictive (older date), a less restrictive renovate
- Using
UV_EXCLUDE_NEWERinstead of--exclude-newerwould be more consistent with the existing code style (UV_EXTRA_URL,UV_INDEX_*patterns). --exclude-newerworks only since uv 0.2.22. Unlikely that someone still uses such old versions but, strictly speaking, it would be necessary to check if a uv constraint is set and if < 0.2.22 to omit this setting. The elegance of the approach viaUV_EXCLUDE_NEWERenv var is that incompatible uv versions would simply ignore the setting.- Might also be worth a hint in renovate docs, considering that uv also explicitly exemplifies using this combination: https://docs.astral.sh/uv/guides/integration/dependency-bots/#dependency-cooldown
5e60f51 to
65df176
Compare
|
Thanks, all four points are addressed in the later commits — min() of both dates, UV_EXCLUDE_NEWER env var (so old uv versions just ignore it), and a docs note linking the uv dependency guide. |
Note that this currently doesn't work for uv, pending this PR renovatebot/renovate#41913 (part of an epic to add this feature for all managers - renovatebot/renovate#41652)
There was a problem hiding this comment.
Please revise the tests as a whole and make them meaningful.
There's no point in ~500 lines of AI slop just to satisfy code coverage. In particular visible with these:
sets UV_EXCLUDE_NEWER on per-package update
-> same path assets UV_EXCLUDE_NEWER env var on lockfile ..., only difference is --upgrade-package vs --upgrade, which is already tested elsewheredoes not set UV_EXCLUDE_NEWER on empty string minimumReleaseAge
-> identical path todoes not set UV_EXCLUDE_NEWER when no minimumReleaseAge('' is falsy)does not set UV_EXCLUDE_NEWER when no minimumReleaseAge even if pyproject has exclude-newer
-> same path asdoes not set UV_EXCLUDE_NEWER when no minimumReleaseAge, pyproject value is never reachedhandles pyproject exclude-newer as local date without time component
-> same path asuses pyproject exclude-newer when more restrictive..., just different input format. Not a distinct code branchhandles exclude-newer as Date object in schema
-> Schema test, not anupdateArtifactscode path. Belongs in schema tests if anywhere.
// EDIT: Here's a suggestion of what it could look like:
describe('UV_EXCLUDE_NEWER with minimumReleaseAge', () => {
let execSnapshots: ReturnType<typeof mockExecAll>;
beforeEach(() => {
vi.spyOn(Date, 'now').mockReturnValue(
new Date('2026-03-13T00:00:00.000Z').getTime(),
);
execSnapshots = mockExecAll();
GlobalConfig.set(adminConfig);
fs.findLocalSiblingOrParent.mockResolvedValueOnce('uv.lock');
fs.readLocalFile.mockResolvedValueOnce('test content');
fs.readLocalFile.mockResolvedValueOnce('changed test content');
getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '3.11.1' }] });
getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '0.2.35' }] });
});
afterEach(() => {
vi.restoreAllMocks();
});
it('sets UV_EXCLUDE_NEWER from minimumReleaseAge', async () => {
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: { isLockFileMaintenance: true, minimumReleaseAge: '3 days' },
updatedDeps: [],
},
parsePyProject('')!,
);
expect(execSnapshots[0].cmd).not.toContain('--exclude-newer');
expect(execSnapshots[0].options?.env).toMatchObject({
UV_EXCLUDE_NEWER: '2026-03-10T00:00:00.000Z',
});
});
it('skips UV_EXCLUDE_NEWER when minimumReleaseAge is absent', async () => {
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: { isLockFileMaintenance: true },
updatedDeps: [],
},
parsePyProject('')!,
);
expect(execSnapshots[0].options?.env?.UV_EXCLUDE_NEWER).toBeUndefined();
});
it('skips UV_EXCLUDE_NEWER on unparseable minimumReleaseAge', async () => {
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: { isLockFileMaintenance: true, minimumReleaseAge: 'invalid garbage' },
updatedDeps: [],
},
parsePyProject('')!,
);
expect(execSnapshots[0].options?.env?.UV_EXCLUDE_NEWER).toBeUndefined();
expect(logger.logger.debug).toHaveBeenCalledWith(
"Invalid minimumReleaseAge value 'invalid garbage', skipping UV_EXCLUDE_NEWER for uv lock",
);
});
it('uses pyproject ISO date when more restrictive than minimumReleaseAge', async () => {
// pyproject (2026-03-05) is earlier than minimumReleaseAge (3 days = 2026-03-10)
const pyproject = parsePyProject(codeBlock`
[tool.uv]
exclude-newer = "2026-03-05T00:00:00.000Z"
`)!;
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: { isLockFileMaintenance: true, minimumReleaseAge: '3 days' },
updatedDeps: [],
},
pyproject,
);
expect(execSnapshots[0].options?.env).toMatchObject({
UV_EXCLUDE_NEWER: '2026-03-05T00:00:00.000Z',
});
});
it('uses minimumReleaseAge when pyproject ISO date is less restrictive', async () => {
// pyproject (2026-03-12) is later than minimumReleaseAge (3 days = 2026-03-10)
const pyproject = parsePyProject(codeBlock`
[tool.uv]
exclude-newer = "2026-03-12T00:00:00.000Z"
`)!;
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: { isLockFileMaintenance: true, minimumReleaseAge: '3 days' },
updatedDeps: [],
},
pyproject,
);
expect(execSnapshots[0].options?.env).toMatchObject({
UV_EXCLUDE_NEWER: '2026-03-10T00:00:00.000Z',
});
});
it('uses pyproject relative duration when more restrictive than minimumReleaseAge', async () => {
// 2 weeks before 2026-03-13 = 2026-02-27, earlier than 3 days (2026-03-10)
const pyproject = parsePyProject(codeBlock`
[tool.uv]
exclude-newer = "2 weeks"
`)!;
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: { isLockFileMaintenance: true, minimumReleaseAge: '3 days' },
updatedDeps: [],
},
pyproject,
);
expect(execSnapshots[0].options?.env).toMatchObject({
UV_EXCLUDE_NEWER: '2026-02-27T00:00:00.000Z',
});
});
it('ignores unparseable pyproject exclude-newer and uses minimumReleaseAge', async () => {
const pyproject = parsePyProject(codeBlock`
[tool.uv]
exclude-newer = "not-a-date"
`)!;
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: { isLockFileMaintenance: true, minimumReleaseAge: '3 days' },
updatedDeps: [],
},
pyproject,
);
expect(execSnapshots[0].options?.env).toMatchObject({
UV_EXCLUDE_NEWER: '2026-03-10T00:00:00.000Z',
});
expect(logger.logger.debug).toHaveBeenCalledWith(
"Invalid exclude-newer value 'not-a-date' in pyproject.toml, ignoring",
);
});
});
RahulGautamSingh
left a comment
There was a problem hiding this comment.
LGTM except open comment and suggested changes.
95d9fa8 to
df77a59
Compare
Co-authored-by: Johannes Feichtner <343448+Churro@users.noreply.github.com> Co-authored-by: RahulGautamSingh <rahultesnik@gmail.com>
… timestamp-optional
db2a318 to
fe42046
Compare
RahulGautamSingh
left a comment
There was a problem hiding this comment.
It would be worth adding one test with a non-maintenance update to confirm the env var is set for regular --upgrade-package commands too.
|
From my perspective, the code is fine but only for the "happy paths":
I've built https://github.com/renovate-demo/41913-uv-lockfile-maintenance to demonstrate the lockfile maintenance case. When run with the code from this PR, we see in logs: So summarizing, I think a fallback similar to the one I proposed in #42552 is needed. Let's probably pause with this PR until a way forward is agreed upon regarding |
Disable lockFileMaintenance is as it doesn't currently respect the minimumReleaseAge for uv. An open PR for adding it has been put on hold for now. renovatebot/renovate#41913 (comment) Instead, we'll use the update-dependencies-action to update the uv lockfile instead (in a following commit)
upgrade-all upgrades with a 7-days-ago excludes-newer setting, as per our dependency update policy. This writes an excludes-newer timestamp into the uv.lock file. We deliberately don't include a excludes-newer setting in pyproject.toml as it interferes with automated security updates which we don't (always) want to be applied with a 7-day lag. Note that this is a workaround until Renovate properly respects minimumReleaseAge for uv lockfileMaintentance (see PR renovatebot/renovate#41913) However, if we leave the timestamp in the uv.lock, any subsequent uv sync will remove it - which means anyone who runs a just check/test/run locally ends up with an unexpected uv.lock change. Running devenv as a subsequent recipe after upgrade-all immediately removes the timestamp, leaving just the dependency updates in place.
upgrade-all upgrades with a 7-days-ago excludes-newer setting, as per our dependency update policy. This writes an excludes-newer timestamp into the uv.lock file. We deliberately don't include a excludes-newer setting in pyproject.toml as it interferes with automated security updates which we don't (always) want to be applied with a 7-day lag. Note that this is a workaround until Renovate properly respects minimumReleaseAge for uv lockfileMaintentance (see PR renovatebot/renovate#41913) However, if we leave the timestamp in the uv.lock, any subsequent uv sync will remove it - which means anyone who runs a just check/test/run locally ends up with an unexpected uv.lock change. Running devenv as a subsequent recipe after upgrade-all immediately removes the timestamp, leaving just the dependency updates in place.
|
|
||
| <!-- prettier-ignore --> | ||
| For uv, Renovate reads `[tool.uv] exclude-newer` from `pyproject.toml` and uses the more restrictive (older) date between it and `minimumReleaseAge`. | ||
| The `exclude-newer` value can be an RFC 3339 timestamp (e.g. `2025-01-01T00:00:00Z`) or a friendly duration (e.g. `2 weeks`), but ISO 8601 durations (e.g. `P14D`) are not supported. |
There was a problem hiding this comment.
There was a recent change in uv, now you set exclude-newer-span for relative durations, and exclude-newer is for absolute timestamps only?
See astral-sh/uv#19024.
Changes
When
minimumReleaseAgeis configured, Renovate now passesUV_EXCLUDE_NEWERas an environment variable touv lock.Additionally, if
pyproject.tomlalready defines[tool.uv] exclude-newer, Renovate compares it with its own computed date and uses the more restrictive (older) one. This prevents Renovate from accidentally widening the package resolution window beyond what the project intended.📖 Related: uv documents recommended
exclude-newerusage for dependency bots in their integration guide.Key changes:
lib/modules/manager/types.ts— addedminimumReleaseAgetoUpdateArtifactsConfiglib/modules/manager/pep621/schema.ts— addedexclude-newertoUvConfigZod schema (handles both TOML string and Date types)lib/modules/manager/pep621/processors/uv.ts—UV_EXCLUDE_NEWERenv var withmin(renovate_date, pyproject_date)logiclib/modules/manager/pep621/processors/uv.spec.ts— 28 tests covering env var usage, min-date comparison, invalid values, and edge casesContext
--exclude-newertouv#41654AI assistance disclosure
opencode (Claude Opus 4.6 / Sonnet 4.6) used for test writing, and code review iteration
Documentation (please check one with an [x])
How I've tested my work (please select one)