From b64bce5909c409c698d48ac27e43d3c67e946c5f Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Jun 2020 22:47:39 -0700 Subject: [PATCH 01/13] [release-notes] add script to generate release notes from PRs --- .gitignore | 4 + package.json | 1 + packages/kbn-dev-utils/tsconfig.json | 3 + packages/kbn-release-notes/package.json | 23 ++ packages/kbn-release-notes/src/cli.ts | 161 ++++++++++ .../kbn-release-notes/src/formats/asciidoc.ts | 78 +++++ packages/kbn-release-notes/src/formats/csv.ts | 74 +++++ .../kbn-release-notes/src/formats/format.ts | 34 +++ .../kbn-release-notes/src/formats/index.ts | 25 ++ packages/kbn-release-notes/src/index.ts | 20 ++ .../src/lib/get_fix_references.test.ts | 66 ++++ .../src/lib/get_fix_references.ts | 29 ++ .../src/lib/get_note_from_description.test.ts | 79 +++++ .../src/lib/get_note_from_description.ts | 35 +++ packages/kbn-release-notes/src/lib/index.ts | 25 ++ .../src/lib/irrelevant_pr_summary.ts | 61 ++++ .../src/lib/is_pr_relevant.ts | 72 +++++ .../kbn-release-notes/src/lib/pull_request.ts | 225 ++++++++++++++ packages/kbn-release-notes/src/lib/streams.ts | 34 +++ .../kbn-release-notes/src/lib/type_helpers.ts | 20 ++ .../kbn-release-notes/src/lib/version.test.ts | 146 +++++++++ packages/kbn-release-notes/src/lib/version.ts | 123 ++++++++ .../src/release_notes_config.ts | 286 ++++++++++++++++++ packages/kbn-release-notes/tsconfig.json | 15 + packages/kbn-release-notes/yarn.lock | 1 + scripts/release_notes.js | 21 ++ yarn.lock | 39 ++- 27 files changed, 1697 insertions(+), 3 deletions(-) create mode 100644 packages/kbn-release-notes/package.json create mode 100644 packages/kbn-release-notes/src/cli.ts create mode 100644 packages/kbn-release-notes/src/formats/asciidoc.ts create mode 100644 packages/kbn-release-notes/src/formats/csv.ts create mode 100644 packages/kbn-release-notes/src/formats/format.ts create mode 100644 packages/kbn-release-notes/src/formats/index.ts create mode 100644 packages/kbn-release-notes/src/index.ts create mode 100644 packages/kbn-release-notes/src/lib/get_fix_references.test.ts create mode 100644 packages/kbn-release-notes/src/lib/get_fix_references.ts create mode 100644 packages/kbn-release-notes/src/lib/get_note_from_description.test.ts create mode 100644 packages/kbn-release-notes/src/lib/get_note_from_description.ts create mode 100644 packages/kbn-release-notes/src/lib/index.ts create mode 100644 packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts create mode 100644 packages/kbn-release-notes/src/lib/is_pr_relevant.ts create mode 100644 packages/kbn-release-notes/src/lib/pull_request.ts create mode 100644 packages/kbn-release-notes/src/lib/streams.ts create mode 100644 packages/kbn-release-notes/src/lib/type_helpers.ts create mode 100644 packages/kbn-release-notes/src/lib/version.test.ts create mode 100644 packages/kbn-release-notes/src/lib/version.ts create mode 100644 packages/kbn-release-notes/src/release_notes_config.ts create mode 100644 packages/kbn-release-notes/tsconfig.json create mode 120000 packages/kbn-release-notes/yarn.lock create mode 100644 scripts/release_notes.js diff --git a/.gitignore b/.gitignore index b8adcf4508db20..d786c681694d23 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ npm-debug.log* # apm plugin /x-pack/plugins/apm/tsconfig.json apm.tsconfig.json + +# release notes script output +report.csv +report.asciidoc diff --git a/package.json b/package.json index d5f738fad0400a..ba36bbffb31d4d 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", + "@kbn/release-notes": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 0ec058eeb8a280..616d9452dd8619 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "rootDir": "./src", + "tsBuildInfoFile": "target/.tsbuildinfo", + "composite": true, "outDir": "target", "target": "ES2019", "declaration": true, diff --git a/packages/kbn-release-notes/package.json b/packages/kbn-release-notes/package.json new file mode 100644 index 00000000000000..0e84b5124610d3 --- /dev/null +++ b/packages/kbn-release-notes/package.json @@ -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.7.2" + } +} \ No newline at end of file diff --git a/packages/kbn-release-notes/src/cli.ts b/packages/kbn-release-notes/src/cli.ts new file mode 100644 index 00000000000000..ecc50811092854 --- /dev/null +++ b/packages/kbn-release-notes/src/cli.ts @@ -0,0 +1,161 @@ +/* + * 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.success(`Found ${prsToReport.length} prs to report on`); + + for (const Format of Formats) { + const format = new Format(version, prsToReport, log); + await asyncPipeline( + streamFromIterable(format.print()), + Fs.createWriteStream(Path.resolve(`${filename}.${Format.extension}`)) + ); + } + }, + { + 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 + `, + } + ); +} diff --git a/packages/kbn-release-notes/src/formats/asciidoc.ts b/packages/kbn-release-notes/src/formats/asciidoc.ts new file mode 100644 index 00000000000000..21d698d7f222f3 --- /dev/null +++ b/packages/kbn-release-notes/src/formats/asciidoc.ts @@ -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 <>. + `); + + 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`; + } + } + } + } + } +} diff --git a/packages/kbn-release-notes/src/formats/csv.ts b/packages/kbn-release-notes/src/formats/csv.ts new file mode 100644 index 00000000000000..2db3fa64b38eb5 --- /dev/null +++ b/packages/kbn-release-notes/src/formats/csv.ts @@ -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) { + 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 + ); + } + } +} diff --git a/packages/kbn-release-notes/src/formats/format.ts b/packages/kbn-release-notes/src/formats/format.ts new file mode 100644 index 00000000000000..c1511ceea8a061 --- /dev/null +++ b/packages/kbn-release-notes/src/formats/format.ts @@ -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 { PullRequest, Version } from '../lib'; + +export abstract class Format { + static extension: string; + + constructor( + protected readonly version: Version, + protected readonly prs: PullRequest[], + protected readonly log: ToolingLog + ) {} + + abstract print(): Iterator; +} diff --git a/packages/kbn-release-notes/src/formats/index.ts b/packages/kbn-release-notes/src/formats/index.ts new file mode 100644 index 00000000000000..3403e445a84ac7 --- /dev/null +++ b/packages/kbn-release-notes/src/formats/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { ArrayItem } from '../lib'; +import { AsciidocFormat } from './asciidoc'; +import { CsvFormat } from './csv'; + +export const FORMATS = [CsvFormat, AsciidocFormat] as const; +export type SomeFormat = ArrayItem; diff --git a/packages/kbn-release-notes/src/index.ts b/packages/kbn-release-notes/src/index.ts new file mode 100644 index 00000000000000..a05bc698bde174 --- /dev/null +++ b/packages/kbn-release-notes/src/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export * from './cli'; diff --git a/packages/kbn-release-notes/src/lib/get_fix_references.test.ts b/packages/kbn-release-notes/src/lib/get_fix_references.test.ts new file mode 100644 index 00000000000000..a549dfbd370178 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/get_fix_references.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { getFixReferences } from './get_fix_references'; + +it('returns all fixed issue mentions in the PR text', () => { + expect( + getFixReferences(` + clOses #1 + closes: #2 + clOse #3 + close: #4 + clOsed #5 + closed: #6 + fiX #7 + fix: #8 + fiXes #9 + fixes: #10 + fiXed #11 + fixed: #12 + reSolve #13 + resolve: #14 + reSolves #15 + resolves: #16 + reSolved #17 + resolved: #18 + `) + ).toMatchInlineSnapshot(` + Array [ + "#1", + "#2", + "#3", + "#4", + "#5", + "#6", + "#7", + "#8", + "#9", + "#10", + "#11", + "#12", + "#13", + "#14", + "#15", + "#16", + "#17", + "#18", + ] + `); +}); diff --git a/packages/kbn-release-notes/src/lib/get_fix_references.ts b/packages/kbn-release-notes/src/lib/get_fix_references.ts new file mode 100644 index 00000000000000..3cab99c3c67da3 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/get_fix_references.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +const FIXES_RE = /(?:closes|close|closed|fix|fixes|fixed|resolve|resolves|resolved)[\s:]*(#\d*)/gi; + +export function getFixReferences(prText: string) { + const fixes: string[] = []; + let match; + while ((match = FIXES_RE.exec(prText))) { + fixes.push(match[1]); + } + return fixes; +} diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts b/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts new file mode 100644 index 00000000000000..23dcb302f090d2 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts @@ -0,0 +1,79 @@ +/* + * 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 MarkdownIt from 'markdown-it'; +import dedent from 'dedent'; + +import { getNoteFromDescription } from './get_note_from_description'; + +it('extracts expected components from html', () => { + const mk = new MarkdownIt(); + + expect( + getNoteFromDescription( + mk.render(dedent` + My PR description + + Fixes: #1234 + + ## Release Note: + + Checkout this feature + `) + ) + ).toMatchInlineSnapshot(`"Checkout this feature"`); + + expect( + getNoteFromDescription( + mk.render(dedent` + My PR description + + Fixes: #1234 + + #### Release Note: + + We fixed an issue + `) + ) + ).toMatchInlineSnapshot(`"We fixed an issue"`); + + expect( + getNoteFromDescription( + mk.render(dedent` + My PR description + + Fixes: #1234 + + Release note: Checkout feature foo + `) + ) + ).toMatchInlineSnapshot(`"Checkout feature foo"`); + + expect( + getNoteFromDescription( + mk.render(dedent` + # Summary + + My PR description + + release note : bar + `) + ) + ).toMatchInlineSnapshot(`"bar"`); +}); diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.ts b/packages/kbn-release-notes/src/lib/get_note_from_description.ts new file mode 100644 index 00000000000000..57df203470a5a4 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/get_note_from_description.ts @@ -0,0 +1,35 @@ +/* + * 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 cheerio from 'cheerio'; + +export function getNoteFromDescription(descriptionHtml: string) { + const $ = cheerio.load(descriptionHtml); + for (const el of $('p,h1,h2,h3,h4,h5').toArray()) { + const text = $(el).text(); + const match = text.match(/^(\s*release note(?:s)?\s*:?\s*)/i); + + if (!match) { + continue; + } + + const note = text.replace(match[1], '').trim(); + return note || $(el).next().text().trim(); + } +} diff --git a/packages/kbn-release-notes/src/lib/index.ts b/packages/kbn-release-notes/src/lib/index.ts new file mode 100644 index 00000000000000..186e01c69050d9 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export * from './pull_request'; +export * from './version'; +export * from './is_pr_relevant'; +export * from './streams'; +export * from './type_helpers'; +export * from './irrelevant_pr_summary'; diff --git a/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts b/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts new file mode 100644 index 00000000000000..1a458a04c7740d --- /dev/null +++ b/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts @@ -0,0 +1,61 @@ +/* + * 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 { PullRequest } from './pull_request'; +import { Version } from './version'; + +export class IrrelevantPrSummary { + private readonly stats = { + 'skipped by label': new Map(), + 'skipped by label regexp': new Map(), + 'skipped by version': new Map(), + }; + + constructor(private readonly log: ToolingLog) {} + + skippedByLabel(pr: PullRequest, label: string) { + this.log.debug(`${pr.terminalLink} skipped, label [${label}] is ignored`); + this.increment('skipped by label', label); + } + + skippedByLabelRegExp(pr: PullRequest, regexp: RegExp, label: string) { + this.log.debug(`${pr.terminalLink} skipped, label [${label}] matches regexp [${regexp}]`); + this.increment('skipped by label regexp', `${regexp}`); + } + + skippedByVersion(pr: PullRequest, earliestVersion: Version) { + this.log.debug(`${pr.terminalLink} skipped, earliest version is [${earliestVersion.label}]`); + this.increment('skipped by version', earliestVersion.label); + } + + private increment(stat: keyof IrrelevantPrSummary['stats'], key: string) { + const n = this.stats[stat].get(key) || 0; + this.stats[stat].set(key, n + 1); + } + + logStats() { + for (const [description, stats] of Object.entries(this.stats)) { + for (const [key, count] of stats) { + this.log.warning(`${count} ${count === 1 ? 'pr was' : 'prs were'} ${description} [${key}]`); + } + } + } +} diff --git a/packages/kbn-release-notes/src/lib/is_pr_relevant.ts b/packages/kbn-release-notes/src/lib/is_pr_relevant.ts new file mode 100644 index 00000000000000..40934f3fe98a10 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/is_pr_relevant.ts @@ -0,0 +1,72 @@ +/* + * 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 } from './version'; +import { PullRequest } from './pull_request'; +import { IGNORE_LABELS, UNKNOWN_AREA, UNKNOWN_ASCIIDOC_SECTION } from '../release_notes_config'; +import { IrrelevantPrSummary } from './irrelevant_pr_summary'; + +export function isPrRelevant( + pr: PullRequest, + version: Version, + includeVersions: Version[], + summary: IrrelevantPrSummary, + log: ToolingLog +) { + for (const label of IGNORE_LABELS) { + if (typeof label === 'string') { + if (pr.labels.includes(label)) { + summary.skippedByLabel(pr, label); + return false; + } + } + + if (label instanceof RegExp) { + const matching = pr.labels.find((l) => label.test(l)); + if (matching) { + summary.skippedByLabelRegExp(pr, label, matching); + return false; + } + } + } + + const [earliestVersion] = Version.sort( + // filter out `includeVersions` so that they won't be considered the "earliest version", only + // versions which are actually before the current `version` or the `version` itself are eligible + pr.versions.filter((v) => !includeVersions.includes(v)), + 'asc' + ); + + if (version !== earliestVersion) { + summary.skippedByVersion(pr, earliestVersion); + return false; + } + + const labels = pr.labels.join(', '); + if (pr.area === UNKNOWN_AREA) { + log.error(`${pr.terminalLink} can't be mapped to an area, labels: [${labels}]`); + } + if (pr.asciidocSection === UNKNOWN_ASCIIDOC_SECTION) { + log.error(`${pr.terminalLink} can't be mapped to an asciidoc section, labels: [${labels}]`); + } + + return true; +} diff --git a/packages/kbn-release-notes/src/lib/pull_request.ts b/packages/kbn-release-notes/src/lib/pull_request.ts new file mode 100644 index 00000000000000..c8630ba68f3256 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/pull_request.ts @@ -0,0 +1,225 @@ +/* + * 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 { inspect } from 'util'; + +import Axios from 'axios'; +import gql from 'graphql-tag'; +import * as GraphqlPrinter from 'graphql/language/printer'; +import { ASTNode } from 'graphql/language/ast'; +import terminalLink from 'terminal-link'; + +import { Version } from './version'; +import { getFixReferences } from './get_fix_references'; +import { + Area, + AsciidocSection, + AREAS, + UNKNOWN_AREA, + ASCIIDOC_SECTIONS, + UNKNOWN_ASCIIDOC_SECTION, +} from '../release_notes_config'; +import { getNoteFromDescription } from './get_note_from_description'; + +const PrNodeFragment = gql` + fragment PrNode on PullRequest { + number + url + title + bodyText + bodyHTML + mergedAt + baseRefName + state + author { + login + ... on User { + name + } + } + labels(first: 100) { + nodes { + name + } + } + } +`; + +export interface PullRequest { + number: number; + url: string; + title: string; + targetBranch: string; + mergedAt: string; + state: string; + labels: string[]; + fixes: string[]; + user: { + name: string; + login: string; + }; + area: Area; + asciidocSection: AsciidocSection; + versions: Version[]; + terminalLink: string; + note?: string; +} + +/** + * Find an AREA or ASCIIDOC_SECTION by checking the passed labels + */ +function findByLabels }>( + types: readonly T[], + labels: string[] +) { + return types.find((a) => + a.labels.some((test: string | RegExp) => + typeof test === 'string' ? labels.includes(test) : labels.some((l) => l.match(test)) + ) + ); +} + +/** + * Send a single request to the Github v4 GraphQL API + */ +async function gqlRequest(token: string, query: ASTNode, variables: Record = {}) { + const resp = await Axios.request({ + url: 'https://api.github.com/graphql', + method: 'POST', + headers: { + 'user-agent': '@kbn/release-notes', + authorization: `bearer ${token}`, + }, + data: { + query: GraphqlPrinter.print(query), + variables, + }, + }); + + return resp.data; +} + +/** + * Convert the Github API response into the structure used by this tool + * + * @param node A GraphQL response from Github using the PrNode fragment + */ +function parsePullRequestNode(node: any): PullRequest { + const labels: string[] = node.labels.nodes.map((l: { name: string }) => l.name); + + return { + number: node.number, + url: node.url, + terminalLink: terminalLink(`#${node.number}`, node.url), + title: node.title, + targetBranch: node.baseRefName, + state: node.state, + mergedAt: node.mergedAt, + labels, + fixes: getFixReferences(node.bodyText), + user: { + login: node.author.login, + name: node.author.name, + }, + area: findByLabels(AREAS, labels) || UNKNOWN_AREA, + asciidocSection: findByLabels(ASCIIDOC_SECTIONS, labels) || UNKNOWN_ASCIIDOC_SECTION, + versions: labels + .map((l) => Version.fromLabel(l)) + .filter((v): v is Version => v instanceof Version), + note: getNoteFromDescription(node.bodyHTML), + }; +} + +/** + * Iterate all of the PRs which have the `version` label + */ +export async function* iterRelevantPullRequests(token: string, version: Version) { + let nextCursor: string | undefined; + let hasNextPage = true; + + while (hasNextPage) { + const resp = await gqlRequest( + token, + gql` + query($cursor: String, $labels: [String!]) { + repository(owner: "elastic", name: "kibana") { + pullRequests(first: 100, after: $cursor, labels: $labels, states: MERGED) { + pageInfo { + hasNextPage + endCursor + } + nodes { + ...PrNode + } + } + } + } + ${PrNodeFragment} + `, + { + cursor: nextCursor, + labels: [version.label], + } + ); + + const pullRequests = resp.data?.repository?.pullRequests; + if (!pullRequests) { + throw new Error(`unexpected github response, unable to fetch PRs: ${inspect(resp)}`); + } + + hasNextPage = pullRequests.pageInfo?.hasNextPage; + nextCursor = pullRequests.pageInfo?.endCursor; + + if (hasNextPage === undefined || (hasNextPage && !nextCursor)) { + throw new Error( + `github response does not include valid pagination information: ${inspect(resp)}` + ); + } + + for (const node of pullRequests.nodes) { + yield parsePullRequestNode(node); + } + } +} + +export async function getPr(token: string, number: number) { + const resp = await gqlRequest( + token, + gql` + query($number: Int!) { + repository(owner: "elastic", name: "kibana") { + pullRequest(number: $number) { + ...PrNode + } + } + } + ${PrNodeFragment} + `, + { + number, + } + ); + + const node = resp.data?.repository?.pullRequest; + if (!node) { + throw new Error(`unexpected github response, unable to fetch PR: ${inspect(resp)}`); + } + + return parsePullRequestNode(node); +} diff --git a/packages/kbn-release-notes/src/lib/streams.ts b/packages/kbn-release-notes/src/lib/streams.ts new file mode 100644 index 00000000000000..f8cb9ec39186ad --- /dev/null +++ b/packages/kbn-release-notes/src/lib/streams.ts @@ -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 { promisify } from 'util'; +import { Readable, pipeline } from 'stream'; + +/** + * @types/node still doesn't have this method that was added + * in 10.17.0 https://nodejs.org/api/stream.html#stream_stream_readable_from_iterable_options + */ +export function streamFromIterable( + iter: Iterable | AsyncIterable +): Readable { + // @ts-ignore + return Readable.from(iter); +} + +export const asyncPipeline = promisify(pipeline); diff --git a/packages/kbn-release-notes/src/lib/type_helpers.ts b/packages/kbn-release-notes/src/lib/type_helpers.ts new file mode 100644 index 00000000000000..c9402b3584951b --- /dev/null +++ b/packages/kbn-release-notes/src/lib/type_helpers.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export type ArrayItem = T extends ReadonlyArray ? X : never; diff --git a/packages/kbn-release-notes/src/lib/version.test.ts b/packages/kbn-release-notes/src/lib/version.test.ts new file mode 100644 index 00000000000000..afef2618656977 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/version.test.ts @@ -0,0 +1,146 @@ +/* + * 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 { Version } from './version'; + +it('parses version labels, returns null on failure', () => { + expect(Version.fromLabel('v1.0.2')).toMatchInlineSnapshot(` + Version { + "label": "v1.0.2", + "major": 1, + "minor": 0, + "patch": 2, + "tag": undefined, + "tagNum": undefined, + "tagOrder": Infinity, + } + `); + expect(Version.fromLabel('v1.0.0')).toMatchInlineSnapshot(` + Version { + "label": "v1.0.0", + "major": 1, + "minor": 0, + "patch": 0, + "tag": undefined, + "tagNum": undefined, + "tagOrder": Infinity, + } + `); + expect(Version.fromLabel('v9.0.2')).toMatchInlineSnapshot(` + Version { + "label": "v9.0.2", + "major": 9, + "minor": 0, + "patch": 2, + "tag": undefined, + "tagNum": undefined, + "tagOrder": Infinity, + } + `); + expect(Version.fromLabel('v9.0.2-alpha0')).toMatchInlineSnapshot(` + Version { + "label": "v9.0.2-alpha0", + "major": 9, + "minor": 0, + "patch": 2, + "tag": "alpha", + "tagNum": 0, + "tagOrder": 1, + } + `); + expect(Version.fromLabel('v9.0.2-beta1')).toMatchInlineSnapshot(` + Version { + "label": "v9.0.2-beta1", + "major": 9, + "minor": 0, + "patch": 2, + "tag": "beta", + "tagNum": 1, + "tagOrder": 2, + } + `); + expect(Version.fromLabel('v9.0')).toMatchInlineSnapshot(`undefined`); + expect(Version.fromLabel('some:area')).toMatchInlineSnapshot(`undefined`); +}); + +it('sorts versions in ascending order', () => { + const versions = [ + 'v1.7.3', + 'v1.7.0', + 'v1.5.0', + 'v2.7.0', + 'v7.0.0-beta2', + 'v7.0.0-alpha1', + 'v2.0.0', + 'v0.0.0', + 'v7.0.0-beta1', + 'v7.0.0', + ].map((l) => Version.fromLabel(l)!); + + const sorted = Version.sort(versions); + + expect(sorted.map((v) => v.label)).toMatchInlineSnapshot(` + Array [ + "v0.0.0", + "v1.5.0", + "v1.7.0", + "v1.7.3", + "v2.0.0", + "v2.7.0", + "v7.0.0-alpha1", + "v7.0.0-beta1", + "v7.0.0-beta2", + "v7.0.0", + ] + `); + + // ensure versions was not mutated + expect(sorted).not.toEqual(versions); +}); + +it('sorts versions in decending order', () => { + const versions = [ + 'v1.7.3', + 'v1.7.0', + 'v1.5.0', + 'v7.0.0-beta1', + 'v2.7.0', + 'v2.0.0', + 'v0.0.0', + 'v7.0.0', + ].map((l) => Version.fromLabel(l)!); + + const sorted = Version.sort(versions, 'desc'); + + expect(sorted.map((v) => v.label)).toMatchInlineSnapshot(` + Array [ + "v7.0.0", + "v7.0.0-beta1", + "v2.7.0", + "v2.0.0", + "v1.7.3", + "v1.7.0", + "v1.5.0", + "v0.0.0", + ] + `); + + // ensure versions was not mutated + expect(sorted).not.toEqual(versions); +}); diff --git a/packages/kbn-release-notes/src/lib/version.ts b/packages/kbn-release-notes/src/lib/version.ts new file mode 100644 index 00000000000000..e0a5c5e177c82a --- /dev/null +++ b/packages/kbn-release-notes/src/lib/version.ts @@ -0,0 +1,123 @@ +/* + * 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. + */ + +const LABEL_RE = /^v(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta)(\d+))?$/; + +const versionCache = new Map(); + +const multiCompare = (...diffs: number[]) => { + for (const diff of diffs) { + if (diff !== 0) { + return diff; + } + } + return 0; +}; + +export class Version { + static fromFlag(flag: string | string[] | boolean | undefined) { + if (typeof flag !== 'string') { + return; + } + + return Version.fromLabel(flag) || Version.fromLabel(`v${flag}`); + } + + static fromFlags(flag: string | string[] | boolean | undefined) { + const flags = Array.isArray(flag) ? flag : [flag]; + const versions: Version[] = []; + + for (const f of flags) { + const version = Version.fromFlag(f); + if (!version) { + return; + } + versions.push(version); + } + + return versions; + } + + static fromLabel(label: string) { + const match = label.match(LABEL_RE); + if (!match) { + return; + } + + const cached = versionCache.get(label); + if (cached) { + return cached; + } + + const [, major, minor, patch, tag, tagNum] = match; + const version = new Version( + parseInt(major, 10), + parseInt(minor, 10), + parseInt(patch, 10), + tag as 'alpha' | 'beta' | undefined, + tagNum ? parseInt(tagNum, 10) : undefined + ); + + versionCache.set(label, version); + return version; + } + + static sort(versions: Version[], dir: 'asc' | 'desc' = 'asc') { + const order = dir === 'asc' ? 1 : -1; + + return versions.slice().sort((a, b) => a.compare(b) * order); + } + + public readonly label = `v${this.major}.${this.minor}.${this.patch}${ + this.tag ? `-${this.tag}${this.tagNum}` : '' + }`; + private readonly tagOrder: number; + + constructor( + public readonly major: number, + public readonly minor: number, + public readonly patch: number, + public readonly tag: 'alpha' | 'beta' | undefined, + public readonly tagNum: number | undefined + ) { + switch (tag) { + case undefined: + this.tagOrder = Infinity; + break; + case 'alpha': + this.tagOrder = 1; + break; + case 'beta': + this.tagOrder = 2; + break; + default: + throw new Error('unexpected tag'); + } + } + + compare(other: Version) { + return multiCompare( + this.major - other.major, + this.minor - other.minor, + this.patch - other.patch, + this.tagOrder - other.tagOrder, + (this.tagNum ?? 0) - (other.tagNum ?? 0) + ); + } +} diff --git a/packages/kbn-release-notes/src/release_notes_config.ts b/packages/kbn-release-notes/src/release_notes_config.ts new file mode 100644 index 00000000000000..b0628a406d8a96 --- /dev/null +++ b/packages/kbn-release-notes/src/release_notes_config.ts @@ -0,0 +1,286 @@ +/* + * 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 { ArrayItem } from './lib'; + +/** + * Exclude any PR from release notes that has a matching label. String + * labels must match exactly, for more complicated use a RegExp + */ +export const IGNORE_LABELS: Array = [ + 'Team:Docs', + ':KibanaApp/fix-it-week', + 'reverted', + /^test/, + 'non-issue', + 'jenkins', + 'build', + 'chore', + 'backport', + 'release_note:skip', + 'release_note:dev_docs', +]; + +export type Area = ArrayItem | typeof UNKNOWN_AREA; +export type AsciidocSection = ArrayItem | typeof UNKNOWN_ASCIIDOC_SECTION; + +/** + * Define areas that are used to categorize changes in the release notes + * based on the labels a PR has. the `labels` array can contain strings, which + * are matched exactly, or regular expressions. The first area, in definition + * order, which has a `label` which matches and label on a PR is the area + * assigned to that PR. + */ +export const AREAS = [ + { + printableName: 'Design', + labels: ['Team:Design', 'Project:Accessibility'], + }, + { + printableName: 'Logstash', + labels: ['App:Logstash', 'Feature:Logstash Pipelines'], + }, + { + printableName: 'Management', + labels: [ + 'Feature:license', + 'Feature:Console', + 'Feature:Search Profiler', + 'Feature:watcher', + 'Feature:Index Patterns', + 'Feature:Kibana Management', + 'Feature:Dev Tools', + 'Feature:Inspector', + 'Feature:Index Management', + 'Feature:Snapshot and Restore', + 'Team:Elasticsearch UI', + 'Feature:FieldFormatters', + 'Feature:CCR', + 'Feature:ILM', + 'Feature:Transforms', + ], + }, + { + printableName: 'Monitoring', + labels: ['Team:Monitoring', 'Feature:Telemetry', 'Feature:Stack Monitoring'], + }, + { + printableName: 'Operations', + labels: ['Team:Operations', 'Feature:License'], + }, + { + printableName: 'Kibana UI', + labels: ['Kibana UI', 'Team:Core UI', 'Feature:Header'], + }, + { + printableName: 'Platform', + labels: [ + 'Team:Platform', + 'Feature:Plugins', + 'Feature:New Platform', + 'Project:i18n', + 'Feature:ExpressionLanguage', + 'Feature:Saved Objects', + 'Team:Stack Services', + 'Feature:NP Migration', + 'Feature:Task Manager', + 'Team:Pulse', + ], + }, + { + printableName: 'Machine Learning', + labels: [ + ':ml', + 'Feature:Anomaly Detection', + 'Feature:Data Frames', + 'Feature:File Data Viz', + 'Feature:ml-results', + 'Feature:Data Frame Analytics', + ], + }, + { + printableName: 'Maps', + labels: ['Team:Geo'], + }, + { + printableName: 'Canvas', + labels: ['Team:Canvas'], + }, + { + printableName: 'QA', + labels: ['Team:QA'], + }, + { + printableName: 'Security', + labels: [ + 'Team:Security', + 'Feature:Security/Spaces', + 'Feature:users and roles', + 'Feature:Security/Authentication', + 'Feature:Security/Authorization', + 'Feature:Security/Feature Controls', + ], + }, + { + printableName: 'Dashboard', + labels: ['Feature:Dashboard', 'Feature:Drilldowns'], + }, + { + printableName: 'Discover', + labels: ['Feature:Discover'], + }, + { + printableName: 'Kibana Home & Add Data', + labels: ['Feature:Add Data', 'Feature:Home'], + }, + { + printableName: 'Querying & Filtering', + labels: [ + 'Feature:Query Bar', + 'Feature:Courier', + 'Feature:Filters', + 'Feature:Timepicker', + 'Feature:Highlight', + 'Feature:KQL', + 'Feature:Rollups', + ], + }, + { + printableName: 'Reporting', + labels: ['Feature:Reporting', 'Team:Reporting Services'], + }, + { + printableName: 'Sharing', + labels: ['Feature:Embedding', 'Feature:SharingURLs'], + }, + { + printableName: 'Visualizations', + labels: [ + 'Feature:Timelion', + 'Feature:TSVB', + 'Feature:Coordinate Map', + 'Feature:Region Map', + 'Feature:Vega', + 'Feature:Gauge Vis', + 'Feature:Tagcloud', + 'Feature:Vis Loader', + 'Feature:Vislib', + 'Feature:Vis Editor', + 'Feature:Aggregations', + 'Feature:Input Control', + 'Feature:Visualizations', + 'Feature:Markdown', + 'Feature:Data Table', + 'Feature:Heatmap', + 'Feature:Pie Chart', + 'Feature:XYAxis', + 'Feature:Graph', + 'Feature:New Feature', + 'Feature:MetricVis', + ], + }, + { + printableName: 'SIEM', + labels: ['Team:SIEM'], + }, + { + printableName: 'Code', + labels: ['Team:Code'], + }, + { + printableName: 'Infrastructure', + labels: ['App:Infrastructure', 'Feature:Infra UI', 'Feature:Service Maps'], + }, + { + printableName: 'Logs', + labels: ['App:Logs', 'Feature:Logs UI'], + }, + { + printableName: 'Uptime', + labels: ['App:Uptime', 'Feature:Uptime', 'Team:uptime'], + }, + { + printableName: 'Beats Management', + labels: ['App:Beats', 'Feature:beats-cm', 'Team:Beats'], + }, + { + printableName: 'APM', + labels: ['Team:apm', /^apm[:\-]/], + }, + { + printableName: 'Lens', + labels: ['App:Lens', 'Feature:Lens'], + }, + { + printableName: 'Alerting', + labels: ['App:Alerting', 'Feature:Alerting', 'Team:Alerting Services', 'Feature:Actions'], + }, + { + printableName: 'Metrics', + labels: ['App:Metrics', 'Feature:Metrics UI', 'Team:logs-metrics-ui'], + }, + { + printableName: 'Data ingest', + labels: ['Ingest', 'Feature:Ingest Node Pipelines'], + }, +] as const; + +export const UNKNOWN_AREA = { + printableName: 'Unknown', + labels: [], +} as const; + +/** + * Define the sections that will be assigned to PRs when generating the + * asciidoc formatted report. The order of the sections determines the + * order they will be rendered in the report + */ +export const ASCIIDOC_SECTIONS = [ + { + id: 'enhancement', + title: 'Enhancements', + labels: ['release_note:enhancement'], + }, + { + id: 'bug', + title: 'Bug fixes', + labels: ['release_note:fix'], + }, + { + id: 'roadmap', + title: 'Roadmap', + labels: ['release_note:roadmap'], + }, + { + id: 'deprecation', + title: 'Deprecations', + labels: ['release_note:deprecation'], + }, + { + id: 'breaking', + title: 'Breaking Changes', + labels: ['release_note:breaking'], + }, +] as const; + +export const UNKNOWN_ASCIIDOC_SECTION = { + id: 'unknown', + title: 'Unknown', + labels: [], +} as const; diff --git a/packages/kbn-release-notes/tsconfig.json b/packages/kbn-release-notes/tsconfig.json new file mode 100644 index 00000000000000..9d541f86a1fae9 --- /dev/null +++ b/packages/kbn-release-notes/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "declaration": true, + "sourceMap": true, + "target": "ES2019" + }, + "include": [ + "src/**/*" + ], + "references": [ + { "path": "../kbn-dev-utils/tsconfig.json" } + ] +} diff --git a/packages/kbn-release-notes/yarn.lock b/packages/kbn-release-notes/yarn.lock new file mode 120000 index 00000000000000..3f82ebc9cdbae3 --- /dev/null +++ b/packages/kbn-release-notes/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/scripts/release_notes.js b/scripts/release_notes.js new file mode 100644 index 00000000000000..f46ee5823d70d1 --- /dev/null +++ b/scripts/release_notes.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +require('../src/setup_node_env/prebuilt_dev_only_entry'); +require('@kbn/release-notes').runReleaseNotesCli(); diff --git a/yarn.lock b/yarn.lock index df16cd891ea6d1..fc250ab6e04c89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4515,9 +4515,9 @@ "@types/node" "*" "@types/node@*", "@types/node@8.10.54", "@types/node@>=10.17.17 <10.20.0", "@types/node@>=8.9.0", "@types/node@^12.0.2": - version "10.17.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" - integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== + version "10.17.26" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.26.tgz#a8a119960bff16b823be4c617da028570779bcfd" + integrity sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw== "@types/nodemailer@^6.2.1": version "6.2.1" @@ -15360,6 +15360,11 @@ graphql-tag@2.10.1, graphql-tag@^2.9.2: resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== +graphql-tag@^2.10.3: + version "2.10.3" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" + integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA== + graphql-toolkit@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/graphql-toolkit/-/graphql-toolkit-0.2.0.tgz#91364b69911d51bc915269a37963f4ea2d5f335c" @@ -15406,6 +15411,13 @@ graphql@^0.13.2: dependencies: iterall "^1.2.1" +graphql@^14.0.0: + version "14.6.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49" + integrity sha512-VKzfvHEKybTKjQVpTFrA5yUq2S9ihcZvfJAtsDBBCuV6wauPu1xl/f9ehgVf0FcEJJs4vz6ysb/ZMkGigQZseg== + dependencies: + iterall "^1.2.2" + graphviz@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/graphviz/-/graphviz-0.0.8.tgz#e599e40733ef80e1653bfe89a5f031ecf2aa4aaa" @@ -17950,6 +17962,11 @@ iterall@^1.1.3, iterall@^1.2.1: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== +iterall@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" + integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== + jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -28481,6 +28498,14 @@ supports-hyperlinks@^1.0.1: has-flag "^2.0.0" supports-color "^5.0.0" +supports-hyperlinks@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" + integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + suricata-sid-db@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/suricata-sid-db/-/suricata-sid-db-1.0.2.tgz#96ceda4db117a9f1282c8f9d785285e5ccf342b1" @@ -28821,6 +28846,14 @@ term-size@^2.1.0: resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== +terminal-link@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + terser-webpack-plugin@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" From da031d3aa9ec07d83c7155e3dc7ae7ee5d66bb38 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Jun 2020 23:32:10 -0700 Subject: [PATCH 02/13] ignore pr references on a new line --- packages/kbn-release-notes/src/lib/get_fix_references.test.ts | 2 ++ packages/kbn-release-notes/src/lib/get_fix_references.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kbn-release-notes/src/lib/get_fix_references.test.ts b/packages/kbn-release-notes/src/lib/get_fix_references.test.ts index a549dfbd370178..bdac66f6cc02f0 100644 --- a/packages/kbn-release-notes/src/lib/get_fix_references.test.ts +++ b/packages/kbn-release-notes/src/lib/get_fix_references.test.ts @@ -40,6 +40,8 @@ it('returns all fixed issue mentions in the PR text', () => { resolves: #16 reSolved #17 resolved: #18 + fixed + #19 `) ).toMatchInlineSnapshot(` Array [ diff --git a/packages/kbn-release-notes/src/lib/get_fix_references.ts b/packages/kbn-release-notes/src/lib/get_fix_references.ts index 3cab99c3c67da3..f45994e90ae899 100644 --- a/packages/kbn-release-notes/src/lib/get_fix_references.ts +++ b/packages/kbn-release-notes/src/lib/get_fix_references.ts @@ -17,7 +17,7 @@ * under the License. */ -const FIXES_RE = /(?:closes|close|closed|fix|fixes|fixed|resolve|resolves|resolved)[\s:]*(#\d*)/gi; +const FIXES_RE = /(?:closes|close|closed|fix|fixes|fixed|resolve|resolves|resolved)[ :]*(#\d*)/gi; export function getFixReferences(prText: string) { const fixes: string[] = []; From a045b72397f9ae6991a887d206cc3baef4d625b0 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Jun 2020 23:43:08 -0700 Subject: [PATCH 03/13] log paths that reports were written to --- packages/kbn-release-notes/src/cli.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/kbn-release-notes/src/cli.ts b/packages/kbn-release-notes/src/cli.ts index ecc50811092854..ee42e18eda9e50 100644 --- a/packages/kbn-release-notes/src/cli.ts +++ b/packages/kbn-release-notes/src/cli.ts @@ -114,14 +114,13 @@ export function runReleaseNotesCli() { ); } - log.success(`Found ${prsToReport.length} prs to report on`); + log.info(`Found ${prsToReport.length} prs to report on`); for (const Format of Formats) { const format = new Format(version, prsToReport, log); - await asyncPipeline( - streamFromIterable(format.print()), - Fs.createWriteStream(Path.resolve(`${filename}.${Format.extension}`)) - ); + const outputPath = Path.resolve(`${filename}.${Format.extension}`); + await asyncPipeline(streamFromIterable(format.print()), Fs.createWriteStream(outputPath)); + log.success(`[${Format.extension}] report written to ${outputPath}`); } }, { From 9f6ed3fb0691f1c807afb86b7d72f52887642175 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 12:58:48 -0700 Subject: [PATCH 04/13] move @kbn/release-notes to dev deps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8eb43a702e7e52..2365d94e0e6150 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,6 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", - "@kbn/release-notes": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", @@ -308,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", From 481c29128231a83ab2c5d52fe5e5d08928f71dfb Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 13:12:37 -0700 Subject: [PATCH 05/13] update typescript in @kbn/release-notes --- packages/kbn-release-notes/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-release-notes/package.json b/packages/kbn-release-notes/package.json index 0e84b5124610d3..25e1816b6cc1ed 100644 --- a/packages/kbn-release-notes/package.json +++ b/packages/kbn-release-notes/package.json @@ -18,6 +18,6 @@ }, "devDependencies": { "markdown-it": "^10.0.0", - "typescript": "3.7.2" + "typescript": "3.9.5" } } \ No newline at end of file From aacec1323cc18b7879b0de9b82f60e64f2d2ae41 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 14:45:17 -0700 Subject: [PATCH 06/13] disallow PRs which match multiple areas/asciidocSections --- packages/kbn-release-notes/src/cli.ts | 11 +-- .../kbn-release-notes/src/formats/asciidoc.ts | 6 +- packages/kbn-release-notes/src/formats/csv.ts | 4 +- .../kbn-release-notes/src/formats/format.ts | 4 +- .../kbn-release-notes/src/lib/classify_pr.ts | 72 +++++++++++++++++++ packages/kbn-release-notes/src/lib/index.ts | 1 + .../src/lib/is_pr_relevant.ts | 15 +--- .../kbn-release-notes/src/lib/pull_request.ts | 35 ++------- .../src/release_notes_config.ts | 62 ++++++++-------- 9 files changed, 125 insertions(+), 85 deletions(-) create mode 100644 packages/kbn-release-notes/src/lib/classify_pr.ts diff --git a/packages/kbn-release-notes/src/cli.ts b/packages/kbn-release-notes/src/cli.ts index ee42e18eda9e50..0e7b681fd9f15e 100644 --- a/packages/kbn-release-notes/src/cli.ts +++ b/packages/kbn-release-notes/src/cli.ts @@ -33,6 +33,7 @@ import { asyncPipeline, IrrelevantPrSummary, isPrRelevant, + classifyPr, } from './lib'; const rootPackageJson = JSON.parse( @@ -85,7 +86,8 @@ export function runReleaseNotesCli() { { version: version.label, includeVersions: includeVersions.map((v) => v.label), - isPrRelevant: isPrRelevant(pr, version, includeVersions, summary, log), + isPrRelevant: isPrRelevant(pr, version, includeVersions, summary), + ...classifyPr(pr, log), pr, }, { depth: 100 } @@ -99,9 +101,9 @@ export function runReleaseNotesCli() { const summary = new IrrelevantPrSummary(log); const prsToReport: PullRequest[] = []; - const prIterable = iterRelevantPullRequests(token, version); + const prIterable = iterRelevantPullRequests(token, version, log); for await (const pr of prIterable) { - if (!isPrRelevant(pr, version, includeVersions, summary, log)) { + if (!isPrRelevant(pr, version, includeVersions, summary)) { continue; } prsToReport.push(pr); @@ -115,9 +117,10 @@ export function runReleaseNotesCli() { } log.info(`Found ${prsToReport.length} prs to report on`); + const classifiedPrs = prsToReport.map((pr) => classifyPr(pr, log)); for (const Format of Formats) { - const format = new Format(version, prsToReport, log); + const format = new Format(version, classifiedPrs, log); const outputPath = Path.resolve(`${filename}.${Format.extension}`); await asyncPipeline(streamFromIterable(format.print()), Fs.createWriteStream(outputPath)); log.success(`[${Format.extension}] report written to ${outputPath}`); diff --git a/packages/kbn-release-notes/src/formats/asciidoc.ts b/packages/kbn-release-notes/src/formats/asciidoc.ts index 21d698d7f222f3..44537d1194df6a 100644 --- a/packages/kbn-release-notes/src/formats/asciidoc.ts +++ b/packages/kbn-release-notes/src/formats/asciidoc.ts @@ -32,9 +32,7 @@ export class AsciidocFormat extends Format { static extension = 'asciidoc'; *print() { - const alphabeticalAreas = AREAS.slice().sort((a, b) => - a.printableName.localeCompare(b.printableName) - ); + const alphabeticalAreas = AREAS.slice().sort((a, b) => a.title.localeCompare(b.title)); yield* lines(` [[release-notes-${this.version.label}]] @@ -63,7 +61,7 @@ export class AsciidocFormat extends Format { continue; } - yield `${area.printableName}::\n`; + 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*/, ''); diff --git a/packages/kbn-release-notes/src/formats/csv.ts b/packages/kbn-release-notes/src/formats/csv.ts index 2db3fa64b38eb5..0cf99edada696e 100644 --- a/packages/kbn-release-notes/src/formats/csv.ts +++ b/packages/kbn-release-notes/src/formats/csv.ts @@ -44,7 +44,7 @@ export class CsvFormat extends Format { *print() { // columns yield row( - 'area', + 'areas', 'versions', 'user', 'title', @@ -58,7 +58,7 @@ export class CsvFormat extends Format { for (const pr of this.prs) { yield row( - pr.area.printableName, + pr.area.title, pr.versions.map((v) => v.label).join(', '), pr.user.name || pr.user.login, pr.title, diff --git a/packages/kbn-release-notes/src/formats/format.ts b/packages/kbn-release-notes/src/formats/format.ts index c1511ceea8a061..41b769ab05de77 100644 --- a/packages/kbn-release-notes/src/formats/format.ts +++ b/packages/kbn-release-notes/src/formats/format.ts @@ -19,14 +19,14 @@ import { ToolingLog } from '@kbn/dev-utils'; -import { PullRequest, Version } from '../lib'; +import { Version, ClassifiedPr } from '../lib'; export abstract class Format { static extension: string; constructor( protected readonly version: Version, - protected readonly prs: PullRequest[], + protected readonly prs: ClassifiedPr[], protected readonly log: ToolingLog ) {} diff --git a/packages/kbn-release-notes/src/lib/classify_pr.ts b/packages/kbn-release-notes/src/lib/classify_pr.ts new file mode 100644 index 00000000000000..28d0b3c6cc759c --- /dev/null +++ b/packages/kbn-release-notes/src/lib/classify_pr.ts @@ -0,0 +1,72 @@ +/* + * 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 { + Area, + AREAS, + UNKNOWN_AREA, + AsciidocSection, + ASCIIDOC_SECTIONS, + UNKNOWN_ASCIIDOC_SECTION, +} from '../release_notes_config'; +import { PullRequest } from './pull_request'; + +export interface ClassifiedPr extends PullRequest { + area: Area; + asciidocSection: AsciidocSection; +} + +export function classifyPr(pr: PullRequest, log: ToolingLog): ClassifiedPr { + const areas = AREAS.filter((a) => + a.labels.some((test: string | RegExp) => + typeof test === 'string' ? pr.labels.includes(test) : pr.labels.some((l) => l.match(test)) + ) + ); + + const asciidocSections = ASCIIDOC_SECTIONS.filter((a) => + a.labels.some((test: string | RegExp) => + typeof test === 'string' ? pr.labels.includes(test) : pr.labels.some((l) => l.match(test)) + ) + ); + + const pickOne = (name: string, options: T[]) => { + if (options.length > 1) { + const matches = options.map((o) => o.title).join(', '); + log.error( + `[${pr.terminalLink}] unable to determine ${name} because mulitple match [${matches}]` + ); + return; + } + + if (options.length === 0) { + log.error(`[${pr.terminalLink}] unable to determine ${name} because none match`); + return; + } + + return options[0]; + }; + + return { + ...pr, + area: pickOne('area', areas) || UNKNOWN_AREA, + asciidocSection: pickOne('asciidoc section', asciidocSections) || UNKNOWN_ASCIIDOC_SECTION, + }; +} diff --git a/packages/kbn-release-notes/src/lib/index.ts b/packages/kbn-release-notes/src/lib/index.ts index 186e01c69050d9..00d8f49cf763fa 100644 --- a/packages/kbn-release-notes/src/lib/index.ts +++ b/packages/kbn-release-notes/src/lib/index.ts @@ -23,3 +23,4 @@ export * from './is_pr_relevant'; export * from './streams'; export * from './type_helpers'; export * from './irrelevant_pr_summary'; +export * from './classify_pr'; diff --git a/packages/kbn-release-notes/src/lib/is_pr_relevant.ts b/packages/kbn-release-notes/src/lib/is_pr_relevant.ts index 40934f3fe98a10..af2ef9440dedeb 100644 --- a/packages/kbn-release-notes/src/lib/is_pr_relevant.ts +++ b/packages/kbn-release-notes/src/lib/is_pr_relevant.ts @@ -17,19 +17,16 @@ * under the License. */ -import { ToolingLog } from '@kbn/dev-utils'; - import { Version } from './version'; import { PullRequest } from './pull_request'; -import { IGNORE_LABELS, UNKNOWN_AREA, UNKNOWN_ASCIIDOC_SECTION } from '../release_notes_config'; +import { IGNORE_LABELS } from '../release_notes_config'; import { IrrelevantPrSummary } from './irrelevant_pr_summary'; export function isPrRelevant( pr: PullRequest, version: Version, includeVersions: Version[], - summary: IrrelevantPrSummary, - log: ToolingLog + summary: IrrelevantPrSummary ) { for (const label of IGNORE_LABELS) { if (typeof label === 'string') { @@ -60,13 +57,5 @@ export function isPrRelevant( return false; } - const labels = pr.labels.join(', '); - if (pr.area === UNKNOWN_AREA) { - log.error(`${pr.terminalLink} can't be mapped to an area, labels: [${labels}]`); - } - if (pr.asciidocSection === UNKNOWN_ASCIIDOC_SECTION) { - log.error(`${pr.terminalLink} can't be mapped to an asciidoc section, labels: [${labels}]`); - } - return true; } diff --git a/packages/kbn-release-notes/src/lib/pull_request.ts b/packages/kbn-release-notes/src/lib/pull_request.ts index c8630ba68f3256..e7bc03c4c16d9d 100644 --- a/packages/kbn-release-notes/src/lib/pull_request.ts +++ b/packages/kbn-release-notes/src/lib/pull_request.ts @@ -23,18 +23,11 @@ import Axios from 'axios'; import gql from 'graphql-tag'; import * as GraphqlPrinter from 'graphql/language/printer'; import { ASTNode } from 'graphql/language/ast'; -import terminalLink from 'terminal-link'; +import makeTerminalLink from 'terminal-link'; +import { ToolingLog } from '@kbn/dev-utils'; import { Version } from './version'; import { getFixReferences } from './get_fix_references'; -import { - Area, - AsciidocSection, - AREAS, - UNKNOWN_AREA, - ASCIIDOC_SECTIONS, - UNKNOWN_ASCIIDOC_SECTION, -} from '../release_notes_config'; import { getNoteFromDescription } from './get_note_from_description'; const PrNodeFragment = gql` @@ -74,27 +67,11 @@ export interface PullRequest { name: string; login: string; }; - area: Area; - asciidocSection: AsciidocSection; versions: Version[]; terminalLink: string; note?: string; } -/** - * Find an AREA or ASCIIDOC_SECTION by checking the passed labels - */ -function findByLabels }>( - types: readonly T[], - labels: string[] -) { - return types.find((a) => - a.labels.some((test: string | RegExp) => - typeof test === 'string' ? labels.includes(test) : labels.some((l) => l.match(test)) - ) - ); -} - /** * Send a single request to the Github v4 GraphQL API */ @@ -121,12 +98,14 @@ async function gqlRequest(token: string, query: ASTNode, variables: Record l.name); return { number: node.number, url: node.url, - terminalLink: terminalLink(`#${node.number}`, node.url), + terminalLink, title: node.title, targetBranch: node.baseRefName, state: node.state, @@ -137,8 +116,6 @@ function parsePullRequestNode(node: any): PullRequest { login: node.author.login, name: node.author.name, }, - area: findByLabels(AREAS, labels) || UNKNOWN_AREA, - asciidocSection: findByLabels(ASCIIDOC_SECTIONS, labels) || UNKNOWN_ASCIIDOC_SECTION, versions: labels .map((l) => Version.fromLabel(l)) .filter((v): v is Version => v instanceof Version), @@ -149,7 +126,7 @@ function parsePullRequestNode(node: any): PullRequest { /** * Iterate all of the PRs which have the `version` label */ -export async function* iterRelevantPullRequests(token: string, version: Version) { +export async function* iterRelevantPullRequests(token: string, version: Version, log: ToolingLog) { let nextCursor: string | undefined; let hasNextPage = true; diff --git a/packages/kbn-release-notes/src/release_notes_config.ts b/packages/kbn-release-notes/src/release_notes_config.ts index b0628a406d8a96..671c8ffcee98a3 100644 --- a/packages/kbn-release-notes/src/release_notes_config.ts +++ b/packages/kbn-release-notes/src/release_notes_config.ts @@ -49,15 +49,15 @@ export type AsciidocSection = ArrayItem | typeof UNKNO */ export const AREAS = [ { - printableName: 'Design', + title: 'Design', labels: ['Team:Design', 'Project:Accessibility'], }, { - printableName: 'Logstash', + title: 'Logstash', labels: ['App:Logstash', 'Feature:Logstash Pipelines'], }, { - printableName: 'Management', + title: 'Management', labels: [ 'Feature:license', 'Feature:Console', @@ -77,19 +77,19 @@ export const AREAS = [ ], }, { - printableName: 'Monitoring', + title: 'Monitoring', labels: ['Team:Monitoring', 'Feature:Telemetry', 'Feature:Stack Monitoring'], }, { - printableName: 'Operations', + title: 'Operations', labels: ['Team:Operations', 'Feature:License'], }, { - printableName: 'Kibana UI', + title: 'Kibana UI', labels: ['Kibana UI', 'Team:Core UI', 'Feature:Header'], }, { - printableName: 'Platform', + title: 'Platform', labels: [ 'Team:Platform', 'Feature:Plugins', @@ -104,7 +104,7 @@ export const AREAS = [ ], }, { - printableName: 'Machine Learning', + title: 'Machine Learning', labels: [ ':ml', 'Feature:Anomaly Detection', @@ -115,19 +115,19 @@ export const AREAS = [ ], }, { - printableName: 'Maps', + title: 'Maps', labels: ['Team:Geo'], }, { - printableName: 'Canvas', + title: 'Canvas', labels: ['Team:Canvas'], }, { - printableName: 'QA', + title: 'QA', labels: ['Team:QA'], }, { - printableName: 'Security', + title: 'Security', labels: [ 'Team:Security', 'Feature:Security/Spaces', @@ -138,19 +138,19 @@ export const AREAS = [ ], }, { - printableName: 'Dashboard', + title: 'Dashboard', labels: ['Feature:Dashboard', 'Feature:Drilldowns'], }, { - printableName: 'Discover', + title: 'Discover', labels: ['Feature:Discover'], }, { - printableName: 'Kibana Home & Add Data', + title: 'Kibana Home & Add Data', labels: ['Feature:Add Data', 'Feature:Home'], }, { - printableName: 'Querying & Filtering', + title: 'Querying & Filtering', labels: [ 'Feature:Query Bar', 'Feature:Courier', @@ -162,15 +162,15 @@ export const AREAS = [ ], }, { - printableName: 'Reporting', + title: 'Reporting', labels: ['Feature:Reporting', 'Team:Reporting Services'], }, { - printableName: 'Sharing', + title: 'Sharing', labels: ['Feature:Embedding', 'Feature:SharingURLs'], }, { - printableName: 'Visualizations', + title: 'Visualizations', labels: [ 'Feature:Timelion', 'Feature:TSVB', @@ -196,53 +196,53 @@ export const AREAS = [ ], }, { - printableName: 'SIEM', + title: 'SIEM', labels: ['Team:SIEM'], }, { - printableName: 'Code', + title: 'Code', labels: ['Team:Code'], }, { - printableName: 'Infrastructure', + title: 'Infrastructure', labels: ['App:Infrastructure', 'Feature:Infra UI', 'Feature:Service Maps'], }, { - printableName: 'Logs', + title: 'Logs', labels: ['App:Logs', 'Feature:Logs UI'], }, { - printableName: 'Uptime', + title: 'Uptime', labels: ['App:Uptime', 'Feature:Uptime', 'Team:uptime'], }, { - printableName: 'Beats Management', + title: 'Beats Management', labels: ['App:Beats', 'Feature:beats-cm', 'Team:Beats'], }, { - printableName: 'APM', + title: 'APM', labels: ['Team:apm', /^apm[:\-]/], }, { - printableName: 'Lens', + title: 'Lens', labels: ['App:Lens', 'Feature:Lens'], }, { - printableName: 'Alerting', + title: 'Alerting', labels: ['App:Alerting', 'Feature:Alerting', 'Team:Alerting Services', 'Feature:Actions'], }, { - printableName: 'Metrics', + title: 'Metrics', labels: ['App:Metrics', 'Feature:Metrics UI', 'Team:logs-metrics-ui'], }, { - printableName: 'Data ingest', + title: 'Data ingest', labels: ['Ingest', 'Feature:Ingest Node Pipelines'], }, ] as const; export const UNKNOWN_AREA = { - printableName: 'Unknown', + title: 'Unknown', labels: [], } as const; From 427728a13c831a98729bd75bf9a70d551fa7130a Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 14:48:20 -0700 Subject: [PATCH 07/13] include "unknown" area/asciidoc section in asciidoc report --- .../kbn-release-notes/src/formats/asciidoc.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/kbn-release-notes/src/formats/asciidoc.ts b/packages/kbn-release-notes/src/formats/asciidoc.ts index 44537d1194df6a..d6c707f009f323 100644 --- a/packages/kbn-release-notes/src/formats/asciidoc.ts +++ b/packages/kbn-release-notes/src/formats/asciidoc.ts @@ -20,7 +20,12 @@ import dedent from 'dedent'; import { Format } from './format'; -import { ASCIIDOC_SECTIONS, AREAS } from '../release_notes_config'; +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')) { @@ -32,7 +37,10 @@ export class AsciidocFormat extends Format { static extension = 'asciidoc'; *print() { - const alphabeticalAreas = AREAS.slice().sort((a, b) => a.title.localeCompare(b.title)); + const sortedAreas = [ + ...AREAS.slice().sort((a, b) => a.title.localeCompare(b.title)), + UNKNOWN_AREA, + ]; yield* lines(` [[release-notes-${this.version.label}]] @@ -41,7 +49,7 @@ export class AsciidocFormat extends Format { Also see <>. `); - for (const section of ASCIIDOC_SECTIONS) { + for (const section of [...ASCIIDOC_SECTIONS, UNKNOWN_ASCIIDOC_SECTION]) { const prsInSection = this.prs.filter((pr) => pr.asciidocSection === section); if (!prsInSection.length) { continue; @@ -54,7 +62,7 @@ export class AsciidocFormat extends Format { === ${section.title} `); - for (const area of alphabeticalAreas) { + for (const area of sortedAreas) { const prsInArea = prsInSection.filter((pr) => pr.area === area); if (!prsInArea.length) { From 0f16504b56d6ec6fb157026d30b3373c66449ed8 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 14:55:16 -0700 Subject: [PATCH 08/13] remove project reference test, doesn't work with --noEmit --- packages/kbn-dev-utils/tsconfig.json | 2 -- packages/kbn-release-notes/tsconfig.json | 3 --- 2 files changed, 5 deletions(-) diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 616d9452dd8619..6987884bf840a6 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -2,8 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "./src", - "tsBuildInfoFile": "target/.tsbuildinfo", - "composite": true, "outDir": "target", "target": "ES2019", "declaration": true, diff --git a/packages/kbn-release-notes/tsconfig.json b/packages/kbn-release-notes/tsconfig.json index 9d541f86a1fae9..6ffa64d91fba0f 100644 --- a/packages/kbn-release-notes/tsconfig.json +++ b/packages/kbn-release-notes/tsconfig.json @@ -8,8 +8,5 @@ }, "include": [ "src/**/*" - ], - "references": [ - { "path": "../kbn-dev-utils/tsconfig.json" } ] } From 6f767eb9d0ad43d1f0b15a9810b1b2fb212613d5 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 15:00:15 -0700 Subject: [PATCH 09/13] [kbn/dev-utils] remove rootDir compilerOption --- packages/kbn-dev-utils/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 6987884bf840a6..0ec058eeb8a280 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "rootDir": "./src", "outDir": "target", "target": "ES2019", "declaration": true, From 53ca55eed05e56951c3a401dfd49e32d551ec7e9 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 16:36:12 -0700 Subject: [PATCH 10/13] use same logic to filter areas and sections --- .../kbn-release-notes/src/lib/classify_pr.ts | 14 +++----- .../src/release_notes_config.ts | 34 ++++++++++++------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/kbn-release-notes/src/lib/classify_pr.ts b/packages/kbn-release-notes/src/lib/classify_pr.ts index 28d0b3c6cc759c..01fb8ad6cbf58f 100644 --- a/packages/kbn-release-notes/src/lib/classify_pr.ts +++ b/packages/kbn-release-notes/src/lib/classify_pr.ts @@ -35,17 +35,13 @@ export interface ClassifiedPr extends PullRequest { } export function classifyPr(pr: PullRequest, log: ToolingLog): ClassifiedPr { - const areas = AREAS.filter((a) => - a.labels.some((test: string | RegExp) => + const filter = (a: Area | AsciidocSection) => + a.labels.some((test) => typeof test === 'string' ? pr.labels.includes(test) : pr.labels.some((l) => l.match(test)) - ) - ); + ); - const asciidocSections = ASCIIDOC_SECTIONS.filter((a) => - a.labels.some((test: string | RegExp) => - typeof test === 'string' ? pr.labels.includes(test) : pr.labels.some((l) => l.match(test)) - ) - ); + const areas = AREAS.filter(filter); + const asciidocSections = ASCIIDOC_SECTIONS.filter(filter); const pickOne = (name: string, options: T[]) => { if (options.length > 1) { diff --git a/packages/kbn-release-notes/src/release_notes_config.ts b/packages/kbn-release-notes/src/release_notes_config.ts index 671c8ffcee98a3..88ab5dfa2fda43 100644 --- a/packages/kbn-release-notes/src/release_notes_config.ts +++ b/packages/kbn-release-notes/src/release_notes_config.ts @@ -17,8 +17,6 @@ * under the License. */ -import { ArrayItem } from './lib'; - /** * Exclude any PR from release notes that has a matching label. String * labels must match exactly, for more complicated use a RegExp @@ -37,9 +35,6 @@ export const IGNORE_LABELS: Array = [ 'release_note:dev_docs', ]; -export type Area = ArrayItem | typeof UNKNOWN_AREA; -export type AsciidocSection = ArrayItem | typeof UNKNOWN_ASCIIDOC_SECTION; - /** * Define areas that are used to categorize changes in the release notes * based on the labels a PR has. the `labels` array can contain strings, which @@ -47,7 +42,13 @@ export type AsciidocSection = ArrayItem | typeof UNKNO * order, which has a `label` which matches and label on a PR is the area * assigned to that PR. */ -export const AREAS = [ + +export interface Area { + title: string; + labels: Array; +} + +export const AREAS: Area[] = [ { title: 'Design', labels: ['Team:Design', 'Project:Accessibility'], @@ -239,19 +240,26 @@ export const AREAS = [ title: 'Data ingest', labels: ['Ingest', 'Feature:Ingest Node Pipelines'], }, -] as const; +]; -export const UNKNOWN_AREA = { +export const UNKNOWN_AREA: Area = { title: 'Unknown', labels: [], -} as const; +}; /** * Define the sections that will be assigned to PRs when generating the * asciidoc formatted report. The order of the sections determines the * order they will be rendered in the report */ -export const ASCIIDOC_SECTIONS = [ + +export interface AsciidocSection { + title: string; + labels: Array; + id: string; +} + +export const ASCIIDOC_SECTIONS: AsciidocSection[] = [ { id: 'enhancement', title: 'Enhancements', @@ -277,10 +285,10 @@ export const ASCIIDOC_SECTIONS = [ title: 'Breaking Changes', labels: ['release_note:breaking'], }, -] as const; +]; -export const UNKNOWN_ASCIIDOC_SECTION = { +export const UNKNOWN_ASCIIDOC_SECTION: AsciidocSection = { id: 'unknown', title: 'Unknown', labels: [], -} as const; +}; From 54c326e094a032cd27cfd8087d02f712e448c3a6 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 19:59:34 -0700 Subject: [PATCH 11/13] classify PRs earlier so warnings are interleaved in output --- packages/kbn-release-notes/src/cli.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/kbn-release-notes/src/cli.ts b/packages/kbn-release-notes/src/cli.ts index 0e7b681fd9f15e..44b4a7a0282d20 100644 --- a/packages/kbn-release-notes/src/cli.ts +++ b/packages/kbn-release-notes/src/cli.ts @@ -28,7 +28,7 @@ import { iterRelevantPullRequests, getPr, Version, - PullRequest, + ClassifiedPr, streamFromIterable, asyncPipeline, IrrelevantPrSummary, @@ -100,13 +100,13 @@ export function runReleaseNotesCli() { log.info(`Loading all PRs with label [${version.label}] to build release notes...`); const summary = new IrrelevantPrSummary(log); - const prsToReport: PullRequest[] = []; + const prsToReport: ClassifiedPr[] = []; const prIterable = iterRelevantPullRequests(token, version, log); for await (const pr of prIterable) { if (!isPrRelevant(pr, version, includeVersions, summary)) { continue; } - prsToReport.push(pr); + prsToReport.push(classifyPr(pr, log)); } summary.logStats(); @@ -117,10 +117,9 @@ export function runReleaseNotesCli() { } log.info(`Found ${prsToReport.length} prs to report on`); - const classifiedPrs = prsToReport.map((pr) => classifyPr(pr, log)); for (const Format of Formats) { - const format = new Format(version, classifiedPrs, log); + 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}`); From 05d42dc9f01eee6ec2d058c5a6b28eeed3e4893b Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 12 Jun 2020 19:59:59 -0700 Subject: [PATCH 12/13] support prs where the author has deleted their github account --- packages/kbn-release-notes/src/lib/pull_request.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/kbn-release-notes/src/lib/pull_request.ts b/packages/kbn-release-notes/src/lib/pull_request.ts index e7bc03c4c16d9d..e61e496642062a 100644 --- a/packages/kbn-release-notes/src/lib/pull_request.ts +++ b/packages/kbn-release-notes/src/lib/pull_request.ts @@ -22,7 +22,7 @@ import { inspect } from 'util'; import Axios from 'axios'; import gql from 'graphql-tag'; import * as GraphqlPrinter from 'graphql/language/printer'; -import { ASTNode } from 'graphql/language/ast'; +import { DocumentNode } from 'graphql/language/ast'; import makeTerminalLink from 'terminal-link'; import { ToolingLog } from '@kbn/dev-utils'; @@ -75,7 +75,11 @@ export interface PullRequest { /** * Send a single request to the Github v4 GraphQL API */ -async function gqlRequest(token: string, query: ASTNode, variables: Record = {}) { +async function gqlRequest( + token: string, + query: DocumentNode, + variables: Record = {} +) { const resp = await Axios.request({ url: 'https://api.github.com/graphql', method: 'POST', @@ -113,8 +117,8 @@ function parsePullRequestNode(node: any): PullRequest { labels, fixes: getFixReferences(node.bodyText), user: { - login: node.author.login, - name: node.author.name, + login: node.author?.login || 'deleted user', + name: node.author?.name, }, versions: labels .map((l) => Version.fromLabel(l)) From 418b4219fce6cfe030c1f63b78ba06785dcc9061 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 15 Jun 2020 10:33:09 -0700 Subject: [PATCH 13/13] log a warning, but still pick the first matching area/asciidoc section --- packages/kbn-release-notes/src/lib/classify_pr.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/kbn-release-notes/src/lib/classify_pr.ts b/packages/kbn-release-notes/src/lib/classify_pr.ts index 01fb8ad6cbf58f..c567935ab7e480 100644 --- a/packages/kbn-release-notes/src/lib/classify_pr.ts +++ b/packages/kbn-release-notes/src/lib/classify_pr.ts @@ -46,10 +46,8 @@ export function classifyPr(pr: PullRequest, log: ToolingLog): ClassifiedPr { const pickOne = (name: string, options: T[]) => { if (options.length > 1) { const matches = options.map((o) => o.title).join(', '); - log.error( - `[${pr.terminalLink}] unable to determine ${name} because mulitple match [${matches}]` - ); - return; + log.warning(`[${pr.terminalLink}] ambiguous ${name}, mulitple match [${matches}]`); + return options[0]; } if (options.length === 0) {