From 4247f45fbd592fd09e136340ccd692e40ac5b640 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Wed, 6 Sep 2023 15:16:21 +0200 Subject: [PATCH] fix: better branch code coverage --- .github/workflows/build.yml | 2 +- lib/modules/datasource/go/index.ts | 6 ++--- lib/modules/datasource/hexpm-bob/index.ts | 2 +- lib/modules/datasource/nuget/v3.ts | 2 +- lib/modules/datasource/packagist/index.ts | 4 +++- lib/modules/datasource/pypi/index.ts | 5 +++-- lib/modules/datasource/repology/index.spec.ts | 16 ++++++++++++++ lib/modules/datasource/repology/index.ts | 1 - .../manager/ansible-galaxy/collections.ts | 8 +++++-- lib/modules/manager/cocoapods/artifacts.ts | 3 ++- lib/modules/manager/cocoapods/extract.ts | 3 ++- lib/modules/manager/gradle/extract/catalog.ts | 4 +++- .../extract/consistent-versions-plugin.ts | 5 +++-- lib/modules/manager/maven-wrapper/extract.ts | 3 ++- .../locked-dependency/package-lock/index.ts | 7 ++++-- .../lockfile/__snapshots__/index.spec.ts.snap | 22 ++++++++++++------- .../manager/terraform/lockfile/index.spec.ts | 18 ++++----------- .../manager/terraform/lockfile/index.ts | 1 - .../terraform/lockfile/update-locked.ts | 7 ++++-- .../platform/github/massage-markdown-links.ts | 7 +++--- lib/modules/platform/gitlab/index.spec.ts | 18 +++++++++++++++ lib/modules/platform/gitlab/index.ts | 20 ++++++++++------- lib/modules/versioning/cargo/index.ts | 4 +++- lib/modules/versioning/composer/index.spec.ts | 18 +++++++++++++++ lib/modules/versioning/distro.spec.ts | 5 +++++ lib/modules/versioning/docker/index.ts | 5 +++-- lib/modules/versioning/generic.spec.ts | 4 ++++ lib/modules/versioning/generic.ts | 5 ++--- lib/modules/versioning/ruby/range.ts | 2 +- .../versioning/semver-coerced/index.spec.ts | 2 +- lib/modules/versioning/ubuntu/index.ts | 10 +++------ lib/util/number.spec.ts | 12 ++++++++++ lib/util/number.ts | 12 ++++++++++ lib/util/string.spec.ts | 1 + lib/util/string.ts | 12 ++++++++-- lib/workers/global/config/parse/file.spec.ts | 2 +- lib/workers/global/config/parse/file.ts | 10 ++++----- lib/workers/global/config/parse/index.ts | 3 ++- .../branch/migrated-data.spec.ts | 12 +++++++++- .../repository/config-migration/pr/index.ts | 3 ++- .../finalize/repository-statistics.spec.ts | 1 - .../update/pr/changelog/release-notes.spec.ts | 2 ++ .../update/pr/changelog/release-notes.ts | 3 ++- .../update/pr/changelog/releases.ts | 3 ++- 44 files changed, 210 insertions(+), 85 deletions(-) create mode 100644 lib/util/number.spec.ts create mode 100644 lib/util/number.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd1b11e43d6b59..4a586350af19c6 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 1406bb7d0dae78..8468524b904bbb 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 fb07083aa1d51f..f1154cdf517332 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 72db618429c52c..d515d3c916e40d 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 9e2b203be86371..0bef353d3379ee 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 ddc76d5b0b1d8c..78231497ceac33 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 7e7df992af3e08..f102a95da374d6 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 b07e76b21d5394..86e026bff0b1e6 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 951618aace5da8..f84bf3bd3ae24d 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 7e7f84629058c6..fc9dcb70941ff6 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 009e4917da12de..f873584432565e 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( /[@/](?github|gitlab)\.com[:/](?[^/]+)\/(?[^/]+)/ - ).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 ed11cab6dd8150..a274e8b4228e50 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 46b971dd027ee1..ea78d1de4358d7 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 ): PackageDependency[] { - 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 12d91fe2440bec..92e834072ef689 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 0bd1e9a04ba28f..3d220be894aa80 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 = {}; 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 41ef8b78e198bf..574bb97beb0d3f 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 354b84fd4fdd82..75e4c93c48a157 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 36cc975097dde6..58c72065e27543 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 5d0988dab9e408..b2bc6eb5f34aa8 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 8fe4a773f6765f..44c3b53616c6a6 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 0fca92571e72a2..047f14dac5ce02 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 24cbda8fbc2a6e..d8981e5c72e645 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 { 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 { 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 1ab7546ce2cac0..7508c5781be024 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 ba67d2f3c3a888..2fbebc88ef45ab 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 5ad329d7c933bf..c4e4c82d4694ff 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 94f5f02cde8291..a196189d5b4021 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 b0903e37dec9e3..d7946528f85fd5 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({}))).toBeNull(); }); it('isCompatible', () => { diff --git a/lib/modules/versioning/generic.ts b/lib/modules/versioning/generic.ts index 32df1a1b9040e5..1697ad6ab74bee 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 a490d9c94c14dc..cb8deaf75717c6 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 95d1ddb87f64c0..ff01ad95fdacff 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 304488dbba56ec..4e6e96eb07d8be 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 00000000000000..4f215c1aa1c8c7 --- /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 00000000000000..2fb488c9d3eac5 --- /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 3394a0236492e8..9df3c52ac20937 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 ce6bad089d9fd5..f82e8f260eee7e 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 3b7d22e34057cd..0a786e8f80a079 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 eeeaf5671e11a4..4ccdca5f392bb6 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 { 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 { 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 { } 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 070d52561d3c8f..beb03cb895da40 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 541d7e7cfdaf5d..db3b27b3464866 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 3d11d730af11ce..9eb0355783f40a 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 { 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 6d65e88af89a6f..66e05794105833 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({ - 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 e073ea22662b6c..9d288ee72c5af0 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({ type: 'gitlab', repository: 'https://gitlab.com/gitlab-org/gitter/webapp/', + sourceDirectory: 'lib', }), versions: [ partial({ @@ -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 b2c4117e2d397a..567a7a55d31bb0 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 eb83f63610a808..9f1f3c8bfe4d35 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); } }