diff --git a/lib/datasource/npm.js b/lib/datasource/npm.js index 36f4ff9d7b99a4c16b229b41e38c9a6e5080c9be..fc01fd8dd27fed598b53751e62b22ecbcbb303f6 100644 --- a/lib/datasource/npm.js +++ b/lib/datasource/npm.js @@ -146,7 +146,17 @@ async function getDependency(name, retries = 5) { let repositoryUrl; if (res.repository) { - repositoryUrl = parse(res.repository.url); + const extraBaseUrls = []; + // istanbul ignore if + if (process.env.GITHUB_ENDPOINT) { + const parsedEndpoint = url.parse(process.env.GITHUB_ENDPOINT); + extraBaseUrls.push( + parsedEndpoint.hostname, + `gist.${parsedEndpoint.hostname}` + ); + } + + repositoryUrl = parse(res.repository.url, { extraBaseUrls }); } if (!repositoryUrl) { repositoryUrl = res.homepage; diff --git a/lib/workers/pr/changelog/index.js b/lib/workers/pr/changelog/index.js index a52a93c4d01f969484e2d09ba1148ea51cb7c9d5..341dbd843caca9815213b1b34519ab6004a9ec61 100644 --- a/lib/workers/pr/changelog/index.js +++ b/lib/workers/pr/changelog/index.js @@ -1,3 +1,5 @@ +const url = require('url'); + const { addReleaseNotes } = require('../release-notes'); const sourceCache = require('./source-cache'); @@ -16,22 +18,77 @@ async function getChangeLogJSON(args) { if (!fromVersion || fromVersion === newVersion) { return null; } - // Return from cache if present - let res = await sourceCache.getChangeLogJSON(args); - if (res) { - return addReleaseNotes(res); - } - let pkg = null; - if (['npm', 'meteor'].includes(manager)) { - pkg = await managerNpm.getPackage(args); - } - if (manager === 'pip_requirements') { - pkg = await managerPip.getPackage(args); + let token; + let endpoint; + let gheBaseURL; + const opts = { + githubBaseURL: 'https://github.com/', + }; + + // istanbul ignore if + if (process.env.GITHUB_ENDPOINT) { + token = process.env.GITHUB_TOKEN; + endpoint = process.env.GITHUB_ENDPOINT; + const parsedEndpoint = url.parse(endpoint); + gheBaseURL = `${parsedEndpoint.protocol}//${parsedEndpoint.hostname}/`; } - res = await sourceGithub.getChangeLogJSON({ ...args, ...pkg }); + try { + // Return from cache if present + let res = await sourceCache.getChangeLogJSON(args); + if (res) { + if (res.project && res.project.githubBaseURL !== gheBaseURL) { + logger.debug('Removing GHE token before calling addReleaseNotes'); + delete process.env.GITHUB_ENDPOINT; + process.env.GITHUB_TOKEN = process.env.GITHUB_COM_TOKEN; + } + } else { + let pkg = null; + if (['npm', 'meteor'].includes(manager)) { + pkg = await managerNpm.getPackage(args); + } + + if (manager === 'pip_requirements') { + pkg = await managerPip.getPackage(args); + } - await sourceCache.setChangeLogJSON(args, res); - return addReleaseNotes(res); + if ( + pkg && + pkg.repositoryUrl && + gheBaseURL && + pkg.repositoryUrl.startsWith(gheBaseURL) + ) { + logger.debug( + 'Found package hosted on internal GHE. Preserving GHE token' + ); + opts.githubBaseURL = gheBaseURL; + } else { + logger.debug('Removing GHE token before calling getChangeLogJSON'); + delete process.env.GITHUB_ENDPOINT; + process.env.GITHUB_TOKEN = process.env.GITHUB_COM_TOKEN; + } + + res = await sourceGithub.getChangeLogJSON({ + ...args, + ...pkg, + ...opts, + }); + + await sourceCache.setChangeLogJSON(args, res); + } + + const output = await addReleaseNotes(res); + + return output; + } finally { + // wrap everything in a try/finally to ensure process.env.GITHUB_TOKEN is restore no matter if + // getChangeLogJSON and addReleaseNotes succed or fails + // istanbul ignore if + if (endpoint !== process.env.GITHUB_ENDPOINT) { + logger.debug('Restoring GHE token and endpoint'); + process.env.GITHUB_TOKEN = token; + process.env.GITHUB_ENDPOINT = endpoint; + } + } } diff --git a/lib/workers/pr/changelog/source-github.js b/lib/workers/pr/changelog/source-github.js index de39aecdea5200b47a6623a78c3a2a74b6e09f9f..6a61089eff941367e721b58c99054037988c0e74 100644 --- a/lib/workers/pr/changelog/source-github.js +++ b/lib/workers/pr/changelog/source-github.js @@ -9,10 +9,9 @@ async function getTags(repository) { try { const versions = {}; - const res = await ghGot( - `https://api.github.com/repos/${repository}/tags?per_page=100`, - { paginate: true } - ); + const res = await ghGot(`repos/${repository}/tags?per_page=100`, { + paginate: true, + }); const tags = (res && res.body) || []; @@ -47,9 +46,7 @@ async function getRepositoryHead(repository, version) { } logger.trace({ repository, version }, 'Looking for commit SHA by date'); try { - const res = await ghGot( - `https://api.github.com/repos/${repository}/commits/@{${version.date}}` - ); + const res = await ghGot(`repos/${repository}/commits/@{${version.date}}`); const commit = res && res.body; return commit && commit.sha; } catch (err) { @@ -59,6 +56,7 @@ async function getRepositoryHead(repository, version) { } async function getChangeLogJSON({ + githubBaseURL, repositoryUrl, fromVersion, newVersion, @@ -67,13 +65,13 @@ async function getChangeLogJSON({ logger.debug('Checking for github source URL manually'); const semverString = `>${fromVersion} <=${newVersion}`; logger.trace(`semverString: ${semverString}`); - if (!(repositoryUrl && repositoryUrl.startsWith('https://github.com/'))) { + if (!(repositoryUrl && repositoryUrl.startsWith(githubBaseURL))) { logger.debug('No repo found manually'); return null; } logger.debug({ url: repositoryUrl }, 'Found github URL manually'); const repository = repositoryUrl - .replace('https://github.com/', '') + .replace(githubBaseURL, '') .replace(/#.*/, ''); if (repository.split('/').length !== 2) { logger.debug('Invalid github URL found'); @@ -106,7 +104,7 @@ async function getChangeLogJSON({ const prevHead = await getHead(prev); const nextHead = await getHead(next); if (prevHead && nextHead) { - release.compare.url = `https://github.com/${repository}/compare/${prevHead}...${nextHead}`; + release.compare.url = `${githubBaseURL}${repository}/compare/${prevHead}...${nextHead}`; } releases.unshift(release); } @@ -114,6 +112,7 @@ async function getChangeLogJSON({ const res = { project: { + githubBaseURL, github: repository, repository: repositoryUrl, }, diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js index 2a01ad300755e60c8c7414072e225ec368d44ab6..36f3fedd7dbac2a9037be3ce90171a42bfe7547f 100644 --- a/lib/workers/pr/index.js +++ b/lib/workers/pr/index.js @@ -103,16 +103,6 @@ async function ensurePr(prConfig) { const commitRepos = []; - let token; - let endpoint; - // istanbul ignore if - if (process.env.GITHUB_ENDPOINT) { - logger.debug('Removing GHE token before calling changelog'); - endpoint = process.env.GITHUB_ENDPOINT; - delete process.env.GITHUB_ENDPOINT; - token = process.env.GITHUB_TOKEN; - process.env.GITHUB_TOKEN = process.env.GITHUB_COM_TOKEN; - } // Get changelog and then generate template strings for (const upgrade of upgrades) { const upgradeKey = `${upgrade.depName}-${upgrade.manager}-${ @@ -149,12 +139,6 @@ async function ensurePr(prConfig) { } config.upgrades.push(upgrade); } - // istanbul ignore if - if (endpoint) { - logger.debug('Restoring GHE token and endpoint'); - process.env.GITHUB_TOKEN = token; - process.env.GITHUB_ENDPOINT = endpoint; - } // Update the config object Object.assign(config, upgrades[0]); diff --git a/lib/workers/pr/release-notes.js b/lib/workers/pr/release-notes.js index 52c3887bda25c8d6c7c3e6b62facf53c6cd7ff55..06be88e0109077488ac27151dbf1090579e8ce3e 100644 --- a/lib/workers/pr/release-notes.js +++ b/lib/workers/pr/release-notes.js @@ -16,9 +16,7 @@ module.exports = { async function getReleaseList(repository) { logger.debug('getReleaseList()'); try { - const res = await ghGot( - `https://api.github.com/repos/${repository}/releases?per_page=100` - ); + const res = await ghGot(`repos/${repository}/releases?per_page=100`); return res.body.map(release => ({ url: release.html_url, id: release.id, @@ -32,19 +30,21 @@ async function getReleaseList(repository) { } } -function massageBody(input) { +function massageBody(input, githubBaseURL) { let body = input || ''; // Convert line returns body = body.replace(/\r\n/g, '\n'); // semantic-release cleanup body = body.replace(/^<a name="[^"]*"><\/a>\n/, ''); body = body.replace( - /^##? \[[^\]]*\]\(https:\/\/github.com\/[^/]*\/[^/]*\/compare\/.*?\n/, + new RegExp( + `^##? \\[[^\\]]*\\]\\(${githubBaseURL}[^/]*\\/[^/]*\\/compare\\/.*?\\n` + ), '' ); // Clean-up unnecessary commits link body = `\n${body}\n`.replace( - /\nhttps:\/\/github.com\/[^/]+\/[^/]+\/compare\/[^\n]+(\n|$)/, + new RegExp(`\\n${githubBaseURL}[^/]+\\/[^/]+\\/compare\\/[^\\n]+(\\n|$)`), '\n' ); // Reduce headings size @@ -56,17 +56,17 @@ function massageBody(input) { return body.trim(); } -async function getReleaseNotes(repository, version) { +async function getReleaseNotes(repository, version, githubBaseURL) { logger.debug(`getReleaseNotes(${repository}, ${version})`); const releaseList = await getReleaseList(repository); let releaseNotes; releaseList.forEach(release => { if (release.tag === version || release.tag === `v${version}`) { releaseNotes = release; - releaseNotes.url = `https://github.com/${repository}/releases/${ + releaseNotes.url = `${githubBaseURL}${repository}/releases/${ release.tag }`; - releaseNotes.body = massageBody(releaseNotes.body); + releaseNotes.body = massageBody(releaseNotes.body, githubBaseURL); if (!releaseNotes.body.length) { releaseNotes = undefined; } @@ -100,11 +100,11 @@ function sectionize(text, level) { return result; } -async function getReleaseNotesMd(repository, version) { +async function getReleaseNotesMd(repository, version, githubBaseURL) { logger.trace(`getReleaseNotes(${repository}, ${version})`); let changelogMd = ''; try { - const apiPrefix = `https://api.github.com/repos/${repository}/contents/`; + const apiPrefix = `repos/${repository}/contents/`; const filesRes = await ghGot(apiPrefix); const files = filesRes.body .map(f => f.name) @@ -141,10 +141,10 @@ async function getReleaseNotesMd(repository, version) { for (const word of title) { if (word.includes(version)) { logger.trace({ body }, 'Found release notes for v' + version); - let url = `https://github.com/${repository}/blob/master/CHANGELOG.md#`; + let url = `${githubBaseURL}${repository}/blob/master/CHANGELOG.md#`; url += title.join('-').replace(/[^A-Za-z0-9-]/g, ''); return { - body: massageBody(body), + body: massageBody(body, githubBaseURL), url, }; } @@ -168,10 +168,18 @@ async function addReleaseNotes(input) { const output = { ...input, versions: [] }; const repository = input.project.github.replace(/\.git$/, ''); for (const v of input.versions) { - let releaseNotes = await getReleaseNotesMd(repository, v.version); + let releaseNotes = await getReleaseNotesMd( + repository, + v.version, + input.project.githubBaseURL + ); if (!releaseNotes) { logger.trace('No markdown release notes found for v' + v.version); - releaseNotes = await getReleaseNotes(repository, v.version); + releaseNotes = await getReleaseNotes( + repository, + v.version, + input.project.githubBaseURL + ); } // Small hack to force display of release notes when there is a compare url if (!releaseNotes && v.compare.url) { diff --git a/test/workers/pr/__snapshots__/changelog.spec.js.snap b/test/workers/pr/__snapshots__/changelog.spec.js.snap index 62e5f122586fcdeaf602bb10b55afb8dba0a1b74..f1a994ef45ca25b28865989752da8b8abdc2a637 100644 --- a/test/workers/pr/__snapshots__/changelog.spec.js.snap +++ b/test/workers/pr/__snapshots__/changelog.spec.js.snap @@ -5,6 +5,7 @@ Object { "hasReleaseNotes": true, "project": Object { "github": "chalk/chalk", + "githubBaseURL": "https://github.com/", "repository": "https://github.com/chalk/chalk", }, "versions": Array [ @@ -57,6 +58,7 @@ Object { "hasReleaseNotes": true, "project": Object { "github": "chalk/chalk", + "githubBaseURL": "https://github.com/", "repository": "https://github.com/chalk/chalk", }, "versions": Array [ @@ -100,11 +102,61 @@ Object { } `; +exports[`workers/pr/changelog getChangeLogJSON supports github enterprise 1`] = ` +Object { + "hasReleaseNotes": true, + "project": Object { + "github": "chalk/chalk", + "githubBaseURL": "https://github-enterprise.example.com/", + "repository": "https://github-enterprise.example.com/chalk/chalk", + }, + "versions": Array [ + Object { + "changes": Array [], + "compare": Object {}, + "date": undefined, + "releaseNotes": undefined, + "version": "2.5.2", + }, + Object { + "changes": Array [], + "compare": Object {}, + "date": "2017-12-24T03:20:46.238Z", + "releaseNotes": undefined, + "version": "2.4.2", + }, + Object { + "changes": Array [], + "compare": Object { + "url": "https://github-enterprise.example.com/chalk/chalk/compare/npm_2.2.2...npm_2.3.0", + }, + "date": "2017-10-24T03:20:46.238Z", + "releaseNotes": Object { + "url": "https://github-enterprise.example.com/chalk/chalk/compare/npm_2.2.2...npm_2.3.0", + }, + "version": "2.3.0", + }, + Object { + "changes": Array [], + "compare": Object { + "url": "https://github-enterprise.example.com/chalk/chalk/compare/npm_1.0.0...npm_2.2.2", + }, + "date": undefined, + "releaseNotes": Object { + "url": "https://github-enterprise.example.com/chalk/chalk/compare/npm_1.0.0...npm_2.2.2", + }, + "version": "2.2.2", + }, + ], +} +`; + exports[`workers/pr/changelog getChangeLogJSON supports pip 1`] = ` Object { "hasReleaseNotes": false, "project": Object { "github": "chalk/chalk", + "githubBaseURL": "https://github.com/", "repository": "https://github.com/chalk/chalk", }, "versions": Array [ @@ -145,6 +197,7 @@ Object { "hasReleaseNotes": true, "project": Object { "github": "chalk/chalk", + "githubBaseURL": "https://github.com/", "repository": "https://github.com/chalk/chalk", }, "versions": Array [ @@ -197,6 +250,7 @@ Object { "hasReleaseNotes": true, "project": Object { "github": "chalk/chalk", + "githubBaseURL": "https://github.com/", "repository": "https://github.com/chalk/chalk", }, "versions": Array [ diff --git a/test/workers/pr/changelog.spec.js b/test/workers/pr/changelog.spec.js index 9f8b5d643fdd6cf953324733b08c2c344b0d1225..95091ba7015b06c3c7e78edf34d273f1f3610f2e 100644 --- a/test/workers/pr/changelog.spec.js +++ b/test/workers/pr/changelog.spec.js @@ -173,5 +173,18 @@ describe('workers/pr/changelog', () => { await getChangeLogJSON({ ...upgrade, manager: 'pip_requirements' }) ).toBe(null); }); + it('supports github enterprise', async () => { + // clear the mock + npmRegistry.getDependency.mockReset(); + const res = npmResponse(); + res.repositoryUrl = 'https://github-enterprise.example.com/chalk/chalk'; + npmRegistry.getDependency.mockReturnValueOnce(Promise.resolve(res)); + + const endpoint = process.env.GITHUB_ENDPOINT; + process.env.GITHUB_ENDPOINT = 'https://github-enterprise.example.com/'; + expect(await getChangeLogJSON({ ...upgrade })).toMatchSnapshot(); + + process.env.GITHUB_ENDPOINT = endpoint; + }); }); }); diff --git a/test/workers/pr/index.spec.js b/test/workers/pr/index.spec.js index 7e6ae6743250f9ad91ead95289c11b5a332e9308..daaab7ba23e3ec58591cca88ffe6cec5879a0f02 100644 --- a/test/workers/pr/index.spec.js +++ b/test/workers/pr/index.spec.js @@ -6,6 +6,7 @@ jest.mock('../../../lib/workers/pr/changelog'); changelogHelper.getChangeLogJSON = jest.fn(); changelogHelper.getChangeLogJSON.mockReturnValue({ project: { + githubBaseURL: 'https://github.com/', github: 'renovateapp/dummy', repository: 'https://github.com/renovateapp/dummy', }, diff --git a/test/workers/pr/release-notes.spec.js b/test/workers/pr/release-notes.spec.js index 4a4ae3beff784eecf88a3daabb05330f07dd49e1..3b2a1f05d17397e337af70abaad754c8c74ce582 100644 --- a/test/workers/pr/release-notes.spec.js +++ b/test/workers/pr/release-notes.spec.js @@ -33,20 +33,32 @@ describe('workers/pr/release-notes', () => { ghGot.mockReturnValueOnce({ body: [{ tag_name: 'v1.0.0' }, { tag_name: 'v1.0.1' }], }); - const res = await getReleaseNotes('some/repository', '1.0.0'); + const res = await getReleaseNotes( + 'some/repository', + '1.0.0', + 'https://github.com/' + ); expect(res).toMatchSnapshot(); }); }); describe('getReleaseNotesMd()', () => { it('handles not found', async () => { - const res = await getReleaseNotesMd('chalk', '2.0.0'); + const res = await getReleaseNotesMd( + 'chalk', + '2.0.0', + 'https://github.com/' + ); expect(res).toBe(null); }); it('handles files mismatch', async () => { ghGot.mockReturnValueOnce({ body: [{ name: 'lib' }, { name: 'README.md' }], }); - const res = await getReleaseNotesMd('chalk', '2.0.0'); + const res = await getReleaseNotesMd( + 'chalk', + '2.0.0', + 'https://github.com/' + ); expect(res).toBe(null); }); it('handles wrong format', async () => { @@ -57,7 +69,11 @@ describe('workers/pr/release-notes', () => { content: Buffer.from('not really markdown').toString('base64'), }, }); - const res = await getReleaseNotesMd('some/repository1', '1.0.0'); + const res = await getReleaseNotesMd( + 'some/repository1', + '1.0.0', + 'https://github.com/' + ); expect(res).toBe(null); }); it('handles bad markdown', async () => { @@ -68,7 +84,11 @@ describe('workers/pr/release-notes', () => { content: Buffer.from(`#\nha\nha\n#\nha\nha`).toString('base64'), }, }); - const res = await getReleaseNotesMd('some/repository2', '1.0.0'); + const res = await getReleaseNotesMd( + 'some/repository2', + '1.0.0', + 'https://github.com/' + ); expect(res).toBe(null); }); it('parses angular.js', async () => { @@ -79,7 +99,11 @@ describe('workers/pr/release-notes', () => { content: Buffer.from(angularJsChangelogMd).toString('base64'), }, }); - const res = await getReleaseNotesMd('angular/angular.js', '1.6.9'); + const res = await getReleaseNotesMd( + 'angular/angular.js', + '1.6.9', + 'https://github.com/' + ); expect(res).not.toBe(null); expect(res).toMatchSnapshot(); }); @@ -91,7 +115,11 @@ describe('workers/pr/release-notes', () => { content: Buffer.from(jestChangelogMd).toString('base64'), }, }); - const res = await getReleaseNotesMd('facebook/jest', '22.0.0'); + const res = await getReleaseNotesMd( + 'facebook/jest', + '22.0.0', + 'https://github.com/' + ); expect(res).not.toBe(null); expect(res).toMatchSnapshot(); }); @@ -103,7 +131,11 @@ describe('workers/pr/release-notes', () => { content: Buffer.from(jsYamlChangelogMd).toString('base64'), }, }); - const res = await getReleaseNotesMd('nodeca/js-yaml', '3.10.0'); + const res = await getReleaseNotesMd( + 'nodeca/js-yaml', + '3.10.0', + 'https://github.com/' + ); expect(res).not.toBe(null); expect(res).toMatchSnapshot(); });