Skip to content

Commit

Permalink
feat(config/package-rules): matchCurrentAge (#23264)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
4 people authored Jan 25, 2024
1 parent 399b96e commit 8970661
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 2 deletions.
26 changes: 26 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2396,6 +2396,32 @@ Use the syntax `!/ /` like the following:
}
```

### matchCurrentAge

Use this field if you want to match packages based on the age of the _current_ (existing, in-repo) version.

For example, if you want to group updates for dependencies where the existing version is more than 2 years old:

```json
{
"packageRules": [
{
"matchCurrentAge": "> 2 years",
"groupName": "old dependencies"
}
]
}
```

The `matchCurrentAge` string must start with one of `>`, `>=`, `<` or `<=`.

Only _one_ date part is supported, so you _cannot_ do `> 1 year 1 month`.
Instead you should do `> 13 months`.

<!-- prettier-ignore -->
!!! note
We recommend you only use the words hour(s), day(s), week(s), month(s) and year(s) in your time ranges.

### matchDepTypes

Use this field if you want to limit a `packageRule` to certain `depType` values.
Expand Down
11 changes: 11 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,17 @@ const options: RenovateOptions[] = [
stage: 'package',
mergeable: true,
},
{
name: 'matchCurrentAge',
description:
'Matches the current age of the package derived from its release timestamp. Valid only within a `packageRules` object.',
type: 'string',
parents: ['packageRules'],
stage: 'package',
mergeable: true,
cli: false,
env: false,
},
{
name: 'matchCategories',
description:
Expand Down
3 changes: 3 additions & 0 deletions lib/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export interface PackageRule
isVulnerabilityAlert?: boolean;
matchFileNames?: string[];
matchBaseBranches?: string[];
matchCurrentAge?: string;
matchManagers?: string[];
matchDatasources?: string[];
matchDepTypes?: string[];
Expand Down Expand Up @@ -502,7 +503,9 @@ export interface PackageRuleInputConfig extends Record<string, unknown> {
manager?: string;
datasource?: string;
packageRules?: (PackageRule & PackageRuleInputConfig)[];
releaseTimestamp?: string | null;
repository?: string;
currentVersionTimestamp?: string;
}

export interface ConfigMigration {
Expand Down
1 change: 1 addition & 0 deletions lib/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export async function validateConfig(
'matchSourceUrls',
'matchUpdateTypes',
'matchConfidence',
'matchCurrentAge',
'matchRepositories',
];
if (key === 'packageRules') {
Expand Down
1 change: 1 addition & 0 deletions lib/modules/datasource/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface Release {
registryUrl?: string;
sourceUrl?: string | undefined;
sourceDirectory?: string;
currentAge?: string;
}

export interface ReleaseResult {
Expand Down
78 changes: 78 additions & 0 deletions lib/util/package-rules/current-age.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { DateTime } from 'luxon';
import { CurrentAgeMatcher } from './current-age';

describe('util/package-rules/current-age', () => {
const matcher = new CurrentAgeMatcher();

describe('match', () => {
const t0 = DateTime.fromISO('2023-07-07', { zone: 'utc' });

beforeAll(() => {
jest.useFakeTimers();
});

beforeEach(() => {
jest.setSystemTime(t0.toMillis());
});

it('returns false if release is older', () => {
const result = matcher.matches(
{
currentVersionTimestamp: '2020-01-01',
},
{
matchCurrentAge: '< 1 year', // younger than 1 year
},
);
expect(result).toBeFalse();
});

it('returns false if release is younger', () => {
const result = matcher.matches(
{
currentVersionTimestamp: '2020-01-01',
},
{
matchCurrentAge: '> 10 years', // older than 10 yrs
},
);
expect(result).toBeFalse();
});

it('returns null if release invalid', () => {
const result = matcher.matches(
{
currentVersionTimestamp: 'abc',
},
{
matchCurrentAge: '> 2 days', // older than 2 days
},
);
expect(result).toBeNull();
});

it('returns false if release undefined', () => {
const result = matcher.matches(
{
currentVersionTimestamp: undefined,
},
{
matchCurrentAge: '> 2 days', // older than 2 days
},
);
expect(result).toBeFalse();
});

it('returns true if age matches', () => {
const result = matcher.matches(
{
currentVersionTimestamp: '2020-01-01',
},
{
matchCurrentAge: '> 3 years', // older than 3 years
},
);
expect(result).toBeTrue();
});
});
});
21 changes: 21 additions & 0 deletions lib/util/package-rules/current-age.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import is from '@sindresorhus/is';
import type { PackageRule, PackageRuleInputConfig } from '../../config/types';
import { satisfiesDateRange } from '../pretty-time';
import { Matcher } from './base';

export class CurrentAgeMatcher extends Matcher {
override matches(
{ currentVersionTimestamp }: PackageRuleInputConfig,
{ matchCurrentAge }: PackageRule,
): boolean | null {
if (!is.string(matchCurrentAge)) {
return null;
}

if (!is.string(currentVersionTimestamp)) {
return false;
}

return satisfiesDateRange(currentVersionTimestamp, matchCurrentAge);
}
}
1 change: 1 addition & 0 deletions lib/util/package-rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function applyPackageRules<T extends PackageRuleInputConfig>(
delete config.matchDepTypes;
delete config.matchCurrentValue;
delete config.matchCurrentVersion;
delete config.matchCurrentAge;
}
}
return config;
Expand Down
2 changes: 2 additions & 0 deletions lib/util/package-rules/matchers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BaseBranchesMatcher } from './base-branches';
import { CategoriesMatcher } from './categories';
import { CurrentAgeMatcher } from './current-age';
import { CurrentValueMatcher } from './current-value';
import { CurrentVersionMatcher } from './current-version';
import { DatasourcesMatcher } from './datasources';
Expand Down Expand Up @@ -45,3 +46,4 @@ matchers.push([new CurrentValueMatcher()]);
matchers.push([new CurrentVersionMatcher()]);
matchers.push([new RepositoriesMatcher()]);
matchers.push([new CategoriesMatcher()]);
matchers.push([new CurrentAgeMatcher()]);
35 changes: 34 additions & 1 deletion lib/util/pretty-time.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { toMs } from './pretty-time';
import { DateTime } from 'luxon';
import { satisfiesDateRange, toMs } from './pretty-time';

describe('util/pretty-time', () => {
it.each`
Expand Down Expand Up @@ -44,4 +45,36 @@ describe('util/pretty-time', () => {
it('returns null for error', () => {
expect(toMs(null as never)).toBeNull();
});

describe('satisfiesDateRange()', () => {
const t0 = DateTime.fromISO('2023-07-07T12:00:00');

beforeAll(() => {
jest.useFakeTimers();
});

beforeEach(() => {
jest.setSystemTime(t0.toMillis());
});

it.each`
date | range | expected
${'2023-01-01'} | ${'< 1 Y'} | ${true}
${'2023-07-07'} | ${'< 1 day'} | ${true}
${'2023-06-09'} | ${'<=1M'} | ${true}
${'2020-01-01'} | ${'>= 1hrs'} | ${true}
${'2023-07-07T11:12:00'} | ${'<= 1hrs'} | ${true}
${'2020-01-01'} | ${'< 2years'} | ${false}
${new Date(Date.now()).toISOString()} | ${'< 3 days'} | ${true}
${new Date(Date.now()).toISOString()} | ${'> 3 months'} | ${false}
${'2020-01-01'} | ${'> 1 millenial'} | ${null}
${'invalid-date'} | ${'> 1 year'} | ${null}
${'2020-01-01'} | ${'1 year'} | ${null}
`(
`satisfiesRange('$date', '$range') === $expected`,
({ date, range, expected }) => {
expect(satisfiesDateRange(date, range)).toBe(expected);
},
);
});
});
42 changes: 42 additions & 0 deletions lib/util/pretty-time.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import is from '@sindresorhus/is';
import { DateTime } from 'luxon';
import ms from 'ms';
import { logger } from '../logger';
import { regEx } from './regex';
Expand Down Expand Up @@ -40,3 +41,44 @@ export function toMs(time: string): number | null {
return null;
}
}

const rangeRegex = regEx(/^(?<operator>(>=|<=|<|>))\s*(?<age>.*)$/);

export function satisfiesDateRange(
date: string,
range: string,
): boolean | null {
const grps = range.trim().match(rangeRegex)?.groups;
if (!grps) {
return null;
}

const { operator, age } = grps;
const luxonDate = DateTime.fromISO(date, { zone: 'utc' });
if (!luxonDate.isValid) {
logger.trace(`Invalid date when computing satisfiesDateRange: '${date}'`);
return null;
}

const dateMs = luxonDate.toMillis();
const ageMs = toMs(age);
if (!is.number(ageMs)) {
return null;
}

const rangeMs = Date.now() - ageMs;

switch (operator) {
case '>':
return dateMs < rangeMs;
case '>=':
return dateMs <= rangeMs;
case '<':
return dateMs > rangeMs;
case '<=':
return dateMs >= rangeMs;
// istanbul ignore next: can never happen
default:
return dateMs === rangeMs;
}
}
Loading

0 comments on commit 8970661

Please sign in to comment.