Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: add ability to generate release notes from conventional commits #1623

Merged
merged 1 commit into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
* [chai](#chai)
* [clean-jsdoc-theme](#clean-jsdoc-theme)
* [commander](#commander)
* [conventional-changelog-conventionalcommits](#conventional-changelog-conventionalcommits)
* [conventional-changelog-writer](#conventional-changelog-writer)
* [conventional-commits-parser](#conventional-commits-parser)
* [eslint-config-prettier](#eslint-config-prettier)
* [eslint-plugin-disable](#eslint-plugin-disable)
* [eslint-plugin-header](#eslint-plugin-header)
Expand All @@ -51,6 +54,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
* [eslint-plugin-sonarjs](#eslint-plugin-sonarjs)
* [eslint](#eslint)
* [express](#express)
* [git-raw-commits](#git-raw-commits)
* [glob](#glob)
* [got](#got)
* [husky](#husky)
Expand Down Expand Up @@ -2233,6 +2237,87 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

```

### conventional-changelog-conventionalcommits

This product includes source derived from [conventional-changelog-conventionalcommits](https://github.com/conventional-changelog/conventional-changelog) ([v5.0.0](https://github.com/conventional-changelog/conventional-changelog/tree/v5.0.0)), distributed under the [ISC License](https://github.com/conventional-changelog/conventional-changelog/blob/v5.0.0/LICENSE.md):

```
### ISC License

Copyright © [conventional-changelog team](https://github.com/conventional-changelog)

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

```

### conventional-changelog-writer

This product includes source derived from [conventional-changelog-writer](https://github.com/conventional-changelog/conventional-changelog) ([v5.0.1](https://github.com/conventional-changelog/conventional-changelog/tree/v5.0.1)), distributed under the [MIT License](https://github.com/conventional-changelog/conventional-changelog/blob/v5.0.1/LICENSE.md):

```
### MIT License

Copyright © [conventional-changelog team](https://github.com/conventional-changelog)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

### conventional-commits-parser

This product includes source derived from [conventional-commits-parser](https://github.com/conventional-changelog/conventional-changelog) ([v3.2.4](https://github.com/conventional-changelog/conventional-changelog/tree/v3.2.4)), distributed under the [MIT License](https://github.com/conventional-changelog/conventional-changelog/blob/v3.2.4/LICENSE.md):

```
### MIT License

Copyright © [conventional-changelog team](https://github.com/conventional-changelog)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

### eslint-config-prettier

This product includes source derived from [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) ([v8.5.0](https://github.com/prettier/eslint-config-prettier/tree/v8.5.0)), distributed under the [MIT License](https://github.com/prettier/eslint-config-prettier/blob/v8.5.0/LICENSE):
Expand Down Expand Up @@ -2631,6 +2716,35 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

```

### git-raw-commits

This product includes source derived from [git-raw-commits](https://github.com/conventional-changelog/conventional-changelog) ([v2.0.11](https://github.com/conventional-changelog/conventional-changelog/tree/v2.0.11)), distributed under the [MIT License](https://github.com/conventional-changelog/conventional-changelog/blob/v2.0.11/LICENSE.md):

```
### MIT License

Copyright © [conventional-changelog team](https://github.com/conventional-changelog)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

```

### glob

This product includes source derived from [glob](https://github.com/isaacs/node-glob) ([v7.2.3](https://github.com/isaacs/node-glob/tree/v7.2.3)), distributed under the [ISC License](https://github.com/isaacs/node-glob/blob/v7.2.3/LICENSE):
Expand Down
255 changes: 255 additions & 0 deletions bin/conventional-changelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
* Copyright 2021 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const conventionalCommitsParser = require('conventional-commits-parser')
const conventionalChangelogWriter = require('conventional-changelog-writer')
const getChangelogConfig = require('conventional-changelog-conventionalcommits')
const gitRawCommits = require('git-raw-commits')
const path = require('node:path')
const stream = require('node:stream')
const { readFile, writeFile } = require('node:fs/promises')
const Github = require('./github')

// TODO: for reviewers: decide if we want to show all of these, or if there are some that should always be hidden
const RELEASE_NOTE_TYPES = [
{ type: 'build', section: 'Build System', rank: 12 },
{ type: 'chore', section: 'Miscellaneous Chores', rank: 8 },
{ type: 'ci', section: 'Continuous Integration', rank: 11 },
{ type: 'docs', section: 'Documentation', rank: 7 },
{ type: 'feat', section: 'Features', rank: 0 },
{ type: 'fix', section: 'Bug Fixes', rank: 1 },
{ type: 'perf', section: 'Performance Improvements', rank: 4 },
{ type: 'refactor', section: 'Code Refactoring', rank: 5 },
{ type: 'revert', section: 'Reverts', rank: 6 },
{ type: 'security', section: 'Security Improvements', rank: 2 },
{ type: 'style', section: 'Styles', rank: 9 },
{ type: 'test', section: 'Tests', rank: 10 }
]
const RELEASEABLE_PREFIXES = RELEASE_NOTE_TYPES.map((type) => type.type)
const ORDERED_TAGS = RELEASE_NOTE_TYPES.sort((a, b) => a.rank - b.rank).map((type) => type.section)

class ConventionalChangelog {
constructor({ newVersion, previousVersion, org = 'newrelic', repo = 'node-newrelic' }) {
this.org = org
this.repo = repo
this.github = new Github(this.org, this.repo)
this.newVersion = newVersion
this.previousVersion = previousVersion
}

/**
* Customized sort function for ensuring that the commit group sections are organized
* in a particular way, based on the rank property of the config in RELEASE_NOTE_TYPES
*
* @param {object} a first comparator
* @param {object} b second comparator
* @returns {number} positive / negative number, or 0
*/
rankedGroupSort(a, b) {
const rankA = ORDERED_TAGS.indexOf(a.title)
const rankB = ORDERED_TAGS.indexOf(b.title)
return rankA - rankB
}

/**
* Function for parsing conventional commit messages from the git log
* and converting into JSON structure
*
* Parsing is done with https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser
* Git entries are generated with https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/git-raw-commits
*
* @returns {object[]} the list of parsed conventional commits from the previous version
*/
async getFormattedCommits() {
const self = this
const config = await getChangelogConfig({ types: RELEASE_NOTE_TYPES })
const commits = []

return new Promise((resolve, reject) => {
const conventionalCommitsStream = gitRawCommits({
format: '%B%n-hash-%n%H',
from: `v${this.previousVersion}`
}).pipe(conventionalCommitsParser(config.parserOpts))

conventionalCommitsStream.on('data', function onData(data) {
if (RELEASEABLE_PREFIXES.includes(data.type)) {
if (data.body) {
// newlines mess with our indentation formatting, so remove them
data.body = data.body.replace(/\n/g, ' ')
}

commits.push(data)
}
})

conventionalCommitsStream.on('error', function onError(err) {
reject(err)
})

conventionalCommitsStream.on('end', async function onEnd() {
await self.addPullRequestMetadata(commits)
resolve(commits)
})
})
}

/**
* Function for adding pull request information to commits
* Pull request info comes from the Github API
*
* @param {object[]} commits list of conventional commits to update
*/
async addPullRequestMetadata(commits) {
for (const [idx, commit] of commits.entries()) {
const pullRequestInfo = await this.github.getPullRequestByCommit(commit.hash)

if (pullRequestInfo) {
commits[idx].pr = { url: pullRequestInfo.html_url, id: pullRequestInfo.number }
}
}
}

/**
* Function for generating our front-matter content in a machine readable format
*
* @param {object[]} commits list of conventional commits
* @returns {object} the entry to add to the JSON changelog
*/
generateJsonChangelog(commits) {
const securityChanges = []
const bugfixChanges = []
const featureChanges = []

commits.forEach((commit) => {
if (commit.type === 'security') {
securityChanges.push(commit.subject)
}

if (commit.type === 'fix') {
bugfixChanges.push(commit.subject)
}

if (commit.type === 'feat') {
featureChanges.push(commit.subject)
}
})

return {
version: this.newVersion,
changes: {
security: securityChanges,
bugfixes: bugfixChanges,
features: featureChanges
}
}
}

/**
* Function for generating our release notes in a human readable format
* Templating is done via Handlebars with https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-writer
* Templates were "borrowed" from https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-conventionalcommits/templates
*
* @param {object[]} commits list of conventional commits
* @returns {string} markdown formatted release notes to be added to the changelog
*/
async generateMarkdownChangelog(commits) {
const self = this
const config = await getChangelogConfig({ types: RELEASE_NOTE_TYPES })
const [mainTemplate, headerTemplate, commitTemplate] = await Promise.all([
readFile(path.resolve(__dirname, './templates/template.hbs'), 'utf-8'),
readFile(path.resolve(__dirname, './templates/header.hbs'), 'utf-8'),
readFile(path.resolve(__dirname, './templates/commit.hbs'), 'utf-8')
])

return new Promise((resolve, reject) => {
const commitsStream = new stream.Stream.Readable({
objectMode: true
})

commits.forEach((commit) => commitsStream.push(commit))
// mark the end of the stream
commitsStream.push(null)

const context = {
host: 'https://github.com',
owner: self.org,
repository: self.repo,
isPatch: false,
version: self.newVersion
}

const markdownFormatter = conventionalChangelogWriter(context, {
...config.writerOpts,
mainTemplate: mainTemplate,
headerPartial: headerTemplate,
commitPartial: commitTemplate,
commitGroupsSort: self.rankedGroupSort
})
const changelogStream = commitsStream.pipe(markdownFormatter)

let content = ''
changelogStream.on('data', function onData(buffer) {
content += buffer.toString()
})

changelogStream.on('error', function onError(err) {
reject(err)
})

changelogStream.on('end', function onEnd() {
resolve(content)
})
})
}

/**
* Function for writing update to our Markdown based changelog
* Markdown changelog is for our customers and docs-website
*
* @param {string} newEntry markdown formatted release notes to be added to the changelog
* @param {string} markdownFile path to the markdown file to update, defaults to NEWS.md
* @returns {void}
*/
async writeMarkdownChangelog(newEntry, markdownFile = '../NEWS.md') {
const filename = path.resolve(__dirname, markdownFile)
const changelog = await readFile(filename, 'utf-8')

const heading = `### v${this.newVersion}`

if (changelog.match(heading)) {
console.log('Version already exists in markdown, skipping update')
return
}

await writeFile(filename, `${newEntry}\n${changelog}`, 'utf-8')
}

/**
* Function for writing update to our JSON based changelog
* JSON changelog is for automating the generation of our agent version metadata front-matter when
* submitting a PR to docs-website after a release
*
* @param {string} newEntry markdown formatted release notes to be added to the changelog
* @param {string} jsonFile path to the markdown file to update, defaults to changelog.json
* @returns {void}
*/
async writeJsonChangelog(newEntry, jsonFile = '../changelog.json') {
const filename = path.resolve(__dirname, jsonFile)
const rawChangelog = await readFile(filename, 'utf-8')
const changelog = JSON.parse(rawChangelog)

if (changelog.entries.find((entry) => entry.version === this.newVersion)) {
console.log('Version already exists in json, skipping update')
return
}

changelog.entries.unshift(newEntry)
await writeFile(filename, JSON.stringify(changelog, null, 2), 'utf-8')
}
}

module.exports = ConventionalChangelog
Loading