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

[7.x] [release-notes] add script to generate release notes from PRs (#68816) #69225

Merged
merged 1 commit into from
Jun 16, 2020
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ npm-debug.log*
# apm plugin
/x-pack/plugins/apm/tsconfig.json
apm.tsconfig.json

# release notes script output
report.csv
report.asciidoc
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@
"@kbn/expect": "1.0.0",
"@kbn/optimizer": "1.0.0",
"@kbn/plugin-generator": "1.0.0",
"@kbn/release-notes": "1.0.0",
"@kbn/test": "1.0.0",
"@kbn/utility-types": "1.0.0",
"@microsoft/api-documenter": "7.7.2",
Expand Down
23 changes: 23 additions & 0 deletions packages/kbn-release-notes/package.json
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"
}
}
162 changes: 162 additions & 0 deletions packages/kbn-release-notes/src/cli.ts
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
`,
}
);
}
84 changes: 84 additions & 0 deletions packages/kbn-release-notes/src/formats/asciidoc.ts
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`;
}
}
}
}
}
}
74 changes: 74 additions & 0 deletions packages/kbn-release-notes/src/formats/csv.ts
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
);
}
}
}
34 changes: 34 additions & 0 deletions packages/kbn-release-notes/src/formats/format.ts
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>;
}
Loading