From a94466c4ee4c9e44ab2021ace57e8554b82351b5 Mon Sep 17 00:00:00 2001 From: Florian Greinacher <florian@greinacher.de> Date: Sat, 20 Apr 2024 08:21:19 +0200 Subject: [PATCH] feat(nuget): allow detecting source URLs via package contents (#28071) Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- docs/usage/self-hosted-experimental.md | 4 + .../nlog/NLog.4.7.3-no-repo.nupkg | Bin 0 -> 2201 bytes .../nuget/__fixtures__/nlog/NLog.4.7.3.nupkg | Bin 0 -> 2227 bytes lib/modules/datasource/nuget/common.ts | 8 +- lib/modules/datasource/nuget/index.spec.ts | 167 +++++++++++++++++- lib/modules/datasource/nuget/types.ts | 2 + lib/modules/datasource/nuget/v3.ts | 77 +++++++- 7 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 lib/modules/datasource/nuget/__fixtures__/nlog/NLog.4.7.3-no-repo.nupkg create mode 100644 lib/modules/datasource/nuget/__fixtures__/nlog/NLog.4.7.3.nupkg diff --git a/docs/usage/self-hosted-experimental.md b/docs/usage/self-hosted-experimental.md index bae03e095f..04124a9ba7 100644 --- a/docs/usage/self-hosted-experimental.md +++ b/docs/usage/self-hosted-experimental.md @@ -141,6 +141,10 @@ This feature is in private beta. If set, Renovate will query the merge-confidence JSON API only for datasources that are part of this list. The expected value for this environment variable is a JSON array of strings. +## `RENOVATE_X_NUGET_DOWNLOAD_NUPKGS` + +If set to any value, Renovate will download `nupkg` files for determining package metadata. + ## `RENOVATE_X_PLATFORM_VERSION` Specify this string for Renovate to skip API checks and provide GitLab/Bitbucket server version directly. diff --git a/lib/modules/datasource/nuget/__fixtures__/nlog/NLog.4.7.3-no-repo.nupkg b/lib/modules/datasource/nuget/__fixtures__/nlog/NLog.4.7.3-no-repo.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..77ce3d810f1adeed673f4a98354a14b11797947a GIT binary patch literal 2201 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 Jpv3{=0RYH|8h`)* literal 0 HcmV?d00001 diff --git a/lib/modules/datasource/nuget/__fixtures__/nlog/NLog.4.7.3.nupkg b/lib/modules/datasource/nuget/__fixtures__/nlog/NLog.4.7.3.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..6ec85f1ec046510454834a7881f0ac9e28af0780 GIT binary patch literal 2227 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 literal 0 HcmV?d00001 diff --git a/lib/modules/datasource/nuget/common.ts b/lib/modules/datasource/nuget/common.ts index e3c9afb28f..14bb38d694 100644 --- a/lib/modules/datasource/nuget/common.ts +++ b/lib/modules/datasource/nuget/common.ts @@ -12,10 +12,14 @@ export function removeBuildMeta(version: string): string { const urlWhitespaceRe = regEx(/\s/g); -export function massageUrl(url: string): string { +export function massageUrl(url: string | null | undefined): string | null { + if (url === null || url === undefined) { + return null; + } + let resultUrl = url; - // During `dotnet pack` certain URLs are being URL decoded which may introduce whitespaces + // During `dotnet pack` certain URLs are being URL decoded which may introduce whitespace // and causes Markdown link generation problems. resultUrl = resultUrl.replace(urlWhitespaceRe, '%20'); diff --git a/lib/modules/datasource/nuget/index.spec.ts b/lib/modules/datasource/nuget/index.spec.ts index db7bbaa7b5..ce25cf7722 100644 --- a/lib/modules/datasource/nuget/index.spec.ts +++ b/lib/modules/datasource/nuget/index.spec.ts @@ -1,8 +1,12 @@ +import { Readable } from 'stream'; import { mockDeep } from 'jest-mock-extended'; +import { join } from 'upath'; import { getPkgReleases } from '..'; import { Fixtures } from '../../../../test/fixtures'; import * as httpMock from '../../../../test/http-mock'; -import { logger } from '../../../../test/util'; +import { logger, mocked } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; +import * as _packageCache from '../../../util/cache/package'; import * as _hostRules from '../../../util/host-rules'; import { id as versioning } from '../../versioning/nuget'; import { parseRegistryUrl } from './common'; @@ -14,6 +18,9 @@ const hostRules: any = _hostRules; jest.mock('../../../util/host-rules', () => mockDeep()); +jest.mock('../../../util/cache/package', () => mockDeep()); +const packageCache = mocked(_packageCache); + const pkgInfoV3FromNuget = Fixtures.get('nunit/v3_nuget_org.xml'); const pkgListV3Registration = Fixtures.get('nunit/v3_registration.json'); @@ -105,6 +112,10 @@ const configV3AzureDevOps = { }; describe('modules/datasource/nuget/index', () => { + beforeEach(() => { + GlobalConfig.reset(); + }); + describe('parseRegistryUrl', () => { it('extracts feed version from registry URL hash (v3)', () => { const parsed = parseRegistryUrl('https://my-registry#protocolVersion=3'); @@ -302,6 +313,160 @@ describe('modules/datasource/nuget/index', () => { ); }); + describe('determine source URL from nupkg', () => { + beforeEach(() => { + GlobalConfig.set({ + cacheDir: join('/tmp/cache'), + }); + process.env.RENOVATE_X_NUGET_DOWNLOAD_NUPKGS = 'true'; + }); + + afterEach(() => { + delete process.env.RENOVATE_X_NUGET_DOWNLOAD_NUPKGS; + }); + + it('can determine source URL from nupkg when PackageBaseAddress is missing', async () => { + const nugetIndex = ` + { + "version": "3.0.0", + "resources": [ + { + "@id": "https://some-registry/v3/metadata", + "@type": "RegistrationsBaseUrl/3.0.0-beta", + "comment": "Get package metadata." + } + ] + } + `; + const nlogRegistration = ` + { + "count": 1, + "items": [ + { + "@id": "https://some-registry/v3/metadata/nlog/4.7.3.json", + "lower": "4.7.3", + "upper": "4.7.3", + "count": 1, + "items": [ + { + "@id": "foo", + "catalogEntry": { + "id": "NLog", + "version": "4.7.3", + "packageContent": "https://some-registry/v3-flatcontainer/nlog/4.7.3/nlog.4.7.3.nupkg" + } + } + ] + } + ] + } + `; + httpMock + .scope('https://some-registry') + .get('/v3/index.json') + .twice() + .reply(200, nugetIndex) + .get('/v3/metadata/nlog/index.json') + .reply(200, nlogRegistration) + .get('/v3-flatcontainer/nlog/4.7.3/nlog.4.7.3.nupkg') + .reply(200, () => { + const readableStream = new Readable(); + readableStream.push(Fixtures.getBinary('nlog/NLog.4.7.3.nupkg')); + readableStream.push(null); + return readableStream; + }); + const res = await getPkgReleases({ + datasource, + versioning, + packageName: 'NLog', + registryUrls: ['https://some-registry/v3/index.json'], + }); + expect(logger.logger.debug).toHaveBeenCalledWith( + 'Determined sourceUrl https://github.com/NLog/NLog.git from https://some-registry/v3-flatcontainer/nlog/4.7.3/nlog.4.7.3.nupkg', + ); + expect(packageCache.set).toHaveBeenCalledWith( + 'datasource-nuget', + 'cache-decorator:source-url:https://some-registry/v3/index.json:NLog', + { + cachedAt: expect.any(String), + value: 'https://github.com/NLog/NLog.git', + }, + 60 * 24 * 7, + ); + expect(res?.sourceUrl).toBeDefined(); + }); + + it('can handle nupkg without repository metadata', async () => { + const nugetIndex = ` + { + "version": "3.0.0", + "resources": [ + { + "@id": "https://some-registry/v3/metadata", + "@type": "RegistrationsBaseUrl/3.0.0-beta", + "comment": "Get package metadata." + } + ] + } + `; + const nlogRegistration = ` + { + "count": 1, + "items": [ + { + "@id": "https://some-registry/v3/metadata/nlog/4.7.3.json", + "lower": "4.7.3", + "upper": "4.7.3", + "count": 1, + "items": [ + { + "@id": "foo", + "catalogEntry": { + "id": "NLog", + "version": "4.7.3", + "packageContent": "https://some-registry/v3-flatcontainer/nlog/4.7.3/nlog.4.7.3.nupkg" + } + } + ] + } + ] + } + `; + httpMock + .scope('https://some-registry') + .get('/v3/index.json') + .twice() + .reply(200, nugetIndex) + .get('/v3/metadata/nlog/index.json') + .reply(200, nlogRegistration) + .get('/v3-flatcontainer/nlog/4.7.3/nlog.4.7.3.nupkg') + .reply(200, () => { + const readableStream = new Readable(); + readableStream.push( + Fixtures.getBinary('nlog/NLog.4.7.3-no-repo.nupkg'), + ); + readableStream.push(null); + return readableStream; + }); + const res = await getPkgReleases({ + datasource, + versioning, + packageName: 'NLog', + registryUrls: ['https://some-registry/v3/index.json'], + }); + expect(packageCache.set).toHaveBeenCalledWith( + 'datasource-nuget', + 'cache-decorator:source-url:https://some-registry/v3/index.json:NLog', + { + cachedAt: expect.any(String), + value: null, + }, + 60 * 24 * 7, + ); + expect(res?.sourceUrl).toBeUndefined(); + }); + }); + it('returns null for non 200 (v3v2)', async () => { httpMock.scope('https://api.nuget.org').get('/v3/index.json').reply(500); httpMock diff --git a/lib/modules/datasource/nuget/types.ts b/lib/modules/datasource/nuget/types.ts index 29aba2a5c6..36fa672e83 100644 --- a/lib/modules/datasource/nuget/types.ts +++ b/lib/modules/datasource/nuget/types.ts @@ -5,11 +5,13 @@ export interface ServicesIndexRaw { }[]; } +// See https://learn.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry export interface CatalogEntry { version: string; published?: string; projectUrl?: string; listed?: boolean; + packageContent?: string; } export interface CatalogPage { diff --git a/lib/modules/datasource/nuget/v3.ts b/lib/modules/datasource/nuget/v3.ts index 2024c175cb..1d0dc587c0 100644 --- a/lib/modules/datasource/nuget/v3.ts +++ b/lib/modules/datasource/nuget/v3.ts @@ -1,9 +1,14 @@ import is from '@sindresorhus/is'; +import extract from 'extract-zip'; import semver from 'semver'; +import upath from 'upath'; import { XmlDocument } from 'xmldoc'; import { logger } from '../../../logger'; import { ExternalHostError } from '../../../types/errors/external-host-error'; import * as packageCache from '../../../util/cache/package'; +import { cache } from '../../../util/cache/package/decorator'; +import * as fs from '../../../util/fs'; +import { ensureCacheDir } from '../../../util/fs'; import { Http, HttpError } from '../../../util/http'; import * as p from '../../../util/promises'; import { regEx } from '../../../util/regex'; @@ -151,8 +156,15 @@ export class NugetV3Api { let homepage: string | null = null; let latestStable: string | null = null; + let nupkgUrl: string | null = null; const releases = catalogEntries.map( - ({ version, published: releaseTimestamp, projectUrl, listed }) => { + ({ + version, + published: releaseTimestamp, + projectUrl, + listed, + packageContent, + }) => { const release: Release = { version: removeBuildMeta(version) }; if (releaseTimestamp) { release.releaseTimestamp = releaseTimestamp; @@ -160,6 +172,7 @@ export class NugetV3Api { if (versioning.isValid(version) && versioning.isStable(version)) { latestStable = removeBuildMeta(version); homepage = projectUrl ? massageUrl(projectUrl) : homepage; + nupkgUrl = massageUrl(packageContent); } if (listed === false) { release.isDeprecated = true; @@ -177,6 +190,7 @@ export class NugetV3Api { const last = catalogEntries.pop()!; latestStable = removeBuildMeta(last.version); homepage ??= last.projectUrl ?? null; + nupkgUrl ??= massageUrl(last.packageContent); } const dep: ReleaseResult = { @@ -189,7 +203,6 @@ export class NugetV3Api { registryUrl, 'PackageBaseAddress', ); - // istanbul ignore else: this is a required v3 api if (is.nonEmptyString(packageBaseAddress)) { const nuspecUrl = `${ensureTrailingSlash( packageBaseAddress, @@ -203,6 +216,18 @@ export class NugetV3Api { if (sourceUrl) { dep.sourceUrl = massageUrl(sourceUrl); } + } else if (nupkgUrl) { + const sourceUrl = await this.getSourceUrlFromNupkg( + http, + registryUrl, + pkgName, + latestStable, + nupkgUrl, + ); + if (sourceUrl) { + dep.sourceUrl = massageUrl(sourceUrl); + logger.debug(`Determined sourceUrl ${sourceUrl} from ${nupkgUrl}`); + } } } catch (err) { // istanbul ignore if: not easy testable with nock @@ -233,4 +258,52 @@ export class NugetV3Api { return dep; } + + @cache({ + namespace: NugetV3Api.cacheNamespace, + key: ( + _http: Http, + registryUrl: string, + packageName: string, + _packageVersion: string | null, + _nupkgUrl: string, + ) => `source-url:${registryUrl}:${packageName}`, + ttlMinutes: 10080, // 1 week + }) + async getSourceUrlFromNupkg( + http: Http, + _registryUrl: string, + packageName: string, + packageVersion: string | null, + nupkgUrl: string, + ): Promise<string | null> { + // istanbul ignore if: experimental feature + if (!process.env.RENOVATE_X_NUGET_DOWNLOAD_NUPKGS) { + logger.once.debug('RENOVATE_X_NUGET_DOWNLOAD_NUPKGS is not set'); + return null; + } + const cacheDir = await ensureCacheDir('nuget'); + const nupkgFile = upath.join( + cacheDir, + `${packageName}.${packageVersion}.nupkg`, + ); + const nupkgContentsDir = upath.join( + cacheDir, + `${packageName}.${packageVersion}`, + ); + const readStream = http.stream(nupkgUrl); + try { + const writeStream = fs.createCacheWriteStream(nupkgFile); + await fs.pipeline(readStream, writeStream); + await extract(nupkgFile, { dir: nupkgContentsDir }); + const nuspecFile = upath.join(nupkgContentsDir, `${packageName}.nuspec`); + const nuspec = new XmlDocument( + await fs.readCacheFile(nuspecFile, 'utf8'), + ); + return nuspec.valueWithPath('metadata.repository@url') ?? null; + } finally { + await fs.rmCache(nupkgFile); + await fs.rmCache(nupkgContentsDir); + } + } } -- GitLab