Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c79fc1c
feat(pep621): add minimumReleaseAge to UpdateArtifactsConfig type
thejoeejoee Mar 13, 2026
1444d77
feat(pep621): append --exclude-newer to uv lock when minimumReleaseAg…
thejoeejoee Mar 13, 2026
a2f416b
test(pep621): add tests for --exclude-newer flag in uv lock commands
thejoeejoee Mar 13, 2026
0b4d5b3
chore: CR
thejoeejoee Mar 15, 2026
2fc7070
refactor(pep621): use UV_EXCLUDE_NEWER env var with pyproject min-dat…
thejoeejoee Mar 15, 2026
335a5d9
docs(pep621): add uv exclude-newer note to minimum-release-age docs
thejoeejoee Mar 15, 2026
341376e
test(pep621): cover z.date().transform() branch in exclude-newer schema
thejoeejoee Mar 15, 2026
f2bd5e3
fix(pep621): address review comments on UV_EXCLUDE_NEWER
thejoeejoee Mar 23, 2026
3c1e7a9
fix(pep621): use Nullish<string> for minimumReleaseAge to match confi…
thejoeejoee Mar 23, 2026
9fb85e9
chore: CR suggestions
thejoeejoee Mar 27, 2026
a6c6c90
refactor(pep621): move schema test to schema.spec.ts, use uv section …
thejoeejoee Mar 27, 2026
9c3d614
fix(pep621): fix prettier formatting in uv.spec.ts
thejoeejoee Mar 27, 2026
fe42046
fix(pep621): skip UV_EXCLUDE_NEWER when minimumReleaseAgeBehaviour is…
thejoeejoee Mar 27, 2026
295c205
docs(pep621): remove npm section from minimum-release-age docs
thejoeejoee Mar 27, 2026
d8c6550
fix(pep621): add missing imports for Nullish and MinimumReleaseAgeBeh…
thejoeejoee Mar 30, 2026
5af02cd
test(pep621): cover UV_EXCLUDE_NEWER for non-maintenance upgrade-pack…
thejoeejoee Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/usage/key-concepts/minimum-release-age.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ To protect against this, it's recommended to ensure that your package manager co
There is ongoing work to [integrate more closely with package manager checks](https://github.com/renovatebot/renovate/issues/41652) to make sure that Renovate's minimum release age configuration is specified when calling package managers that support it.
If you have a package manager you'd like supported, please raise a [Suggest an Idea Discussion](https://github.com/renovatebot/renovate/discussions/new?category=suggest-an-idea).

#### uv

<!-- prettier-ignore -->
Comment thread
thejoeejoee marked this conversation as resolved.
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.

@simonhaenisch simonhaenisch May 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

See uv's [dependency bot integration guide](https://docs.astral.sh/uv/guides/integration/dependency-bots/#dependency-cooldown) for details on configuring `exclude-newer`.

### What happens if the datasource and/or registry does not provide a release timestamp, when using `minimumReleaseAge`?

<!-- prettier-ignore -->
Expand Down
184 changes: 184 additions & 0 deletions lib/modules/manager/pep621/processors/uv.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,5 +891,189 @@ describe('modules/manager/pep621/processors/uv', () => {
},
]);
});

describe('UV_EXCLUDE_NEWER with minimumReleaseAge', () => {
let execSnapshots: ReturnType<typeof mockExecAll>;

beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-03-13T00:00:00.000Z'));
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();
Comment thread
thejoeejoee marked this conversation as resolved.
vi.useRealTimers();
});

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].options?.env).toMatchObject({
UV_EXCLUDE_NEWER: '2026-03-10T00:00:00.000Z',
});
});

it('skips UV_EXCLUDE_NEWER when minimumReleaseAge 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 when minimumReleaseAgeBehaviour is timestamp-optional', async () => {
await processor.updateArtifacts(
{
packageFileName: 'folder/pyproject.toml',
newPackageFileContent: '',
config: {
isLockFileMaintenance: true,
minimumReleaseAge: '3 days',
minimumReleaseAgeBehaviour: 'timestamp-optional',
},
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', async () => {
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 less restrictive', async () => {
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', async () => {
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,
);
// 2 weeks = 14 days ago = 2026-02-27, which is older than 3 days ago = 2026-03-10
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",
);
});
});
});
});
46 changes: 45 additions & 1 deletion lib/modules/manager/pep621/processors/uv.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isString } from '@sindresorhus/is';
import { isNullOrUndefined, isString } from '@sindresorhus/is';
import { DateTime } from 'luxon';
import { quote } from 'shlex';
import { TEMPORARY_ERROR } from '../../../../constants/error-messages.ts';
import { logger } from '../../../../logger/index.ts';
Expand All @@ -14,6 +15,7 @@ import {
} from '../../../../util/fs/index.ts';
import { getGitEnvironmentVariables } from '../../../../util/git/auth.ts';
import { find } from '../../../../util/host-rules.ts';
import { toMs } from '../../../../util/pretty-time.ts';
import { Result } from '../../../../util/result.ts';
import { parseUrl } from '../../../../util/url.ts';
import { PypiDatasource } from '../../../datasource/pypi/index.ts';
Expand Down Expand Up @@ -215,6 +217,48 @@ export class UvProcessor extends BasePyProjectProcessor {
} else {
cmd = generateCMD(updatedDeps);
}

// See https://docs.astral.sh/uv/guides/integration/dependency-bots/#dependency-cooldown
// Skip UV_EXCLUDE_NEWER when minimumReleaseAgeBehaviour is 'timestamp-optional',
// as simple API doesn't return releaseTimestamp and the user opted out of requiring it
if (
config.minimumReleaseAge &&
config.minimumReleaseAgeBehaviour !== 'timestamp-optional'
) {
const ms = toMs(config.minimumReleaseAge);
if (isNullOrUndefined(ms)) {
logger.debug(
`Invalid minimumReleaseAge value '${config.minimumReleaseAge}', skipping UV_EXCLUDE_NEWER for uv lock`,
);
} else {
let excludeNewerDate = DateTime.now().minus(ms).toUTC();

const pyprojectExcludeNewer = project.tool?.uv?.['exclude-newer'];
if (pyprojectExcludeNewer) {
let pyprojectDate = DateTime.fromISO(pyprojectExcludeNewer, {
zone: 'utc',
});
Comment thread
thejoeejoee marked this conversation as resolved.
if (!pyprojectDate.isValid) {
const durationMs = toMs(pyprojectExcludeNewer);
if (!isNullOrUndefined(durationMs)) {
pyprojectDate = DateTime.now().minus(durationMs).toUTC();
}
}
if (pyprojectDate.isValid) {
if (pyprojectDate < excludeNewerDate) {
excludeNewerDate = pyprojectDate;
}
} else {
logger.debug(
`Invalid exclude-newer value '${pyprojectExcludeNewer}' in pyproject.toml, ignoring`,
);
}
}

extraEnv.UV_EXCLUDE_NEWER = excludeNewerDate.toISO();
}
}

await exec(cmd, execOptions);

// check for changes
Expand Down
16 changes: 16 additions & 0 deletions lib/modules/manager/pep621/schema.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PyProject } from './schema.ts';

describe('modules/manager/pep621/schema', () => {
describe('UvConfig', () => {
it('handles exclude-newer as Date object', () => {
const result = PyProject.parse({
tool: {
uv: { 'exclude-newer': new Date('2026-03-05T00:00:00.000Z') },
},
});
expect(result.tool?.uv?.['exclude-newer']).toBe(
'2026-03-05T00:00:00.000Z',
);
});
});
});
3 changes: 3 additions & 0 deletions lib/modules/manager/pep621/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ const UvConfig = z.object({
'dev-dependencies': LooseArray(
Pep508Dependency(depTypes.uvDevDependencies),
).catch([]),
'exclude-newer': z
.union([z.string(), z.date().transform((d) => d.toISOString())])
.optional(),
'required-version': z.string().optional(),
sources: LooseRecord(
// uv applies the same normalization as for Python dependencies on sources
Expand Down
4 changes: 4 additions & 0 deletions lib/modules/manager/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReleaseType } from 'semver';
import type {
MatchStringsStrategy,
MinimumReleaseAgeBehaviour,
ToolSettingsOptions,
UpdateType,
ValidationMessage,
Expand All @@ -9,6 +10,7 @@ import type { Category } from '../../constants/index.ts';
import type {
MaybePromise,
ModuleApi,
Nullish,
RangeStrategy,
SkipReason,
StageName,
Expand Down Expand Up @@ -50,6 +52,8 @@ export interface UpdateArtifactsConfig {
skipArtifactsUpdate?: boolean;
lockFiles?: string[];
toolSettings?: ToolSettingsOptions;
minimumReleaseAge?: Nullish<string>;
minimumReleaseAgeBehaviour?: MinimumReleaseAgeBehaviour;
}

export interface RangeConfig<T = Record<string, any>> extends ManagerData<T> {
Expand Down
Loading