Remove the no longer used release-notes script (#97806)

* Remove the no longer used release-notes script

* Commit missing file
This commit is contained in:
Tim Roes 2021-04-21 20:58:39 +02:00 committed by GitHub
parent c60411ed4a
commit 2744f70466
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 10 additions and 1508 deletions

View file

@ -1,5 +1,4 @@
**/*.js.snap
**/graphql/types.ts
/.es
/.chromium
/build

View file

@ -77,7 +77,6 @@
"**/deepmerge": "^4.2.2",
"**/fast-deep-equal": "^3.1.1",
"globby/fast-glob": "3.2.5",
"**/graphql-toolkit/lodash": "^4.17.21",
"**/hoist-non-react-statics": "^3.3.2",
"**/isomorphic-fetch/node-fetch": "^2.6.1",
"**/istanbul-instrumenter-loader/schema-utils": "1.0.0",
@ -193,10 +192,10 @@
"compare-versions": "3.5.1",
"concat-stream": "1.6.2",
"constate": "^1.3.2",
"cronstrue": "^1.51.0",
"content-disposition": "0.5.3",
"copy-to-clipboard": "^3.0.8",
"core-js": "^3.6.5",
"cronstrue": "^1.51.0",
"cytoscape": "^3.10.0",
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
@ -231,8 +230,6 @@
"glob": "^7.1.2",
"glob-all": "^3.2.1",
"globby": "^11.0.3",
"graphql": "^0.13.2",
"graphql-tag": "^2.10.3",
"handlebars": "4.7.7",
"he": "^1.2.0",
"history": "^4.9.0",
@ -274,9 +271,9 @@
"lodash": "^4.17.21",
"lru-cache": "^4.1.5",
"lz-string": "^1.4.4",
"markdown-it": "^10.0.0",
"mapbox-gl": "1.13.1",
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
"markdown-it": "^10.0.0",
"md5": "^2.1.0",
"memoize-one": "^5.0.0",
"mime": "^2.4.4",
@ -298,12 +295,12 @@
"object-path-immutable": "^3.1.1",
"opn": "^5.5.0",
"oppsy": "^2.0.0",
"p-limit": "^3.0.1",
"p-map": "^4.0.0",
"p-retry": "^4.2.0",
"papaparse": "^5.2.0",
"pdfmake": "^0.1.65",
"pegjs": "0.10.0",
"p-limit": "^3.0.1",
"pluralize": "3.1.0",
"pngjs": "^3.4.0",
"polished": "^1.9.2",
@ -335,19 +332,19 @@
"react-monaco-editor": "^0.41.2",
"react-popper-tooltip": "^2.10.1",
"react-query": "^3.13.10",
"react-redux": "^7.2.0",
"react-resizable": "^1.7.5",
"react-resize-detector": "^4.2.0",
"react-reverse-portal": "^1.0.4",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-router-redux": "^4.0.8",
"react-shortcuts": "^2.0.0",
"react-sizeme": "^2.3.6",
"react-syntax-highlighter": "^15.3.1",
"react-redux": "^7.2.0",
"react-resizable": "^1.7.5",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-tiny-virtual-list": "^2.2.0",
"react-virtualized": "^9.21.2",
"react-use": "^15.3.8",
"react-virtualized": "^9.21.2",
"react-vis": "^1.8.1",
"react-visibility-sensor": "^5.1.1",
"reactcss": "1.2.3",
@ -376,8 +373,8 @@
"strip-ansi": "^6.0.0",
"style-it": "^2.1.3",
"styled-components": "^5.1.0",
"symbol-observable": "^1.2.0",
"suricata-sid-db": "^1.0.2",
"symbol-observable": "^1.2.0",
"tabbable": "1.1.3",
"tar": "4.4.13",
"tinycolor2": "1.4.1",
@ -521,7 +518,6 @@
"@types/getos": "^3.0.0",
"@types/git-url-parse": "^9.0.0",
"@types/glob": "^7.1.2",
"@types/graphql": "^0.13.2",
"@types/gulp": "^4.0.6",
"@types/gulp-zip": "^4.0.1",
"@types/hapi__cookie": "^10.1.1",
@ -735,8 +731,8 @@
"jest-circus": "^26.6.3",
"jest-cli": "^26.6.3",
"jest-diff": "^26.6.2",
"jest-environment-jsdom-thirteen": "^1.0.1",
"jest-environment-jsdom": "^26.6.2",
"jest-environment-jsdom-thirteen": "^1.0.1",
"jest-raw-loader": "^1.0.1",
"jest-silent-reporter": "^0.2.1",
"jest-snapshot": "^26.6.2",

View file

@ -6,5 +6,4 @@
* Side Public License, v 1.
*/
export * from './release_notes';
export * from './api_docs';

View file

@ -1,152 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import Fs from 'fs';
import Path from 'path';
import { inspect } from 'util';
import { REPO_ROOT } from '@kbn/utils';
import { run, createFlagError, createFailError } from '@kbn/dev-utils';
import { FORMATS, SomeFormat } from './formats';
import {
PrApi,
Version,
ClassifiedPr,
streamFromIterable,
asyncPipeline,
IrrelevantPrSummary,
isPrRelevant,
classifyPr,
} from './lib';
const rootPackageJson = JSON.parse(
Fs.readFileSync(Path.resolve(REPO_ROOT, 'package.json'), 'utf8')
);
const extensions = FORMATS.map((f) => f.extension);
export function runReleaseNotesCli() {
run(
async ({ flags, log }) => {
const token = flags.token;
if (!token || typeof token !== 'string') {
throw createFlagError('--token must be defined');
}
const prApi = new PrApi(log, token);
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 prApi.getPr(number);
log.success(
inspect(
{
version: version.label,
includeVersions: includeVersions.map((v) => v.label),
isPrRelevant: isPrRelevant(pr, version, includeVersions, summary),
...classifyPr(pr, log),
pr,
},
{ depth: 100 }
)
);
summary.logStats();
return;
}
log.info(`Loading all PRs with label [${version.label}] to build release notes...`);
const summary = new IrrelevantPrSummary(log);
const prsToReport: ClassifiedPr[] = [];
const prIterable = prApi.iterRelevantPullRequests(version);
for await (const pr of prIterable) {
if (!isPrRelevant(pr, version, includeVersions, summary)) {
continue;
}
prsToReport.push(classifyPr(pr, log));
}
summary.logStats();
if (!prsToReport.length) {
throw createFailError(
`All PRs with label [${version.label}] were filtered out by the config. Run again with --debug for more info.`
);
}
log.info(`Found ${prsToReport.length} prs to report on`);
for (const Format of Formats) {
const format = new Format(version, prsToReport, log);
const outputPath = Path.resolve(`${filename}.${Format.extension}`);
await asyncPipeline(streamFromIterable(format.print()), Fs.createWriteStream(outputPath));
log.success(`[${Format.extension}] report written to ${outputPath}`);
}
},
{
usage: `node scripts/release_notes --token {token} --version {version}`,
flags: {
alias: {
version: 'v',
include: 'i',
},
string: ['token', 'version', 'format', 'filename', 'include', 'debug-pr'],
default: {
filename: 'report',
version: rootPackageJson.version,
format: extensions,
},
help: `
--token (required) The Github access token to use for requests
--version, -v The version to fetch PRs by, PRs with version labels prior to
this one will be ignored (see --include-version) (default ${
rootPackageJson.version
})
--include, -i A version that is before --version but shouldn't be considered
"released" and cause PRs with a matching label to be excluded from
release notes. Use this when PRs are labeled with a version that
is less that --version and is expected to be released after
--version, can be specified multiple times.
--format Only produce a certain format, options: "${extensions.join('", "')}"
--filename Output filename, defaults to "report"
--debug-pr Fetch and print the details for a single PR, disabling reporting
`,
},
description: `
Fetch details from Github PRs for generating release notes
`,
}
);
}

View file

@ -1,73 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import dedent from 'dedent';
import { Format } from './format';
import {
ASCIIDOC_SECTIONS,
UNKNOWN_ASCIIDOC_SECTION,
AREAS,
UNKNOWN_AREA,
} from '../release_notes_config';
function* lines(body: string) {
for (const line of dedent(body).split('\n')) {
yield `${line}\n`;
}
}
export class AsciidocFormat extends Format {
static extension = 'asciidoc';
*print() {
const sortedAreas = [
...AREAS.slice().sort((a, b) => a.title.localeCompare(b.title)),
UNKNOWN_AREA,
];
yield* lines(`
[[release-notes-${this.version.label}]]
== ${this.version.label} Release Notes
Also see <<breaking-changes-${this.version.major}.${this.version.minor}>>.
`);
for (const section of [...ASCIIDOC_SECTIONS, UNKNOWN_ASCIIDOC_SECTION]) {
const prsInSection = this.prs.filter((pr) => pr.asciidocSection === section);
if (!prsInSection.length) {
continue;
}
yield '\n';
yield* lines(`
[float]
[[${section.id}-${this.version.label}]]
=== ${section.title}
`);
for (const area of sortedAreas) {
const prsInArea = prsInSection.filter((pr) => pr.area === area);
if (!prsInArea.length) {
continue;
}
yield `${area.title}::\n`;
for (const pr of prsInArea) {
const fixes = pr.fixes.length ? `[Fixes ${pr.fixes.join(', ')}] ` : '';
const strippedTitle = pr.title.replace(/^\s*\[[^\]]+\]\s*/, '');
yield `* ${fixes}${strippedTitle} {kibana-pull}${pr.number}[#${pr.number}]\n`;
if (pr.note) {
yield ` - ${pr.note}\n`;
}
}
}
}
}
}

View file

@ -1,63 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Format } from './format';
/**
* Escape a value to conform to field and header encoding defined at https://tools.ietf.org/html/rfc4180
*/
function esc(value: string | number) {
if (typeof value === 'number') {
return String(value);
}
if (!value.includes(',') && !value.includes('\n') && !value.includes('"')) {
return value;
}
return `"${value.split('"').join('""')}"`;
}
function row(...fields: Array<string | number>) {
return fields.map(esc).join(',') + '\r\n';
}
export class CsvFormat extends Format {
static extension = 'csv';
*print() {
// columns
yield row(
'areas',
'versions',
'user',
'title',
'number',
'url',
'date',
'fixes',
'labels',
'state'
);
for (const pr of this.prs) {
yield row(
pr.area.title,
pr.versions.map((v) => v.label).join(', '),
pr.user.name || pr.user.login,
pr.title,
pr.number,
pr.url,
pr.mergedAt,
pr.fixes.join(', '),
pr.labels.join(', '),
pr.state
);
}
}
}

View file

@ -1,23 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ToolingLog } from '@kbn/dev-utils';
import { Version, ClassifiedPr } from '../lib';
export abstract class Format {
static extension: string;
constructor(
protected readonly version: Version,
protected readonly prs: ClassifiedPr[],
protected readonly log: ToolingLog
) {}
abstract print(): Iterator<string>;
}

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ArrayItem } from '../lib';
import { AsciidocFormat } from './asciidoc';
import { CsvFormat } from './csv';
export const FORMATS = [CsvFormat, AsciidocFormat] as const;
export type SomeFormat = ArrayItem<typeof FORMATS>;

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './cli';

View file

@ -1,55 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ToolingLog } from '@kbn/dev-utils';
import {
Area,
AREAS,
UNKNOWN_AREA,
AsciidocSection,
ASCIIDOC_SECTIONS,
UNKNOWN_ASCIIDOC_SECTION,
} from '../release_notes_config';
import { PullRequest } from './pr_api';
export interface ClassifiedPr extends PullRequest {
area: Area;
asciidocSection: AsciidocSection;
}
export function classifyPr(pr: PullRequest, log: ToolingLog): ClassifiedPr {
const filter = (a: Area | AsciidocSection) =>
a.labels.some((test) =>
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 = <T extends Area | AsciidocSection>(name: string, options: T[]) => {
if (options.length > 1) {
const matches = options.map((o) => o.title).join(', ');
log.warning(`[${pr.terminalLink}] ambiguous ${name}, mulitple match [${matches}]`);
return options[0];
}
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,
};
}

View file

@ -1,57 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
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
fixed
#19
`)
).toMatchInlineSnapshot(`
Array [
"#1",
"#2",
"#3",
"#4",
"#5",
"#6",
"#7",
"#8",
"#9",
"#10",
"#11",
"#12",
"#13",
"#14",
"#15",
"#16",
"#17",
"#18",
]
`);
});

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const FIXES_RE = /(?:closes|close|closed|fix|fixes|fixed|resolve|resolves|resolved)[ :]*(#\d*)/gi;
export function getFixReferences(prText: string) {
const fixes: string[] = [];
let match;
while ((match = FIXES_RE.exec(prText))) {
fixes.push(match[1]);
}
return fixes;
}

View file

@ -1,72 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
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
`),
'release note'
)
).toMatchInlineSnapshot(`"Checkout this feature"`);
expect(
getNoteFromDescription(
mk.render(dedent`
My PR description
Fixes: #1234
#### Dev docs:
We fixed an issue
`),
'dev docs'
)
).toMatchInlineSnapshot(`"We fixed an issue"`);
expect(
getNoteFromDescription(
mk.render(dedent`
My PR description
Fixes: #1234
OTHER TITLE: Checkout feature foo
`),
'other title'
)
).toMatchInlineSnapshot(`"Checkout feature foo"`);
expect(
getNoteFromDescription(
mk.render(dedent`
# Summary
My PR description
release note : bar
`),
'release note'
)
).toMatchInlineSnapshot(`"bar"`);
});

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import cheerio from 'cheerio';
export function getNoteFromDescription(descriptionHtml: string, header: string) {
const re = new RegExp(`^(\\s*${header.toLowerCase()}(?:s)?\\s*:?\\s*)`, 'i');
const $ = cheerio.load(descriptionHtml);
for (const el of $('p,h1,h2,h3,h4,h5').toArray()) {
const text = $(el).text();
const match = text.match(re);
if (!match) {
continue;
}
const note = text.replace(match[1], '').trim();
return note || $(el).next().text().trim();
}
}

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './pr_api';
export * from './version';
export * from './is_pr_relevant';
export * from './streams';
export * from './type_helpers';
export * from './irrelevant_pr_summary';
export * from './classify_pr';

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ToolingLog } from '@kbn/dev-utils';
import { PullRequest } from './pr_api';
import { Version } from './version';
export class IrrelevantPrSummary {
private readonly stats = {
'skipped by label': new Map<string, number>(),
'skipped by label regexp': new Map<string, number>(),
'skipped by version': new Map<string, number>(),
};
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}]`);
}
}
}
}

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Version } from './version';
import { PullRequest } from './pr_api';
import { IGNORE_LABELS } from '../release_notes_config';
import { IrrelevantPrSummary } from './irrelevant_pr_summary';
export function isPrRelevant(
pr: PullRequest,
version: Version,
includeVersions: Version[],
summary: IrrelevantPrSummary
) {
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;
}
return true;
}

View file

@ -1,222 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { inspect } from 'util';
import Axios from 'axios';
import gql from 'graphql-tag';
import * as GraphqlPrinter from 'graphql/language/printer';
import { DocumentNode } from 'graphql/language/ast';
import makeTerminalLink from 'terminal-link';
import { ToolingLog, isAxiosResponseError } from '@kbn/dev-utils';
import { Version } from './version';
import { getFixReferences } from './get_fix_references';
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;
};
versions: Version[];
terminalLink: string;
note?: string;
}
export class PrApi {
constructor(private readonly log: ToolingLog, private readonly token: string) {}
async getPr(number: number) {
const resp = await this.gqlRequest(
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 this.parsePullRequestNode(node);
}
/**
* Iterate all of the PRs which have the `version` label
*/
async *iterRelevantPullRequests(version: Version) {
let nextCursor: string | undefined;
let hasNextPage = true;
while (hasNextPage) {
const resp = await this.gqlRequest(
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 this.parsePullRequestNode(node);
}
}
}
/**
* Convert the Github API response into the structure used by this tool
*
* @param node A GraphQL response from Github using the PrNode fragment
*/
private parsePullRequestNode(node: any): PullRequest {
const terminalLink = makeTerminalLink(`#${node.number}`, node.url);
const labels: string[] = node.labels.nodes.map((l: { name: string }) => l.name);
return {
number: node.number,
url: node.url,
terminalLink,
title: node.title,
targetBranch: node.baseRefName,
state: node.state,
mergedAt: node.mergedAt,
labels,
fixes: getFixReferences(node.bodyText),
user: {
login: node.author?.login || 'deleted user',
name: node.author?.name,
},
versions: labels
.map((l) => Version.fromLabel(l))
.filter((v): v is Version => v instanceof Version),
note:
getNoteFromDescription(node.bodyHTML, 'release note') ||
getNoteFromDescription(node.bodyHTML, 'dev docs'),
};
}
/**
* Send a single request to the Github v4 GraphQL API
*/
private async gqlRequest(query: DocumentNode, variables: Record<string, unknown> = {}) {
let attempt = 0;
while (true) {
attempt += 1;
try {
const resp = await Axios.request({
url: 'https://api.github.com/graphql',
method: 'POST',
headers: {
'user-agent': '@kbn/release-notes',
authorization: `bearer ${this.token}`,
},
data: {
query: GraphqlPrinter.print(query),
variables,
},
});
return resp.data;
} catch (error) {
if (!isAxiosResponseError(error) || error.response.status < 500) {
// rethrow error unless it is a 500+ response from github
throw error;
}
const { status, data } = error.response;
const resp = inspect(data);
if (attempt === 5) {
throw new Error(
`${status} response from Github, attempted request ${attempt} times: [${resp}]`
);
}
const delay = attempt * 2000;
this.log.debug(`Github responded with ${status}, retrying in ${delay} ms: [${resp}]`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}
}
}

View file

@ -1,23 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
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<string | Buffer> | AsyncIterable<string | Buffer>
): Readable {
// @ts-ignore
return Readable.from(iter);
}
export const asyncPipeline = promisify(pipeline);

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type ArrayItem<T extends readonly any[]> = T extends ReadonlyArray<infer X> ? X : never;

View file

@ -1,135 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
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);
});

View file

@ -1,112 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const LABEL_RE = /^v(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta)(\d+))?$/;
const versionCache = new Map<string, Version>();
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)
);
}
}

View file

@ -1,283 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* 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<RegExp | string> = [
'Team:Docs',
':KibanaApp/fix-it-week',
'reverted',
/^test/,
'non-issue',
'jenkins',
'build',
'chore',
'backport',
'release_note:skip',
'release_note:dev_docs',
];
/**
* 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 interface Area {
title: string;
labels: Array<string | RegExp>;
}
export const AREAS: Area[] = [
{
title: 'Design',
labels: ['Team:Design', 'Project:Accessibility'],
},
{
title: 'Logstash',
labels: ['App:Logstash', 'Feature:Logstash Pipelines'],
},
{
title: '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',
],
},
{
title: 'Monitoring',
labels: ['Team:Monitoring', 'Feature:Telemetry', 'Feature:Stack Monitoring'],
},
{
title: 'Operations',
labels: ['Team:Operations', 'Feature:License'],
},
{
title: 'Kibana UI',
labels: ['Kibana UI', 'Team:Core UI', 'Feature:Header'],
},
{
title: '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',
],
},
{
title: 'Machine Learning',
labels: [
':ml',
'Feature:Anomaly Detection',
'Feature:Data Frames',
'Feature:File Data Viz',
'Feature:ml-results',
'Feature:Data Frame Analytics',
],
},
{
title: 'Maps',
labels: ['Team:Geo'],
},
{
title: 'QA',
labels: ['Team:QA'],
},
{
title: 'Security',
labels: [
'Team:Security',
'Feature:Security/Spaces',
'Feature:users and roles',
'Feature:Security/Authentication',
'Feature:Security/Authorization',
'Feature:Security/Feature Controls',
],
},
{
title: 'Canvas',
labels: ['Feature:Canvas'],
},
{
title: 'Dashboard',
labels: ['Feature:Dashboard', 'Feature:Drilldowns'],
},
{
title: 'Discover',
labels: ['Feature:Discover'],
},
{
title: 'Kibana Home & Add Data',
labels: ['Feature:Add Data', 'Feature:Home'],
},
{
title: 'Querying & Filtering',
labels: [
'Feature:Query Bar',
'Feature:Courier',
'Feature:Filters',
'Feature:Timepicker',
'Feature:Highlight',
'Feature:KQL',
'Feature:Rollups',
],
},
{
title: 'Reporting',
labels: ['Feature:Reporting', 'Team:Reporting Services'],
},
{
title: 'Sharing',
labels: ['Feature:Embedding', 'Feature:SharingURLs'],
},
{
title: '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',
],
},
{
title: 'SIEM',
labels: ['Team:SIEM'],
},
{
title: 'Code',
labels: ['Team:Code'],
},
{
title: 'Infrastructure',
labels: ['App:Infrastructure', 'Feature:Infra UI', 'Feature:Service Maps'],
},
{
title: 'Logs',
labels: ['App:Logs', 'Feature:Logs UI'],
},
{
title: 'Uptime',
labels: ['App:Uptime', 'Feature:Uptime', 'Team:uptime'],
},
{
title: 'Beats Management',
labels: ['App:Beats', 'Feature:beats-cm', 'Team:Beats'],
},
{
title: 'APM',
labels: ['Team:apm', /^apm[:\-]/],
},
{
title: 'Lens',
labels: ['App:Lens', 'Feature:Lens'],
},
{
title: 'Alerting',
labels: ['App:Alerting', 'Feature:Alerting', 'Team:Alerting Services', 'Feature:Actions'],
},
{
title: 'Metrics',
labels: ['App:Metrics', 'Feature:Metrics UI', 'Team:logs-metrics-ui'],
},
{
title: 'Data ingest',
labels: ['Ingest', 'Feature:Ingest Node Pipelines'],
},
];
export const UNKNOWN_AREA: Area = {
title: 'Unknown',
labels: [],
};
/**
* 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 interface AsciidocSection {
title: string;
labels: Array<string | RegExp>;
id: string;
}
export const ASCIIDOC_SECTIONS: AsciidocSection[] = [
{
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'],
},
];
export const UNKNOWN_ASCIIDOC_SECTION: AsciidocSection = {
id: 'unknown',
title: 'Unknown',
labels: [],
};

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
require('../src/setup_node_env/no_transpilation');
require('@kbn/docs-utils').runReleaseNotesCli();

View file

@ -4881,11 +4881,6 @@
dependencies:
"@types/node" "*"
"@types/graphql@^0.13.2":
version "0.13.4"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.13.4.tgz#55ae9c29f0fd6b85ee536f5c72b4769d5c5e06b1"
integrity sha512-B4yel4ro2nTb3v0pYO8vO6SjgvFJSrwUY+IO6TUSLdOSB+gQFslylrhRCHxvXMIhxB71mv5PEE9dAX+24S8sew==
"@types/gulp-zip@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/gulp-zip/-/gulp-zip-4.0.1.tgz#96cd0b994219f9ae3bbbec7ec3baa043fba9d9ef"
@ -14705,18 +14700,6 @@ graphlib@^2.1.8:
dependencies:
lodash "^4.17.15"
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@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270"
integrity sha512-QZ5BL8ZO/B20VA8APauGBg3GyEgZ19eduvpLWoq5x7gMmWnHoy8rlQWPLmWgFvo1yNgjSEFMesmS4R6pPr7xog==
dependencies:
iterall "^1.2.1"
grid-index@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/grid-index/-/grid-index-1.1.0.tgz#97f8221edec1026c8377b86446a7c71e79522ea7"
@ -16770,11 +16753,6 @@ istanbul-reports@^3.0.2:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
iterall@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
iterate-iterator@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6"