diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b713a3..ae8841f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,7 @@ repos: - "click>=8.0.3,!=8.1.4" - "attrs>=20.3.0" - "ruamel.yaml>=0.16.6" + - "jinja2>=2.11.3" pass_filenames: false args: [--config-file=pyproject.toml] diff --git a/component-config.yaml.sample b/component-config.yaml.sample new file mode 100644 index 0000000..4e59210 --- /dev/null +++ b/component-config.yaml.sample @@ -0,0 +1,39 @@ +issues: + - summary: "Errata respin {{ ERRATUM.respin_count }}" + description: "{{ ERRATUM.srpms }}" + assignee: '{{ ERRATUM.people_assigned_to }}' + type: task + id: errata_task + parent: errata_epic + on_respin: close + + - summary: "Testing ER#{{ ERRATUM.event.id }} {{ ERRATUM.summary }}" + description: "{{ ERRATUM.url }}" + assignee: '{{ ERRATUM.people_assigned_to }}' + type: epic + id: errata_epic + on_respin: keep + + - summary: "Errata filelist check" + description: "Compare errata filelist with a previously released advisory" + assignee: '{{ ERRATUM.people_assigned_to }}' + type: subtask + id: subtask_filelist + parent: errata_task + on_respin: close + + - summary: "SPEC file review" + description: "Review changes made in the SPEC file" + assignee: '{{ ERRATUM.people_assigned_to }}' + type: subtask + id: subtask_spec + parent: errata_task + on_respin: close + + - summary: "rpminspect review" + description: "Review rpminspect results in the CI Dashboard for all builds" + assignee: '{{ ERRATUM.people_assigned_to }}' + type: subtask + id: subtask_rpminspect + parent: errata_task + on_respin: close diff --git a/newa/__init__.py b/newa/__init__.py index 4a25f62..8f76fd7 100644 --- a/newa/__init__.py +++ b/newa/__init__.py @@ -1,9 +1,10 @@ import io from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, Any, Optional, TypeVar import attrs +import jinja2 import ruamel.yaml import ruamel.yaml.nodes import ruamel.yaml.representer @@ -40,6 +41,47 @@ def _represent_enum( return yaml +def default_template_environment() -> jinja2.Environment: + """ + Create a Jinja2 environment with default settings. + + Adds common filters, and enables block trimming and left strip. + """ + + environment = jinja2.Environment() + + environment.trim_blocks = True + environment.lstrip_blocks = True + + return environment + + +def render_template( + template: str, + environment: Optional[jinja2.Environment] = None, + **variables: Any, + ) -> str: + """ + Render a template. + + :param template: template to render. + :param environment: Jinja2 environment to use. + :param variables: variables to pass to the template. + """ + + environment = environment or default_template_environment() + + try: + return environment.from_string(template).render(**variables).strip() + + except jinja2.exceptions.TemplateSyntaxError as exc: + raise Exception( + f"Could not parse template at line {exc.lineno}.") from exc + + except jinja2.exceptions.TemplateError as exc: + raise Exception("Could not render template.") from exc + + class EventType(Enum): """ Event types """ @@ -127,3 +169,37 @@ class ErratumJob(Job): @property def id(self) -> str: return f'{self.event.id} @ {self.erratum.release}' + + +# +# Component configuration +# +class IssueType(Enum): + EPIC = 'epic' + TASK = 'task' + SUBTASK = 'subtask' + + +class OnRespinAction(Enum): + # TODO: what's the default? It would simplify the class a bit. + KEEP = 'keep' + CLOSE = 'close' + + +@define +class IssueAction: # type: ignore[no-untyped-def] + summary: str + description: str + assignee: str + id: str + on_respin: Optional[OnRespinAction] = field( # type: ignore[var-annotated] + converter=lambda value: OnRespinAction(value) if value else None) + type: IssueType = field(converter=IssueType) + parent: Optional[str] = None + + +@define +class ErratumConfig(Serializable): + issues: list[IssueAction] = field( # type: ignore[var-annotated] + factory=list, converter=lambda issues: [ + IssueAction(**issue) for issue in issues]) diff --git a/newa/cli.py b/newa/cli.py index fb87edb..fd47493 100644 --- a/newa/cli.py +++ b/newa/cli.py @@ -2,11 +2,12 @@ import os.path from collections.abc import Iterable, Iterator from pathlib import Path +from typing import Any import click from attrs import define -from . import ErratumJob +from . import ErratumConfig, ErratumJob, render_template logging.basicConfig( format='%(asctime)s %(message)s', @@ -94,14 +95,42 @@ def cmd_jira(ctx: CLIContext) -> None: for erratum_job in ctx.load_erratum_jobs('event-'): # read Jira issue configuration - # get list of matching actions - - # for action in actions: - # create epic - # or create task - # or create sutask - # if subtask assoc. with recipes - # clone object with yaml + config = ErratumConfig.from_yaml_file(Path('component-config.yaml.sample')) + + # TODO: record created/existing issues. Instead of `Any`, maybe something + # from the Jira library would be stored. Or just a Jira ticket ID. + known_issues: dict[str, Any] = {} + + # Iterate over issue actions. Take one, if it's not possible to finish it, + # put it back at the end of the queue. + issue_actions = config.issues[:] + + while issue_actions: + action = issue_actions.pop(0) + + print(f'* Would create a {action.type.name} issue:') + print(f' summary: {action.summary}') + print(f' summary: {action.description}') + + if action.id in known_issues: + raise Exception(f'Issue "{action.id}" is already created!') + + if action.parent and action.parent not in known_issues: + print(f' !! Parent issue, "{action.parent}", is unknown, will try later') + print() + + issue_actions.append(action) + continue + + print() + print(f' Issue would be assigned to {action.assignee}.') + print(f' rendered: >>{render_template(action.assignee, ERRATUM=erratum_job)}<<') + print(f' Will remember the issue as `{action.id}`.') + if action.parent: + print(f' Issue would have issue `{action.parent}` as its parent.') + print() + + known_issues[action.id] = True # erratum_job.issue = ... # what's recipe? doesn't it belong to "schedule"? diff --git a/pyproject.toml b/pyproject.toml index 77e87a7..2777316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,8 @@ keywords = [ dependencies = [ "click>=8.0.3,!=8.1.4", "attrs>=20.3.0", - "ruamel.yaml>=0.16.6" + "ruamel.yaml>=0.16.6", + "jinja2>=2.11.3" ] [project.optional-dependencies]