From d37111ca888de1cab211875f668909d255767c3d Mon Sep 17 00:00:00 2001 From: Florian Greinacher Date: Tue, 20 Oct 2020 10:45:25 +0200 Subject: [PATCH] feat(nuget): add support for updating lock files (#7375) --- .../__snapshots__/artifacts.spec.ts.snap | 204 +++++++++++++++++ lib/manager/nuget/artifacts.spec.ts | 214 ++++++++++++++++++ lib/manager/nuget/artifacts.ts | 128 +++++++++++ lib/manager/nuget/extract.ts | 76 +------ lib/manager/nuget/index.ts | 1 + lib/manager/nuget/util.ts | 82 +++++++ 6 files changed, 634 insertions(+), 71 deletions(-) create mode 100644 lib/manager/nuget/__snapshots__/artifacts.spec.ts.snap create mode 100644 lib/manager/nuget/artifacts.spec.ts create mode 100644 lib/manager/nuget/artifacts.ts create mode 100644 lib/manager/nuget/util.ts diff --git a/lib/manager/nuget/__snapshots__/artifacts.spec.ts.snap b/lib/manager/nuget/__snapshots__/artifacts.spec.ts.snap new file mode 100644 index 00000000000000..95c1fb964c960e --- /dev/null +++ b/lib/manager/nuget/__snapshots__/artifacts.spec.ts.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`updateArtifacts aborts if lock file is unchanged 1`] = ` +Array [ + Object { + "cmd": "dotnet restore project.csproj --force-evaluate", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`updateArtifacts aborts if no lock file found 1`] = `Array []`; + +exports[`updateArtifacts authenticates at registries 1`] = ` +Array [ + Object { + "cmd": "dotnet nuget update source myRegistry --username some-username --password some-password --store-password-in-clear-text", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, + Object { + "cmd": "dotnet restore project.csproj --force-evaluate", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, + Object { + "cmd": "dotnet nuget update source myRegistry --username '' --password '' --store-password-in-clear-text", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`updateArtifacts catches errors 1`] = ` +Array [ + Object { + "artifactError": Object { + "lockFile": "packages.lock.json", + "stderr": "not found", + }, + }, +] +`; + +exports[`updateArtifacts does not update lock file when no deps changed 1`] = `Array []`; + +exports[`updateArtifacts does not update lock file when non-proj file is changed 1`] = `Array []`; + +exports[`updateArtifacts performs lock file maintenance 1`] = ` +Array [ + Object { + "cmd": "dotnet restore project.csproj --force-evaluate", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`updateArtifacts supports docker mode 1`] = ` +Array [ + Object { + "cmd": "docker pull renovate/dotnet", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker ps --filter name=renovate_dotnet -aq", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker run --rm --name=renovate_dotnet --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache\\":\\"/tmp/renovate/cache\\" -w \\"/tmp/github/some/repo\\" renovate/dotnet bash -l -c \\"dotnet restore project.csproj --force-evaluate\\"", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`updateArtifacts supports global mode 1`] = ` +Array [ + Object { + "cmd": "dotnet restore project.csproj --force-evaluate", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; + +exports[`updateArtifacts updates lock file 1`] = ` +Array [ + Object { + "cmd": "dotnet restore project.csproj --force-evaluate", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "maxBuffer": 10485760, + "timeout": 900000, + }, + }, +] +`; diff --git a/lib/manager/nuget/artifacts.spec.ts b/lib/manager/nuget/artifacts.spec.ts new file mode 100644 index 00000000000000..20670eda4c3e3c --- /dev/null +++ b/lib/manager/nuget/artifacts.spec.ts @@ -0,0 +1,214 @@ +import { exec as _exec } from 'child_process'; +import { join } from 'upath'; +import { envMock, mockExecAll } from '../../../test/execUtil'; +import { fs, mocked } from '../../../test/util'; +import { setUtilConfig } from '../../util'; +import { BinarySource } from '../../util/exec/common'; +import * as docker from '../../util/exec/docker'; +import * as _env from '../../util/exec/env'; +import * as _hostRules from '../../util/host-rules'; +import * as nuget from './artifacts'; +import { determineRegistries as _determineRegistries } from './util'; + +jest.mock('child_process'); +jest.mock('../../util/exec/env'); +jest.mock('../../util/fs'); +jest.mock('../../util/host-rules'); +jest.mock('./util'); + +const exec: jest.Mock = _exec as any; +const env = mocked(_env); +const determineRegistries: jest.Mock = _determineRegistries as any; +const hostRules = mocked(_hostRules); + +const config = { + // `join` fixes Windows CI + localDir: join('/tmp/github/some/repo'), + cacheDir: join('/tmp/renovate/cache'), + dockerUser: 'foobar', +}; + +describe('updateArtifacts', () => { + beforeEach(async () => { + jest.resetAllMocks(); + jest.resetModules(); + env.getChildProcessEnv.mockReturnValue(envMock.basic); + await setUtilConfig(config); + docker.resetPrefetchedImages(); + }); + it('aborts if no lock file found', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: ['foo', 'bar'], + newPackageFileContent: '{}', + config, + }) + ).toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('aborts if lock file is unchanged', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: ['foo', 'bar'], + newPackageFileContent: '{}', + config, + }) + ).toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('updates lock file', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: ['dep'], + newPackageFileContent: '{}', + config, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('does not update lock file when non-proj file is changed', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any); + expect( + await nuget.updateArtifacts({ + packageFileName: 'otherfile.props', + updatedDeps: ['dep'], + newPackageFileContent: '{}', + config, + }) + ).toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('does not update lock file when no deps changed', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: [], + newPackageFileContent: '{}', + config, + }) + ).toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('performs lock file maintenance', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: [], + newPackageFileContent: '{}', + config: { + ...config, + isLockFileMaintenance: true, + }, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + + it('supports docker mode', async () => { + jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce(); + await setUtilConfig({ ...config, binarySource: BinarySource.Docker }); + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: ['dep'], + newPackageFileContent: '{}', + config, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('supports global mode', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: ['dep'], + newPackageFileContent: '{}', + config: { + ...config, + binarySource: BinarySource.Global, + }, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); + it('catches errors', async () => { + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.writeLocalFile.mockImplementationOnce(() => { + throw new Error('not found'); + }); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: ['dep'], + newPackageFileContent: '{}', + config, + }) + ).toMatchSnapshot(); + }); + it('authenticates at registries', async () => { + const execSnapshots = mockExecAll(exec); + fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json'); + fs.readLocalFile.mockResolvedValueOnce('Current packages.lock.json' as any); + fs.readLocalFile.mockResolvedValueOnce('New packages.lock.json' as any); + determineRegistries.mockResolvedValueOnce([ + { + name: 'myRegistry', + url: 'https://my-registry.example.org', + }, + ] as never); + hostRules.find.mockImplementationOnce((search) => { + if ( + search.hostType === 'nuget' && + search.url === 'https://my-registry.example.org' + ) { + return { + username: 'some-username', + password: 'some-password', + }; + } + return undefined; + }); + expect( + await nuget.updateArtifacts({ + packageFileName: 'project.csproj', + updatedDeps: ['dep'], + newPackageFileContent: '{}', + config, + }) + ).not.toBeNull(); + expect(execSnapshots).toMatchSnapshot(); + }); +}); diff --git a/lib/manager/nuget/artifacts.ts b/lib/manager/nuget/artifacts.ts new file mode 100644 index 00000000000000..05be802d067eb7 --- /dev/null +++ b/lib/manager/nuget/artifacts.ts @@ -0,0 +1,128 @@ +import { id } from '../../datasource/nuget'; +import { logger } from '../../logger'; +import { ExecOptions, exec } from '../../util/exec'; +import { + getSiblingFileName, + readLocalFile, + writeLocalFile, +} from '../../util/fs'; +import * as hostRules from '../../util/host-rules'; +import { + UpdateArtifact, + UpdateArtifactsConfig, + UpdateArtifactsResult, +} from '../common'; +import { determineRegistries } from './util'; + +async function authenticate( + packageFileName: string, + config: UpdateArtifactsConfig, + cmds: string[] +): Promise { + const registries = ( + (await determineRegistries(packageFileName, config.localDir)) || [] + ).filter((registry) => registry.name != null); + for (const registry of registries) { + const { username, password } = hostRules.find({ + hostType: id, + url: registry.url, + }); + if (username && password) { + // Add registry credentials from host rules. + cmds.unshift( + `dotnet nuget update source ${registry.name} --username ${username} --password ${password} --store-password-in-clear-text` + ); + // Ensure that credentials are removed as soon as not necessary anymore. + cmds.push( + `dotnet nuget update source ${registry.name} --username '' --password '' --store-password-in-clear-text` + ); + } + } +} + +async function runDotnetRestore( + packageFileName: string, + config: UpdateArtifactsConfig +): Promise { + const execOptions: ExecOptions = { + docker: { + image: 'renovate/dotnet', + }, + }; + const cmds = [`dotnet restore ${packageFileName} --force-evaluate`]; + await authenticate(packageFileName, config, cmds); + logger.debug({ cmd: cmds }, 'dotnet command'); + await exec(cmds, execOptions); +} + +export async function updateArtifacts({ + packageFileName, + newPackageFileContent, + config, + updatedDeps, +}: UpdateArtifact): Promise { + logger.debug(`nuget.updateArtifacts(${packageFileName})`); + + if (!/(?:cs|vb|fs)proj$/i.test(packageFileName)) { + // This could be implemented in the future if necessary. + // It's not that easy though because the questions which + // project file to restore how to determine which lock files + // have been changed in such cases. + logger.debug( + { packageFileName }, + 'Not updating lock file for non project files' + ); + return null; + } + + const lockFileName = getSiblingFileName( + packageFileName, + 'packages.lock.json' + ); + const existingLockFileContent = await readLocalFile(lockFileName, 'utf8'); + if (!existingLockFileContent) { + logger.debug( + { packageFileName }, + 'No lock file found beneath package file.' + ); + return null; + } + + try { + if (updatedDeps.length === 0 && config.isLockFileMaintenance !== true) { + logger.debug( + `Not updating lock file because no deps changed and no lock file maintenance.` + ); + return null; + } + + await writeLocalFile(packageFileName, newPackageFileContent); + + await runDotnetRestore(packageFileName, config); + + const newLockFileContent = await readLocalFile(lockFileName, 'utf8'); + if (existingLockFileContent === newLockFileContent) { + logger.debug(`Lock file is unchanged`); + return null; + } + logger.debug('Returning updated lock file'); + return [ + { + file: { + name: lockFileName, + contents: await readLocalFile(lockFileName), + }, + }, + ]; + } catch (err) { + logger.debug({ err }, 'Failed to generate lock file'); + return [ + { + artifactError: { + lockFile: lockFileName, + stderr: err.message, + }, + }, + ]; + } +} diff --git a/lib/manager/nuget/extract.ts b/lib/manager/nuget/extract.ts index 3f087d2fe23b1f..2a54f003f26474 100644 --- a/lib/manager/nuget/extract.ts +++ b/lib/manager/nuget/extract.ts @@ -1,78 +1,12 @@ -import * as path from 'path'; -import findUp from 'find-up'; import { XmlDocument } from 'xmldoc'; import * as datasourceNuget from '../../datasource/nuget'; import { logger } from '../../logger'; import { SkipReason } from '../../types'; -import { clone } from '../../util/clone'; -import { readFile } from '../../util/fs'; import { get } from '../../versioning'; import * as semverVersioning from '../../versioning/semver'; import { ExtractConfig, PackageDependency, PackageFile } from '../common'; import { DotnetToolsManifest } from './types'; - -async function readFileAsXmlDocument(file: string): Promise { - try { - return new XmlDocument(await readFile(file, 'utf8')); - } catch (err) { - logger.debug({ err }, `failed to parse '${file}' as XML document`); - return undefined; - } -} - -async function determineRegistryUrls( - packageFile: string, - localDir: string -): Promise { - // Valid file names taken from https://github.com/NuGet/NuGet.Client/blob/f64621487c0b454eda4b98af853bf4a528bef72a/src/NuGet.Core/NuGet.Configuration/Settings/Settings.cs#L34 - const nuGetConfigFileNames = ['nuget.config', 'NuGet.config', 'NuGet.Config']; - const nuGetConfigPath = await findUp(nuGetConfigFileNames, { - cwd: path.dirname(path.join(localDir, packageFile)), - type: 'file', - }); - - if (nuGetConfigPath?.startsWith(localDir) !== true) { - return undefined; - } - - logger.debug({ nuGetConfigPath }, 'found NuGet.config'); - const nuGetConfig = await readFileAsXmlDocument(nuGetConfigPath); - if (!nuGetConfig) { - return undefined; - } - - const packageSources = nuGetConfig.childNamed('packageSources'); - if (!packageSources) { - return undefined; - } - - const registryUrls = clone(datasourceNuget.defaultRegistryUrls); - for (const child of packageSources.children) { - if (child.type === 'element') { - if (child.name === 'clear') { - logger.debug(`clearing registry URLs`); - registryUrls.length = 0; - } else if (child.name === 'add') { - const isHttpUrl = /^https?:\/\//i.test(child.attr.value); - if (isHttpUrl) { - let registryUrl = child.attr.value; - if (child.attr.protocolVersion) { - registryUrl += `#protocolVersion=${child.attr.protocolVersion}`; - } - logger.debug({ registryUrl }, 'adding registry URL'); - registryUrls.push(registryUrl); - } else { - logger.debug( - { registryUrl: child.attr.value }, - 'ignoring local registry URL' - ); - } - } - // child.name === 'remove' not supported - } - } - return registryUrls; -} +import { determineRegistries } from './util'; /** * https://docs.microsoft.com/en-us/nuget/concepts/package-versioning @@ -128,10 +62,10 @@ export async function extractPackageFile( logger.trace({ packageFile }, 'nuget.extractPackageFile()'); const versioning = get(config.versioning || semverVersioning.id); - const registryUrls = await determineRegistryUrls( - packageFile, - config.localDir - ); + const registries = await determineRegistries(packageFile, config.localDir); + const registryUrls = registries + ? registries.map((registry) => registry.url) + : undefined; if (packageFile.endsWith('.config/dotnet-tools.json')) { const deps: PackageDependency[] = []; diff --git a/lib/manager/nuget/index.ts b/lib/manager/nuget/index.ts index 32d3de691697fc..4311964f62c0d5 100644 --- a/lib/manager/nuget/index.ts +++ b/lib/manager/nuget/index.ts @@ -1,6 +1,7 @@ import { LANGUAGE_DOT_NET } from '../../constants/languages'; export { extractPackageFile } from './extract'; +export { updateArtifacts } from './artifacts'; export const language = LANGUAGE_DOT_NET; diff --git a/lib/manager/nuget/util.ts b/lib/manager/nuget/util.ts new file mode 100644 index 00000000000000..90f1af1c49c165 --- /dev/null +++ b/lib/manager/nuget/util.ts @@ -0,0 +1,82 @@ +import * as path from 'path'; +import findUp from 'find-up'; +import { XmlDocument } from 'xmldoc'; +import * as datasourceNuget from '../../datasource/nuget'; +import { logger } from '../../logger'; +import { readFile } from '../../util/fs'; + +async function readFileAsXmlDocument(file: string): Promise { + try { + return new XmlDocument(await readFile(file, 'utf8')); + } catch (err) { + logger.debug({ err }, `failed to parse '${file}' as XML document`); + return undefined; + } +} + +export interface Registry { + readonly url: string; + readonly name?: string; +} + +export async function determineRegistries( + packageFile: string, + localDir: string +): Promise { + // Valid file names taken from https://github.com/NuGet/NuGet.Client/blob/f64621487c0b454eda4b98af853bf4a528bef72a/src/NuGet.Core/NuGet.Configuration/Settings/Settings.cs#L34 + const nuGetConfigFileNames = ['nuget.config', 'NuGet.config', 'NuGet.Config']; + const nuGetConfigPath = await findUp(nuGetConfigFileNames, { + cwd: path.dirname(path.join(localDir, packageFile)), + type: 'file', + }); + + if (nuGetConfigPath?.startsWith(localDir) !== true) { + return undefined; + } + + logger.debug({ nuGetConfigPath }, 'found NuGet.config'); + const nuGetConfig = await readFileAsXmlDocument(nuGetConfigPath); + if (!nuGetConfig) { + return undefined; + } + + const packageSources = nuGetConfig.childNamed('packageSources'); + if (!packageSources) { + return undefined; + } + + const registries = datasourceNuget.defaultRegistryUrls.map( + (registryUrl) => + ({ + url: registryUrl, + } as Registry) + ); + for (const child of packageSources.children) { + if (child.type === 'element') { + if (child.name === 'clear') { + logger.debug(`clearing registry URLs`); + registries.length = 0; + } else if (child.name === 'add') { + const isHttpUrl = /^https?:\/\//i.test(child.attr.value); + if (isHttpUrl) { + let registryUrl = child.attr.value; + if (child.attr.protocolVersion) { + registryUrl += `#protocolVersion=${child.attr.protocolVersion}`; + } + logger.debug({ registryUrl }, 'adding registry URL'); + registries.push({ + name: child.attr.key, + url: registryUrl, + }); + } else { + logger.debug( + { registryUrl: child.attr.value }, + 'ignoring local registry URL' + ); + } + } + // child.name === 'remove' not supported + } + } + return registries; +}