summaryrefslogtreecommitdiff
path: root/hacking
diff options
context:
space:
mode:
authorMatt Clay <matt@mystile.com>2023-04-05 15:24:21 -0700
committerGitHub <noreply@github.com>2023-04-05 15:24:21 -0700
commit163f297d7f3235e2996c1b8f1ad4a5337b90745f (patch)
tree3dbfd022c443ece0c2ffa1f8629907f1c096e9cb /hacking
parentfc5c0aadc92837cda5d81cadd9402ef88a562455 (diff)
downloadansible-163f297d7f3235e2996c1b8f1ad4a5337b90745f.tar.gz
Extend bulk issue creator (#80423)
It can now be used to create feature requests, not just deprecation bug reports.
Diffstat (limited to 'hacking')
-rwxr-xr-xhacking/create-bulk-issues.py (renamed from hacking/create_deprecation_bug_reports.py)173
1 files changed, 164 insertions, 9 deletions
diff --git a/hacking/create_deprecation_bug_reports.py b/hacking/create-bulk-issues.py
index e14df4be38..d2651415df 100755
--- a/hacking/create_deprecation_bug_reports.py
+++ b/hacking/create-bulk-issues.py
@@ -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