Skip to content

Commit 5eb6c7e

Browse files
authored
Merge pull request #1664 from IdsTeepe/feat/dotnet-based-nuget-query
Feat: Dynamically get query service from nuget package source
2 parents 2169f9f + e38db95 commit 5eb6c7e

File tree

4 files changed

+140
-22
lines changed

4 files changed

+140
-22
lines changed

dist/tools/libs/tools.mjs

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ import * as os from 'node:os';
44
import * as path from 'node:path';
55
import { s as semverExports } from './semver.mjs';
66

7+
var NugetServiceType = /* @__PURE__ */ ((NugetServiceType2) => {
8+
NugetServiceType2["Catalog"] = "Catalog";
9+
NugetServiceType2["PackageBaseAddress"] = "PackageBaseAddress";
10+
NugetServiceType2["PackageDetailsUriTemplate"] = "PackageDetailsUriTemplate";
11+
NugetServiceType2["PackagePublish"] = "PackagePublish";
12+
NugetServiceType2["ReadmeUriTemplate"] = "ReadmeUriTemplate";
13+
NugetServiceType2["RegistrationsBaseUrl"] = "RegistrationsBaseUrl";
14+
NugetServiceType2["ReportAbuseUriTemplate"] = "ReportAbuseUriTemplate";
15+
NugetServiceType2["RepositorySignatures"] = "RepositorySignatures";
16+
NugetServiceType2["SearchAutocompleteService"] = "SearchAutocompleteService";
17+
NugetServiceType2["SearchQueryService"] = "SearchQueryService";
18+
NugetServiceType2["SymbolPackagePublish"] = "SymbolPackagePublish";
19+
NugetServiceType2["VulnerabilityInfo"] = "VulnerabilityInfo";
20+
return NugetServiceType2;
21+
})(NugetServiceType || {});
22+
723
class ArgumentsBuilder {
824
args = [];
925
isWindows = os.platform() === "win32";
@@ -167,7 +183,6 @@ class DotnetTool {
167183
constructor(buildAgent) {
168184
this.buildAgent = buildAgent;
169185
}
170-
static nugetRoot = "https://azuresearch-usnc.nuget.org/query";
171186
disableTelemetry() {
172187
this.buildAgent.info("Disable Telemetry");
173188
this.buildAgent.setVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "true");
@@ -318,23 +333,58 @@ class DotnetTool {
318333
}
319334
return path.normalize(workDir);
320335
}
321-
async queryLatestMatch(toolName, versionSpec, includePrerelease) {
322-
this.buildAgent.info(
323-
`Querying tool versions for ${toolName}${versionSpec ? `@${versionSpec}` : ""} ${includePrerelease ? "including pre-releases" : ""}`
324-
);
336+
async getQueryServices() {
337+
const builder = new ArgumentsBuilder().addArgument("nuget").addArgument("list").addArgument("source").addKeyValue("format", "short");
338+
const result = await this.execute("dotnet", builder.build());
339+
const nugetSources = [
340+
...(result.stdout ?? "").matchAll(/^E (?<index>.+)/gm)
341+
].map((m) => m.groups.index);
342+
if (!nugetSources.length) {
343+
this.buildAgent.error("Failed to fetch an enabled package source for dotnet.");
344+
return [];
345+
}
346+
const sources = [];
347+
for (const nugetSource of nugetSources) {
348+
const nugetIndex = await fetch(nugetSource);
349+
if (!nugetIndex?.ok) {
350+
this.buildAgent.warn(`Failed to fetch data from NuGet source ${nugetSource}.`);
351+
continue;
352+
}
353+
const resources = (await nugetIndex.json())?.resources;
354+
const serviceUrl = resources?.find((s) => s["@type"].startsWith(NugetServiceType.SearchQueryService))?.["@id"];
355+
if (!serviceUrl) {
356+
this.buildAgent.warn(`Could not find a ${NugetServiceType.SearchQueryService} in NuGet source ${nugetSource}`);
357+
continue;
358+
}
359+
sources.push(serviceUrl);
360+
}
361+
return sources;
362+
}
363+
async queryVersionsFromNugetSource(serviceUrl, toolName, includePrerelease) {
325364
const toolNameParam = encodeURIComponent(toolName.toLowerCase());
326365
const prereleaseParam = includePrerelease ? "true" : "false";
327-
const downloadPath = `${DotnetTool.nugetRoot}?q=${toolNameParam}&prerelease=${prereleaseParam}&semVerLevel=2.0.0&take=1`;
366+
const downloadPath = `${serviceUrl}?q=${toolNameParam}&prerelease=${prereleaseParam}&semVerLevel=2.0.0&take=1`;
328367
const response = await fetch(downloadPath);
329368
if (!response || !response.ok) {
330-
this.buildAgent.info(`failed to query latest version for ${toolName} from ${downloadPath}. Status code: ${response ? response.status : "unknown"}`);
331-
return null;
369+
this.buildAgent.warn(`failed to query latest version for ${toolName} from ${downloadPath}. Status code: ${response ? response.status : "unknown"}`);
370+
return [];
332371
}
333372
const { data } = await response.json();
334373
const versions = data[0].versions.map((x) => x.version);
335-
if (!versions || !versions.length) {
374+
return versions ?? [];
375+
}
376+
async queryLatestMatch(toolName, versionSpec, includePrerelease) {
377+
this.buildAgent.info(
378+
`Querying tool versions for ${toolName}${versionSpec ? `@${versionSpec}` : ""} ${includePrerelease ? "including pre-releases" : ""}`
379+
);
380+
const queryServices = await this.getQueryServices();
381+
if (!queryServices.length) {
336382
return null;
337383
}
384+
let versions = (await Promise.all(
385+
queryServices.map(async (service) => await this.queryVersionsFromNugetSource(service, toolName, includePrerelease))
386+
)).flat();
387+
versions = [...new Set(versions)];
338388
this.buildAgent.debug(`got versions: ${versions.join(", ")}`);
339389
const version = semverExports.maxSatisfying(versions, versionSpec, { includePrerelease });
340390
if (version) {

dist/tools/libs/tools.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/common/dotnet-tool.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as path from 'node:path'
66
import * as semver from 'semver'
77
import { type IBuildAgent, type ExecResult } from '@agents/common'
88
import { ISettingsProvider } from './settings'
9-
import { NugetVersions } from './models'
9+
import { NugetServiceIndex, NugetServiceType, NugetVersions } from './models'
1010
import { ArgumentsBuilder } from './arguments-builder'
1111

1212
export interface IDotnetTool {
@@ -18,8 +18,6 @@ export interface IDotnetTool {
1818
}
1919

2020
export abstract class DotnetTool implements IDotnetTool {
21-
private static readonly nugetRoot: string = 'https://azuresearch-usnc.nuget.org/query'
22-
2321
constructor(protected buildAgent: IBuildAgent) {}
2422

2523
abstract get packageName(): string
@@ -228,29 +226,74 @@ export abstract class DotnetTool implements IDotnetTool {
228226
return path.normalize(workDir)
229227
}
230228

231-
private async queryLatestMatch(toolName: string, versionSpec: string, includePrerelease: boolean): Promise<string | null> {
232-
this.buildAgent.info(
233-
`Querying tool versions for ${toolName}${versionSpec ? `@${versionSpec}` : ''} ${includePrerelease ? 'including pre-releases' : ''}`
234-
)
229+
private async getQueryServices(): Promise<string[]> {
230+
// Use dotnet tool to get the first enabled nuget source.
231+
const builder = new ArgumentsBuilder().addArgument('nuget').addArgument('list').addArgument('source').addKeyValue('format', 'short')
232+
const result = await this.execute('dotnet', builder.build())
233+
234+
// Each line of the output starts with either E (enabled) or D (disabled), followed by a space and index url.
235+
const nugetSources = [...(result.stdout ?? '').matchAll(/^E (?<index>.+)/gm)].map(m => m.groups!.index)
235236

237+
if (!nugetSources.length) {
238+
this.buildAgent.error('Failed to fetch an enabled package source for dotnet.')
239+
return []
240+
}
241+
242+
const sources: string[] = []
243+
for (const nugetSource of nugetSources) {
244+
// Fetch the nuget source index to obtain the query service
245+
const nugetIndex = await fetch(nugetSource)
246+
if (!nugetIndex?.ok) {
247+
this.buildAgent.warn(`Failed to fetch data from NuGet source ${nugetSource}.`)
248+
continue
249+
}
250+
251+
// Parse the nuget service index and get the (first / primary) query service
252+
const resources = ((await nugetIndex.json()) as NugetServiceIndex)?.resources
253+
const serviceUrl = resources?.find(s => s['@type'].startsWith(NugetServiceType.SearchQueryService))?.['@id']
254+
255+
if (!serviceUrl) {
256+
this.buildAgent.warn(`Could not find a ${NugetServiceType.SearchQueryService} in NuGet source ${nugetSource}`)
257+
continue
258+
}
259+
sources.push(serviceUrl)
260+
}
261+
return sources
262+
}
263+
264+
private async queryVersionsFromNugetSource(serviceUrl: string, toolName: string, includePrerelease: boolean): Promise<string[]> {
236265
const toolNameParam = encodeURIComponent(toolName.toLowerCase())
237266
const prereleaseParam = includePrerelease ? 'true' : 'false'
238-
const downloadPath = `${DotnetTool.nugetRoot}?q=${toolNameParam}&prerelease=${prereleaseParam}&semVerLevel=2.0.0&take=1`
267+
const downloadPath = `${serviceUrl}?q=${toolNameParam}&prerelease=${prereleaseParam}&semVerLevel=2.0.0&take=1`
239268

240269
const response = await fetch(downloadPath)
241270

242271
if (!response || !response.ok) {
243-
this.buildAgent.info(`failed to query latest version for ${toolName} from ${downloadPath}. Status code: ${response ? response.status : 'unknown'}`)
244-
return null
272+
this.buildAgent.warn(`failed to query latest version for ${toolName} from ${downloadPath}. Status code: ${response ? response.status : 'unknown'}`)
273+
return []
245274
}
246-
247275
const { data } = (await response.json()) as NugetVersions
248276

249277
const versions = data[0].versions.map(x => x.version)
250-
if (!versions || !versions.length) {
278+
279+
return versions ?? []
280+
}
281+
282+
private async queryLatestMatch(toolName: string, versionSpec: string, includePrerelease: boolean): Promise<string | null> {
283+
this.buildAgent.info(
284+
`Querying tool versions for ${toolName}${versionSpec ? `@${versionSpec}` : ''} ${includePrerelease ? 'including pre-releases' : ''}`
285+
)
286+
287+
const queryServices = await this.getQueryServices()
288+
if (!queryServices.length) {
251289
return null
252290
}
253291

292+
let versions = (
293+
await Promise.all(queryServices.map(async service => await this.queryVersionsFromNugetSource(service, toolName, includePrerelease)))
294+
).flat()
295+
versions = [...new Set(versions)] // remove duplicates
296+
254297
this.buildAgent.debug(`got versions: ${versions.join(', ')}`)
255298

256299
const version = semver.maxSatisfying(versions, versionSpec, { includePrerelease })

src/tools/common/models.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,28 @@ export type IRunner = {
1212
}
1313

1414
export type NugetVersions = { data: { versions: { version: string }[] }[] }
15+
16+
/** See the {@link https://learn.microsoft.com/en-us/nuget/api/service-index|NuGet Server API spec}*/
17+
export type NugetServiceIndex = {
18+
version: string
19+
resources: {
20+
'@type': `${NugetServiceType}${`/${string}` | ''}`
21+
'@id': string
22+
comment: string | undefined
23+
}[]
24+
}
25+
26+
export enum NugetServiceType {
27+
Catalog = 'Catalog',
28+
PackageBaseAddress = 'PackageBaseAddress',
29+
PackageDetailsUriTemplate = 'PackageDetailsUriTemplate',
30+
PackagePublish = 'PackagePublish',
31+
ReadmeUriTemplate = 'ReadmeUriTemplate',
32+
RegistrationsBaseUrl = 'RegistrationsBaseUrl',
33+
ReportAbuseUriTemplate = 'ReportAbuseUriTemplate',
34+
RepositorySignatures = 'RepositorySignatures',
35+
SearchAutocompleteService = 'SearchAutocompleteService',
36+
SearchQueryService = 'SearchQueryService',
37+
SymbolPackagePublish = 'SymbolPackagePublish',
38+
VulnerabilityInfo = 'VulnerabilityInfo'
39+
}

0 commit comments

Comments
 (0)