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

[release-notes] add script to generate release notes from PRs #68816

Merged
merged 21 commits into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b64bce5
[release-notes] add script to generate release notes from PRs
spalger Jun 11, 2020
da031d3
ignore pr references on a new line
spalger Jun 11, 2020
56e6c3f
Merge branch 'master' of github.com:elastic/kibana into implement/rel…
spalger Jun 11, 2020
a045b72
log paths that reports were written to
spalger Jun 11, 2020
2d995a3
Merge branch 'master' of github.com:elastic/kibana into implement/rel…
spalger Jun 12, 2020
9f6ed3f
move @kbn/release-notes to dev deps
spalger Jun 12, 2020
481c291
update typescript in @kbn/release-notes
spalger Jun 12, 2020
aacec13
disallow PRs which match multiple areas/asciidocSections
spalger Jun 12, 2020
427728a
include "unknown" area/asciidoc section in asciidoc report
spalger Jun 12, 2020
0f16504
remove project reference test, doesn't work with --noEmit
spalger Jun 12, 2020
6f767eb
[kbn/dev-utils] remove rootDir compilerOption
spalger Jun 12, 2020
66b3768
Merge branch 'master' of github.com:elastic/kibana into implement/rel…
spalger Jun 12, 2020
53ca55e
use same logic to filter areas and sections
spalger Jun 12, 2020
3137f7f
Merge branch 'master' of github.com:elastic/kibana into implement/rel…
spalger Jun 12, 2020
54c326e
classify PRs earlier so warnings are interleaved in output
spalger Jun 13, 2020
05d42dc
support prs where the author has deleted their github account
spalger Jun 13, 2020
634f6e2
Merge branch 'master' of github.com:elastic/kibana into implement/rel…
spalger Jun 13, 2020
8b16a2d
Merge branch 'master' into implement/release-notes-script
elasticmachine Jun 13, 2020
7d73955
Merge branch 'master' of github.com:elastic/kibana into implement/rel…
spalger Jun 15, 2020
418b421
log a warning, but still pick the first matching area/asciidoc section
spalger Jun 15, 2020
a1d677e
Merge branch 'implement/release-notes-script' of github.com:spalger/k…
spalger Jun 15, 2020
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 @@ -307,6 +307,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
3 changes: 3 additions & 0 deletions packages/kbn-dev-utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"tsBuildInfoFile": "target/.tsbuildinfo",
"composite": true,
"outDir": "target",
"target": "ES2019",
"declaration": true,
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"
}
}
160 changes: 160 additions & 0 deletions packages/kbn-release-notes/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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,
PullRequest,
streamFromIterable,
asyncPipeline,
IrrelevantPrSummary,
isPrRelevant,
} 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, 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: PullRequest[] = [];
const prIterable = iterRelevantPullRequests(token, version);
for await (const pr of prIterable) {
if (!isPrRelevant(pr, version, includeVersions, summary, log)) {
continue;
}
prsToReport.push(pr);
}
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
`,
}
);
}
78 changes: 78 additions & 0 deletions packages/kbn-release-notes/src/formats/asciidoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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, AREAS } 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 alphabeticalAreas = AREAS.slice().sort((a, b) =>
a.printableName.localeCompare(b.printableName)
);

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) {
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 alphabeticalAreas) {
const prsInArea = prsInSection.filter((pr) => pr.area === area);

if (!prsInArea.length) {
continue;
}

yield `${area.printableName}::\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(
'area',
'versions',
'user',
'title',
'number',
'url',
'date',
'fixes',
'labels',
'state'
);

for (const pr of this.prs) {
yield row(
pr.area.printableName,
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
);
}
}
}
Loading