From c70c72f14baf37c030f582e1f7fd0132b6b3d6d2 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@arkins.net> Date: Fri, 2 Feb 2018 12:37:16 +0100 Subject: [PATCH] feat: release notes in pull requests (sourced from github releases) (#1465) Detects and embeds release notes found on GitHub when an npm dependency specifies a GitHub repository as its source and that repository has made use of the "Releases" feature. --- lib/config/templates/default/pr-body.hbs | 14 ++++ lib/config/templates/group/pr-body.hbs | 29 ++++++- lib/workers/pr/changelog.js | 6 +- lib/workers/pr/index.js | 3 + lib/workers/pr/release-notes.js | 80 +++++++++++++++++++ .../pr/__snapshots__/changelog.spec.js.snap | 3 + .../__snapshots__/release-notes.spec.js.snap | 3 + test/workers/pr/changelog.spec.js | 28 +++---- test/workers/pr/release-notes.spec.js | 16 ++++ 9 files changed, 163 insertions(+), 19 deletions(-) create mode 100644 lib/workers/pr/release-notes.js create mode 100644 test/workers/pr/__snapshots__/release-notes.spec.js.snap create mode 100644 test/workers/pr/release-notes.spec.js diff --git a/lib/config/templates/default/pr-body.hbs b/lib/config/templates/default/pr-body.hbs index df143288a6..0e9ebf2ded 100644 --- a/lib/config/templates/default/pr-body.hbs +++ b/lib/config/templates/default/pr-body.hbs @@ -12,6 +12,20 @@ This PR also includes an upgrade to the corresponding [@types/{{{depName}}}](htt {{#if isPin}} **Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open. {{/if}} +{{#if hasReleaseNotes}} +# Release Notes + +{{#each releases as |release|}} +{{#if release.releaseNotes}} +### [`v{{{release.version}}}`]({{{release.releaseNotes.url}}}) + +{{{release.releaseNotes.body}}} + +--- + +{{/if}} +{{/each}} +{{/if}} {{#if hasCommits}} # Commits diff --git a/lib/config/templates/group/pr-body.hbs b/lib/config/templates/group/pr-body.hbs index 9e057c159c..0ce0b136f4 100644 --- a/lib/config/templates/group/pr-body.hbs +++ b/lib/config/templates/group/pr-body.hbs @@ -8,7 +8,30 @@ This Pull Request renovates the package group "{{{groupName}}}". - {{#if repositoryUrl}}[{{{upgrade.depName}}}]({{upgrade.repositoryUrl}}){{else}}`{{{depName}}}`{{/if}}: from `{{{upgrade.currentVersion}}}` to `{{{upgrade.newVersion}}}` {{/each}} -{{#unless isPin}} +{{#if hasReleaseNotes}} +# Release Notes +{{#each upgrades as |upgrade|}} +{{#if upgrade.hasReleaseNotes}} +<details> +<summary>{{upgrade.githubName}}</summary> + +{{#each upgrade.releases as |release|}} +{{#if release.releaseNotes}} +### [`v{{{release.version}}}`]({{{release.releaseNotes.url}}}) + +{{{release.releaseNotes.body}}} + +--- + +{{/if}} +{{/each}} + +</details> +{{/if}} +{{/each}} +{{/if}} + +{{#if hasCommits}} # Commits {{#each upgrades as |upgrade|}} @@ -26,10 +49,10 @@ This Pull Request renovates the package group "{{{groupName}}}". {{/each}} </details> + {{/if}} {{/each}} -{{/unless}} -<br /> +{{/if}} {{#if isPin}} **Important**: Renovate will wait until you have merged this Pin request before creating PRs for any *upgrades*. If you do not wish to pin anything, please update your config accordingly instead of leaving this PR open. diff --git a/lib/workers/pr/changelog.js b/lib/workers/pr/changelog.js index 16525f1f39..2f03e9092d 100644 --- a/lib/workers/pr/changelog.js +++ b/lib/workers/pr/changelog.js @@ -1,6 +1,7 @@ const os = require('os'); const changelog = require('changelog'); const cacache = require('cacache/en'); +const { addReleaseNotes } = require('./release-notes'); module.exports = { getChangeLogJSON, @@ -19,7 +20,8 @@ async function getChangeLogJSON(depName, fromVersion, newVersion) { try { const cacheVal = await cacache.get(cachePath, cacheKey); logger.debug(`Returning cached version of ${depName}`); - return JSON.parse(cacheVal.data.toString()); + const cachedResult = JSON.parse(cacheVal.data.toString()); + return addReleaseNotes(cachedResult); } catch (err) { logger.debug('Cache miss'); } @@ -42,7 +44,7 @@ async function getChangeLogJSON(depName, fromVersion, newVersion) { }); } await cacache.put(cachePath, cacheKey, JSON.stringify(res)); - return res; + return addReleaseNotes(res); } catch (err) { logger.debug({ err }, `getChangeLogJSON error`); return null; diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js index fd4a69afcc..46648423f0 100644 --- a/lib/workers/pr/index.js +++ b/lib/workers/pr/index.js @@ -91,6 +91,7 @@ async function ensurePr(prConfig) { ); if (logJSON) { upgrade.githubName = logJSON.project.github; + upgrade.hasReleaseNotes = logJSON.hasReleaseNotes; upgrade.releases = []; if (!commitRepos.includes(upgrade.githubName)) { commitRepos.push(upgrade.githubName); @@ -132,6 +133,8 @@ async function ensurePr(prConfig) { if (config.warnings && config.warnings.length) { config.hasWarnings = true; } + config.hasReleaseNotes = config.upgrades.some(upg => upg.hasReleaseNotes); + config.hasCommits = config.upgrades.some(upg => upg.hasCommits); const prTitle = handlebars.compile(config.prTitle)(config); let prBody = handlebars.compile(config.prBody)(config); diff --git a/lib/workers/pr/release-notes.js b/lib/workers/pr/release-notes.js new file mode 100644 index 0000000000..05a599daa3 --- /dev/null +++ b/lib/workers/pr/release-notes.js @@ -0,0 +1,80 @@ +const ghGot = require('../../platform/github/gh-got-wrapper'); + +module.exports = { + getReleaseList, + massageBody, + getReleaseNotes, + addReleaseNotes, +}; + +async function getReleaseList(repository) { + logger.debug('getReleaseList()'); + try { + const res = await ghGot(`repos/${repository}/releases`); + return res.body.map(release => ({ + url: release.html_url, + id: release.id, + tag: release.tag_name, + name: release.name, + body: release.body, + })); + } catch (err) /* istanbul ignore next */ { + logger.error({ err }, 'getReleaseList error'); + return []; + } +} + +function massageBody(input = '') { + // Convert line returns + let body = input.replace(/\r\n/g, '\n'); + // semantic-release cleanup + body = body.replace(/^<a name="[^"]*"><\/a>\n/, ''); + body = body.replace( + /^##? \[[^\]]*\]\(https:\/\/github.com\/[^/]*\/[^/]*\/compare\/.*?\n/, + '' + ); + // Clean-up unnecessary commits link + body = `\n${body}\n`.replace( + /\nhttps:\/\/github.com\/[^/]+\/[^/]+\/compare\/[^\n]+(\n|$)/, + '\n' + ); + // Reduce headings size + body = body + .replace(/\n#### /g, '\n##### ') + .replace(/\n(#{1,3}) /g, '\n##$1 '); + // Trim whitespace + return body.trim(); +} + +async function getReleaseNotes(repository, version) { + 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.body = massageBody(releaseNotes.body); + if (!releaseNotes.body.length) { + releaseNotes = undefined; + } + } + }); + logger.debug({ releaseNotes }); + return releaseNotes; +} + +async function addReleaseNotes(input) { + if (!(input.project && input.project.github && input.versions)) { + logger.debug('Missing project or versions'); + return input; + } + const output = { ...input, versions: [] }; + for (const v of input.versions) { + const releaseNotes = await getReleaseNotes(input.project.github, v.version); + logger.debug({ releaseNotes }); + output.versions.push({ ...v, releaseNotes }); + output.hasReleaseNotes = !!releaseNotes; + } + logger.debug({ output }); + return output; +} diff --git a/test/workers/pr/__snapshots__/changelog.spec.js.snap b/test/workers/pr/__snapshots__/changelog.spec.js.snap index 3da083db35..268593f10c 100644 --- a/test/workers/pr/__snapshots__/changelog.spec.js.snap +++ b/test/workers/pr/__snapshots__/changelog.spec.js.snap @@ -2,6 +2,7 @@ exports[`workers/pr/changelog getChangeLogJSON sorts JSON 1`] = ` Object { + "hasReleaseNotes": false, "project": Object { "github": "chalk/chalk", "repository": "https://github.com/chalk/chalk", @@ -41,6 +42,7 @@ Object { }, ], "date": "2017-10-24T03:20:46.238Z", + "releaseNotes": undefined, "version": "2.2.2", }, Object { @@ -57,6 +59,7 @@ Object { }, ], "date": "2017-10-24T04:12:55.953Z", + "releaseNotes": undefined, "version": "2.3.0", }, ], diff --git a/test/workers/pr/__snapshots__/release-notes.spec.js.snap b/test/workers/pr/__snapshots__/release-notes.spec.js.snap new file mode 100644 index 0000000000..d9380236a6 --- /dev/null +++ b/test/workers/pr/__snapshots__/release-notes.spec.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`workers/pr/release-notes getReleaseNotes() gets release notes 1`] = `undefined`; diff --git a/test/workers/pr/changelog.spec.js b/test/workers/pr/changelog.spec.js index bd8a5dfdbc..e26b93f445 100644 --- a/test/workers/pr/changelog.spec.js +++ b/test/workers/pr/changelog.spec.js @@ -27,6 +27,20 @@ describe('workers/pr/changelog', () => { await changelogHelper.getChangeLogJSON('renovate', '1.0.0', '2.0.0') ).toMatchObject({ a: 1 }); }); + it('returns cached JSON', async () => { + changelog.generate = jest.fn(() => ({ a: 2 })); + expect( + await changelogHelper.getChangeLogJSON('renovate', '1.0.0', '2.0.0') + ).toMatchObject({ a: 1 }); + }); + it('filters unnecessary warns', async () => { + changelog.generate = jest.fn(() => { + throw new Error('Unknown Github Repo'); + }); + expect( + await changelogHelper.getChangeLogJSON('renovate', '1.0.0', '3.0.0') + ).toBe(null); + }); it('sorts JSON', async () => { changelog.generate = jest.fn(() => ({ project: { @@ -94,19 +108,5 @@ describe('workers/pr/changelog', () => { await changelogHelper.getChangeLogJSON('chalk', '2.2.2', '2.3.0') ).toMatchSnapshot(); }); - it('returns cached JSON', async () => { - changelog.generate = jest.fn(() => ({ a: 2 })); - expect( - await changelogHelper.getChangeLogJSON('renovate', '1.0.0', '2.0.0') - ).toMatchObject({ a: 1 }); - }); - it('filters unnecessary warns', async () => { - changelog.generate = jest.fn(() => { - throw new Error('Unknown Github Repo'); - }); - expect( - await changelogHelper.getChangeLogJSON('renovate', '1.0.0', '3.0.0') - ).toBe(null); - }); }); }); diff --git a/test/workers/pr/release-notes.spec.js b/test/workers/pr/release-notes.spec.js new file mode 100644 index 0000000000..59071c1a59 --- /dev/null +++ b/test/workers/pr/release-notes.spec.js @@ -0,0 +1,16 @@ +const ghGot = require('gh-got'); +const { getReleaseNotes } = require('../../../lib/workers/pr/release-notes'); + +jest.mock('gh-got'); + +describe('workers/pr/release-notes', () => { + describe('getReleaseNotes()', () => { + it('gets release notes', async () => { + ghGot.mockReturnValueOnce({ + body: [{ tag_name: 'v1.0.0' }, { tag_name: 'v1.0.1' }], + }); + const res = await getReleaseNotes('some/repository', '1.0.0'); + expect(res).toMatchSnapshot(); + }); + }); +}); -- GitLab