From 87575f49a35ddbd043d2108523198d84dcd960ee Mon Sep 17 00:00:00 2001 From: Sam Neirinck Date: Thu, 14 Jun 2018 11:15:52 +0200 Subject: [PATCH] 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 --- lib/config/definitions.js | 12 +++ lib/datasource/nuget.js | 52 ++++++++++++ lib/manager/index.js | 1 + lib/manager/nuget/extract.js | 28 +++++++ lib/manager/nuget/index.js | 9 ++ lib/manager/nuget/package.js | 55 ++++++++++++ lib/manager/nuget/update.js | 22 +++++ package.json | 1 + test/_fixtures/nuget/sample.csproj | 37 ++++++++ test/_fixtures/nuget/sample.nuspec | 41 +++++++++ test/datasource/nuget.spec.js | 44 ++++++++++ .../nuget/__snapshots__/extract.spec.js.snap | 84 +++++++++++++++++++ .../nuget/__snapshots__/package.spec.js.snap | 15 ++++ test/manager/nuget/extract.spec.js | 20 +++++ test/manager/nuget/package.spec.js | 48 +++++++++++ test/manager/nuget/update.spec.js | 29 +++++++ .../extract/__snapshots__/index.spec.js.snap | 3 + website/docs/configuration-options.md | 2 + yarn.lock | 21 +++++ 19 files changed, 524 insertions(+) create mode 100644 lib/datasource/nuget.js create mode 100644 lib/manager/nuget/extract.js create mode 100644 lib/manager/nuget/index.js create mode 100644 lib/manager/nuget/package.js create mode 100644 lib/manager/nuget/update.js create mode 100644 test/_fixtures/nuget/sample.csproj create mode 100644 test/_fixtures/nuget/sample.nuspec create mode 100644 test/datasource/nuget.spec.js create mode 100644 test/manager/nuget/__snapshots__/extract.spec.js.snap create mode 100644 test/manager/nuget/__snapshots__/package.spec.js.snap create mode 100644 test/manager/nuget/extract.spec.js create mode 100644 test/manager/nuget/package.spec.js create mode 100644 test/manager/nuget/update.spec.js diff --git a/lib/config/definitions.js b/lib/config/definitions.js index df8d53ebe0..b01f2c7d01 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -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() { diff --git a/lib/datasource/nuget.js b/lib/datasource/nuget.js new file mode 100644 index 0000000000..80fafb9036 --- /dev/null +++ b/lib/datasource/nuget.js @@ -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; + } +} diff --git a/lib/manager/index.js b/lib/manager/index.js index 236458e779..ba990736aa 100644 --- a/lib/manager/index.js +++ b/lib/manager/index.js @@ -10,6 +10,7 @@ const managerList = [ 'nvm', 'pip_requirements', 'travis', + 'nuget', ]; const managers = {}; for (const manager of managerList) { diff --git a/lib/manager/nuget/extract.js b/lib/manager/nuget/extract.js new file mode 100644 index 0000000000..5dddce0c00 --- /dev/null +++ b/lib/manager/nuget/extract.js @@ -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( + / 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; +} diff --git a/lib/manager/nuget/update.js b/lib/manager/nuget/update.js new file mode 100644 index 0000000000..7def06d743 --- /dev/null +++ b/lib/manager/nuget/update.js @@ -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; + } +} diff --git a/package.json b/package.json index 97969708fa..d2246a7a96 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/_fixtures/nuget/sample.csproj b/test/_fixtures/nuget/sample.csproj new file mode 100644 index 0000000000..d5d2473e4a --- /dev/null +++ b/test/_fixtures/nuget/sample.csproj @@ -0,0 +1,37 @@ + + + + netcoreapp1.1 + 0.1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/test/_fixtures/nuget/sample.nuspec b/test/_fixtures/nuget/sample.nuspec new file mode 100644 index 0000000000..920db03eec --- /dev/null +++ b/test/_fixtures/nuget/sample.nuspec @@ -0,0 +1,41 @@ + + + + Newtonsoft.Json + 11.0.2 + Json.NET + James Newton-King + James Newton-King + false + https://raw.github.com/JamesNK/Newtonsoft.Json/master/LICENSE.md + https://www.newtonsoft.com/json + https://www.newtonsoft.com/content/images/nugeticon.png + Json.NET is a popular high-performance JSON framework for .NET + Copyright © James Newton-King 2008 + json + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/datasource/nuget.spec.js b/test/datasource/nuget.spec.js new file mode 100644 index 0000000000..0c0097301c --- /dev/null +++ b/test/datasource/nuget.spec.js @@ -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' + ); + }); + }); +}); diff --git a/test/manager/nuget/__snapshots__/extract.spec.js.snap b/test/manager/nuget/__snapshots__/extract.spec.js.snap new file mode 100644 index 0000000000..3440eca4e7 --- /dev/null +++ b/test/manager/nuget/__snapshots__/extract.spec.js.snap @@ -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 [], +} +`; diff --git a/test/manager/nuget/__snapshots__/package.spec.js.snap b/test/manager/nuget/__snapshots__/package.spec.js.snap new file mode 100644 index 0000000000..938ba62890 --- /dev/null +++ b/test/manager/nuget/__snapshots__/package.spec.js.snap @@ -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", + }, +] +`; diff --git a/test/manager/nuget/extract.spec.js b/test/manager/nuget/extract.spec.js new file mode 100644 index 0000000000..dfd46149f6 --- /dev/null +++ b/test/manager/nuget/extract.spec.js @@ -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(); + }); + }); +}); diff --git a/test/manager/nuget/package.spec.js b/test/manager/nuget/package.spec.js new file mode 100644 index 0000000000..a430426979 --- /dev/null +++ b/test/manager/nuget/package.spec.js @@ -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([]); + }); + }); +}); diff --git a/test/manager/nuget/update.spec.js b/test/manager/nuget/update.spec.js new file mode 100644 index 0000000000..0e2eb24108 --- /dev/null +++ b/test/manager/nuget/update.spec.js @@ -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); + }); + }); +}); diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap index 9cfcebb26f..dbc6dd67a3 100644 --- a/test/workers/repository/extract/__snapshots__/index.spec.js.snap +++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap @@ -26,6 +26,9 @@ Object { "npm": Array [ Object {}, ], + "nuget": Array [ + Object {}, + ], "nvm": Array [ Object {}, ], diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md index c20d8cdfce..137e780ac6 100644 --- a/website/docs/configuration-options.md +++ b/website/docs/configuration-options.md @@ -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. diff --git a/yarn.lock b/yarn.lock index aa72a24665..4f933fb86d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"