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

Co-authored-by: spalger <spalger@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Spencer 2020-06-15 18:27:25 -07:00 committed by GitHub
parent e50af74664
commit 1282cb6d19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1745 additions and 3 deletions

4
.gitignore vendored
View file

@ -52,3 +52,7 @@ npm-debug.log*
# apm plugin
/x-pack/plugins/apm/tsconfig.json
apm.tsconfig.json
# release notes script output
report.csv
report.asciidoc

View file

@ -301,6 +301,7 @@
"@kbn/expect": "1.0.0",
"@kbn/optimizer": "1.0.0",
"@kbn/plugin-generator": "1.0.0",
"@kbn/release-notes": "1.0.0",
"@kbn/test": "1.0.0",
"@kbn/utility-types": "1.0.0",
"@microsoft/api-documenter": "7.7.2",

View file

@ -0,0 +1,23 @@
{
"name": "@kbn/release-notes",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "target/index.js",
"scripts": {
"kbn:bootstrap": "tsc",
"kbn:watch": "tsc --watch"
},
"dependencies": {
"@kbn/dev-utils": "1.0.0",
"axios": "^0.19.2",
"cheerio": "0.22.0",
"dedent": "^0.7.0",
"graphql": "^14.0.0",
"graphql-tag": "^2.10.3",
"terminal-link": "^2.1.1"
},
"devDependencies": {
"markdown-it": "^10.0.0",
"typescript": "3.9.5"
}
}

View file

@ -0,0 +1,162 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Fs from 'fs';
import Path from 'path';
import { inspect } from 'util';
import { run, createFlagError, createFailError, REPO_ROOT } from '@kbn/dev-utils';
import { FORMATS, SomeFormat } from './formats';
import {
iterRelevantPullRequests,
getPr,
Version,
ClassifiedPr,
streamFromIterable,
asyncPipeline,
IrrelevantPrSummary,
isPrRelevant,
classifyPr,
} from './lib';
const rootPackageJson = JSON.parse(
Fs.readFileSync(Path.resolve(REPO_ROOT, 'package.json'), 'utf8')
);
const extensions = FORMATS.map((f) => f.extension);
export function runReleaseNotesCli() {
run(
async ({ flags, log }) => {
const token = flags.token;
if (!token || typeof token !== 'string') {
throw createFlagError('--token must be defined');
}
const version = Version.fromFlag(flags.version);
if (!version) {
throw createFlagError('unable to parse --version, use format "v{major}.{minor}.{patch}"');
}
const includeVersions = Version.fromFlags(flags.include || []);
if (!includeVersions) {
throw createFlagError('unable to parse --include, use format "v{major}.{minor}.{patch}"');
}
const Formats: SomeFormat[] = [];
for (const flag of Array.isArray(flags.format) ? flags.format : [flags.format]) {
const Format = FORMATS.find((F) => F.extension === flag);
if (!Format) {
throw createFlagError(`--format must be one of "${extensions.join('", "')}"`);
}
Formats.push(Format);
}
const filename = flags.filename;
if (!filename || typeof filename !== 'string') {
throw createFlagError('--filename must be a string');
}
if (flags['debug-pr']) {
const number = parseInt(String(flags['debug-pr']), 10);
if (Number.isNaN(number)) {
throw createFlagError('--debug-pr must be a pr number when specified');
}
const summary = new IrrelevantPrSummary(log);
const pr = await getPr(token, number);
log.success(
inspect(
{
version: version.label,
includeVersions: includeVersions.map((v) => v.label),
isPrRelevant: isPrRelevant(pr, version, includeVersions, summary),
...classifyPr(pr, log),
pr,
},
{ depth: 100 }
)
);
summary.logStats();
return;
}
log.info(`Loading all PRs with label [${version.label}] to build release notes...`);
const summary = new IrrelevantPrSummary(log);
const prsToReport: ClassifiedPr[] = [];
const prIterable = iterRelevantPullRequests(token, version, log);
for await (const pr of prIterable) {
if (!isPrRelevant(pr, version, includeVersions, summary)) {
continue;
}
prsToReport.push(classifyPr(pr, log));
}
summary.logStats();
if (!prsToReport.length) {
throw createFailError(
`All PRs with label [${version.label}] were filtered out by the config. Run again with --debug for more info.`
);
}
log.info(`Found ${prsToReport.length} prs to report on`);
for (const Format of Formats) {
const format = new Format(version, prsToReport, log);
const outputPath = Path.resolve(`${filename}.${Format.extension}`);
await asyncPipeline(streamFromIterable(format.print()), Fs.createWriteStream(outputPath));
log.success(`[${Format.extension}] report written to ${outputPath}`);
}
},
{
usage: `node scripts/release_notes --token {token} --version {version}`,
flags: {
alias: {
version: 'v',
include: 'i',
},
string: ['token', 'version', 'format', 'filename', 'include', 'debug-pr'],
default: {
filename: 'report',
version: rootPackageJson.version,
format: extensions,
},
help: `
--token (required) The Github access token to use for requests
--version, -v The version to fetch PRs by, PRs with version labels prior to
this one will be ignored (see --include-version) (default ${
rootPackageJson.version
})
--include, -i A version that is before --version but shouldn't be considered
"released" and cause PRs with a matching label to be excluded from
release notes. Use this when PRs are labeled with a version that
is less that --version and is expected to be released after
--version, can be specified multiple times.
--format Only produce a certain format, options: "${extensions.join('", "')}"
--filename Output filename, defaults to "report"
--debug-pr Fetch and print the details for a single PR, disabling reporting
`,
},
description: `
Fetch details from Github PRs for generating release notes
`,
}
);
}

View file

@ -0,0 +1,84 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import dedent from 'dedent';
import { Format } from './format';
import {
ASCIIDOC_SECTIONS,
UNKNOWN_ASCIIDOC_SECTION,
AREAS,
UNKNOWN_AREA,
} from '../release_notes_config';
function* lines(body: string) {
for (const line of dedent(body).split('\n')) {
yield `${line}\n`;
}
}
export class AsciidocFormat extends Format {
static extension = 'asciidoc';
*print() {
const sortedAreas = [
...AREAS.slice().sort((a, b) => a.title.localeCompare(b.title)),
UNKNOWN_AREA,
];
yield* lines(`
[[release-notes-${this.version.label}]]
== ${this.version.label} Release Notes
Also see <<breaking-changes-${this.version.major}.${this.version.minor}>>.
`);
for (const section of [...ASCIIDOC_SECTIONS, UNKNOWN_ASCIIDOC_SECTION]) {
const prsInSection = this.prs.filter((pr) => pr.asciidocSection === section);
if (!prsInSection.length) {
continue;
}
yield '\n';
yield* lines(`
[float]
[[${section.id}-${this.version.label}]]
=== ${section.title}
`);
for (const area of sortedAreas) {
const prsInArea = prsInSection.filter((pr) => pr.area === area);
if (!prsInArea.length) {
continue;
}
yield `${area.title}::\n`;
for (const pr of prsInArea) {
const fixes = pr.fixes.length ? `[Fixes ${pr.fixes.join(', ')}] ` : '';
const strippedTitle = pr.title.replace(/^\s*\[[^\]]+\]\s*/, '');
yield `* ${fixes}${strippedTitle} {pull}${pr.number}[#${pr.number}]\n`;
if (pr.note) {
yield ` - ${pr.note}\n`;
}
}
}
}
}
}

View file

@ -0,0 +1,74 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Format } from './format';
/**
* Escape a value to conform to field and header encoding defined at https://tools.ietf.org/html/rfc4180
*/
function esc(value: string | number) {
if (typeof value === 'number') {
return String(value);
}
if (!value.includes(',') && !value.includes('\n') && !value.includes('"')) {
return value;
}
return `"${value.split('"').join('""')}"`;
}
function row(...fields: Array<string | number>) {
return fields.map(esc).join(',') + '\r\n';
}
export class CsvFormat extends Format {
static extension = 'csv';
*print() {
// columns
yield row(
'areas',
'versions',
'user',
'title',
'number',
'url',
'date',
'fixes',
'labels',
'state'
);
for (const pr of this.prs) {
yield row(
pr.area.title,
pr.versions.map((v) => v.label).join(', '),
pr.user.name || pr.user.login,
pr.title,
pr.number,
pr.url,
pr.mergedAt,
pr.fixes.join(', '),
pr.labels.join(', '),
pr.state
);
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ToolingLog } from '@kbn/dev-utils';
import { Version, ClassifiedPr } from '../lib';
export abstract class Format {
static extension: string;
constructor(
protected readonly version: Version,
protected readonly prs: ClassifiedPr[],
protected readonly log: ToolingLog
) {}
abstract print(): Iterator<string>;
}

View file

@ -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<typeof FORMATS>;

View file

@ -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';

View file

@ -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 { 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 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

@ -0,0 +1,68 @@
/*
* 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
fixed
#19
`)
).toMatchInlineSnapshot(`
Array [
"#1",
"#2",
"#3",
"#4",
"#5",
"#6",
"#7",
"#8",
"#9",
"#10",
"#11",
"#12",
"#13",
"#14",
"#15",
"#16",
"#17",
"#18",
]
`);
});

View file

@ -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)[ :]*(#\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

@ -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"`);
});

View file

@ -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();
}
}

View file

@ -0,0 +1,26 @@
/*
* 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';
export * from './classify_pr';

View file

@ -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<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

@ -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 { Version } from './version';
import { PullRequest } from './pull_request';
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

@ -0,0 +1,206 @@
/*
* 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 { DocumentNode } from 'graphql/language/ast';
import makeTerminalLink from 'terminal-link';
import { ToolingLog } 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;
}
/**
* Send a single request to the Github v4 GraphQL API
*/
async function gqlRequest(
token: string,
query: DocumentNode,
variables: Record<string, unknown> = {}
) {
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 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),
};
}
/**
* Iterate all of the PRs which have the `version` label
*/
export async function* iterRelevantPullRequests(token: string, version: Version, log: ToolingLog) {
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);
}

View file

@ -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<string | Buffer> | AsyncIterable<string | Buffer>
): Readable {
// @ts-ignore
return Readable.from(iter);
}
export const asyncPipeline = promisify(pipeline);

View file

@ -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 readonly any[]> = T extends ReadonlyArray<infer X> ? X : never;

View file

@ -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);
});

View file

@ -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<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

@ -0,0 +1,294 @@
/*
* 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.
*/
/**
* 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: 'Canvas',
labels: ['Team:Canvas'],
},
{
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: '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

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"declaration": true,
"sourceMap": true,
"target": "ES2019"
},
"include": [
"src/**/*"
]
}

View file

@ -0,0 +1 @@
../../yarn.lock

21
scripts/release_notes.js Normal file
View file

@ -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();

View file

@ -5306,9 +5306,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"
@ -16093,6 +16093,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"
@ -16139,6 +16144,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"
@ -18627,6 +18639,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"
@ -29064,6 +29081,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"
@ -29416,6 +29441,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, terser-webpack-plugin@^1.4.3:
version "1.4.4"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz#2c63544347324baafa9a56baaddf1634c8abfc2f"