feat: nuget support

Adds basic support for renovating C# project files. The scope is initially limited to:

- .Csproj only (no VB.NET / F#)
- SDK style csproj's only (this is the default in .net core anyway)
- Limited to nuget.org support (no custom repository support)

Closes #935, Closes #2050
This commit is contained in:
Sam Neirinck 2018-06-14 11:15:52 +02:00 committed by Rhys Arkins
parent cca41dc2fa
commit 87575f49a3
19 changed files with 524 additions and 0 deletions

View file

@ -1000,6 +1000,18 @@ const options = [
mergeable: true,
cli: false,
},
{
name: 'nuget',
description: 'Configuration object for C#/Nuget',
stage: 'package',
type: 'json',
default: {
enabled: false,
fileMatch: ['(^|/)*\\.csproj$'],
},
mergeable: true,
cli: false,
},
];
function getOptions() {

52
lib/datasource/nuget.js Normal file
View file

@ -0,0 +1,52 @@
const got = require('got');
const xmlParser = require('fast-xml-parser');
module.exports = {
getVersions,
getNuspec,
};
const map = new Map();
const headers = {};
async function getVersions(name, retries = 5) {
logger.trace(`getVersions(${name})`);
const url = `https://api.nuget.org/v3-flatcontainer/${name.toLowerCase()}/index.json`;
try {
const result = (await got(url, {
cache: process.env.RENOVATE_SKIP_CACHE ? undefined : map,
json: true,
retries,
headers,
})).body;
return result.versions;
} catch (err) {
logger.warn({ err, name }, 'nuget getVersions failures: Unknown error');
return null;
}
}
async function getNuspec(name, version, retries = 5) {
logger.trace(`getNuspec(${name} - ${version})`);
const url = `https://api.nuget.org/v3-flatcontainer/${name.toLowerCase()}/${version}/${name.toLowerCase()}.nuspec`;
try {
const result = await got(url, {
cache: process.env.RENOVATE_SKIP_CACHE ? undefined : map,
json: false,
retries,
headers,
});
const nuspec = xmlParser.parse(result.body, { ignoreAttributes: false });
return nuspec.package;
} catch (err) {
logger.warn({ err, name }, 'nuget getNuspec failures: Unknown error');
return null;
}
}

View file

@ -10,6 +10,7 @@ const managerList = [
'nvm',
'pip_requirements',
'travis',
'nuget',
];
const managers = {};
for (const manager of managerList) {

View file

@ -0,0 +1,28 @@
module.exports = {
extractDependencies,
};
function extractDependencies(content) {
logger.debug('nuget.extractDependencies()');
const deps = [];
let lineNumber = 0;
for (const line of content.split('\n')) {
const match = line.match(
/<PackageReference.*Include\s*=\s*"([^"]+)".*Version\s*=\s*"([^"]+)"/
);
if (match) {
const depName = match[1];
const currentVersion = match[2];
deps.push({
depType: 'nuget',
depName,
currentVersion,
lineNumber,
});
}
lineNumber += 1;
}
return { deps };
}

View file

@ -0,0 +1,9 @@
const { extractDependencies } = require('./extract');
const { getPackageUpdates } = require('./package');
const { updateDependency } = require('./update');
module.exports = {
extractDependencies,
getPackageUpdates,
updateDependency,
};

View file

@ -0,0 +1,55 @@
const nugetApi = require('../../datasource/nuget');
const {
semverSort,
isStable,
isGreaterThan,
getMajor,
getMinor,
isValid,
} = require('../../versioning/semver');
module.exports = {
getPackageUpdates,
};
async function getPackageUpdates(config) {
const { currentVersion, depName, lineNumber, ignoreUnstable } = config;
const upgrades = [];
logger.debug('nuget.getPackageUpdates()');
logger.trace({ config });
const versions = await nugetApi.getVersions(depName);
if (versions === undefined) {
logger.warn('No versions retrieved from nuget');
return upgrades;
}
const applicableVersions = ignoreUnstable
? versions.filter(v => isStable(v))
: versions;
const newVersion = applicableVersions.sort(semverSort).pop();
if (!isValid(currentVersion)) {
logger.debug({ newVersion }, 'Skipping non-semver current version.');
} else if (!isValid(newVersion)) {
logger.debug({ newVersion }, 'Skipping non-semver newVersion version.');
} else if (
newVersion !== undefined &&
isGreaterThan(newVersion, currentVersion)
) {
const upgrade = {};
upgrade.newVersion = newVersion;
upgrade.newVersionMajor = getMajor(newVersion);
upgrade.newVersionMinor = getMinor(newVersion);
upgrade.type =
getMajor(newVersion) > getMajor(currentVersion) ? 'major' : 'minor';
upgrade.lineNumber = lineNumber;
upgrade.changeLogFromVersion = currentVersion;
upgrade.changeLogToVersion = newVersion;
upgrades.push(upgrade);
}
return upgrades;
}

View file

@ -0,0 +1,22 @@
module.exports = {
updateDependency,
};
function updateDependency(fileContent, upgrade) {
try {
logger.debug(`nuget.updateDependency(): ${upgrade.newFrom}`);
const lines = fileContent.split('\n');
const lineToChange = lines[upgrade.lineNumber];
const regex = /(Version\s*=\s*")([^"]+)/;
const newLine = lineToChange.replace(regex, `$1${upgrade.newVersion}`);
if (newLine === lineToChange) {
logger.debug('No changes necessary');
return fileContent;
}
lines[upgrade.lineNumber] = newLine;
return lines.join('\n');
} catch (err) {
logger.info({ err }, 'Error setting new Dockerfile value');
return null;
}
}

View file

@ -67,6 +67,7 @@
"detect-indent": "5.0.0",
"email-addresses": "3.0.1",
"fast-clone": "1.5.3",
"fast-xml-parser": "3.10.0",
"fs-extra": "6.0.1",
"get-installed-path": "4.0.8",
"gh-got": "7.0.0",

View file

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<Version>0.1.0</Version>
</PropertyGroup>
<ItemGroup>
<Folder Include="Controllers\" />
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.5.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<PackageReference Include="Serilog" Version="2.4.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Literate" Version="2.1.0" />
<PackageReference Include="Stateless" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/01/nuspec.xsd">
<metadata minClientVersion="2.12">
<id>Newtonsoft.Json</id>
<version>11.0.2</version>
<title>Json.NET</title>
<authors>James Newton-King</authors>
<owners>James Newton-King</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<licenseUrl>https://raw.github.com/JamesNK/Newtonsoft.Json/master/LICENSE.md</licenseUrl>
<projectUrl>https://www.newtonsoft.com/json</projectUrl>
<iconUrl>https://www.newtonsoft.com/content/images/nugeticon.png</iconUrl>
<description>Json.NET is a popular high-performance JSON framework for .NET</description>
<copyright>Copyright © James Newton-King 2008</copyright>
<tags>json</tags>
<repository type="git" url="https://github.com/JamesNK/Newtonsoft.Json.git" />
<dependencies>
<group targetFramework=".NETFramework2.0" />
<group targetFramework=".NETFramework3.5" />
<group targetFramework=".NETFramework4.0" />
<group targetFramework=".NETFramework4.5" />
<group targetFramework=".NETPortable0.0-Profile259" />
<group targetFramework=".NETPortable0.0-Profile328" />
<group targetFramework=".NETStandard1.0">
<dependency id="Microsoft.CSharp" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="NETStandard.Library" version="1.6.1" exclude="Build,Analyzers" />
<dependency id="System.ComponentModel.TypeConverter" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Runtime.Serialization.Primitives" version="4.3.0" exclude="Build,Analyzers" />
</group>
<group targetFramework=".NETStandard1.3">
<dependency id="Microsoft.CSharp" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="NETStandard.Library" version="1.6.1" exclude="Build,Analyzers" />
<dependency id="System.ComponentModel.TypeConverter" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Runtime.Serialization.Formatters" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Runtime.Serialization.Primitives" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Xml.XmlDocument" version="4.3.0" exclude="Build,Analyzers" />
</group>
<group targetFramework=".NETStandard2.0" />
</dependencies>
</metadata>
</package>

View file

@ -0,0 +1,44 @@
const fs = require('fs');
const nuget = require('../../lib/datasource/nuget');
const got = require('got');
const withRepositoryInNuspec = fs.readFileSync(
'test/_fixtures/nuget/sample.nuspec',
'utf8'
);
jest.mock('got');
describe('api/nuget', () => {
describe('getVersions', () => {
it('returns null if errored', async () => {
got.mockReturnValueOnce({});
const nuspec = await nuget.getVersions('MyPackage');
expect(nuspec).toBe(null);
});
it('returns versions list', async () => {
got.mockReturnValueOnce({
body: { versions: ['1.0.0', '2.0.0', '2.1.0', '2.1.1-alpha'] },
});
const versions = await nuget.getVersions('MyPackage');
expect(versions).toHaveLength(4);
});
});
describe('getNuspec', () => {
it('returns null if errored', async () => {
got.mockReturnValueOnce({});
const nuspec = await nuget.getNuspec('MyPackage', '1.0.0.0');
expect(nuspec).toBe(null);
});
it('returns json-ified nuspec with attributes', async () => {
got.mockReturnValueOnce({ headers: {}, body: withRepositoryInNuspec });
const nuspec = await nuget.getNuspec('MyPackage', '1.0.0.0');
expect(nuspec.metadata.id).toBe('Newtonsoft.Json');
expect(nuspec.metadata.version).toBe('11.0.2');
expect(nuspec.metadata.repository['@_url']).toBe(
'https://github.com/JamesNK/Newtonsoft.Json.git'
);
});
});
});

View file

@ -0,0 +1,84 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/nuget/extract extractDependencies() extracts all dependencies 1`] = `
Array [
Object {
"currentVersion": "4.5.0",
"depName": "Autofac",
"depType": "nuget",
"lineNumber": 12,
},
Object {
"currentVersion": "4.1.0",
"depName": "Autofac.Extensions.DependencyInjection",
"depType": "nuget",
"lineNumber": 13,
},
Object {
"currentVersion": "1.1.2",
"depName": "Microsoft.AspNetCore.Hosting",
"depType": "nuget",
"lineNumber": 14,
},
Object {
"currentVersion": "1.1.3",
"depName": "Microsoft.AspNetCore.Mvc.Core",
"depType": "nuget",
"lineNumber": 15,
},
Object {
"currentVersion": "1.1.2",
"depName": "Microsoft.AspNetCore.Server.Kestrel",
"depType": "nuget",
"lineNumber": 16,
},
Object {
"currentVersion": "1.1.2",
"depName": "Microsoft.Extensions.Configuration.Json",
"depType": "nuget",
"lineNumber": 17,
},
Object {
"currentVersion": "1.1.2",
"depName": "Microsoft.Extensions.Logging.Debug",
"depType": "nuget",
"lineNumber": 18,
},
Object {
"currentVersion": "10.0.2",
"depName": "Newtonsoft.Json",
"depType": "nuget",
"lineNumber": 19,
},
Object {
"currentVersion": "2.4.0",
"depName": "Serilog",
"depType": "nuget",
"lineNumber": 20,
},
Object {
"currentVersion": "1.4.0",
"depName": "Serilog.Extensions.Logging",
"depType": "nuget",
"lineNumber": 21,
},
Object {
"currentVersion": "2.1.0",
"depName": "Serilog.Sinks.Literate",
"depType": "nuget",
"lineNumber": 22,
},
Object {
"currentVersion": "3.1.0",
"depName": "Stateless",
"depType": "nuget",
"lineNumber": 23,
},
]
`;
exports[`lib/manager/nuget/extract extractDependencies() returns empty for invalid csproj 1`] = `
Object {
"deps": Array [],
}
`;

View file

@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib/manager/nuget/package getPackageUpdates() returns higher version if available 1`] = `
Array [
Object {
"changeLogFromVersion": "2.3.0",
"changeLogToVersion": "2.4.0",
"lineNumber": 1337,
"newVersion": "2.4.0",
"newVersionMajor": 2,
"newVersionMinor": 4,
"type": "minor",
},
]
`;

View file

@ -0,0 +1,20 @@
const fs = require('fs');
const { extractDependencies } = require('../../../lib/manager/nuget/extract');
const sample = fs.readFileSync('test/_fixtures/nuget/sample.csproj', 'utf8');
describe('lib/manager/nuget/extract', () => {
describe('extractDependencies()', () => {
let config;
beforeEach(() => {
config = {};
});
it('returns empty for invalid csproj', () => {
expect(extractDependencies('nothing here', config)).toMatchSnapshot();
});
it('extracts all dependencies', () => {
const res = extractDependencies(sample, config).deps;
expect(res).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,48 @@
const { getPackageUpdates } = require('../../../lib/manager/nuget/package');
const nugetApi = require('../../../lib/datasource/nuget');
const defaultConfig = require('../../../lib/config/defaults').getConfig();
nugetApi.getNuspec = jest.fn();
nugetApi.getVersions = jest.fn();
describe('lib/manager/nuget/package', () => {
describe('getPackageUpdates()', () => {
let config;
beforeEach(() => {
config = {
...defaultConfig,
depName: 'some-dep',
currentVersion: '2.3.0',
lineNumber: 1337,
ignoreUnstable: true,
};
});
it('returns empty if no versions are found', async () => {
expect(await getPackageUpdates(config)).toEqual([]);
});
it('returns empty if current version is not semver', async () => {
config.currentVersion = undefined;
nugetApi.getVersions.mockReturnValueOnce(['1.0.0']);
expect(await getPackageUpdates(config)).toEqual([]);
});
it('returns empty if latest version is not semver', async () => {
nugetApi.getVersions.mockReturnValueOnce(['5.0.0.0']);
expect(await getPackageUpdates(config)).toEqual([]);
});
it('returns empty if highest version is current version', async () => {
nugetApi.getVersions.mockReturnValueOnce([
'1.0.0',
config.currentVersion,
]);
expect(await getPackageUpdates(config)).toEqual([]);
});
it('returns higher version if available', async () => {
nugetApi.getVersions.mockReturnValueOnce(['1.0.0', '2.3.1', '2.4.0']);
expect(await getPackageUpdates(config)).toMatchSnapshot();
});
it('ignores unstable version if specified', async () => {
nugetApi.getVersions.mockReturnValueOnce(['3.0.0-alpha1']);
expect(await getPackageUpdates(config)).toEqual([]);
});
});
});

View file

@ -0,0 +1,29 @@
const fs = require('fs');
const nugetUpdater = require('../../../lib/manager/nuget/update');
const csProj = fs.readFileSync('test/_fixtures/nuget/sample.csproj', 'utf8');
describe('manager/nuget/update', () => {
describe('updateDependency', () => {
it('replaces existing value', () => {
const upgrade = {
lineNumber: 13,
newVersion: '5.0.0',
};
const res = nugetUpdater.updateDependency(csProj, upgrade);
expect(res).not.toEqual(csProj);
});
it('keeps intact when same version', () => {
const upgrade = {
lineNumber: 13,
newVersion: '4.1.0',
};
const res = nugetUpdater.updateDependency(csProj, upgrade);
expect(res).toEqual(csProj);
});
it('returns null on errors', () => {
const res = nugetUpdater.updateDependency(csProj, null);
expect(res).toBe(null);
});
});
});

View file

@ -26,6 +26,9 @@ Object {
"npm": Array [
Object {},
],
"nuget": Array [
Object {},
],
"nvm": Array [
Object {},
],

View file

@ -304,6 +304,8 @@ See https://renovateapp.com/docs/deep-dives/private-modules for details on how t
See https://renovateapp.com/docs/deep-dives/private-modules for details on how this is used.
## nuget
## nvm
For settings common to all node.js version updates (e.g. travis, nvm, etc) you can use the `node` object instead.

View file

@ -1895,6 +1895,12 @@ fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
fast-xml-parser@3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.10.0.tgz#cea8fb0960ce5b43e2ae37379fbdf75ed3107c1f"
dependencies:
nimnjs "^1.3.2"
fb-watchman@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
@ -4197,6 +4203,21 @@ nice-try@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
nimn-date-parser@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/nimn-date-parser/-/nimn-date-parser-1.0.0.tgz#4ce55d1fd5ea206bbe82b76276f7b7c582139351"
nimn_schema_builder@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/nimn_schema_builder/-/nimn_schema_builder-1.1.0.tgz#b370ccf5b647d66e50b2dcfb20d0aa12468cd247"
nimnjs@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/nimnjs/-/nimnjs-1.3.2.tgz#a6a877968d87fad836375a4f616525e55079a5ba"
dependencies:
nimn-date-parser "^1.0.0"
nimn_schema_builder "^1.0.0"
nock@9.3.3:
version "9.3.3"
resolved "https://registry.yarnpkg.com/nock/-/nock-9.3.3.tgz#d9f4cd3c011afeadaf5ccf1b243f6e353781cda0"