Extend bulk issue creator (#80423)
It can now be used to create feature requests, not just deprecation bug reports.
This commit is contained in:
parent
fc5c0aadc9
commit
163f297d7f
|
@ -8,11 +8,14 @@ import abc
|
|||
import argparse
|
||||
import dataclasses
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
# noinspection PyPackageRequirements
|
||||
import argcomplete
|
||||
|
@ -31,19 +34,83 @@ class Issue:
|
|||
summary: str
|
||||
body: str
|
||||
project: str
|
||||
labels: list[str] | None = None
|
||||
|
||||
def create(self) -> str:
|
||||
cmd = ['gh', 'issue', 'create', '--title', self.title, '--body', self.body, '--project', self.project]
|
||||
|
||||
if self.labels:
|
||||
for label in self.labels:
|
||||
cmd.extend(('--label', label))
|
||||
|
||||
process = subprocess.run(cmd, capture_output=True, check=True)
|
||||
url = process.stdout.decode().strip()
|
||||
return url
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Feature:
|
||||
title: str
|
||||
summary: str
|
||||
component: str
|
||||
labels: list[str] | None = None
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict[str, t.Any]) -> Feature:
|
||||
title = data.get('title')
|
||||
summary = data.get('summary')
|
||||
component = data.get('component')
|
||||
labels = data.get('labels')
|
||||
|
||||
if not isinstance(title, str):
|
||||
raise RuntimeError(f'`title` is not `str`: {title}')
|
||||
|
||||
if not isinstance(summary, str):
|
||||
raise RuntimeError(f'`summary` is not `str`: {summary}')
|
||||
|
||||
if not isinstance(component, str):
|
||||
raise RuntimeError(f'`component` is not `str`: {component}')
|
||||
|
||||
if not isinstance(labels, list) or not all(isinstance(item, str) for item in labels):
|
||||
raise RuntimeError(f'`labels` is not `list[str]`: {labels}')
|
||||
|
||||
return Feature(
|
||||
title=title,
|
||||
summary=summary,
|
||||
component=component,
|
||||
labels=labels,
|
||||
)
|
||||
|
||||
def create_issue(self, project: str) -> Issue:
|
||||
body = f'''
|
||||
### Summary
|
||||
|
||||
{self.summary}
|
||||
|
||||
### Issue Type
|
||||
|
||||
Feature Idea
|
||||
|
||||
### Component Name
|
||||
|
||||
`{self.component}`
|
||||
'''
|
||||
|
||||
return Issue(
|
||||
title=self.title,
|
||||
summary=self.summary,
|
||||
body=body.strip(),
|
||||
project=project,
|
||||
labels=self.labels,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class BugReport:
|
||||
title: str
|
||||
summary: str
|
||||
component: str
|
||||
labels: list[str] | None = None
|
||||
|
||||
def create_issue(self, project: str) -> Issue:
|
||||
body = f'''
|
||||
|
@ -89,6 +156,7 @@ N/A
|
|||
summary=self.summary,
|
||||
body=body.strip(),
|
||||
project=project,
|
||||
labels=self.labels,
|
||||
)
|
||||
|
||||
|
||||
|
@ -174,14 +242,49 @@ TEST_OPTIONS = {
|
|||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Args:
|
||||
tests: list[str]
|
||||
create: bool
|
||||
verbose: bool
|
||||
|
||||
def run(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class DeprecationArgs(Args):
|
||||
tests: list[str]
|
||||
|
||||
def run(self) -> None:
|
||||
deprecated_command(self)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class FeatureArgs(Args):
|
||||
source: pathlib.Path
|
||||
|
||||
def run(self) -> None:
|
||||
feature_command(self)
|
||||
|
||||
|
||||
def parse_args() -> Args:
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
create_common_arguments(parser)
|
||||
|
||||
subparser = parser.add_subparsers(required=True)
|
||||
|
||||
create_deprecation_parser(subparser)
|
||||
create_feature_parser(subparser)
|
||||
|
||||
args = invoke_parser(parser)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def create_deprecation_parser(subparser) -> None:
|
||||
parser: argparse.ArgumentParser = subparser.add_parser('deprecation')
|
||||
parser.set_defaults(type=DeprecationArgs)
|
||||
parser.set_defaults(command=deprecated_command)
|
||||
|
||||
parser.add_argument(
|
||||
'--test',
|
||||
dest='tests',
|
||||
|
@ -190,6 +293,25 @@ def parse_args() -> Args:
|
|||
help='sanity test name',
|
||||
)
|
||||
|
||||
create_common_arguments(parser)
|
||||
|
||||
|
||||
def create_feature_parser(subparser) -> None:
|
||||
parser: argparse.ArgumentParser = subparser.add_parser('feature')
|
||||
parser.set_defaults(type=FeatureArgs)
|
||||
parser.set_defaults(command=feature_command)
|
||||
|
||||
parser.add_argument(
|
||||
'--source',
|
||||
type=pathlib.Path,
|
||||
default=pathlib.Path('issues.yml'),
|
||||
help='YAML file containing issue details (default: %(default)s)',
|
||||
)
|
||||
|
||||
create_common_arguments(parser)
|
||||
|
||||
|
||||
def create_common_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
'--create',
|
||||
action='store_true',
|
||||
|
@ -203,20 +325,20 @@ def parse_args() -> Args:
|
|||
help='verbose output',
|
||||
)
|
||||
|
||||
|
||||
def invoke_parser(parser: argparse.ArgumentParser) -> Args:
|
||||
if argcomplete:
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
if not parsed_args.tests:
|
||||
parsed_args.tests = list(TEST_OPTIONS)
|
||||
|
||||
kvp = {}
|
||||
args_type = parsed_args.type
|
||||
|
||||
for field in dataclasses.fields(Args):
|
||||
for field in dataclasses.fields(args_type):
|
||||
kvp[field.name] = getattr(parsed_args, field.name)
|
||||
|
||||
args = Args(**kvp)
|
||||
args = args_type(**kvp)
|
||||
|
||||
return args
|
||||
|
||||
|
@ -238,7 +360,7 @@ def run_sanity_test(test_name: str) -> list[str]:
|
|||
return messages
|
||||
|
||||
|
||||
def create_issues(test_type: t.Type[Deprecation], messages: list[str]) -> list[Issue]:
|
||||
def create_issues_from_deprecation_messages(test_type: t.Type[Deprecation], messages: list[str]) -> list[Issue]:
|
||||
deprecations = [test_type.parse(message) for message in messages]
|
||||
bug_reports = [deprecation.create_bug_report() for deprecation in deprecations]
|
||||
issues = [bug_report.create_issue(PROJECT) for bug_report in bug_reports]
|
||||
|
@ -251,14 +373,47 @@ def info(message: str) -> None:
|
|||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
args.run()
|
||||
|
||||
|
||||
def deprecated_command(args: DeprecationArgs) -> None:
|
||||
issues: list[Issue] = []
|
||||
|
||||
for test in args.tests:
|
||||
for test in args.tests or list(TEST_OPTIONS):
|
||||
test_type = TEST_OPTIONS[test]
|
||||
info(f'Running "{test}" sanity test...')
|
||||
messages = run_sanity_test(test)
|
||||
issues.extend(create_issues(test_type, messages))
|
||||
issues.extend(create_issues_from_deprecation_messages(test_type, messages))
|
||||
|
||||
create_issues(args, issues)
|
||||
|
||||
|
||||
def feature_command(args: FeatureArgs) -> None:
|
||||
with args.source.open() as source_file:
|
||||
source = yaml.safe_load(source_file)
|
||||
|
||||
default: dict[str, t.Any] = source.get('default', {})
|
||||
features: list[dict[str, t.Any]] = source.get('features', [])
|
||||
|
||||
if not isinstance(default, dict):
|
||||
raise RuntimeError('`default` must be `dict[str, ...]`')
|
||||
|
||||
if not isinstance(features, list):
|
||||
raise RuntimeError('`features` must be `list[dict[str, ...]]`')
|
||||
|
||||
issues: list[Issue] = []
|
||||
|
||||
for feature in features:
|
||||
data = default.copy()
|
||||
data.update(feature)
|
||||
|
||||
feature = Feature.from_dict(data)
|
||||
issues.append(feature.create_issue(PROJECT))
|
||||
|
||||
create_issues(args, issues)
|
||||
|
||||
|
||||
def create_issues(args: Args, issues: list[Issue]) -> None:
|
||||
if not issues:
|
||||
info('No issues found.')
|
||||
return
|
|
@ -49,7 +49,7 @@ def assemble_files_to_ship(complete_file_list):
|
|||
'hacking/cgroup_perf_recap_graph.py',
|
||||
'hacking/create_deprecated_issues.py',
|
||||
'hacking/deprecated_issue_template.md',
|
||||
'hacking/create_deprecation_bug_reports.py',
|
||||
'hacking/create-bulk-issues.py',
|
||||
'hacking/fix_test_syntax.py',
|
||||
'hacking/get_library.py',
|
||||
'hacking/metadata-tool.py',
|
||||
|
|
Loading…
Reference in New Issue