diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd1b11e43d6b5983ba0ac5835d18fdcd3a74aac3..4a586350af19c652984004bbee9c67d9c94dce55 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.99 \ + --branches 99.4 \ --functions 100 \ --lines 100 \ --statements 100 diff --git a/lib/modules/datasource/go/index.ts b/lib/modules/datasource/go/index.ts index 1406bb7d0dae78b887725ce59abff35910faacd5..8468524b904bbbd46d2fe94adc430f8f940d90d6 100644 --- a/lib/modules/datasource/go/index.ts +++ b/lib/modules/datasource/go/index.ts @@ -77,16 +77,16 @@ export class GoDatasource extends Datasource { switch (source.datasource) { case GitTagsDatasource.id: { - return this.direct.git.getDigest?.(source, tag) ?? null; + return this.direct.git.getDigest(source, tag); } case GithubTagsDatasource.id: { return this.direct.github.getDigest(source, tag); } case BitbucketTagsDatasource.id: { - return this.direct.bitbucket.getDigest?.(source, tag) ?? null; + return this.direct.bitbucket.getDigest(source, tag); } case GitlabTagsDatasource.id: { - return this.direct.gitlab.getDigest?.(source, tag) ?? null; + return this.direct.gitlab.getDigest(source, tag); } /* istanbul ignore next: can never happen, makes lint happy */ default: { diff --git a/lib/modules/datasource/hexpm-bob/index.ts b/lib/modules/datasource/hexpm-bob/index.ts index fb07083aa1d51f3e5c086c2aa9bdec5a606c132f..f1154cdf517332f2af2ad66cc9e259ea822316d2 100644 --- a/lib/modules/datasource/hexpm-bob/index.ts +++ b/lib/modules/datasource/hexpm-bob/index.ts @@ -27,7 +27,7 @@ export class HexpmBobDatasource extends Datasource { @cache({ namespace: `datasource-${datasource}`, key: ({ registryUrl, packageName }: GetReleasesConfig) => - `${registryUrl ?? defaultRegistryUrl}:${packageName}`, + `${registryUrl}:${packageName}`, }) async getReleases({ registryUrl, diff --git a/lib/modules/datasource/nuget/v3.ts b/lib/modules/datasource/nuget/v3.ts index 72db618429c52c0c3226e112ae65bad49269beb8..d515d3c916e40dc95f64007278ea0fb3ec06a55a 100644 --- a/lib/modules/datasource/nuget/v3.ts +++ b/lib/modules/datasource/nuget/v3.ts @@ -146,7 +146,7 @@ export async function getReleases( return null; } - // istanbul ignore if: only happens when no stable version exists + // istanbul ignore next: only happens when no stable version exists if (latestStable === null && catalogPages.length) { const last = catalogEntries.pop()!; latestStable = removeBuildMeta(last.version); diff --git a/lib/modules/datasource/packagist/index.ts b/lib/modules/datasource/packagist/index.ts index 9e2b203be86371d0e72b817f6993f90cffcf9ce2..0bef353d3379eec0a1481228783f076956f0c86c 100644 --- a/lib/modules/datasource/packagist/index.ts +++ b/lib/modules/datasource/packagist/index.ts @@ -69,7 +69,9 @@ export class PackagistDatasource extends Datasource { regFile: RegistryFile ): string { const { key, hash } = regFile; - const fileName = hash ? key.replace('%hash%', hash) : key; + const fileName = hash + ? key.replace('%hash%', hash) + : /* istanbul ignore next: hard to test */ key; const url = resolveBaseUrl(regUrl, fileName); return url; } diff --git a/lib/modules/datasource/pypi/index.ts b/lib/modules/datasource/pypi/index.ts index ddc76d5b0b1d8c4f6a5c367a79978b4a0ceb6f19..78231497ceac33550a4491e6eb2900be38881a42 100644 --- a/lib/modules/datasource/pypi/index.ts +++ b/lib/modules/datasource/pypi/index.ts @@ -1,6 +1,7 @@ import url from 'node:url'; import changelogFilenameRegex from 'changelog-filename-regex'; import { logger } from '../../../logger'; +import { coerceArray } from '../../../util/array'; import { parse } from '../../../util/html'; import { regEx } from '../../../util/regex'; import { ensureTrailingSlash } from '../../../util/url'; @@ -148,7 +149,7 @@ export class PypiDatasource extends Datasource { if (dep.releases) { const versions = Object.keys(dep.releases); dependency.releases = versions.map((version) => { - const releases = dep.releases?.[version] ?? []; + const releases = coerceArray(dep.releases?.[version]); const { upload_time: releaseTimestamp } = releases[0] || {}; const isDeprecated = releases.some(({ yanked }) => yanked); const result: Release = { @@ -262,7 +263,7 @@ export class PypiDatasource extends Datasource { } const versions = Object.keys(releases); dependency.releases = versions.map((version) => { - const versionReleases = releases[version] ?? []; + const versionReleases = coerceArray(releases[version]); const isDeprecated = versionReleases.some(({ yanked }) => yanked); const result: Release = { version }; if (isDeprecated) { diff --git a/lib/modules/datasource/repology/index.spec.ts b/lib/modules/datasource/repology/index.spec.ts index 7e7df992af3e08bd7c1d473ca5e331188534447e..f102a95da374d6ccf026eb072d2172cf55e72436 100644 --- a/lib/modules/datasource/repology/index.spec.ts +++ b/lib/modules/datasource/repology/index.spec.ts @@ -2,6 +2,7 @@ import { getPkgReleases } from '..'; import { Fixtures } from '../../../../test/fixtures'; import * as httpMock from '../../../../test/http-mock'; import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages'; +import * as hostRules from '../../../util/host-rules'; import { id as versioning } from '../../versioning/loose'; import type { RepologyPackage } from './types'; import { RepologyDatasource } from './index'; @@ -56,6 +57,10 @@ const fixtureJdk = Fixtures.get(`openjdk.json`); const fixturePython = Fixtures.get(`python.json`); describe('modules/datasource/repology/index', () => { + beforeEach(() => { + hostRules.clear(); + }); + describe('getReleases', () => { it('returns null for empty result', async () => { mockResolverCall('debian_stable', 'nginx', 'binname', { @@ -202,6 +207,17 @@ describe('modules/datasource/repology/index', () => { ).rejects.toThrow(EXTERNAL_HOST_ERROR); }); + it('throws on disabled host', async () => { + hostRules.add({ matchHost: repologyHost, enabled: false }); + expect( + await getPkgReleases({ + datasource, + versioning, + packageName: 'debian_stable/nginx', + }) + ).toBeNull(); + }); + it('returns correct version for binary package', async () => { mockResolverCall('debian_stable', 'nginx', 'binname', { status: 200, diff --git a/lib/modules/datasource/repology/index.ts b/lib/modules/datasource/repology/index.ts index b07e76b21d53943222ed82d60717fadd432b1d29..86e026bff0b1e67af1c1dd6968956c43a1ba78ca 100644 --- a/lib/modules/datasource/repology/index.ts +++ b/lib/modules/datasource/repology/index.ts @@ -219,7 +219,6 @@ export class RepologyDatasource extends Datasource { return { releases }; } catch (err) { if (err.message === HOST_DISABLED) { - // istanbul ignore next logger.trace({ packageName, err }, 'Host disabled'); } else { logger.warn( diff --git a/lib/modules/manager/ansible-galaxy/collections.ts b/lib/modules/manager/ansible-galaxy/collections.ts index 951618aace5da8c884539fc5fd3b3543c4ff7a2c..f84bf3bd3ae24da272112f26a609ea85e177e01f 100644 --- a/lib/modules/manager/ansible-galaxy/collections.ts +++ b/lib/modules/manager/ansible-galaxy/collections.ts @@ -33,7 +33,9 @@ function interpretLine( if (value?.startsWith('git@')) { localDependency.packageName = value; } else { - localDependency.registryUrls = value ? [value] : []; + localDependency.registryUrls = value + ? [value] + : /* istanbul ignore next: should have test */ []; } break; } @@ -83,7 +85,9 @@ function handleGitDep( function handleGalaxyDep(dep: AnsibleGalaxyPackageDependency): void { dep.datasource = GalaxyCollectionDatasource.id; dep.depName = dep.managerData.name; - dep.registryUrls = dep.managerData.source ? [dep.managerData.source] : []; + dep.registryUrls = dep.managerData.source + ? /* istanbul ignore next: should have test */ [dep.managerData.source] + : []; dep.currentValue = dep.managerData.version; } diff --git a/lib/modules/manager/cocoapods/artifacts.ts b/lib/modules/manager/cocoapods/artifacts.ts index 7e7f84629058c68bd27639fa83cbe26998ba6ea2..fc9dcb70941ff6397119917b477f4459b89d22df 100644 --- a/lib/modules/manager/cocoapods/artifacts.ts +++ b/lib/modules/manager/cocoapods/artifacts.ts @@ -2,6 +2,7 @@ import { quote } from 'shlex'; import upath from 'upath'; import { TEMPORARY_ERROR } from '../../../constants/error-messages'; import { logger } from '../../../logger'; +import { coerceArray } from '../../../util/array'; import { exec } from '../../../util/exec'; import type { ExecOptions } from '../../../util/exec/types'; import { @@ -134,7 +135,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/cocoapods/extract.ts b/lib/modules/manager/cocoapods/extract.ts index 009e4917da12deaedc733e6b9a73120e10ce8e02..f873584432565e0af1c8369de26d79b0e1a09d25 100644 --- a/lib/modules/manager/cocoapods/extract.ts +++ b/lib/modules/manager/cocoapods/extract.ts @@ -1,6 +1,7 @@ import { logger } from '../../../logger'; import { getSiblingFileName, localPathExists } from '../../../util/fs'; import { newlineRegex, regEx } from '../../../util/regex'; +import { coerceString } from '../../../util/string'; import { GitTagsDatasource } from '../../datasource/git-tags'; import { GithubTagsDatasource } from '../../datasource/github-tags'; import { GitlabTagsDatasource } from '../../datasource/gitlab-tags'; @@ -54,7 +55,7 @@ export function gitDep(parsedLine: ParsedLine): PackageDependency | null { const platformMatch = regEx( /[@/](?<platform>github|gitlab)\.com[:/](?<account>[^/]+)\/(?<repo>[^/]+)/ - ).exec(git ?? ''); + ).exec(coerceString(git)); if (platformMatch?.groups) { const { account, repo, platform } = platformMatch.groups; diff --git a/lib/modules/manager/gradle/extract/catalog.ts b/lib/modules/manager/gradle/extract/catalog.ts index ed11cab6dd8150018343ae86f013a08dd0ec86db..a274e8b4228e508801b09b7f9c5e4d0b7262cccc 100644 --- a/lib/modules/manager/gradle/extract/catalog.ts +++ b/lib/modules/manager/gradle/extract/catalog.ts @@ -227,7 +227,9 @@ function extractDependency({ : null; if (isArtifactDescriptor(descriptor)) { const { group, name } = descriptor; - const groupName = is.nullOrUndefined(versionRef) ? group : versionRef; // usage of common variable should have higher priority than other values + const groupName = is.nullOrUndefined(versionRef) + ? group + : /* istanbul ignore next: hard to test */ versionRef; // usage of common variable should have higher priority than other values return { depName: `${group}:${name}`, groupName, diff --git a/lib/modules/manager/gradle/extract/consistent-versions-plugin.ts b/lib/modules/manager/gradle/extract/consistent-versions-plugin.ts index 46b971dd027ee1af1bb684447bdb84cb5a108a0b..ea78d1de4358d7a5e2408cecbe62a5d1a638f543 100644 --- a/lib/modules/manager/gradle/extract/consistent-versions-plugin.ts +++ b/lib/modules/manager/gradle/extract/consistent-versions-plugin.ts @@ -1,6 +1,7 @@ import { logger } from '../../../../logger'; import * as fs from '../../../../util/fs'; import { newlineRegex, regEx } from '../../../../util/regex'; +import { coerceString } from '../../../../util/string'; import type { PackageDependency } from '../../types'; import type { GradleManagerData } from '../types'; import { isDependencyString, versionLikeSubstring } from '../utils'; @@ -59,9 +60,9 @@ export function parseGcv( propsFileName: string, fileContents: Record<string, string | null> ): PackageDependency<GradleManagerData>[] { - const propsFileContent = fileContents[propsFileName] ?? ''; + const propsFileContent = coerceString(fileContents[propsFileName]); const lockFileName = fs.getSiblingFileName(propsFileName, VERSIONS_LOCK); - const lockFileContent = fileContents[lockFileName] ?? ''; + const lockFileContent = coerceString(fileContents[lockFileName]); const lockFileMap = parseLockFile(lockFileContent); const [propsFileExactMap, propsFileRegexMap] = parsePropsFile(propsFileContent); diff --git a/lib/modules/manager/maven-wrapper/extract.ts b/lib/modules/manager/maven-wrapper/extract.ts index 12d91fe2440bec900fe35f619dc16ba01a4f09bc..92e834072ef6893f02d278c21a6d902c488e821f 100644 --- a/lib/modules/manager/maven-wrapper/extract.ts +++ b/lib/modules/manager/maven-wrapper/extract.ts @@ -1,4 +1,5 @@ import { logger } from '../../../logger'; +import { coerceArray } from '../../../util/array'; import { newlineRegex, regEx } from '../../../util/regex'; import { MavenDatasource } from '../../datasource/maven'; import { id as versioning } from '../../versioning/maven'; @@ -15,7 +16,7 @@ const WRAPPER_URL_REGEX = regEx( ); function extractVersions(fileContent: string): MavenVersionExtract { - const lines = fileContent?.split(newlineRegex) ?? []; + const lines = coerceArray(fileContent?.split(newlineRegex)); const maven = extractLineInfo(lines, DISTRIBUTION_URL_REGEX) ?? undefined; const wrapper = extractLineInfo(lines, WRAPPER_URL_REGEX) ?? undefined; return { maven, wrapper }; diff --git a/lib/modules/manager/npm/update/locked-dependency/package-lock/index.ts b/lib/modules/manager/npm/update/locked-dependency/package-lock/index.ts index 0bd1e9a04ba28f5cacf4546a481de5003ec33176..3d220be894aa800c0190c3fb5de8cddf2247befc 100644 --- a/lib/modules/manager/npm/update/locked-dependency/package-lock/index.ts +++ b/lib/modules/manager/npm/update/locked-dependency/package-lock/index.ts @@ -151,7 +151,9 @@ export async function updateLockedDependency( logger.debug( `${depName} can be updated to ${newVersion} in-range with matching constraint "${constraint}" in ${ // TODO: types (#22198) - parentDepName ? `${parentDepName}@${parentVersion!}` : packageFile + parentDepName + ? `${parentDepName}@${parentVersion!}` + : /* istanbul ignore next: hard to test */ packageFile }` ); } else if (parentDepName && parentVersion) { @@ -242,7 +244,8 @@ export async function updateLockedDependency( newPackageJsonContent = parentUpdateResult.files[packageFile] || newPackageJsonContent; newLockFileContent = - parentUpdateResult.files[lockFile] || newLockFileContent; + parentUpdateResult.files[lockFile] || + /* istanbul ignore next: hard to test */ newLockFileContent; } const files: Record<string, string> = {}; if (newLockFileContent) { diff --git a/lib/modules/manager/terraform/lockfile/__snapshots__/index.spec.ts.snap b/lib/modules/manager/terraform/lockfile/__snapshots__/index.spec.ts.snap index 41ef8b78e198bf7003b4caa65250434a071ae7f2..574bb97beb0d3fc5ae3aa40ed66b5891dca3453e 100644 --- a/lib/modules/manager/terraform/lockfile/__snapshots__/index.spec.ts.snap +++ b/lib/modules/manager/terraform/lockfile/__snapshots__/index.spec.ts.snap @@ -94,11 +94,22 @@ provider "registry.terraform.io/hashicorp/azurerm" { } provider "registry.terraform.io/hashicorp/random" { - version = "2.2.2" + version = "2.2.1" constraints = "~> 2.2" hashes = [ - "h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=", - "h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=", + "h1:Zg1Bpi6vr7b0H6no8kVDfEucn5pvNALivdrVKVHarGs=", + "zh:072ce92b0138ee65df2e4e2e6e5f6632fa12a7e6453b91399bad89291855d426", + "zh:5731987fe61051515f449033e456ee55207caf17ef41096eb82247810585f53b", + "zh:6f18b10175708bb5839e1f2082dcc02651b876786cd54ec415a091f3821807c3", + "zh:7fa7737661380d18cba3cdc71c4ec6f2fd281b9d61112f6b48d06ca8bbf97771", + "zh:8466cb8fbb4de887b23039082a6e3dc85aeabce86dd808e2a7a65e4e1c51dbae", + "zh:888c63417701c13bbe785ab11dc690d4803e6a2156318cf188970b7b6400b99e", + "zh:a231df55d36fbad1a6705f5d3be4f7459a73ec76117d13f22aa83c10fc610278", + "zh:b62d9a4cd64a2d229070260f4abfef476ebbd7c5511b43e9cdccf23ce938f630", + "zh:b6bd1a325f909bb93f7c9bef00eb306bef1e406cbdf557901d755a3e7a4a5448", + "zh:b9f59afc23cc5567075f76313214baa1e5ce909325229e23c9a4666f7b26e7f7", + "zh:d040220c09b8d9d6bd937572bd5b14bc069af2b883185a873460530d8a1de6e6", + "zh:f254c1f943eb016ae07ebe91b23f813dc79f2064616c65f98c8f64ce23be90c4", ] } ", @@ -114,11 +125,6 @@ exports[`modules/manager/terraform/lockfile/index do full lock file maintenance "hashicorp/azurerm", "2.56.0", ], - [ - "https://registry.terraform.io", - "hashicorp/random", - "2.2.2", - ], ] `; diff --git a/lib/modules/manager/terraform/lockfile/index.spec.ts b/lib/modules/manager/terraform/lockfile/index.spec.ts index 354b84fd4fdd8204ed494458092125e972ea5075..75e4c93c48a15715deacc9fbaa741801c2e14134 100644 --- a/lib/modules/manager/terraform/lockfile/index.spec.ts +++ b/lib/modules/manager/terraform/lockfile/index.spec.ts @@ -439,20 +439,10 @@ describe('modules/manager/terraform/lockfile/index', () => { }, ], }) - .mockResolvedValueOnce({ + .mockResolvedValueOnce( // random - releases: [ - { - version: '2.2.1', - }, - { - version: '2.2.2', - }, - { - version: '3.0.0', - }, - ], - }); + null + ); mockHash.mockResolvedValue([ 'h1:lDsKRxDRXPEzA4AxkK4t+lJd3IQIP2UoaplJGjQSp2s=', 'h1:6zB2hX7YIOW26OrKsLJn0uLMnjqbPNxcz9RhlWEuuSY=', @@ -480,7 +470,7 @@ describe('modules/manager/terraform/lockfile/index', () => { }) ); - expect(mockHash.mock.calls).toBeArrayOfSize(2); + expect(mockHash.mock.calls).toBeArrayOfSize(1); expect(mockHash.mock.calls).toMatchSnapshot(); }); diff --git a/lib/modules/manager/terraform/lockfile/index.ts b/lib/modules/manager/terraform/lockfile/index.ts index 36cc975097dde674877b46c7cdaa4518d2efd684..58c72065e27543cbf7e54de237b9be9872aab0df 100644 --- a/lib/modules/manager/terraform/lockfile/index.ts +++ b/lib/modules/manager/terraform/lockfile/index.ts @@ -28,7 +28,6 @@ async function updateAllLocks( packageName: lock.packageName, }; const { releases } = (await getPkgReleases(updateConfig)) ?? {}; - // istanbul ignore if: needs test if (!releases) { return null; } diff --git a/lib/modules/manager/terraform/lockfile/update-locked.ts b/lib/modules/manager/terraform/lockfile/update-locked.ts index 5d0988dab9e408f453fa1279eae56514ba513f7f..b2bc6eb5f34aa888599837fccdecb2b1713b518d 100644 --- a/lib/modules/manager/terraform/lockfile/update-locked.ts +++ b/lib/modules/manager/terraform/lockfile/update-locked.ts @@ -1,4 +1,5 @@ import { logger } from '../../../../logger'; +import { coerceString } from '../../../../util/string'; import type { UpdateLockedConfig, UpdateLockedResult } from '../../types'; import { extractLocks } from './util'; @@ -12,8 +13,10 @@ export function updateLockedDependency( `terraform.updateLockedDependency: ${depName}@${currentVersion} -> ${newVersion} [${lockFile}]` ); try { - const locked = extractLocks(lockFileContent ?? ''); - const lockedDep = locked?.find((dep) => dep.packageName === depName ?? ''); + const locked = extractLocks(coerceString(lockFileContent)); + const lockedDep = locked?.find( + (dep) => dep.packageName === coerceString(depName) + ); if (lockedDep?.version === newVersion) { return { status: 'already-updated' }; } diff --git a/lib/modules/platform/github/massage-markdown-links.ts b/lib/modules/platform/github/massage-markdown-links.ts index 8fe4a773f6765fa21975b6160227cff84528cf65..44c3b53616c6a6f40d0c4b1bbfebf65e77f15038 100644 --- a/lib/modules/platform/github/massage-markdown-links.ts +++ b/lib/modules/platform/github/massage-markdown-links.ts @@ -2,6 +2,7 @@ import type { Content } from 'mdast'; import remark from 'remark'; import type { Plugin, Transformer } from 'unified'; import { logger } from '../../../logger'; +import { coerceNumber } from '../../../util/number'; import { regEx } from '../../../util/regex'; interface UrlMatch { @@ -20,8 +21,8 @@ function massageLink(input: string): string { function collectLinkPosition(input: string, matches: UrlMatch[]): Plugin { const transformer = (tree: Content): void => { - const startOffset: number = tree.position?.start.offset ?? 0; - const endOffset: number = tree.position?.end.offset ?? 0; + const startOffset = coerceNumber(tree.position?.start.offset); + const endOffset = coerceNumber(tree.position?.end.offset); if (tree.type === 'link') { const substr = input.slice(startOffset, endOffset); @@ -39,7 +40,7 @@ function collectLinkPosition(input: string, matches: UrlMatch[]): Plugin { const urlMatches = [...tree.value.matchAll(globalUrlReg)]; for (const match of urlMatches) { const [url] = match; - const start = startOffset + (match.index ?? 0); + const start = startOffset + coerceNumber(match.index); const end = start + url.length; const newUrl = massageLink(url); matches.push({ start, end, replaceTo: `[${url}](${newUrl})` }); diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts index 0fca92571e72a2c8b965216e30d4ec3f25b2cf88..047f14dac5ce02ad057f1f951f1a41f9692a7182 100644 --- a/lib/modules/platform/gitlab/index.spec.ts +++ b/lib/modules/platform/gitlab/index.spec.ts @@ -846,6 +846,24 @@ describe('modules/platform/gitlab/index', () => { ); expect(res).toBe('green'); }); + + it('returns yellow if unknown status found', async () => { + const scope = await initRepo(); + scope + .get( + '/api/v4/projects/some%2Frepo/repository/commits/0d9c7726c3d628b7e28af234595cfd20febdbf8e/statuses' + ) + .reply(200, [ + { name: 'context-1', status: 'pending' }, + { name: 'some-context', status: 'something' }, + { name: 'context-3', status: 'failed' }, + ]); + const res = await gitlab.getBranchStatusCheck( + 'somebranch', + 'some-context' + ); + expect(res).toBe('yellow'); + }); }); describe('setBranchStatus', () => { diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts index 24cbda8fbc2a6ed67a2e4aedc26a4e8b3ccfd646..d8981e5c72e64515c3c0339313105bdbac21025d 100644 --- a/lib/modules/platform/gitlab/index.ts +++ b/lib/modules/platform/gitlab/index.ts @@ -17,6 +17,7 @@ import { } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import type { BranchStatus } from '../../../types'; +import { coerceArray } from '../../../util/array'; import * as git from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; import { setBaseUrl } from '../../../util/http/gitlab'; @@ -176,9 +177,10 @@ export async function getRepos(config?: AutodiscoverConfig): Promise<string[]> { throw err; } } - -function urlEscape(str: string): string { - return str ? str.replace(regEx(/\//g), '%2F') : str; +function urlEscape(str: string): string; +function urlEscape(str: string | undefined): string | undefined; +function urlEscape(str: string | undefined): string | undefined { + return str?.replace(regEx(/\//g), '%2F'); } export async function getRawFile( @@ -187,7 +189,7 @@ export async function getRawFile( branchOrTag?: string ): Promise<string | null> { const escapedFileName = urlEscape(fileName); - const repo = urlEscape(repoName ?? config.repository); + const repo = urlEscape(repoName) ?? config.repository; const url = `projects/${repo}/repository/files/${escapedFileName}?ref=` + (branchOrTag ?? `HEAD`); @@ -243,11 +245,13 @@ function getRepoUrl( const { protocol, host, pathname } = parseUrl(defaults.endpoint)!; const newPathname = pathname.slice(0, pathname.indexOf('/api')); const url = URL.format({ - protocol: protocol.slice(0, -1) || 'https', + protocol: + protocol.slice(0, -1) || + /* istanbul ignore next: should never happen */ 'https', // TODO: types (#22198) auth: `oauth2:${opts.token!}`, host, - pathname: newPathname + '/' + repository + '.git', + pathname: `${newPathname}/${repository}.git`, }); logger.debug(`Using URL based on configured endpoint, url:${url}`); return url; @@ -1097,7 +1101,7 @@ export async function addReviewers( return; } - mr.reviewers = mr.reviewers ?? []; + mr.reviewers = coerceArray(mr.reviewers); const existingReviewers = mr.reviewers.map((r) => r.username); const existingReviewerIDs = mr.reviewers.map((r) => r.id); @@ -1144,7 +1148,7 @@ export async function deleteLabel( logger.debug(`Deleting label ${label} from #${issueNo}`); try { const pr = await getPr(issueNo); - const labels = (pr.labels ?? []) + const labels = coerceArray(pr.labels) .filter((l: string) => l !== label) .join(','); await gitlabApi.putJson( diff --git a/lib/modules/versioning/cargo/index.ts b/lib/modules/versioning/cargo/index.ts index 1ab7546ce2cac0c604c1535556c2c8d5f1d88f51..7508c5781be024ba0be0eb1b2d1669b8707e863f 100644 --- a/lib/modules/versioning/cargo/index.ts +++ b/lib/modules/versioning/cargo/index.ts @@ -109,7 +109,9 @@ function getNewValue({ currentVersion, newVersion, }); - let newCargo = newSemver ? npm2cargo(newSemver) : null; + let newCargo = newSemver + ? npm2cargo(newSemver) + : /* istanbul ignore next: should never happen */ null; // istanbul ignore if if (!newCargo) { logger.info( diff --git a/lib/modules/versioning/composer/index.spec.ts b/lib/modules/versioning/composer/index.spec.ts index ba67d2f3c3a888d65dff3e413f8ec881d6523297..2fbebc88ef45ab450e4027b362a7c7e2189eca15 100644 --- a/lib/modules/versioning/composer/index.spec.ts +++ b/lib/modules/versioning/composer/index.spec.ts @@ -1,9 +1,26 @@ import { api as semver } from '.'; describe('modules/versioning/composer/index', () => { + it.each` + version | expected + ${'1.2.0'} | ${1} + ${''} | ${null} + `('getMajor("$version") === $expected', ({ version, expected }) => { + expect(semver.getMajor(version)).toBe(expected); + }); + + it.each` + version | expected + ${'1.2.0'} | ${2} + ${''} | ${null} + `('getMinor("$version") === $expected', ({ version, expected }) => { + expect(semver.getMinor(version)).toBe(expected); + }); + it.each` version | expected ${'1.2.0'} | ${0} + ${''} | ${null} `('getPatch("$version") === $expected', ({ version, expected }) => { expect(semver.getPatch(version)).toBe(expected); }); @@ -202,6 +219,7 @@ describe('modules/versioning/composer/index', () => { ${'^5'} | ${'update-lockfile'} | ${'5.1.0'} | ${'6.0.0'} | ${'^6'} ${'^0.4.0'} | ${'replace'} | ${'0.4'} | ${'0.5'} | ${'^0.5.0'} ${'^0.4.0'} | ${'replace'} | ${'0.4'} | ${'1.0'} | ${'^1.0.0'} + ${'^0.4.0'} | ${'replace'} | ${null} | ${'1.0'} | ${'1.0'} `( 'getNewValue("$currentValue", "$rangeStrategy", "$currentVersion", "$newVersion") === "$expected"', ({ currentValue, rangeStrategy, currentVersion, newVersion, expected }) => { diff --git a/lib/modules/versioning/distro.spec.ts b/lib/modules/versioning/distro.spec.ts index 5ad329d7c933bfa2387eaabd724d4bcdbc301c16..c4e4c82d4694ffb325e4e3e5655cdfcd4bee8b79 100644 --- a/lib/modules/versioning/distro.spec.ts +++ b/lib/modules/versioning/distro.spec.ts @@ -158,4 +158,9 @@ describe('modules/versioning/distro', () => { it('retrieves non-existent release schedule', () => { expect(di.getSchedule('20.06')).toBeNull(); }); + + it('works with debian', () => { + const di = new DistroInfo('data/debian-distro-info.json'); + expect(di.isEolLts('trixie')).toBe(true); + }); }); diff --git a/lib/modules/versioning/docker/index.ts b/lib/modules/versioning/docker/index.ts index 94f5f02cde82912312d43dad1e65638c816f5e6c..a196189d5b4021ca3d1f48c2a0b57aac04d0876f 100644 --- a/lib/modules/versioning/docker/index.ts +++ b/lib/modules/versioning/docker/index.ts @@ -1,4 +1,5 @@ import { regEx } from '../../../util/regex'; +import { coerceString } from '../../../util/string'; import { GenericVersion, GenericVersioningApi } from '../generic'; import type { VersioningApi } from '../types'; @@ -70,8 +71,8 @@ class DockerVersioningApi extends GenericVersioningApi { } // equals - const suffix1 = parsed1.suffix ?? ''; - const suffix2 = parsed2.suffix ?? ''; + const suffix1 = coerceString(parsed1.suffix); + const suffix2 = coerceString(parsed2.suffix); return suffix2.localeCompare(suffix1); } diff --git a/lib/modules/versioning/generic.spec.ts b/lib/modules/versioning/generic.spec.ts index b0903e37dec9e36146d90dfb557a228b03e9986f..d7946528f85fd5cd050c8256f75b3fc2a489713c 100644 --- a/lib/modules/versioning/generic.spec.ts +++ b/lib/modules/versioning/generic.spec.ts @@ -1,4 +1,6 @@ +import { partial } from '../../../test/util'; import { GenericVersion, GenericVersioningApi } from './generic'; +import type { NewValueConfig } from './types'; describe('modules/versioning/generic', () => { const optionalFunctions = [ @@ -104,6 +106,8 @@ describe('modules/versioning/generic', () => { newVersion: '3.2.1', }) ).toBe('3.2.1'); + + expect(api.getNewValue(partial<NewValueConfig>({}))).toBeNull(); }); it('isCompatible', () => { diff --git a/lib/modules/versioning/generic.ts b/lib/modules/versioning/generic.ts index 32df1a1b9040e53c060173eb1b57567901b30adb..1697ad6ab74bee98f418a8c1389455277d92e998 100644 --- a/lib/modules/versioning/generic.ts +++ b/lib/modules/versioning/generic.ts @@ -129,9 +129,8 @@ export abstract class GenericVersioningApi< return result ?? null; } - getNewValue(newValueConfig: NewValueConfig): string { - const { newVersion } = newValueConfig || {}; - return newVersion; + getNewValue({ newVersion }: NewValueConfig): string | null { + return newVersion ?? null; } sortVersions(version: string, other: string): number { diff --git a/lib/modules/versioning/ruby/range.ts b/lib/modules/versioning/ruby/range.ts index a490d9c94c14dc1fd3c3d3a05603408827036b9b..cb8deaf75717c6c101474b61ab274bdf726541d1 100644 --- a/lib/modules/versioning/ruby/range.ts +++ b/lib/modules/versioning/ruby/range.ts @@ -28,7 +28,7 @@ const parse = (range: string): Range => { const match = regExp.exec(value); if (match?.groups) { - const { version = '', operator = '', delimiter = ' ' } = match.groups; + const { version, operator = '', delimiter } = match.groups; return { version, operator, delimiter }; } diff --git a/lib/modules/versioning/semver-coerced/index.spec.ts b/lib/modules/versioning/semver-coerced/index.spec.ts index 95d1ddb87f64c00f03c873ac9d1f4933b2188036..ff01ad95fdacffa806e15ea1ea711c28f4d207ca 100644 --- a/lib/modules/versioning/semver-coerced/index.spec.ts +++ b/lib/modules/versioning/semver-coerced/index.spec.ts @@ -44,7 +44,7 @@ describe('modules/versioning/semver-coerced/index', () => { }); it('invalid version', () => { - expect(semverCoerced.getMajor('xxx')).toBeNull(); + expect(semverCoerced.getMinor('xxx')).toBeNull(); }); }); diff --git a/lib/modules/versioning/ubuntu/index.ts b/lib/modules/versioning/ubuntu/index.ts index 304488dbba56ec298f91b771ebaffec3f377b8f6..4e6e96eb07d8be70c2cd9aab8b82ff28ea5aa90a 100644 --- a/lib/modules/versioning/ubuntu/index.ts +++ b/lib/modules/versioning/ubuntu/index.ts @@ -1,4 +1,5 @@ import { regEx } from '../../../util/regex'; +import { coerceString } from '../../../util/string'; import { DistroInfo } from '../distro'; import type { NewValueConfig, VersioningApi } from '../types'; @@ -44,7 +45,7 @@ function isStable(version: string): boolean { const match = ver.match(regEx(/^\d+.\d+/)); - if (!di.isReleased(match ? match[0] : ver)) { + if (!di.isReleased(coerceString(match?.[0], ver))) { return false; } @@ -126,12 +127,7 @@ function minSatisfyingVersion( return getSatisfyingVersion(versions, range); } -function getNewValue({ - currentValue, - rangeStrategy, - currentVersion, - newVersion, -}: NewValueConfig): string { +function getNewValue({ currentValue, newVersion }: NewValueConfig): string { if (di.isCodename(currentValue)) { return di.getCodenameByVersion(newVersion); } diff --git a/lib/util/number.spec.ts b/lib/util/number.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f215c1aa1c8c7664fa13de20e81f9c529b871e1 --- /dev/null +++ b/lib/util/number.spec.ts @@ -0,0 +1,12 @@ +import { coerceNumber } from './number'; + +describe('util/number', () => { + it.each` + val | def | expected + ${1} | ${2} | ${1} + ${undefined} | ${2} | ${2} + ${undefined} | ${undefined} | ${0} + `('coerceNumber($val, $def) = $expected', ({ val, def, expected }) => { + expect(coerceNumber(val, def)).toBe(expected); + }); +}); diff --git a/lib/util/number.ts b/lib/util/number.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fb488c9d3eac5ac802d32b3b7037d8f6a36994f --- /dev/null +++ b/lib/util/number.ts @@ -0,0 +1,12 @@ +/** + * Coerces a value to a number with optional default value. + * @param val the value to coerce + * @param def default value + * @returns cocerced value + */ +export function coerceNumber( + val: number | null | undefined, + def?: number +): number { + return val ?? def ?? 0; +} diff --git a/lib/util/string.spec.ts b/lib/util/string.spec.ts index 3394a0236492e86c63eaac676694fbeec77e4df4..9df3c52ac209371c740da67975e60760c9f56251 100644 --- a/lib/util/string.spec.ts +++ b/lib/util/string.spec.ts @@ -38,5 +38,6 @@ describe('util/string', () => { expect(coerceString('')).toBe(''); expect(coerceString(undefined)).toBe(''); expect(coerceString(null)).toBe(''); + expect(coerceString(null, 'foo')).toBe('foo'); }); }); diff --git a/lib/util/string.ts b/lib/util/string.ts index ce6bad089d9fd5ed19653391fc1a17f1d11510b1..f82e8f260eee7e9af94bf256a5c018315122afaa 100644 --- a/lib/util/string.ts +++ b/lib/util/string.ts @@ -83,6 +83,14 @@ export function copystr(x: string): string { return buf.toString('utf8'); } -export function coerceString(val: string | null | undefined): string { - return val ?? ''; +/** + * Coerce a value to a string with optional default value. + * @param val value to coerce + * @returns the coerced value. + */ +export function coerceString( + val: string | null | undefined, + def?: string +): string { + return val ?? def ?? ''; } diff --git a/lib/workers/global/config/parse/file.spec.ts b/lib/workers/global/config/parse/file.spec.ts index 3b7d22e34057cd82998fd63062e6d9e70693883f..0a786e8f80a079978be06b5c8a8ef8dbd68e84e3 100644 --- a/lib/workers/global/config/parse/file.spec.ts +++ b/lib/workers/global/config/parse/file.spec.ts @@ -38,7 +38,7 @@ describe('workers/global/config/parse/file', () => { ['.renovaterc', '.renovaterc'], ['JSON5 config file', 'config.json5'], ['YAML config file', 'config.yaml'], - ])('parses %s', async (fileType, filePath) => { + ])('parses %s', async (_fileType, filePath) => { const configFile = upath.resolve(__dirname, './__fixtures__/', filePath); expect( await file.getConfig({ RENOVATE_CONFIG_FILE: configFile }) diff --git a/lib/workers/global/config/parse/file.ts b/lib/workers/global/config/parse/file.ts index eeeaf5671e11a4bccc8a32491ff9701f99286c70..4ccdca5f392bb64515c08beada1431bdfeb56796 100644 --- a/lib/workers/global/config/parse/file.ts +++ b/lib/workers/global/config/parse/file.ts @@ -23,7 +23,9 @@ export async function getParsedContent(file: string): Promise<RenovateConfig> { return JSON5.parse(await readSystemFile(file, 'utf8')); case '.js': { const tmpConfig = await import(file); - let config = tmpConfig.default ? tmpConfig.default : tmpConfig; + let config = tmpConfig.default + ? tmpConfig.default + : /* istanbul ignore next: hard to test */ tmpConfig; // Allow the config to be a function if (is.function_(config)) { config = config(); @@ -54,7 +56,6 @@ export async function getConfig(env: NodeJS.ProcessEnv): Promise<AllConfig> { try { config = await getParsedContent(configFile); } catch (err) { - // istanbul ignore if if (err instanceof SyntaxError || err instanceof TypeError) { logger.fatal(`Could not parse config file \n ${err.stack!}`); process.exit(1); @@ -69,10 +70,9 @@ export async function getConfig(env: NodeJS.ProcessEnv): Promise<AllConfig> { } else if (env.RENOVATE_CONFIG_FILE) { logger.fatal('No custom config file found on disk'); process.exit(1); - } else { - // istanbul ignore next: we can ignore this - logger.debug('No config file found on disk - skipping'); } + // istanbul ignore next: we can ignore this + logger.debug('No config file found on disk - skipping'); } await deleteNonDefaultConfig(env); // Try deletion only if RENOVATE_CONFIG_FILE is specified diff --git a/lib/workers/global/config/parse/index.ts b/lib/workers/global/config/parse/index.ts index 070d52561d3c8fbf21e123f2699d7f40b4063712..beb03cb895da40366ba4cbfd928f6968848120cc 100644 --- a/lib/workers/global/config/parse/index.ts +++ b/lib/workers/global/config/parse/index.ts @@ -3,6 +3,7 @@ import type { AllConfig } from '../../../../config/types'; import { mergeChildConfig } from '../../../../config/utils'; import { addStream, logger, setContext } from '../../../../logger'; import { detectAllGlobalConfig } from '../../../../modules/manager'; +import { coerceArray } from '../../../../util/array'; import { ensureDir, getParentDir, readSystemFile } from '../../../../util/fs'; import { addSecretForSanitizing } from '../../../../util/sanitize'; import { ensureTrailingSlash } from '../../../../util/url'; @@ -95,7 +96,7 @@ export async function parseConfigs( if (config.detectHostRulesFromEnv) { const hostRules = hostRulesFromEnv(env); - config.hostRules = [...(config.hostRules ?? []), ...hostRules]; + config.hostRules = [...coerceArray(config.hostRules), ...hostRules]; } // Get global config logger.trace({ config }, 'Full config'); diff --git a/lib/workers/repository/config-migration/branch/migrated-data.spec.ts b/lib/workers/repository/config-migration/branch/migrated-data.spec.ts index 541d7e7cfdaf5d9cf0a8f2ed6f28aa63f853c84c..db3b27b34648668c2c710fdcede2a913ac947a3b 100644 --- a/lib/workers/repository/config-migration/branch/migrated-data.spec.ts +++ b/lib/workers/repository/config-migration/branch/migrated-data.spec.ts @@ -6,7 +6,7 @@ import { migrateConfig } from '../../../../config/migration'; import { logger } from '../../../../logger'; import { readLocalFile } from '../../../../util/fs'; import { detectRepoFileConfig } from '../../init/merge'; -import { MigratedDataFactory } from './migrated-data'; +import { MigratedDataFactory, applyPrettierFormatting } from './migrated-data'; jest.mock('../../../../config/migration'); jest.mock('../../../../util/git'); @@ -190,5 +190,15 @@ describe('workers/repository/config-migration/branch/migrated-data', () => { MigratedDataFactory.applyPrettierFormatting(migratedData) ).resolves.toEqual(formatted); }); + + it('formats with default 2 spaces', async () => { + mockedFunction(scm.getFileList).mockResolvedValue(['.prettierrc']); + await expect( + applyPrettierFormatting(migratedData.content, 'json', { + amount: 0, + indent: ' ', + }) + ).resolves.toEqual(formattedMigratedData.content); + }); }); }); diff --git a/lib/workers/repository/config-migration/pr/index.ts b/lib/workers/repository/config-migration/pr/index.ts index 3d11d730af11ce8656fa7689075c16ee70a2bd00..9eb0355783f40ac486bab75ffb938fd0ae643bab 100644 --- a/lib/workers/repository/config-migration/pr/index.ts +++ b/lib/workers/repository/config-migration/pr/index.ts @@ -6,6 +6,7 @@ import { platform } from '../../../../modules/platform'; import { hashBody } from '../../../../modules/platform/pr-body'; import { scm } from '../../../../modules/platform/scm'; import { emojify } from '../../../../util/emoji'; +import { coerceString } from '../../../../util/string'; import * as template from '../../../../util/template'; import { joinUrlParts } from '../../../../util/url'; import { getPlatformPrOptions } from '../../update/pr'; @@ -21,7 +22,7 @@ export async function ensureConfigMigrationPr( ): Promise<void> { logger.debug('ensureConfigMigrationPr()'); const docsLink = joinUrlParts( - config.productLinks?.documentation ?? '', + coerceString(config.productLinks?.documentation), 'configuration-options/#configmigration' ); const branchName = getMigrationBranchName(config); diff --git a/lib/workers/repository/finalize/repository-statistics.spec.ts b/lib/workers/repository/finalize/repository-statistics.spec.ts index 6d65e88af89a6f89a0521ac8fd917197970e73a3..66e05794105833bbb0139219eeb8f4c2e189bae2 100644 --- a/lib/workers/repository/finalize/repository-statistics.spec.ts +++ b/lib/workers/repository/finalize/repository-statistics.spec.ts @@ -174,7 +174,6 @@ describe('workers/repository/finalize/repository-statistics', () => { const branches: BranchCache[] = [{ ...branchCache, branchName: 'b1' }]; const cache = partial<RepoCacheData>({ - scan: {}, branches, }); getCacheSpy.mockReturnValueOnce(cache); diff --git a/lib/workers/repository/update/pr/changelog/release-notes.spec.ts b/lib/workers/repository/update/pr/changelog/release-notes.spec.ts index e073ea22662b6cb91aed1a80096e14ce3c7818a1..9d288ee72c5af08250f042a9f8301010ea8a42c2 100644 --- a/lib/workers/repository/update/pr/changelog/release-notes.spec.ts +++ b/lib/workers/repository/update/pr/changelog/release-notes.spec.ts @@ -144,6 +144,7 @@ describe('workers/repository/update/pr/changelog/release-notes', () => { project: partial<ChangeLogProject>({ type: 'gitlab', repository: 'https://gitlab.com/gitlab-org/gitter/webapp/', + sourceDirectory: 'lib', }), versions: [ partial<ChangeLogRelease>({ @@ -159,6 +160,7 @@ describe('workers/repository/update/pr/changelog/release-notes', () => { project: { repository: 'https://gitlab.com/gitlab-org/gitter/webapp/', type: 'gitlab', + sourceDirectory: 'lib', }, versions: [ { diff --git a/lib/workers/repository/update/pr/changelog/release-notes.ts b/lib/workers/repository/update/pr/changelog/release-notes.ts index b2c4117e2d397a4bb0753804579361d6d52b734f..567a7a55d31bb0e137f9211e355ea12ba25ebfb1 100644 --- a/lib/workers/repository/update/pr/changelog/release-notes.ts +++ b/lib/workers/repository/update/pr/changelog/release-notes.ts @@ -7,6 +7,7 @@ import * as packageCache from '../../../../../util/cache/package'; import { detectPlatform } from '../../../../../util/common'; import { linkify } from '../../../../../util/markdown'; import { newlineRegex, regEx } from '../../../../../util/regex'; +import { coerceString } from '../../../../../util/string'; import { validateUrl } from '../../../../../util/url'; import type { BranchUpgradeConfig } from '../../../../types'; import * as bitbucket from './bitbucket'; @@ -81,7 +82,7 @@ export function massageBody( input: string | undefined | null, baseUrl: string ): string { - let body = input ?? ''; + let body = coerceString(input); // Convert line returns body = body.replace(regEx(/\r\n/g), '\n'); // semantic-release cleanup diff --git a/lib/workers/repository/update/pr/changelog/releases.ts b/lib/workers/repository/update/pr/changelog/releases.ts index eb83f63610a808084a16650c4c433e5903bee02e..9f1f3c8bfe4d35b48ec210806b24cf00789ec5fc 100644 --- a/lib/workers/repository/update/pr/changelog/releases.ts +++ b/lib/workers/repository/update/pr/changelog/releases.ts @@ -6,6 +6,7 @@ import { isGetPkgReleasesConfig, } from '../../../../../modules/datasource'; import { VersioningApi, get } from '../../../../../modules/versioning'; +import { coerceArray } from '../../../../../util/array'; import type { BranchUpgradeConfig } from '../../../../types'; function matchesMMP(version: VersioningApi, v1: string, v2: string): boolean { @@ -57,7 +58,7 @@ export async function getInRangeReleases( matchesUnstable(version, newVersion, release.version) ); if (version.valueToVersion) { - for (const release of releases || []) { + for (const release of coerceArray(releases)) { release.version = version.valueToVersion(release.version); } }