Skip to content

Commit

Permalink
feat(nuget): allow detecting source URLs via package contents
Browse files Browse the repository at this point in the history
  • Loading branch information
fgreinacher committed Mar 21, 2024
1 parent 5f4af40 commit 1bb8271
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 2 deletions.
Binary file not shown.
75 changes: 75 additions & 0 deletions lib/modules/datasource/nuget/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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 { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';

Check failure on line 9 in lib/modules/datasource/nuget/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

'RepoGlobalConfig' is defined but never used.

Check failure on line 9 in lib/modules/datasource/nuget/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint-other

'RepoGlobalConfig' is declared but its value is never read.
import * as _hostRules from '../../../util/host-rules';
import { id as versioning } from '../../versioning/nuget';
import { parseRegistryUrl } from './common';
Expand Down Expand Up @@ -302,6 +306,77 @@ describe('modules/datasource/nuget/index', () => {
);
});

it('can determine source URL from nupkg when PackageBaseAddress is missing', async () => {
GlobalConfig.set({
cacheDir: join('/tmp/cache'),
});

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(
{
sourceUrl: 'https://github.com/NLog/NLog.git',
nupkgUrl:
'https://some-registry/v3-flatcontainer/nlog/4.7.3/nlog.4.7.3.nupkg',
},
'Determined sourceUrl from nupkgUrl',
);
expect(res?.sourceUrl).toBeDefined();
});

it('returns null for non 200 (v3v2)', async () => {
httpMock.scope('https://api.nuget.org').get('/v3/index.json').reply(500);
httpMock
Expand Down
1 change: 1 addition & 0 deletions lib/modules/datasource/nuget/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface CatalogEntry {
published?: string;
projectUrl?: string;
listed?: boolean;
packageContent?: string;
}

export interface CatalogPage {
Expand Down
57 changes: 55 additions & 2 deletions lib/modules/datasource/nuget/v3.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
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 * 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';
Expand Down Expand Up @@ -160,15 +164,23 @@ export async function getReleases(

let homepage: string | null = null;
let latestStable: string | null = null;
let latestNupkgUrl: string | null = null;
const releases = catalogEntries.map(
({ version, published: releaseTimestamp, projectUrl, listed }) => {
({
version,
published: releaseTimestamp,
projectUrl,
listed,
packageContent: nupkgUrl,
}) => {
const release: Release = { version: removeBuildMeta(version) };
if (releaseTimestamp) {
release.releaseTimestamp = releaseTimestamp;
}
if (versioning.isValid(version) && versioning.isStable(version)) {
latestStable = removeBuildMeta(version);
homepage = projectUrl ? massageUrl(projectUrl) : homepage;
latestNupkgUrl = nupkgUrl ? massageUrl(nupkgUrl) : null;
}
if (listed === false) {
release.isDeprecated = true;
Expand Down Expand Up @@ -198,7 +210,6 @@ export async function getReleases(
registryUrl,
'PackageBaseAddress',
);
// istanbul ignore else: this is a required v3 api
if (is.nonEmptyString(packageBaseAddress)) {
const nuspecUrl = `${ensureTrailingSlash(
packageBaseAddress,
Expand All @@ -212,6 +223,20 @@ export async function getReleases(
if (sourceUrl) {
dep.sourceUrl = massageUrl(sourceUrl);
}
} else if (latestNupkgUrl) {
const sourceUrl = await getSourceUrlFromNupkg(
http,
pkgName,
latestStable,
latestNupkgUrl,
);
if (sourceUrl) {
dep.sourceUrl = massageUrl(sourceUrl);
logger.debug(
{ sourceUrl, nupkgUrl: latestNupkgUrl },
`Determined sourceUrl from nupkgUrl`,
);
}
}
} catch (err) {
// istanbul ignore if: not easy testable with nock
Expand Down Expand Up @@ -242,3 +267,31 @@ export async function getReleases(

return dep;
}

async function getSourceUrlFromNupkg(
http: Http,
packageName: string,
packageVersion: string | null,
nupkgUrl: string,
): Promise<string | undefined> {
const cacheDir = await ensureCacheDir(`nuget`);
const readStream = http.stream(nupkgUrl);
try {
const nupkgFile = upath.join(
cacheDir,
`${packageName}.${packageVersion}.nupkg`,
);
const writeStream = fs.createCacheWriteStream(nupkgFile);
await fs.pipeline(readStream, writeStream);
const contentsDir = upath.join(
cacheDir,
`${packageName}.${packageVersion}`,
);
await extract(nupkgFile, { dir: contentsDir });
const nuspecFile = upath.join(contentsDir, `${packageName}.nuspec`);
const nuspec = new XmlDocument(await fs.readCacheFile(nuspecFile, 'utf8'));
return nuspec.valueWithPath('metadata.repository@url');
} finally {
await fs.rmCache(cacheDir);
}
}
Loading

0 comments on commit 1bb8271

Please sign in to comment.