diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a913e289a605467039dbcfff77d2ea3c6d1b8435..cd1b11e43d6b5983ba0ac5835d18fdcd3a74aac3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -404,7 +404,7 @@ jobs: - name: Check coverage threshold run: | pnpm nyc check-coverage -t ./coverage/nyc \ - --branches 98 \ + --branches 98.99 \ --functions 100 \ --lines 100 \ --statements 100 diff --git a/jest.config.ts b/jest.config.ts index 76b3eaa996f6ea440f403488fbf312a580f9dc92..d354593760a4d7fda201fd004c7ed5b8f7495f09 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -213,7 +213,9 @@ const config: JestConfig = { cacheDirectory: '.cache/jest', clearMocks: true, collectCoverage: true, - coverageReporters: ci ? ['lcovonly', 'json'] : ['html', 'text-summary'], + coverageReporters: ci + ? ['lcovonly', 'json'] + : ['html', 'text-summary', 'json'], transform: { '\\.ts$': [ 'ts-jest', diff --git a/lib/config/presets/internal/auto-generate-replacements.ts b/lib/config/presets/internal/auto-generate-replacements.ts index 83b07c015bb98561cf841281ed30612c4a4d6162..ece2db24bf0316355d200fd366af04b5a4e3ed61 100644 --- a/lib/config/presets/internal/auto-generate-replacements.ts +++ b/lib/config/presets/internal/auto-generate-replacements.ts @@ -1,3 +1,4 @@ +import { coerceArray } from '../../../util/array'; import type { PackageRule } from '../../types'; import type { Preset } from '../types'; @@ -45,7 +46,7 @@ export function addPresets( presets: Record<string, Preset>, ...templates: PresetTemplate[] ): void { - const ext = presets.all?.extends ?? []; + const ext = coerceArray(presets.all?.extends); for (const template of templates) { const { title, description, packageRules } = template; presets[title] = { diff --git a/lib/config/presets/internal/monorepo.ts b/lib/config/presets/internal/monorepo.ts index f5d8ca165458f2a9d5285770304e71a1c3af987a..d57743e8e754e49f4740ca2b4e5e8ffa0b638a0d 100644 --- a/lib/config/presets/internal/monorepo.ts +++ b/lib/config/presets/internal/monorepo.ts @@ -1,4 +1,4 @@ -import is from '@sindresorhus/is'; +import { toArray } from '../../../util/array'; import type { Preset } from '../types'; /* eslint sort-keys: ["error", "asc", {caseSensitive: false, natural: true}] */ @@ -482,20 +482,20 @@ export const presets: Record<string, Preset> = {}; for (const [name, value] of Object.entries(repoGroups)) { presets[name] = { description: `${name} monorepo`, - matchSourceUrls: is.array(value) ? value : [value], + matchSourceUrls: toArray(value), }; } for (const [name, value] of Object.entries(orgGroups)) { presets[name] = { description: `${name} monorepo`, - matchSourceUrlPrefixes: is.array(value) ? value : [value], + matchSourceUrlPrefixes: toArray(value), }; } for (const [name, value] of Object.entries(patternGroups)) { presets[name] = { description: `${name} monorepo`, - matchPackagePatterns: is.array(value) ? value : [value], + matchPackagePatterns: toArray(value), }; } diff --git a/lib/modules/datasource/bitbucket-tags/index.spec.ts b/lib/modules/datasource/bitbucket-tags/index.spec.ts index d2340ea5791070f0dd8bce2d5cd09c85c99ddf78..02f7022a9acc7c1bdf0ee52815c1e1e2b514a72b 100644 --- a/lib/modules/datasource/bitbucket-tags/index.spec.ts +++ b/lib/modules/datasource/bitbucket-tags/index.spec.ts @@ -124,5 +124,23 @@ describe('modules/datasource/bitbucket-tags/index', () => { expect(res).toBeString(); expect(res).toBe('123'); }); + + it('returns null for missing hash', async () => { + const body = { + name: 'v1.0.0', + }; + httpMock + .scope('https://api.bitbucket.org') + .get('/2.0/repositories/some/dep2/refs/tags/v1.0.0') + .reply(200, body); + const res = await getDigest( + { + datasource, + packageName: 'some/dep2', + }, + 'v1.0.0' + ); + expect(res).toBeNull(); + }); }); }); diff --git a/lib/modules/datasource/conan/index.spec.ts b/lib/modules/datasource/conan/index.spec.ts index e9025f4386118d78a2000b08b93f3f08a977ca96..f87096f168f93aac4b2ffdc1297807ceb4e5690d 100644 --- a/lib/modules/datasource/conan/index.spec.ts +++ b/lib/modules/datasource/conan/index.spec.ts @@ -51,6 +51,17 @@ describe('modules/datasource/conan/index', () => { '3a9b47caee2e2c1d3fb7d97788339aa8' ); }); + + it('returns null for missing revision', async () => { + const version = '1.8.1'; + httpMock + .scope(nonDefaultRegistryUrl) + .get(`/v2/conans/poco/${version}/_/_/revisions`) + .reply(200, []); + digestConfig.packageName = `poco/${version}@_/_`; + digestConfig.currentDigest = '4fc13d60fd91ba44fefe808ad719a5af'; + expect(await getDigest(digestConfig, version)).toBeNull(); + }); }); describe('getReleases', () => { @@ -180,6 +191,22 @@ describe('modules/datasource/conan/index', () => { }); }); + it('works with empty releases', async () => { + httpMock + .scope('https://api.github.com') + .get( + '/repos/conan-io/conan-center-index/contents/recipes/poco/config.yml' + ) + .reply(200, ''); + expect( + await getPkgReleases({ + ...config, + registryUrls: [defaultRegistryUrl], + packageName: 'poco/1.2@_/_', + }) + ).toBeNull(); + }); + it('rejects userAndChannel for Conan Center', async () => { expect( await getPkgReleases({ diff --git a/lib/modules/datasource/conda/index.spec.ts b/lib/modules/datasource/conda/index.spec.ts index 0085c36c69122e32d49eeb5f535f73f7e8228e09..e77c16d2ae26aa629d145f94051a37515b2b20db 100644 --- a/lib/modules/datasource/conda/index.spec.ts +++ b/lib/modules/datasource/conda/index.spec.ts @@ -31,7 +31,10 @@ describe('modules/datasource/conda/index', () => { }); it('returns null for empty result', async () => { - httpMock.scope(defaultRegistryUrl).get(depUrl).reply(200, {}); + httpMock + .scope(defaultRegistryUrl) + .get(depUrl) + .reply(200, { versions: [] }); expect( await getPkgReleases({ datasource, diff --git a/lib/modules/datasource/dart-version/index.spec.ts b/lib/modules/datasource/dart-version/index.spec.ts index ae41cd725dfa84e473d562fae021a8053f2b6ddb..4d612e7e566f823dd00e3cd9f80d1aae2e5ca9aa 100644 --- a/lib/modules/datasource/dart-version/index.spec.ts +++ b/lib/modules/datasource/dart-version/index.spec.ts @@ -34,7 +34,14 @@ describe('modules/datasource/dart-version/index', () => { }); it('returns null for empty 200 OK', async () => { - httpMock.scope(baseUrl).get(urlPath).reply(200, []); + const scope = httpMock.scope(baseUrl); + for (const channel of channels) { + scope + .get( + `/storage/v1/b/dart-archive/o?delimiter=%2F&prefix=channels%2F${channel}%2Frelease%2F&alt=json` + ) + .reply(200, { prefixes: [] }); + } expect( await getPkgReleases({ datasource, diff --git a/lib/modules/datasource/endoflife-date/index.spec.ts b/lib/modules/datasource/endoflife-date/index.spec.ts index b564776ba189667cfb18d02a91f15212d813c240..2c29e0e3c385724606532a9770348215d6a484b2 100644 --- a/lib/modules/datasource/endoflife-date/index.spec.ts +++ b/lib/modules/datasource/endoflife-date/index.spec.ts @@ -100,7 +100,7 @@ describe('modules/datasource/endoflife-date/index', () => { }); it('returns null for empty result', async () => { - httpMock.scope(registryUrl).get(eksMockPath).reply(200, {}); + httpMock.scope(registryUrl).get(eksMockPath).reply(200, []); expect( await getPkgReleases({ datasource, diff --git a/lib/modules/datasource/flutter-version/index.spec.ts b/lib/modules/datasource/flutter-version/index.spec.ts index 8e333b66583e43b3c546f22449e77bf86846f9a0..5af2bb5e92d008ea778afab85e023a74868e9e93 100644 --- a/lib/modules/datasource/flutter-version/index.spec.ts +++ b/lib/modules/datasource/flutter-version/index.spec.ts @@ -32,7 +32,7 @@ describe('modules/datasource/flutter-version/index', () => { }); it('returns null for empty 200 OK', async () => { - httpMock.scope(baseUrl).get(urlPath).reply(200, []); + httpMock.scope(baseUrl).get(urlPath).reply(200, { releases: [] }); expect( await getPkgReleases({ datasource, diff --git a/lib/modules/datasource/flutter-version/index.ts b/lib/modules/datasource/flutter-version/index.ts index 803da054fa0cbc1de9ea10bd90f1ffbd3b62add2..293134377f733401bf77463b54eb685fdea73a24 100644 --- a/lib/modules/datasource/flutter-version/index.ts +++ b/lib/modules/datasource/flutter-version/index.ts @@ -54,10 +54,9 @@ export class FlutterVersionDatasource extends Datasource { releaseTimestamp: release_date, isStable: channel === 'stable', })); + return result.releases.length ? result : null; } catch (err) { this.handleGenericErrors(err); } - - return result.releases.length ? result : null; } } diff --git a/lib/modules/datasource/hermit/index.ts b/lib/modules/datasource/hermit/index.ts index cb95421f5fe3f7206c16d6f923a08adff25ab480..5fd91d5b4ae682e92c8e285693b21b359daf88ac 100644 --- a/lib/modules/datasource/hermit/index.ts +++ b/lib/modules/datasource/hermit/index.ts @@ -5,6 +5,7 @@ import { getApiBaseUrl } from '../../../util/github/url'; import { GithubHttp } from '../../../util/http/github'; import { regEx } from '../../../util/regex'; import { streamToString } from '../../../util/streams'; +import { coerceString } from '../../../util/string'; import { parseUrl } from '../../../util/url'; import { id } from '../../versioning/hermit'; import { Datasource } from '../datasource'; @@ -106,8 +107,8 @@ export class HermitDatasource extends Datasource { }) async getHermitSearchManifest(u: URL): Promise<HermitSearchResult[] | null> { const registryUrl = u.toString(); - const host = u.host ?? ''; - const groups = this.pathRegex.exec(u.pathname ?? '')?.groups; + const host = coerceString(u.host); + const groups = this.pathRegex.exec(coerceString(u.pathname))?.groups; if (!groups) { logger.warn( { registryUrl }, diff --git a/lib/modules/datasource/index.ts b/lib/modules/datasource/index.ts index 3bc7181d22bd811f9b9d21ac4d18a31ea500db3c..cd6b3b155f50f562db7a4af47a194712af946240 100644 --- a/lib/modules/datasource/index.ts +++ b/lib/modules/datasource/index.ts @@ -3,6 +3,7 @@ import { dequal } from 'dequal'; import { HOST_DISABLED } from '../../constants/error-messages'; import { logger } from '../../logger'; import { ExternalHostError } from '../../types/errors/external-host-error'; +import { coerceArray } from '../../util/array'; import * as memCache from '../../util/cache/memory'; import * as packageCache from '../../util/cache/package'; import { clone } from '../../util/clone'; @@ -149,10 +150,10 @@ async function mergeRegistries( continue; } if (combinedRes) { - for (const existingRelease of combinedRes.releases || []) { + for (const existingRelease of coerceArray(combinedRes.releases)) { existingRelease.registryUrl ??= combinedRes.registryUrl; } - for (const additionalRelease of res.releases || []) { + for (const additionalRelease of coerceArray(res.releases)) { additionalRelease.registryUrl = res.registryUrl; } combinedRes = { ...res, ...combinedRes }; diff --git a/lib/modules/datasource/jenkins-plugins/index.spec.ts b/lib/modules/datasource/jenkins-plugins/index.spec.ts index b45707cb40b97e13237fc8a73f5d537952badebf..3d386e61e858fa8f6d554214572e8f3d85ed6d3f 100644 --- a/lib/modules/datasource/jenkins-plugins/index.spec.ts +++ b/lib/modules/datasource/jenkins-plugins/index.spec.ts @@ -22,7 +22,6 @@ const jenkinsPluginsVersions: JenkinsPluginsVersionsResponse = { '1.0.0': { version: '1.0.0', url: 'https://download.example.com', - buildDate: 'Jan 01, 2020', }, '2.0.0': { version: '2.0.0', @@ -83,7 +82,6 @@ describe('modules/datasource/jenkins-plugins/index', () => { releases: [ { downloadUrl: 'https://download.example.com', - releaseTimestamp: '2020-01-01T00:00:00.000Z', version: '1.0.0', }, { diff --git a/lib/modules/datasource/pod/index.spec.ts b/lib/modules/datasource/pod/index.spec.ts index 3f90ac87f0dd11e66cea1dd732b39414f58aa231..9ac7b8c9af0b80a702bbdf3450df1b4f20b1b3d7 100644 --- a/lib/modules/datasource/pod/index.spec.ts +++ b/lib/modules/datasource/pod/index.spec.ts @@ -1,6 +1,7 @@ import { getPkgReleases } from '..'; import * as httpMock from '../../../../test/http-mock'; import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages'; +import * as hostRules from '../../../util/host-rules'; import * as rubyVersioning from '../../versioning/ruby'; import { PodDatasource } from '.'; @@ -20,6 +21,7 @@ describe('modules/datasource/pod/index', () => { describe('getReleases', () => { beforeEach(() => { jest.resetAllMocks(); + hostRules.clear(); }); it('returns null for invalid inputs', async () => { @@ -37,6 +39,16 @@ describe('modules/datasource/pod/index', () => { ).toBeNull(); }); + it('returns null disabled host', async () => { + hostRules.add({ matchHost: cocoapodsHost, enabled: false }); + expect( + await getPkgReleases({ + datasource: PodDatasource.id, + packageName: 'foobar', + }) + ).toBeNull(); + }); + it('returns null for empty result', async () => { // FIXME: why get request? httpMock @@ -119,6 +131,14 @@ describe('modules/datasource/pod/index', () => { await expect(getPkgReleases(config)).rejects.toThrow(EXTERNAL_HOST_ERROR); }); + it('throws for 500', async () => { + httpMock + .scope(cocoapodsHost) + .get('/all_pods_versions_a_c_b.txt') + .reply(500); + await expect(getPkgReleases(config)).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + it('returns null for unknown error', async () => { httpMock .scope(cocoapodsHost) diff --git a/lib/modules/datasource/pod/index.ts b/lib/modules/datasource/pod/index.ts index 6cf771fa196d5916d8d3dd515b54494673f5ccd0..92cb4aadd85827bdf0d8905f44855a4a4e39669a 100644 --- a/lib/modules/datasource/pod/index.ts +++ b/lib/modules/datasource/pod/index.ts @@ -68,7 +68,6 @@ function handleError(packageName: string, err: HttpError): void { } else if (statusCode === 404) { logger.debug(errorData, 'Package lookup error'); } else if (err.message === HOST_DISABLED) { - // istanbul ignore next logger.trace(errorData, 'Host disabled'); } else { logger.warn(errorData, 'CocoaPods lookup failure: Unknown error'); @@ -77,8 +76,8 @@ function handleError(packageName: string, err: HttpError): void { function isDefaultRepo(url: string): boolean { const match = githubRegex.exec(url); - if (match) { - const { account, repo } = match.groups ?? {}; + if (match?.groups) { + const { account, repo } = match.groups; return ( account.toLowerCase() === 'cocoapods' && repo.toLowerCase() === 'specs' ); // https://github.com/CocoaPods/Specs.git @@ -228,9 +227,9 @@ export class PodDatasource extends Datasource { let result: ReleaseResult | null = null; const match = githubRegex.exec(baseUrl); - if (match) { + if (match?.groups) { baseUrl = massageGithubUrl(baseUrl); - const { hostURL, account, repo } = match?.groups ?? {}; + const { hostURL, account, repo } = match.groups; const opts = { hostURL, account, repo }; result = await this.getReleasesFromGithub(podName, opts); } else { diff --git a/lib/modules/datasource/sbt-package/util.spec.ts b/lib/modules/datasource/sbt-package/util.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ca0304265a06341089bd989879923a605092647 --- /dev/null +++ b/lib/modules/datasource/sbt-package/util.spec.ts @@ -0,0 +1,7 @@ +import { getLatestVersion } from './util'; + +describe('modules/datasource/sbt-package/util', () => { + it('gets latest version', () => { + expect(getLatestVersion(['1.0.0', '3.0.0', '2.0.0'])).toBe('3.0.0'); + }); +}); diff --git a/lib/modules/datasource/sbt-package/util.ts b/lib/modules/datasource/sbt-package/util.ts index 7af9db18ccdba526f37de00606a98eb29f4b6a77..c67973a4efd7fd23a5862be1fab5416e664db65c 100644 --- a/lib/modules/datasource/sbt-package/util.ts +++ b/lib/modules/datasource/sbt-package/util.ts @@ -1,3 +1,4 @@ +import { coerceArray } from '../../../util/array'; import { regEx } from '../../../util/regex'; import { compare } from '../../versioning/maven/compare'; @@ -7,7 +8,7 @@ export function parseIndexDir( content: string, filterFn = (x: string): boolean => !regEx(/^\.+/).test(x) ): string[] { - const unfiltered = content.match(linkRegExp) ?? []; + const unfiltered = coerceArray(content.match(linkRegExp)); return unfiltered.filter(filterFn); } diff --git a/lib/modules/datasource/terraform-module/index.ts b/lib/modules/datasource/terraform-module/index.ts index 17bbf9063ac9833e4e9bca1f0dea5784fd71bf18..e85a5c7c99934c99c8512f0c6f49f2f46fd626db 100644 --- a/lib/modules/datasource/terraform-module/index.ts +++ b/lib/modules/datasource/terraform-module/index.ts @@ -1,6 +1,7 @@ import { logger } from '../../../logger'; import { cache } from '../../../util/cache/package/decorator'; import { regEx } from '../../../util/regex'; +import { coerceString } from '../../../util/string'; import * as hashicorpVersioning from '../../versioning/hashicorp'; import type { GetReleasesConfig, ReleaseResult } from '../types'; import { TerraformDatasource } from './base'; @@ -163,7 +164,7 @@ export class TerraformModuleDatasource extends TerraformDatasource { private static getRegistryRepository( packageName: string, - registryUrl = '' + registryUrl: string | undefined ): RegistryRepository { let registry: string; const split = packageName.split('/'); @@ -171,7 +172,7 @@ export class TerraformModuleDatasource extends TerraformDatasource { [registry] = split; split.shift(); } else { - registry = registryUrl; + registry = coerceString(registryUrl); } if (!regEx(/^https?:\/\//).test(registry)) { registry = `https://${registry}`; diff --git a/lib/modules/manager/cake/__fixtures__/build.cake b/lib/modules/manager/cake/__fixtures__/build.cake index c678319560c5386e394ad62deff37d539d210283..faffeb069f47149f69c100554bdce372d5a6232b 100644 --- a/lib/modules/manager/cake/__fixtures__/build.cake +++ b/lib/modules/manager/cake/__fixtures__/build.cake @@ -1,5 +1,5 @@ foo -#addin nuget:?package=Foo.Foo&version=1.1.1 +#addin nuget:?package=Foo.Foo #addin "nuget:?package=Bim.Bim&version=6.6.6" #tool nuget:https://example.com?package=Bar.Bar&version=2.2.2 #module nuget:file:///tmp/?package=Baz.Baz&version=3.3.3 diff --git a/lib/modules/manager/cake/__snapshots__/index.spec.ts.snap b/lib/modules/manager/cake/__snapshots__/index.spec.ts.snap deleted file mode 100644 index 1ab20c798f5a793a7edcd23368b2f8cb1320f678..0000000000000000000000000000000000000000 --- a/lib/modules/manager/cake/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`modules/manager/cake/index extracts 1`] = ` -{ - "deps": [ - { - "currentValue": "1.1.1", - "datasource": "nuget", - "depName": "Foo.Foo", - }, - { - "currentValue": "6.6.6", - "datasource": "nuget", - "depName": "Bim.Bim", - }, - { - "currentValue": "2.2.2", - "datasource": "nuget", - "depName": "Bar.Bar", - "registryUrls": [ - "https://example.com", - ], - }, - { - "currentValue": "3.3.3", - "datasource": "nuget", - "depName": "Baz.Baz", - "skipReason": "unsupported-url", - }, - { - "currentValue": "1.0.3", - "datasource": "nuget", - "depName": "Cake.7zip", - }, - { - "currentValue": "1.0.0", - "datasource": "nuget", - "depName": "Cake.asciidoctorj", - }, - ], -} -`; diff --git a/lib/modules/manager/cake/index.spec.ts b/lib/modules/manager/cake/index.spec.ts index e129be4c12256c8e21ce0ea6ca66aac18c7900af..0fb2e913fc9116b2825d3897b426f353f05be5c1 100644 --- a/lib/modules/manager/cake/index.spec.ts +++ b/lib/modules/manager/cake/index.spec.ts @@ -3,9 +3,9 @@ import { extractPackageFile } from '.'; describe('modules/manager/cake/index', () => { it('extracts', () => { - expect(extractPackageFile(Fixtures.get('build.cake'))).toMatchSnapshot({ + expect(extractPackageFile(Fixtures.get('build.cake'))).toMatchObject({ deps: [ - { depName: 'Foo.Foo', currentValue: '1.1.1' }, + { depName: 'Foo.Foo', currentValue: undefined }, { depName: 'Bim.Bim', currentValue: '6.6.6' }, { depName: 'Bar.Bar', registryUrls: ['https://example.com'] }, { depName: 'Baz.Baz', skipReason: 'unsupported-url' }, diff --git a/lib/modules/manager/custom/regex/strategies.ts b/lib/modules/manager/custom/regex/strategies.ts index 182c43e693684976b46cd5edd2022c934afdd8fc..a77c072be4748fade1a3b4e6a6056f7ffdad6f6c 100644 --- a/lib/modules/manager/custom/regex/strategies.ts +++ b/lib/modules/manager/custom/regex/strategies.ts @@ -12,7 +12,7 @@ import { export function handleAny( content: string, - packageFile: string, + _packageFile: string, config: RegexManagerConfig ): PackageDependency[] { return config.matchStrings @@ -20,7 +20,12 @@ export function handleAny( .flatMap((regex) => regexMatchAll(regex, content)) // match all regex to content, get all matches, reduce to single array .map((matchResult) => createDependency( - { groups: matchResult.groups ?? {}, replaceString: matchResult[0] }, + { + groups: + matchResult.groups ?? + /* istanbul ignore next: can this happen? */ {}, + replaceString: matchResult[0], + }, config ) ) @@ -30,7 +35,7 @@ export function handleAny( export function handleCombination( content: string, - packageFile: string, + _packageFile: string, config: RegexManagerConfig ): PackageDependency[] { const matches = config.matchStrings @@ -43,7 +48,7 @@ export function handleCombination( const extraction = matches .map((match) => ({ - groups: match.groups ?? {}, + groups: match.groups ?? /* istanbul ignore next: can this happen? */ {}, replaceString: match?.groups?.currentValue ?? match?.groups?.currentDigest ? match[0] @@ -93,7 +98,7 @@ function processRecursive(parameters: RecursionParameter): PackageDependency[] { }, config ); - return result ? [result] : []; + return result ? [result] : /* istanbul ignore next: can this happen? */ []; } return regexMatchAll(regexes[index], content).flatMap((match) => { return processRecursive({ diff --git a/lib/modules/manager/docker-compose/extract.ts b/lib/modules/manager/docker-compose/extract.ts index be6014ab0db79b48945c6ac52019717da64b901e..a61843992d883ac7d6dd07f7a691b852392170f9 100644 --- a/lib/modules/manager/docker-compose/extract.ts +++ b/lib/modules/manager/docker-compose/extract.ts @@ -71,7 +71,9 @@ export function extractPackageFile( // Image name/tags for services are only eligible for update if they don't // use variables and if the image is not built locally - const deps = Object.values(services || {}) + const deps = Object.values( + services || /* istanbul ignore next: can never happen */ {} + ) .filter((service) => is.string(service?.image) && !service?.build) .map((service) => { const dep = getDep(service.image, true, extractConfig.registryAliases); diff --git a/lib/modules/manager/helm-values/__fixtures__/multi_and_nested_image_values.yaml b/lib/modules/manager/helm-values/__fixtures__/multi_and_nested_image_values.yaml index afb39690328d719c9ce4cd0d028e6336d56db9a9..5664de2a21cfa7c9060b806aadd90635676f3431 100644 --- a/lib/modules/manager/helm-values/__fixtures__/multi_and_nested_image_values.yaml +++ b/lib/modules/manager/helm-values/__fixtures__/multi_and_nested_image_values.yaml @@ -28,4 +28,4 @@ empty_key: coreImage: registry: docker.io repository: bitnami/harbor-core - tag: 2.1.3-debian-10-r38 + version: 2.1.3-debian-10-r38 diff --git a/lib/modules/manager/jsonnet-bundler/artifacts.ts b/lib/modules/manager/jsonnet-bundler/artifacts.ts index c3216055fd4287cf0dca482855cffd90f0de1602..19027353bd3521906dc773b31ab501d8aa8fd33b 100644 --- a/lib/modules/manager/jsonnet-bundler/artifacts.ts +++ b/lib/modules/manager/jsonnet-bundler/artifacts.ts @@ -1,6 +1,7 @@ import { quote } from 'shlex'; import { TEMPORARY_ERROR } from '../../../constants/error-messages'; import { logger } from '../../../logger'; +import { coerceArray } from '../../../util/array'; import { exec } from '../../../util/exec'; import type { ExecOptions, ToolConstraint } from '../../../util/exec/types'; import { readLocalFile } from '../../../util/fs'; @@ -66,7 +67,7 @@ export async function updateArtifacts( const res: UpdateArtifactsResult[] = []; - for (const f of status.modified ?? []) { + for (const f of coerceArray(status.modified)) { res.push({ file: { type: 'addition', @@ -75,7 +76,7 @@ export async function updateArtifacts( }, }); } - for (const f of status.not_added ?? []) { + for (const f of coerceArray(status.not_added)) { res.push({ file: { type: 'addition', @@ -84,7 +85,7 @@ export async function updateArtifacts( }, }); } - for (const f of status.deleted ?? []) { + for (const f of coerceArray(status.deleted)) { res.push({ file: { type: 'deletion', diff --git a/lib/modules/manager/jsonnet-bundler/extract.ts b/lib/modules/manager/jsonnet-bundler/extract.ts index 28d1f427153c2d6c1bb250bac7325054888489fe..f2565b3c5e4fa25a798cf52a757fda0bf9f013b0 100644 --- a/lib/modules/manager/jsonnet-bundler/extract.ts +++ b/lib/modules/manager/jsonnet-bundler/extract.ts @@ -1,6 +1,7 @@ import { join } from 'upath'; import { logger } from '../../../logger'; import { coerceArray } from '../../../util/array'; +import { coerceString } from '../../../util/string'; import { parseUrl } from '../../../util/url'; import type { PackageDependency, PackageFileContent } from '../types'; import type { Dependency, JsonnetFile } from './types'; @@ -52,7 +53,7 @@ function extractDependency(dependency: Dependency): PackageDependency | null { const depName = join( gitRemote.host, gitRemote.pathname.replace(/\.git$/, ''), - dependency.source.git.subdir ?? '' + coerceString(dependency.source.git.subdir) ); return { diff --git a/lib/modules/manager/mint/extract.spec.ts b/lib/modules/manager/mint/extract.spec.ts index 561159c3459c174d1cb45f499860b5b03f359e38..d0506a0b1e54dca43f8cfc5d34bae0101912205e 100644 --- a/lib/modules/manager/mint/extract.spec.ts +++ b/lib/modules/manager/mint/extract.spec.ts @@ -3,6 +3,10 @@ import { extractPackageFile } from '.'; describe('modules/manager/mint/extract', () => { describe('extractPackageFile()', () => { + it('returns null for empty', () => { + expect(extractPackageFile('')).toBeNull(); + }); + it('Mintfile With Version Description', () => { const res = extractPackageFile(codeBlock` SwiftGen/SwiftGen@6.6.1 diff --git a/lib/modules/manager/woodpecker/extract.spec.ts b/lib/modules/manager/woodpecker/extract.spec.ts index 90774059fe7125a0b32e0a514e6979d59f80875f..d08ca2bbcdf1a4eacafa908acb5010fef31f9b03 100644 --- a/lib/modules/manager/woodpecker/extract.spec.ts +++ b/lib/modules/manager/woodpecker/extract.spec.ts @@ -11,6 +11,7 @@ describe('modules/manager/woodpecker/extract', () => { it('returns null for non-object YAML', () => { expect(extractPackageFile('nothing here', '', {})).toBeNull(); + expect(extractPackageFile('clone: null', '', {})).toBeNull(); }); it('returns null for malformed YAML', () => { diff --git a/lib/modules/platform/codecommit/index.spec.ts b/lib/modules/platform/codecommit/index.spec.ts index 6dfc43483ab9df301798cd20872819bb8210fd65..e82ff378cfda9d0550ef796e55710fe8552f255a 100644 --- a/lib/modules/platform/codecommit/index.spec.ts +++ b/lib/modules/platform/codecommit/index.spec.ts @@ -42,6 +42,9 @@ describe('modules/platform/codecommit/index', () => { }); beforeEach(() => { + delete process.env.AWS_REGION; + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; codeCommitClient.reset(); config.prList = undefined; config.repository = undefined; @@ -70,7 +73,6 @@ describe('modules/platform/codecommit/index', () => { }); it('should init with env vars', async () => { - const temp = process.env.AWS_REGION; process.env.AWS_REGION = 'REGION'; await expect( codeCommit.initPlatform({ @@ -80,7 +82,6 @@ describe('modules/platform/codecommit/index', () => { ).resolves.toEqual({ endpoint: 'https://git-codecommit.REGION.amazonaws.com/', }); - process.env.AWS_REGION = temp; }); it('should ', async () => { @@ -588,6 +589,14 @@ describe('modules/platform/codecommit/index', () => { const res = await codeCommit.getJsonFile('file.json'); expect(res).toEqual({ foo: 'bar' }); }); + + it('returns null', async () => { + codeCommitClient + .on(GetFileCommand) + .resolvesOnce({ fileContent: undefined }); + const res = await codeCommit.getJsonFile('file.json'); + expect(res).toBeNull(); + }); }); describe('getRawFile()', () => { diff --git a/lib/modules/platform/codecommit/index.ts b/lib/modules/platform/codecommit/index.ts index dbc6b4109a961df988367d114b03a4e7c4ccef4a..c9877db7f1062f039a95e69d0358819c519e4e6b 100644 --- a/lib/modules/platform/codecommit/index.ts +++ b/lib/modules/platform/codecommit/index.ts @@ -13,6 +13,7 @@ import { } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import type { BranchStatus, PrState } from '../../../types'; +import { coerceArray } from '../../../util/array'; import * as git from '../../../util/git'; import { regEx } from '../../../util/regex'; import { sanitize } from '../../../util/sanitize'; @@ -163,7 +164,7 @@ export async function getPrList(): Promise<CodeCommitPr[]> { return fetchedPrs; } - const prIds = listPrsResponse.pullRequestIds ?? []; + const prIds = coerceArray(listPrsResponse.pullRequestIds); for (const prId of prIds) { const prRes = await client.getPr(prId); @@ -291,7 +292,7 @@ export async function getRepos(): Promise<string[]> { const res: string[] = []; - const repoNames = reposRes?.repositories ?? []; + const repoNames = coerceArray(reposRes?.repositories); for (const repo of repoNames) { if (repo.repositoryName) { diff --git a/lib/modules/platform/utils/pr-body.ts b/lib/modules/platform/utils/pr-body.ts index a64c78bfc7ca05a0a13bf4cdb63ec3bb78700f4a..4dcf7cc4c0b6498d1a128d30a36417d4e2bcfd9d 100644 --- a/lib/modules/platform/utils/pr-body.ts +++ b/lib/modules/platform/utils/pr-body.ts @@ -11,14 +11,14 @@ export function smartTruncate(input: string, len: number): string { } const reMatch = re.exec(input); - if (!reMatch) { + if (!reMatch?.groups) { return input.substring(0, len); } const divider = `\n\n</details>\n\n---\n\n### Configuration`; - const preNotes = reMatch.groups?.preNotes ?? ''; - const releaseNotes = reMatch.groups?.releaseNotes ?? ''; - const postNotes = reMatch.groups?.postNotes ?? ''; + const preNotes = reMatch.groups.preNotes; + const releaseNotes = reMatch.groups.releaseNotes; + const postNotes = reMatch.groups.postNotes; const availableLength = len - (preNotes.length + postNotes.length + divider.length); diff --git a/lib/modules/versioning/debian/index.spec.ts b/lib/modules/versioning/debian/index.spec.ts index 8db760b072d4cd2449e4e3c718faeef6348158fa..ce19fa9a1227e322de255f5cc28b27dc48f228ab 100644 --- a/lib/modules/versioning/debian/index.spec.ts +++ b/lib/modules/versioning/debian/index.spec.ts @@ -11,10 +11,6 @@ describe('modules/versioning/debian/index', () => { Settings.now = () => dt.valueOf(); }); - afterEach(() => { - jest.resetAllMocks(); - }); - it.each` version | expected ${undefined} | ${false} diff --git a/lib/modules/versioning/debian/index.ts b/lib/modules/versioning/debian/index.ts index d1c9b740ea34068a21f6168b0ee6f173d11bde0a..310df655bd5897820c7e52777ee7ea4544b4d188 100644 --- a/lib/modules/versioning/debian/index.ts +++ b/lib/modules/versioning/debian/index.ts @@ -30,7 +30,7 @@ export class DebianVersioningApi extends GenericVersioningApi { const schedule = this._distroInfo.getSchedule( this._rollingReleases.getVersionByLts(version) ); - return (isValid && schedule && RELEASE_PROP in schedule) ?? false; + return isValid && schedule !== null && RELEASE_PROP in schedule; } override isStable(version: string): boolean { @@ -43,7 +43,6 @@ export class DebianVersioningApi extends GenericVersioningApi { override getNewValue({ currentValue, rangeStrategy, - currentVersion, newVersion, }: NewValueConfig): string { if (rangeStrategy === 'pin') { @@ -83,7 +82,10 @@ export class DebianVersioningApi extends GenericVersioningApi { // newVersion is [oldold|old|]stable // current value is numeric if (this._rollingReleases.has(newVersion)) { - return this._rollingReleases.schedule(newVersion)?.version ?? newVersion; + return ( + this._rollingReleases.schedule(newVersion)?.version ?? + /* istanbul ignore next: should never happen */ newVersion + ); } return this._distroInfo.getVersionByCodename(newVersion); diff --git a/lib/modules/versioning/pep440/range.ts b/lib/modules/versioning/pep440/range.ts index a3d60c1d27382293481476f4b1bd5a4dc4de3711..54eca71de4b53654c39d36a0cf715d9da36978d9 100644 --- a/lib/modules/versioning/pep440/range.ts +++ b/lib/modules/versioning/pep440/range.ts @@ -2,6 +2,7 @@ import { gte, lt, lte, satisfies } from '@renovatebot/pep440'; import { parse as parseRange } from '@renovatebot/pep440/lib/specifier.js'; import { parse as parseVersion } from '@renovatebot/pep440/lib/version.js'; import { logger } from '../../../logger'; +import { coerceArray } from '../../../util/array'; import { regEx } from '../../../util/regex'; import type { NewValueConfig } from '../types'; @@ -28,8 +29,9 @@ type UserPolicy = * @returns A {@link UserPolicy} */ function getRangePrecision(ranges: Range[]): UserPolicy { - const bound: number[] = - parseVersion((ranges[1] || ranges[0]).version)?.release ?? []; + const bound = coerceArray( + parseVersion((ranges[1] || ranges[0]).version)?.release + ); let rangePrecision = -1; // range is defined by a single bound. // ie. <1.2.2.3, @@ -39,7 +41,7 @@ function getRangePrecision(ranges: Range[]): UserPolicy { } // Range is defined by both upper and lower bounds. if (ranges.length === 2) { - const lowerBound: number[] = parseVersion(ranges[0].version)?.release ?? []; + const lowerBound = coerceArray(parseVersion(ranges[0].version)?.release); rangePrecision = bound.findIndex((el, index) => el > lowerBound[index]); } // Tune down Major precision if followed by a zero @@ -74,11 +76,12 @@ function getFutureVersion( newVersion: string, baseVersion?: string ): number[] { - const toRelease: number[] = parseVersion(newVersion)?.release ?? []; - const baseRelease: number[] = - parseVersion(baseVersion ?? newVersion)?.release ?? []; + const toRelease = coerceArray(parseVersion(newVersion)?.release); + const baseRelease = coerceArray( + parseVersion(baseVersion ?? newVersion)?.release + ); return baseRelease.map((_, index) => { - const toPart: number = toRelease[index] ?? 0; + const toPart = toRelease[index] ?? 0; if (index < policy) { return toPart; } @@ -303,8 +306,8 @@ function updateRangeValue( return range.operator + futureVersion + '.*'; } if (range.operator === '~=') { - const baseVersion = parseVersion(range.version)?.release ?? []; - const futureVersion = parseVersion(newVersion)?.release ?? []; + const baseVersion = coerceArray(parseVersion(range.version)?.release); + const futureVersion = coerceArray(parseVersion(newVersion)?.release); const baseLen = baseVersion.length; const newVerLen = futureVersion.length; // trim redundant trailing version specifiers @@ -410,7 +413,7 @@ function handleWidenStrategy( return newRanges.map((range) => { // newVersion is over the upper bound if (range.operator === '<' && gte(newVersion, range.version)) { - const upperBound = parseVersion(range.version)?.release ?? []; + const upperBound = coerceArray(parseVersion(range.version)?.release); const len = upperBound.length; // Match the precision of the smallest specifier if other than 0 if (upperBound[len - 1] !== 0) { @@ -474,7 +477,7 @@ function handleReplaceStrategy( return '>=' + newVersion; } // update the lower bound to reflect the accepted new version - const lowerBound = parseVersion(range.version)?.release ?? []; + const lowerBound = coerceArray(parseVersion(range.version)?.release); const rangePrecision = lowerBound.length - 1; let newBase = getFutureVersion(rangePrecision, newVersion); if (trimZeros) { diff --git a/lib/modules/versioning/redhat/index.ts b/lib/modules/versioning/redhat/index.ts index 21e468c907c5c69a9bbea760f1990510a7a4d1ab..e9503582388877992e138acbdcde94f86c2bc6a8 100644 --- a/lib/modules/versioning/redhat/index.ts +++ b/lib/modules/versioning/redhat/index.ts @@ -20,7 +20,7 @@ class RedhatVersioningApi extends GenericVersioningApi { const { major, minor, patch, releaseMajor, releaseMinor } = matches; const release = [ - typeof major === 'undefined' ? 0 : Number.parseInt(major, 10), + Number.parseInt(major, 10), typeof minor === 'undefined' ? 0 : Number.parseInt(minor, 10), typeof patch === 'undefined' ? 0 : Number.parseInt(patch, 10), typeof releaseMajor === 'undefined' diff --git a/lib/modules/versioning/rez/index.ts b/lib/modules/versioning/rez/index.ts index d094875f0560486c3cdfe872b1bc1c05ac87ee6b..06ab51161d2a63eb190f97302bcd96cfe9ec8e56 100644 --- a/lib/modules/versioning/rez/index.ts +++ b/lib/modules/versioning/rez/index.ts @@ -1,5 +1,6 @@ import type { RangeStrategy } from '../../../types/versioning'; import { regEx } from '../../../util/regex'; +import { coerceString } from '../../../util/string'; import { api as npm } from '../npm'; import { api as pep440 } from '../pep440'; import type { NewValueConfig, VersioningApi } from '../types'; @@ -160,10 +161,12 @@ function getNewValue({ const lowerAscVersionCurrent = matchAscRange.groups.range_lower_asc_version; const upperAscVersionCurrent = matchAscRange.groups.range_upper_asc_version; const [lowerBoundAscPep440, upperBoundAscPep440] = pep440Value.split(', '); - const lowerAscVersionNew = - regEx(versionGroup).exec(lowerBoundAscPep440)?.[0] ?? ''; - const upperAscVersionNew = - regEx(versionGroup).exec(upperBoundAscPep440)?.[0] ?? ''; + const lowerAscVersionNew = coerceString( + regEx(versionGroup).exec(lowerBoundAscPep440)?.[0] + ); + const upperAscVersionNew = coerceString( + regEx(versionGroup).exec(upperBoundAscPep440)?.[0] + ); const lowerBoundAscNew = lowerBoundAscCurrent.replace( lowerAscVersionCurrent, lowerAscVersionNew @@ -189,10 +192,12 @@ function getNewValue({ const [lowerBoundDescPep440, upperBoundDescPep440] = pep440Value.split(', '); - const upperDescVersionNew = - regEx(versionGroup).exec(upperBoundDescPep440)?.[0] ?? ''; - const lowerDescVersionNew = - regEx(versionGroup).exec(lowerBoundDescPep440)?.[0] ?? ''; + const upperDescVersionNew = coerceString( + regEx(versionGroup).exec(upperBoundDescPep440)?.[0] + ); + const lowerDescVersionNew = coerceString( + regEx(versionGroup).exec(lowerBoundDescPep440)?.[0] + ); const upperBoundDescNew = upperBoundDescCurrent.replace( upperDescVersionCurrent, upperDescVersionNew diff --git a/lib/modules/versioning/swift/index.spec.ts b/lib/modules/versioning/swift/index.spec.ts index ee47bb36f37ad65a83a42ad61e46da00752221cb..b79c6addd3b61a7f6b7cec6ce37b651809144f93 100644 --- a/lib/modules/versioning/swift/index.spec.ts +++ b/lib/modules/versioning/swift/index.spec.ts @@ -61,6 +61,7 @@ describe('modules/versioning/swift/index', () => { versions | range | expected ${['1.2.3', '1.2.4', '1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'} ${['v1.2.3', 'v1.2.4', 'v1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'} + ${['v1.2.3', 'v1.2.4', 'v1.2.5']} | ${''} | ${null} `( 'minSatisfyingVersion($versions, "$range") === "$expected"', ({ versions, range, expected }) => { @@ -73,6 +74,7 @@ describe('modules/versioning/swift/index', () => { ${['1.2.3', '1.2.4', '1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'} ${['v1.2.3', 'v1.2.4', 'v1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'} ${['1.2.3', '1.2.4', '1.2.5']} | ${'..."1.2.4"'} | ${'1.2.4'} + ${['1.2.3', '1.2.4', '1.2.5']} | ${''} | ${null} `( 'getSatisfyingVersion($versions, "$range") === "$expected"', ({ versions, range, expected }) => { @@ -86,6 +88,7 @@ describe('modules/versioning/swift/index', () => { ${'v1.2.3'} | ${'..."1.2.4"'} | ${false} ${'1.2.3'} | ${'"1.2.4"...'} | ${true} ${'v1.2.3'} | ${'"1.2.4"...'} | ${true} + ${'v1.2.3'} | ${''} | ${false} `( 'isLessThanRange("$version", "$range") === "$expected"', ({ version, range, expected }) => { @@ -99,6 +102,7 @@ describe('modules/versioning/swift/index', () => { ${'v1.2.4'} | ${'..."1.2.4"'} | ${true} ${'1.2.4'} | ${'..."1.2.3"'} | ${false} ${'v1.2.4'} | ${'..."1.2.3"'} | ${false} + ${'v1.2.4'} | ${''} | ${false} `( 'matches("$version", "$range") === "$expected"', ({ version, range, expected }) => { diff --git a/lib/util/array.spec.ts b/lib/util/array.spec.ts index 57356be7de65df1acf9636fedee1203b8a02b1d6..965aa6257050aca08c983b0dfc40a26f71439f44 100644 --- a/lib/util/array.spec.ts +++ b/lib/util/array.spec.ts @@ -1,4 +1,4 @@ -import { isNotNullOrUndefined } from './array'; +import { isNotNullOrUndefined, toArray } from './array'; describe('util/array', () => { it.each` @@ -9,4 +9,13 @@ describe('util/array', () => { `('.isNotNullOrUndefined', ({ a, exp }) => { expect(isNotNullOrUndefined(a)).toEqual(exp); }); + + it.each` + a | exp + ${null} | ${[null]} + ${undefined} | ${[undefined]} + ${[]} | ${[]} + `('.toArray', ({ a, exp }) => { + expect(toArray(a)).toEqual(exp); + }); }); diff --git a/lib/util/array.ts b/lib/util/array.ts index 0175fabf26f8e43f4716198b737c81196ccf23f2..f4ad9412fee1dbe5716389aef367262621365eb7 100644 --- a/lib/util/array.ts +++ b/lib/util/array.ts @@ -19,3 +19,12 @@ export function isNotNullOrUndefined<T>( ): value is T { return !is.nullOrUndefined(value); } + +/** + * Converts a single value or an array of values to an array of values. + * @param value a single value or an array of values + * @returns array of values + */ +export function toArray<T>(value: T | T[]): T[] { + return is.array(value) ? value : [value]; +} diff --git a/lib/util/string.spec.ts b/lib/util/string.spec.ts index 202b15e5d1eb4d797bc9c0a7d3bb7803080f40c8..3394a0236492e86c63eaac676694fbeec77e4df4 100644 --- a/lib/util/string.spec.ts +++ b/lib/util/string.spec.ts @@ -1,4 +1,4 @@ -import { looseEquals, replaceAt } from './string'; +import { coerceString, looseEquals, replaceAt } from './string'; describe('util/string', () => { describe('replaceAt', () => { @@ -32,4 +32,11 @@ describe('util/string', () => { expect(looseEquals(null, '')).toBeFalse(); }); }); + + it('coerceString', () => { + expect(coerceString('foo')).toBe('foo'); + expect(coerceString('')).toBe(''); + expect(coerceString(undefined)).toBe(''); + expect(coerceString(null)).toBe(''); + }); }); diff --git a/lib/util/string.ts b/lib/util/string.ts index d62a1be1a242c0e4124a505b0f217544602ec876..ce6bad089d9fd5ed19653391fc1a17f1d11510b1 100644 --- a/lib/util/string.ts +++ b/lib/util/string.ts @@ -82,3 +82,7 @@ export function copystr(x: string): string { buf.write(x, 'utf8'); return buf.toString('utf8'); } + +export function coerceString(val: string | null | undefined): string { + return val ?? ''; +} diff --git a/lib/workers/repository/model/commit-message-factory.ts b/lib/workers/repository/model/commit-message-factory.ts index 3feb7c594eb519fb789ca598b5bd5abf1b76753e..e0c65ffa72d1461b03362e85f6b6a71647e15a4c 100644 --- a/lib/workers/repository/model/commit-message-factory.ts +++ b/lib/workers/repository/model/commit-message-factory.ts @@ -1,4 +1,5 @@ import type { RenovateSharedConfig } from '../../../config/types'; +import { coerceString } from '../../../util/string'; import type { CommitMessage } from './commit-message'; import { CustomCommitMessage } from './custom-commit-message'; import { SemanticCommitMessage } from './semantic-commit-message'; @@ -29,8 +30,8 @@ export class CommitMessageFactory { private createSemanticCommitMessage(): SemanticCommitMessage { const message = new SemanticCommitMessage(); - message.type = this._config.semanticCommitType ?? ''; - message.scope = this._config.semanticCommitScope ?? ''; + message.type = coerceString(this._config.semanticCommitType); + message.scope = coerceString(this._config.semanticCommitScope); return message; } diff --git a/lib/workers/repository/model/semantic-commit-message.ts b/lib/workers/repository/model/semantic-commit-message.ts index a3e5d15888d9067d71307df9c1b05c0359ec8c38..1b20315b332400e49c3269b808a9e7175c7e7da9 100644 --- a/lib/workers/repository/model/semantic-commit-message.ts +++ b/lib/workers/repository/model/semantic-commit-message.ts @@ -27,11 +27,11 @@ export class SemanticCommitMessage extends CommitMessage { static fromString(value: string): SemanticCommitMessage | undefined { const match = value.match(SemanticCommitMessage.REGEXP); - if (!match) { + if (!match?.groups) { return undefined; } - const { groups = {} } = match; + const { groups } = match; const message = new SemanticCommitMessage(); message.type = groups.type; message.scope = groups.scope;