-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[release-notes] add script to generate release notes from PRs (#68816)
Co-authored-by: spalger <spalger@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
- Loading branch information
1 parent
7e2ab7f
commit cc1758d
Showing
27 changed files
with
1,745 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"name": "@kbn/release-notes", | ||
"version": "1.0.0", | ||
"license": "Apache-2.0", | ||
"main": "target/index.js", | ||
"scripts": { | ||
"kbn:bootstrap": "tsc", | ||
"kbn:watch": "tsc --watch" | ||
}, | ||
"dependencies": { | ||
"@kbn/dev-utils": "1.0.0", | ||
"axios": "^0.19.2", | ||
"cheerio": "0.22.0", | ||
"dedent": "^0.7.0", | ||
"graphql": "^14.0.0", | ||
"graphql-tag": "^2.10.3", | ||
"terminal-link": "^2.1.1" | ||
}, | ||
"devDependencies": { | ||
"markdown-it": "^10.0.0", | ||
"typescript": "3.9.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import Fs from 'fs'; | ||
import Path from 'path'; | ||
import { inspect } from 'util'; | ||
|
||
import { run, createFlagError, createFailError, REPO_ROOT } from '@kbn/dev-utils'; | ||
|
||
import { FORMATS, SomeFormat } from './formats'; | ||
import { | ||
iterRelevantPullRequests, | ||
getPr, | ||
Version, | ||
ClassifiedPr, | ||
streamFromIterable, | ||
asyncPipeline, | ||
IrrelevantPrSummary, | ||
isPrRelevant, | ||
classifyPr, | ||
} from './lib'; | ||
|
||
const rootPackageJson = JSON.parse( | ||
Fs.readFileSync(Path.resolve(REPO_ROOT, 'package.json'), 'utf8') | ||
); | ||
const extensions = FORMATS.map((f) => f.extension); | ||
|
||
export function runReleaseNotesCli() { | ||
run( | ||
async ({ flags, log }) => { | ||
const token = flags.token; | ||
if (!token || typeof token !== 'string') { | ||
throw createFlagError('--token must be defined'); | ||
} | ||
|
||
const version = Version.fromFlag(flags.version); | ||
if (!version) { | ||
throw createFlagError('unable to parse --version, use format "v{major}.{minor}.{patch}"'); | ||
} | ||
|
||
const includeVersions = Version.fromFlags(flags.include || []); | ||
if (!includeVersions) { | ||
throw createFlagError('unable to parse --include, use format "v{major}.{minor}.{patch}"'); | ||
} | ||
|
||
const Formats: SomeFormat[] = []; | ||
for (const flag of Array.isArray(flags.format) ? flags.format : [flags.format]) { | ||
const Format = FORMATS.find((F) => F.extension === flag); | ||
if (!Format) { | ||
throw createFlagError(`--format must be one of "${extensions.join('", "')}"`); | ||
} | ||
Formats.push(Format); | ||
} | ||
|
||
const filename = flags.filename; | ||
if (!filename || typeof filename !== 'string') { | ||
throw createFlagError('--filename must be a string'); | ||
} | ||
|
||
if (flags['debug-pr']) { | ||
const number = parseInt(String(flags['debug-pr']), 10); | ||
if (Number.isNaN(number)) { | ||
throw createFlagError('--debug-pr must be a pr number when specified'); | ||
} | ||
|
||
const summary = new IrrelevantPrSummary(log); | ||
const pr = await getPr(token, number); | ||
log.success( | ||
inspect( | ||
{ | ||
version: version.label, | ||
includeVersions: includeVersions.map((v) => v.label), | ||
isPrRelevant: isPrRelevant(pr, version, includeVersions, summary), | ||
...classifyPr(pr, log), | ||
pr, | ||
}, | ||
{ depth: 100 } | ||
) | ||
); | ||
summary.logStats(); | ||
return; | ||
} | ||
|
||
log.info(`Loading all PRs with label [${version.label}] to build release notes...`); | ||
|
||
const summary = new IrrelevantPrSummary(log); | ||
const prsToReport: ClassifiedPr[] = []; | ||
const prIterable = iterRelevantPullRequests(token, version, log); | ||
for await (const pr of prIterable) { | ||
if (!isPrRelevant(pr, version, includeVersions, summary)) { | ||
continue; | ||
} | ||
prsToReport.push(classifyPr(pr, log)); | ||
} | ||
summary.logStats(); | ||
|
||
if (!prsToReport.length) { | ||
throw createFailError( | ||
`All PRs with label [${version.label}] were filtered out by the config. Run again with --debug for more info.` | ||
); | ||
} | ||
|
||
log.info(`Found ${prsToReport.length} prs to report on`); | ||
|
||
for (const Format of Formats) { | ||
const format = new Format(version, prsToReport, log); | ||
const outputPath = Path.resolve(`${filename}.${Format.extension}`); | ||
await asyncPipeline(streamFromIterable(format.print()), Fs.createWriteStream(outputPath)); | ||
log.success(`[${Format.extension}] report written to ${outputPath}`); | ||
} | ||
}, | ||
{ | ||
usage: `node scripts/release_notes --token {token} --version {version}`, | ||
flags: { | ||
alias: { | ||
version: 'v', | ||
include: 'i', | ||
}, | ||
string: ['token', 'version', 'format', 'filename', 'include', 'debug-pr'], | ||
default: { | ||
filename: 'report', | ||
version: rootPackageJson.version, | ||
format: extensions, | ||
}, | ||
help: ` | ||
--token (required) The Github access token to use for requests | ||
--version, -v The version to fetch PRs by, PRs with version labels prior to | ||
this one will be ignored (see --include-version) (default ${ | ||
rootPackageJson.version | ||
}) | ||
--include, -i A version that is before --version but shouldn't be considered | ||
"released" and cause PRs with a matching label to be excluded from | ||
release notes. Use this when PRs are labeled with a version that | ||
is less that --version and is expected to be released after | ||
--version, can be specified multiple times. | ||
--format Only produce a certain format, options: "${extensions.join('", "')}" | ||
--filename Output filename, defaults to "report" | ||
--debug-pr Fetch and print the details for a single PR, disabling reporting | ||
`, | ||
}, | ||
description: ` | ||
Fetch details from Github PRs for generating release notes | ||
`, | ||
} | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import dedent from 'dedent'; | ||
|
||
import { Format } from './format'; | ||
import { | ||
ASCIIDOC_SECTIONS, | ||
UNKNOWN_ASCIIDOC_SECTION, | ||
AREAS, | ||
UNKNOWN_AREA, | ||
} from '../release_notes_config'; | ||
|
||
function* lines(body: string) { | ||
for (const line of dedent(body).split('\n')) { | ||
yield `${line}\n`; | ||
} | ||
} | ||
|
||
export class AsciidocFormat extends Format { | ||
static extension = 'asciidoc'; | ||
|
||
*print() { | ||
const sortedAreas = [ | ||
...AREAS.slice().sort((a, b) => a.title.localeCompare(b.title)), | ||
UNKNOWN_AREA, | ||
]; | ||
|
||
yield* lines(` | ||
[[release-notes-${this.version.label}]] | ||
== ${this.version.label} Release Notes | ||
Also see <<breaking-changes-${this.version.major}.${this.version.minor}>>. | ||
`); | ||
|
||
for (const section of [...ASCIIDOC_SECTIONS, UNKNOWN_ASCIIDOC_SECTION]) { | ||
const prsInSection = this.prs.filter((pr) => pr.asciidocSection === section); | ||
if (!prsInSection.length) { | ||
continue; | ||
} | ||
|
||
yield '\n'; | ||
yield* lines(` | ||
[float] | ||
[[${section.id}-${this.version.label}]] | ||
=== ${section.title} | ||
`); | ||
|
||
for (const area of sortedAreas) { | ||
const prsInArea = prsInSection.filter((pr) => pr.area === area); | ||
|
||
if (!prsInArea.length) { | ||
continue; | ||
} | ||
|
||
yield `${area.title}::\n`; | ||
for (const pr of prsInArea) { | ||
const fixes = pr.fixes.length ? `[Fixes ${pr.fixes.join(', ')}] ` : ''; | ||
const strippedTitle = pr.title.replace(/^\s*\[[^\]]+\]\s*/, ''); | ||
yield `* ${fixes}${strippedTitle} {pull}${pr.number}[#${pr.number}]\n`; | ||
if (pr.note) { | ||
yield ` - ${pr.note}\n`; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import { Format } from './format'; | ||
|
||
/** | ||
* Escape a value to conform to field and header encoding defined at https://tools.ietf.org/html/rfc4180 | ||
*/ | ||
function esc(value: string | number) { | ||
if (typeof value === 'number') { | ||
return String(value); | ||
} | ||
|
||
if (!value.includes(',') && !value.includes('\n') && !value.includes('"')) { | ||
return value; | ||
} | ||
|
||
return `"${value.split('"').join('""')}"`; | ||
} | ||
|
||
function row(...fields: Array<string | number>) { | ||
return fields.map(esc).join(',') + '\r\n'; | ||
} | ||
|
||
export class CsvFormat extends Format { | ||
static extension = 'csv'; | ||
|
||
*print() { | ||
// columns | ||
yield row( | ||
'areas', | ||
'versions', | ||
'user', | ||
'title', | ||
'number', | ||
'url', | ||
'date', | ||
'fixes', | ||
'labels', | ||
'state' | ||
); | ||
|
||
for (const pr of this.prs) { | ||
yield row( | ||
pr.area.title, | ||
pr.versions.map((v) => v.label).join(', '), | ||
pr.user.name || pr.user.login, | ||
pr.title, | ||
pr.number, | ||
pr.url, | ||
pr.mergedAt, | ||
pr.fixes.join(', '), | ||
pr.labels.join(', '), | ||
pr.state | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import { ToolingLog } from '@kbn/dev-utils'; | ||
|
||
import { Version, ClassifiedPr } from '../lib'; | ||
|
||
export abstract class Format { | ||
static extension: string; | ||
|
||
constructor( | ||
protected readonly version: Version, | ||
protected readonly prs: ClassifiedPr[], | ||
protected readonly log: ToolingLog | ||
) {} | ||
|
||
abstract print(): Iterator<string>; | ||
} |
Oops, something went wrong.