diff --git a/lib/modules/manager/nuget/artifacts.spec.ts b/lib/modules/manager/nuget/artifacts.spec.ts index 4dfbbc4a04de05f9ece7e791d8185ff6adbf0a94..33a7465d63028b4973cecae107f4d10b588dcc0e 100644 --- a/lib/modules/manager/nuget/artifacts.spec.ts +++ b/lib/modules/manager/nuget/artifacts.spec.ts @@ -15,7 +15,7 @@ jest.mock('../../../util/host-rules', () => mockDeep()); jest.mock('../../../util/git'); jest.mock('./util'); -const { getDefaultRegistries } = mocked(util); +const { getDefaultRegistries, findGlobalJson } = mocked(util); process.env.CONTAINERBASE = 'true'; @@ -230,12 +230,14 @@ describe('modules/manager/nuget/artifacts', () => { fs.getLocalFiles.mockResolvedValueOnce({ 'packages.lock.json': 'New packages.lock.json', }); + + findGlobalJson.mockResolvedValueOnce({ sdk: { version: '7.0.100' } }); expect( await nuget.updateArtifacts({ packageFileName: 'project.csproj', updatedDeps: [{ depName: 'dep' }], newPackageFileContent: '{}', - config: { ...config, constraints: { dotnet: '7.0.100' } }, + config, }), ).toEqual([ { diff --git a/lib/modules/manager/nuget/artifacts.ts b/lib/modules/manager/nuget/artifacts.ts index c60e04262e6363756e1c5ede3661631983999c36..88dcdd92e89471c9986bd0b641059d87f8926720 100644 --- a/lib/modules/manager/nuget/artifacts.ts +++ b/lib/modules/manager/nuget/artifacts.ts @@ -25,7 +25,11 @@ import { NUGET_CENTRAL_FILE, getDependentPackageFiles, } from './package-tree'; -import { getConfiguredRegistries, getDefaultRegistries } from './util'; +import { + findGlobalJson, + getConfiguredRegistries, + getDefaultRegistries, +} from './util'; async function createCachedNuGetConfigFile( nugetCacheDir: string, @@ -55,6 +59,9 @@ async function runDotnetRestore( packageFileName, ); + const dotnetVersion = + config.constraints?.dotnet ?? + (await findGlobalJson(packageFileName))?.sdk?.version; const execOptions: ExecOptions = { docker: {}, userConfiguredEnv: config.env, @@ -62,9 +69,7 @@ async function runDotnetRestore( NUGET_PACKAGES: join(nugetCacheDir, 'packages'), MSBUILDDISABLENODEREUSE: '1', }, - toolConstraints: [ - { toolName: 'dotnet', constraint: config.constraints?.dotnet }, - ], + toolConstraints: [{ toolName: 'dotnet', constraint: dotnetVersion }], }; const cmds = [ diff --git a/lib/modules/manager/nuget/extract/global-manifest.ts b/lib/modules/manager/nuget/extract/global-manifest.ts index ae9c6e9f99c0427ee028650996e2b7b6527e6c9e..d7e36b7f66c8ead034b9d36808ab67a86151e4f7 100644 --- a/lib/modules/manager/nuget/extract/global-manifest.ts +++ b/lib/modules/manager/nuget/extract/global-manifest.ts @@ -2,11 +2,8 @@ import { logger } from '../../../../logger'; import { DotnetVersionDatasource } from '../../../datasource/dotnet-version'; import { NugetDatasource } from '../../../datasource/nuget'; import type { PackageDependency, PackageFileContent } from '../../types'; -import type { - MsbuildGlobalManifest, - NugetPackageDependency, - Registry, -} from '../types'; +import { GlobalJson } from '../schema'; +import type { NugetPackageDependency, Registry } from '../types'; import { applyRegistries } from '../util'; export function extractMsbuildGlobalManifest( @@ -15,10 +12,10 @@ export function extractMsbuildGlobalManifest( registries: Registry[] | undefined, ): PackageFileContent | null { const deps: PackageDependency[] = []; - let manifest: MsbuildGlobalManifest; + let manifest: GlobalJson; let extractedConstraints: Record<string, string> | undefined; try { - manifest = JSON.parse(content); + manifest = GlobalJson.parse(content); } catch { logger.debug({ packageFile }, `Invalid JSON`); return null; diff --git a/lib/modules/manager/nuget/schema.ts b/lib/modules/manager/nuget/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..c71dabc94a1fc177f3d3cec3c5d0a9f9f0238161 --- /dev/null +++ b/lib/modules/manager/nuget/schema.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { Jsonc } from '../../../util/schema-utils'; + +/** + * The roll-forward policy to use when selecting an SDK version, either as a fallback when a specific SDK version is missing or as a directive to use a later version. A version must be specified with a rollForward value, unless you're setting it to latestMajor. The default roll forward behavior is determined by the matching rules. + * + * https://learn.microsoft.com/de-de/dotnet/core/tools/global-json#rollforward + */ +const RollForwardSchema = z.enum([ + 'patch', + 'feature', + 'minor', + 'major', + 'latestPatch', + 'latestFeature', + 'latestMinor', + 'latestMajor', + 'disable', +]); +export type RollForward = z.infer<typeof RollForwardSchema>; + +/** + * global.json schema + * + * https://learn.microsoft.com/en-us/dotnet/core/tools/global-json#allowprerelease + */ +export const GlobalJsonSchema = z.object({ + /** + * Specifies information about the .NET SDK to select. + */ + sdk: z + .object({ + /** + * The version of the .NET SDK to use. + * + * https://learn.microsoft.com/de-de/dotnet/core/tools/global-json#version + */ + version: z.string().optional(), + /** + * The roll-forward policy to use when selecting an SDK version, either as a fallback when a specific SDK version is missing or as a directive to use a later version. A version must be specified with a rollForward value, unless you're setting it to latestMajor. The default roll forward behavior is determined by the matching rules. + * + * https://learn.microsoft.com/de-de/dotnet/core/tools/global-json#rollforward + */ + rollForward: RollForwardSchema.optional(), + /** + * Indicates whether the SDK resolver should consider prerelease versions when selecting the SDK version to use. + * + * https://learn.microsoft.com/de-de/dotnet/core/tools/global-json#allowprerelease + */ + allowPrerelease: z.boolean().optional(), + }) + .optional(), + + /** + * Lets you control the project SDK version in one place rather than in each individual project. For more information, see How project SDKs are resolved. + * + * https://learn.microsoft.com/de-de/dotnet/core/tools/global-json#msbuild-sdks + */ + 'msbuild-sdks': z.record(z.string()).optional(), +}); + +export const GlobalJson = Jsonc.pipe(GlobalJsonSchema); +export type GlobalJson = z.infer<typeof GlobalJson>; diff --git a/lib/modules/manager/nuget/types.ts b/lib/modules/manager/nuget/types.ts index 0eef41beaf9885b33a31cc043a45eb9bd0de8ef9..9d34b776f14fb81474a2afd292f83269318f685c 100644 --- a/lib/modules/manager/nuget/types.ts +++ b/lib/modules/manager/nuget/types.ts @@ -17,17 +17,6 @@ export interface Registry { readonly name?: string; sourceMappedPackagePatterns?: string[]; } - -export interface MsbuildGlobalManifest { - readonly sdk?: MsbuildSdk; - readonly 'msbuild-sdks'?: Record<string, string>; -} - -export interface MsbuildSdk { - readonly version: string; - readonly rollForward: string; -} - export interface ProjectFile { readonly isLeaf: boolean; readonly name: string; diff --git a/lib/modules/manager/nuget/util.spec.ts b/lib/modules/manager/nuget/util.spec.ts index 0956e2014c0383034a7c7e66bfd477d5fb34e47d..a1a01dec1040d1e1fbae3577a4a2372549f53691 100644 --- a/lib/modules/manager/nuget/util.spec.ts +++ b/lib/modules/manager/nuget/util.spec.ts @@ -3,7 +3,12 @@ import { XmlDocument } from 'xmldoc'; import { fs } from '../../../../test/util'; import type { Registry } from './types'; import { bumpPackageVersion } from './update'; -import { applyRegistries, findVersion, getConfiguredRegistries } from './util'; +import { + applyRegistries, + findGlobalJson, + findVersion, + getConfiguredRegistries, +} from './util'; jest.mock('../../../util/fs'); @@ -340,4 +345,34 @@ describe('modules/manager/nuget/util', () => { }); }); }); + + describe('findGlobalJson', () => { + it('not found', async () => { + fs.findLocalSiblingOrParent.mockResolvedValueOnce(null); + const globalJson = await findGlobalJson('project.csproj'); + expect(globalJson).toBeNull(); + }); + + it('no content', async () => { + fs.findLocalSiblingOrParent.mockResolvedValueOnce('global.json'); + const globalJson = await findGlobalJson('project.csproj'); + expect(globalJson).toBeNull(); + }); + + it('fails to parse', async () => { + fs.findLocalSiblingOrParent.mockResolvedValueOnce('global.json'); + fs.readLocalFile.mockResolvedValueOnce('{'); + const globalJson = await findGlobalJson('project.csproj'); + expect(globalJson).toBeNull(); + }); + + it('parses', async () => { + fs.findLocalSiblingOrParent.mockResolvedValueOnce('global.json'); + fs.readLocalFile.mockResolvedValueOnce( + '{ /* This is comment */ "sdk": { "version": "5.0.100" }, "some": true }', + ); + const globalJson = await findGlobalJson('project.csproj'); + expect(globalJson).toEqual({ sdk: { version: '5.0.100' } }); + }); + }); }); diff --git a/lib/modules/manager/nuget/util.ts b/lib/modules/manager/nuget/util.ts index dd210357d3a863be90624801e0a2a2cd0bd53866..3f04f4318e758bc0a7fbf94f13360555080cd245 100644 --- a/lib/modules/manager/nuget/util.ts +++ b/lib/modules/manager/nuget/util.ts @@ -2,10 +2,15 @@ import upath from 'upath'; import type { XmlElement } from 'xmldoc'; import { XmlDocument } from 'xmldoc'; import { logger } from '../../../logger'; -import { findUpLocal, readLocalFile } from '../../../util/fs'; +import { + findLocalSiblingOrParent, + findUpLocal, + readLocalFile, +} from '../../../util/fs'; import { minimatch } from '../../../util/minimatch'; import { regEx } from '../../../util/regex'; import { nugetOrg } from '../../datasource/nuget'; +import { GlobalJson } from './schema'; import type { NugetPackageDependency, Registry } from './types'; export async function readFileAsXmlDocument( @@ -207,3 +212,31 @@ function sortPatterns( return a[0].localeCompare(b[0]) * -1; } + +export async function findGlobalJson( + packageFile: string, +): Promise<GlobalJson | null> { + const globalJsonPath = await findLocalSiblingOrParent( + packageFile, + 'global.json', + ); + if (!globalJsonPath) { + return null; + } + + const content = await readLocalFile(globalJsonPath, 'utf8'); + if (!content) { + logger.debug({ packageFile, globalJsonPath }, 'Failed to read global.json'); + return null; + } + + const result = await GlobalJson.safeParseAsync(content); + if (!result.success) { + logger.debug( + { packageFile, globalJsonPath, err: result.error }, + 'Failed to parse global.json', + ); + return null; + } + return result.data; +}