From 1615d262264fd5db4bee76802c3542bc0fd36556 Mon Sep 17 00:00:00 2001 From: Gabriel-Ladzaretti <97394622+Gabriel-Ladzaretti@users.noreply.github.com> Date: Tue, 21 Mar 2023 20:37:38 +0200 Subject: [PATCH] feat(packageRules): add merge confidence matcher (#21049) Co-authored-by: Rhys Arkins Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Michael Kriese --- docs/usage/configuration-options.md | 33 +++++++ lib/config/options/index.ts | 15 +++ lib/config/types.ts | 3 + lib/config/validation.ts | 1 + lib/modules/manager/types.ts | 2 + lib/util/package-rules/index.spec.ts | 55 +++++++++++ lib/util/package-rules/matchers.ts | 2 + lib/util/package-rules/merge-confidence.ts | 19 ++++ .../repository/process/lookup/generate.ts | 16 ++- .../repository/process/lookup/index.spec.ts | 98 +++++++++++++++++++ .../repository/process/lookup/index.ts | 2 +- 11 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 lib/util/package-rules/merge-confidence.ts diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index f7fdb909328d84..67f8f640df594d 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -2135,6 +2135,39 @@ For example to apply a special label for Major updates: } ``` +### matchConfidence + + +!!! warning + This configuration option needs a Mend API key, and is in private beta testing only. + API keys are not available for free or via the `renovatebot/renovate` repository. + +For example to group high merge confidence updates: + +```json +{ + "packageRules": [ + { + "matchConfidence": ["high", "very high"], + "groupName": "high merge confidence" + } + ] +} +``` + +Tokens can be configured via `hostRules` using the `"merge-confidence"` `hostType`: + +```json +{ + "hostRules": [ + { + "hostType": "merge-confidence", + "token": "********" + } + ] +} +``` + ### customChangelogUrl Use this field to set the source URL for a package, including overriding an existing one. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 428b8dc17720f6..e140838639592a 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1234,6 +1234,21 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchConfidence', + description: + 'Merge confidence levels to match against (`low`, `neutral`, `high`, `very high`). Valid only within `packageRules` object.', + type: 'array', + subType: 'string', + allowedValues: ['low', 'neutral', 'high', 'very high'], + allowString: true, + stage: 'package', + parent: 'packageRules', + mergeable: true, + cli: false, + env: false, + experimental: true, + }, { name: 'matchUpdateTypes', description: diff --git a/lib/config/types.ts b/lib/config/types.ts index f6e8d56952fdbc..aedf88d6237cdc 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -2,6 +2,7 @@ import type { LogLevel } from 'bunyan'; import type { PlatformId } from '../constants'; import type { HostRule } from '../types'; import type { GitNoVerifyOption } from '../util/git/types'; +import type { MergeConfidence } from '../util/merge-confidence/types'; export type RenovateConfigStage = | 'global' @@ -328,6 +329,7 @@ export interface PackageRule matchSourceUrlPrefixes?: string[]; matchSourceUrls?: string[]; matchUpdateTypes?: UpdateType[]; + matchConfidence?: MergeConfidence[]; registryUrls?: string[] | null; } @@ -458,6 +460,7 @@ export interface PackageRuleInputConfig extends Record { currentVersion?: string; lockedVersion?: string; updateType?: UpdateType; + mergeConfidenceLevel?: MergeConfidence | undefined; isBump?: boolean; sourceUrl?: string | null; language?: string; diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 52d7e9a1deda0a..f5c7b59c5c5657 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -333,6 +333,7 @@ export async function validateConfig( 'matchSourceUrlPrefixes', 'matchSourceUrls', 'matchUpdateTypes', + 'matchConfidence', ]; if (key === 'packageRules') { for (const [subIndex, packageRule] of val.entries()) { diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts index fffd4ac2ddd717..80f84fe48094df 100644 --- a/lib/modules/manager/types.ts +++ b/lib/modules/manager/types.ts @@ -8,6 +8,7 @@ import type { import type { ProgrammingLanguage } from '../../constants'; import type { ModuleApi, RangeStrategy, SkipReason } from '../../types'; import type { FileChange } from '../../util/git/types'; +import type { MergeConfidence } from '../../util/merge-confidence/types'; export type Result = T | Promise; @@ -94,6 +95,7 @@ export interface LookupUpdate { pendingVersions?: string[]; newVersion?: string; updateType?: UpdateType; + mergeConfidenceLevel?: MergeConfidence | undefined; userStrings?: Record; checksumUrl?: string; downloadUrl?: string; diff --git a/lib/util/package-rules/index.spec.ts b/lib/util/package-rules/index.spec.ts index a64c37ba2fef07..dc3d30bc62882e 100644 --- a/lib/util/package-rules/index.spec.ts +++ b/lib/util/package-rules/index.spec.ts @@ -1,6 +1,7 @@ import type { PackageRuleInputConfig, UpdateType } from '../../config/types'; import { DockerDatasource } from '../../modules/datasource/docker'; import { OrbDatasource } from '../../modules/datasource/orb'; +import type { MergeConfidence } from '../merge-confidence/types'; import { applyPackageRules } from './index'; type TestConfig = PackageRuleInputConfig & { @@ -625,6 +626,60 @@ describe('util/package-rules/index', () => { expect(res.x).toBeUndefined(); }); + it('matches matchConfidence', () => { + const config: TestConfig = { + packageRules: [ + { + matchConfidence: ['high'], + x: 1, + }, + ], + }; + const dep = { + depType: 'dependencies', + depName: 'a', + mergeConfidenceLevel: 'high' as MergeConfidence, + }; + const res = applyPackageRules({ ...config, ...dep }); + expect(res.x).toBe(1); + }); + + it('non-matches matchConfidence', () => { + const config: TestConfig = { + packageRules: [ + { + matchConfidence: ['high'], + x: 1, + }, + ], + }; + const dep = { + depType: 'dependencies', + depName: 'a', + mergeConfidenceLevel: 'low' as MergeConfidence, + }; + const res = applyPackageRules({ ...config, ...dep }); + expect(res.x).toBeUndefined(); + }); + + it('does not match matchConfidence when there is no mergeConfidenceLevel', () => { + const config: TestConfig = { + packageRules: [ + { + matchConfidence: ['high'], + x: 1, + }, + ], + }; + const dep = { + depType: 'dependencies', + depName: 'a', + mergeConfidenceLevel: undefined, + }; + const res = applyPackageRules({ ...config, ...dep }); + expect(res.x).toBeUndefined(); + }); + it('filters naked depType', () => { const config: TestConfig = { packageRules: [ diff --git a/lib/util/package-rules/matchers.ts b/lib/util/package-rules/matchers.ts index 6697839344baac..2a11fdd8180d58 100644 --- a/lib/util/package-rules/matchers.ts +++ b/lib/util/package-rules/matchers.ts @@ -8,6 +8,7 @@ import { DepTypesMatcher } from './dep-types'; import { FilesMatcher } from './files'; import { LanguagesMatcher } from './languages'; import { ManagersMatcher } from './managers'; +import { MergeConfidenceMatcher } from './merge-confidence'; import { PackageNameMatcher } from './package-names'; import { PackagePatternsMatcher } from './package-patterns'; import { PackagePrefixesMatcher } from './package-prefixes'; @@ -36,6 +37,7 @@ matchers.push([new BaseBranchesMatcher()]); matchers.push([new ManagersMatcher()]); matchers.push([new DatasourcesMatcher()]); matchers.push([new UpdateTypesMatcher()]); +matchers.push([new MergeConfidenceMatcher()]); matchers.push([new SourceUrlsMatcher(), new SourceUrlPrefixesMatcher()]); matchers.push([new CurrentValueMatcher()]); matchers.push([new CurrentVersionMatcher()]); diff --git a/lib/util/package-rules/merge-confidence.ts b/lib/util/package-rules/merge-confidence.ts new file mode 100644 index 00000000000000..a29a6cfb55d43c --- /dev/null +++ b/lib/util/package-rules/merge-confidence.ts @@ -0,0 +1,19 @@ +import is from '@sindresorhus/is'; +import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { Matcher } from './base'; + +export class MergeConfidenceMatcher extends Matcher { + override matches( + { mergeConfidenceLevel }: PackageRuleInputConfig, + { matchConfidence }: PackageRule + ): boolean | null { + if (is.nullOrUndefined(matchConfidence)) { + return null; + } + return ( + is.array(matchConfidence) && + is.nonEmptyString(mergeConfidenceLevel) && + matchConfidence.includes(mergeConfidenceLevel) + ); + } +} diff --git a/lib/workers/repository/process/lookup/generate.ts b/lib/workers/repository/process/lookup/generate.ts index 3df6376504b689..5ad3cefff1d30d 100644 --- a/lib/workers/repository/process/lookup/generate.ts +++ b/lib/workers/repository/process/lookup/generate.ts @@ -1,19 +1,21 @@ +import is from '@sindresorhus/is'; import { logger } from '../../../../logger'; import type { Release } from '../../../../modules/datasource'; import type { LookupUpdate } from '../../../../modules/manager/types'; import type { VersioningApi } from '../../../../modules/versioning'; import type { RangeStrategy } from '../../../../types'; +import { getMergeConfidenceLevel } from '../../../../util/merge-confidence'; import type { LookupUpdateConfig } from './types'; import { getUpdateType } from './update-type'; -export function generateUpdate( +export async function generateUpdate( config: LookupUpdateConfig, versioning: VersioningApi, rangeStrategy: RangeStrategy, currentVersion: string, bucket: string, release: Release -): LookupUpdate { +): Promise { const newVersion = release.version; const update: LookupUpdate = { bucket, @@ -77,6 +79,16 @@ export function generateUpdate( update.updateType = update.updateType ?? getUpdateType(config, versioning, currentVersion, newVersion); + const { datasource, packageName, packageRules } = config; + if (packageRules?.some((pr) => is.nonEmptyArray(pr.matchConfidence))) { + update.mergeConfidenceLevel = await getMergeConfidenceLevel( + datasource, + packageName, + currentVersion, + newVersion, + update.updateType + ); + } if (!versioning.isVersion(update.newValue)) { update.isRange = true; } diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts index 5cea45a80f47b7..2039816e067f5f 100644 --- a/lib/workers/repository/process/lookup/index.spec.ts +++ b/lib/workers/repository/process/lookup/index.spec.ts @@ -1,3 +1,4 @@ +import * as hostRules from '../../../../../lib/util/host-rules'; import { Fixtures } from '../../../../../test/fixtures'; import * as httpMock from '../../../../../test/http-mock'; import { getConfig, partial } from '../../../../../test/util'; @@ -14,7 +15,11 @@ import { id as gitVersioningId } from '../../../../modules/versioning/git'; import { id as npmVersioningId } from '../../../../modules/versioning/npm'; import { id as pep440VersioningId } from '../../../../modules/versioning/pep440'; import { id as poetryVersioningId } from '../../../../modules/versioning/poetry'; +import type { HostRule } from '../../../../types'; +import * as memCache from '../../../../util/cache/memory'; import * as githubGraphql from '../../../../util/github/graphql'; +import { initConfig, resetConfig } from '../../../../util/merge-confidence'; +import * as McApi from '../../../../util/merge-confidence'; import type { LookupUpdateConfig } from './types'; import * as lookup from '.'; @@ -67,6 +72,7 @@ describe('workers/repository/process/lookup/index', () => { // TODO: fix mocks afterEach(() => { httpMock.clear(false); + hostRules.clear(); }); describe('.lookupUpdates()', () => { @@ -2082,5 +2088,97 @@ describe('workers/repository/process/lookup/index', () => { }, ]); }); + + describe('handles merge confidence', () => { + const defaultApiBaseUrl = 'https://badges.renovateapi.com/'; + const getMergeConfidenceSpy = jest.spyOn( + McApi, + 'getMergeConfidenceLevel' + ); + const hostRule: HostRule = { + hostType: 'merge-confidence', + token: 'some-token', + }; + + beforeEach(() => { + hostRules.add(hostRule); + initConfig(); + memCache.reset(); + }); + + afterEach(() => { + resetConfig(); + }); + + it('gets a merge confidence level for a given update when corresponding packageRule is in use', async () => { + const datasource = NpmDatasource.id; + const packageName = 'webpack'; + const newVersion = '3.8.1'; + const currentValue = '3.7.0'; + config.packageRules = [{ matchConfidence: ['high'] }]; + config.currentValue = currentValue; + config.packageName = packageName; + config.datasource = datasource; + httpMock + .scope('https://registry.npmjs.org') + .get('/webpack') + .reply(200, webpackJson); + httpMock + .scope(defaultApiBaseUrl) + .get( + `/api/mc/json/${datasource}/${packageName}/${currentValue}/${newVersion}` + ) + .reply(200, { confidence: 'high' }); + + const lookupUpdates = (await lookup.lookupUpdates(config)).updates; + + expect(lookupUpdates).toMatchObject([ + { + mergeConfidenceLevel: `high`, + }, + ]); + }); + + it('does not get a merge confidence level when no packageRule is set', async () => { + config.currentValue = '3.7.0'; + config.packageName = 'webpack'; + config.datasource = NpmDatasource.id; + httpMock + .scope('https://registry.npmjs.org') + .get('/webpack') + .reply(200, webpackJson); + + const lookupUpdates = (await lookup.lookupUpdates(config)).updates; + + expect(getMergeConfidenceSpy).toHaveBeenCalledTimes(0); + expect(lookupUpdates).not.toMatchObject([ + { + mergeConfidenceLevel: expect.anything(), + }, + ]); + }); + + it('does not set merge confidence value when API is not in use', async () => { + const datasource = NpmDatasource.id; + config.packageRules = [{ matchConfidence: ['high'] }]; + config.currentValue = '3.7.0'; + config.packageName = 'webpack'; + config.datasource = datasource; + hostRules.clear(); // reset merge confidence + initConfig(); + httpMock + .scope('https://registry.npmjs.org') + .get('/webpack') + .reply(200, webpackJson); + + const lookupUpdates = (await lookup.lookupUpdates(config)).updates; + + expect(lookupUpdates).not.toMatchObject([ + { + mergeConfidenceLevel: expect.anything(), + }, + ]); + }); + }); }); }); diff --git a/lib/workers/repository/process/lookup/index.ts b/lib/workers/repository/process/lookup/index.ts index 70c1e0595a6f5c..5b1b0ba7b4d8c5 100644 --- a/lib/workers/repository/process/lookup/index.ts +++ b/lib/workers/repository/process/lookup/index.ts @@ -285,7 +285,7 @@ export async function lookupUpdates( return res; } const newVersion = release.version; - const update = generateUpdate( + const update = await generateUpdate( config, versioning, // TODO #7154