Skip to content

Commit

Permalink
Move jira_issues to prerequisites
Browse files Browse the repository at this point in the history
  • Loading branch information
hluk committed Nov 18, 2024
1 parent 5e83e21 commit 3c62167
Show file tree
Hide file tree
Showing 19 changed files with 231 additions and 129 deletions.
7 changes: 2 additions & 5 deletions examples/rules/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,24 @@
target_date: "start_date - 7|days"
- rule: "Dependent Rule 1"
- rule: "Dependent Rule 2"
jira_issues:
- id: main
- jira_issue_id: main
template: "examples/jira/main.yaml"
subtasks:
- id: add_beta_repos
template: "examples/jira/add_beta_repos.yaml"
- id: notify_team
template: "examples/jira/notify_team.yaml"
- id: secondary
- jira_issue_id: secondary
template: "examples/jira/secondary.yaml"

- version: 1
name: "Dependent Rule 1"
prerequisites:
- schedule_task: "TASK"
target_date: "start_date - 3|weeks"
jira_issues: []

- version: 1
name: "Dependent Rule 2"
prerequisites:
- schedule_task: "TASK"
target_date: "start_date - 2|weeks"
jira_issues: []
16 changes: 0 additions & 16 deletions src/retasc/models/jira_issue.py

This file was deleted.

34 changes: 2 additions & 32 deletions src/retasc/models/parse_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import logging
import os
from collections import defaultdict
from collections.abc import Iterator
from dataclasses import dataclass, field
from glob import iglob
from itertools import chain
Expand All @@ -11,14 +10,11 @@
from pydantic import ValidationError

from retasc.models.rule import Rule
from retasc.utils import to_comma_separated

logger = logging.getLogger(__name__)


def to_comma_separated(items: list) -> str:
return ", ".join(sorted(repr(str(x)) for x in items))


class RuleParsingError(RuntimeError):
pass

Expand All @@ -40,17 +36,6 @@ def parse_yaml_objects(rule_file: str) -> list[dict]:
return [data]


def template_filenames(rule: Rule) -> Iterator[str]:
for issue in rule.jira_issues:
yield issue.template
yield from (x.template for x in issue.subtasks)


def template_paths(rule: Rule, templates_path: str) -> Iterator[str]:
for file in template_filenames(rule):
yield f"{templates_path}/{file}"


@dataclass
class ParseState:
"""Keeps state for parsing and validation."""
Expand Down Expand Up @@ -98,28 +83,14 @@ def validate_existing_dependent_rules(self) -> None:
if errors:
self._add_invalid_rule_error(rule, "\n ".join(errors))

def validate_existing_jira_templates(self, templates_path: str) -> None:
for rule in self.rules.values():
missing_files = [
file
for file in template_paths(rule, templates_path)
if not os.path.isfile(file)
]
if missing_files:
file_list = to_comma_separated(missing_files)
self._add_invalid_rule_error(
rule,
f"Jira issue template files not found: {file_list}",
)

def _add_invalid_rule_error(self, rule: Rule, error: str) -> None:
filename = self.rule_files[rule.name][0]
self.errors.append(
f"Invalid rule {rule.name!r} (file {filename!r}):\n {error}"
)


def parse_rules(path: str, templates_path: str = ".") -> dict[str, Rule]:
def parse_rules(path: str) -> dict[str, Rule]:
"""
Parses rules in path recursively to dict with rule name as key and the rule
as value.
Expand All @@ -131,7 +102,6 @@ def parse_rules(path: str, templates_path: str = ".") -> dict[str, Rule]:

state.validate_unique_rule_names()
state.validate_existing_dependent_rules()
state.validate_existing_jira_templates(templates_path)

if state.errors:
error_list = "\n".join(state.errors)
Expand Down
8 changes: 7 additions & 1 deletion src/retasc/models/prerequisites/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from .condition import PrerequisiteCondition
from .jira_issue import PrerequisiteJiraIssue
from .rule import PrerequisiteRule
from .schedule import PrerequisiteSchedule

type Prerequisite = PrerequisiteCondition | PrerequisiteSchedule | PrerequisiteRule
type Prerequisite = (
PrerequisiteCondition
| PrerequisiteSchedule
| PrerequisiteRule
| PrerequisiteJiraIssue
)
2 changes: 1 addition & 1 deletion src/retasc/models/prerequisites/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def validation_errors(self, rules) -> list[str]:
"""Return validation errors if any."""
return []

def state(self, context) -> ReleaseRuleState:
def update_state(self, context) -> ReleaseRuleState:
"""Update template variables if needed and returns current state."""
raise NotImplementedError()

Expand Down
2 changes: 1 addition & 1 deletion src/retasc/models/prerequisites/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class PrerequisiteCondition(PrerequisiteBase):
""").strip()
)

def state(self, context) -> ReleaseRuleState:
def update_state(self, context) -> ReleaseRuleState:
is_completed = context.template.evaluate(self.condition)
context.report.set("result", is_completed)
return ReleaseRuleState.Completed if is_completed else ReleaseRuleState.Pending
Expand Down
93 changes: 93 additions & 0 deletions src/retasc/models/prerequisites/jira_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import os
from collections.abc import Iterator
from itertools import takewhile

from pydantic import BaseModel, Field

from retasc.models.release_rule_state import ReleaseRuleState
from retasc.utils import to_comma_separated

from .base import PrerequisiteBase


class JiraIssueTemplate(BaseModel):
id: str = Field(description="Unique identifier for the issue.")
template: str = Field(description="Path to the Jira issue template YAML file")


class PrerequisiteJiraIssue(PrerequisiteBase):
"""Prerequisite Rule."""

jira_issue_id: str = Field(description="Unique identifier for the issue.")
template: str = Field(description="Path to the Jira issue template YAML file")
subtasks: list[JiraIssueTemplate] = Field(default_factory=list)

def validation_errors(self, rules) -> list[str]:
errors = []

missing_files = {
file for file in template_paths(self) if not os.path.isfile(file)
}
if missing_files:
file_list = to_comma_separated(missing_files)
errors.append(f"Jira issue template files not found: {file_list}")

own_issue_ids = set(jira_issue_ids(self))
preceding_issue_ids = {
issue_id
for prereq in takewhile(
lambda x: x is not self, jira_issue_prerequisites(rules)
)
for issue_id in jira_issue_ids(prereq)
}
duplicate_issue_ids = own_issue_ids.intersection(preceding_issue_ids)
if duplicate_issue_ids:
id_list = to_comma_separated(duplicate_issue_ids)
errors.append(f"Jira issue ID(s) already used elsewhere: {id_list}")

return errors

def update_state(self, context) -> ReleaseRuleState:
"""Return Completed only if all rules were closed."""
label = f"retasc-id-{self.jira_issue_id}"
if label in context.closed_issue_labels:
return ReleaseRuleState.Completed
# TODO: Create new or update existing issue from the template if
# context.prerequisites_state is ReleaseRuleState.InProgress
return ReleaseRuleState.InProgress

def section_name(self) -> str:
return f"jira: {self.jira_issue_id!r}"


def templates_root() -> str:
return os.getenv("RETASC_JIRA_TEMPLATES_ROOT", ".")


def template_filenames(prereq: PrerequisiteJiraIssue) -> Iterator[str]:
yield prereq.template
for x in prereq.subtasks:
yield x.template


def template_paths(prereq: PrerequisiteJiraIssue) -> Iterator[str]:
root = templates_root()
for file in template_filenames(prereq):
yield f"{root}/{file}"


def jira_issue_ids(prereq: PrerequisiteJiraIssue) -> Iterator[str]:
yield prereq.jira_issue_id
for x in prereq.subtasks:
yield x.id


def jira_issue_prerequisites(rules):
for rule in rules:
for prereq in rule.prerequisites:
if isinstance(prereq, PrerequisiteJiraIssue):
yield prereq
# Ignore this from coverage since rules is always non-empty and the
# iteration always stops at a specific prerequisite.
return # pragma: no cover
4 changes: 2 additions & 2 deletions src/retasc/models/prerequisites/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ def validation_errors(self, rules) -> list[str]:
return [f"Dependent rule does not exist: {self.rule!r}"]
return []

def state(self, context) -> ReleaseRuleState:
def update_state(self, context) -> ReleaseRuleState:
"""Return Completed only if all rules were closed."""
rule = context.template.render(self.rule)
return context.rules[rule].state(context=context)
return context.rules[rule].update_state(context=context)

def section_name(self) -> str:
return f"rule: {self.rule!r}"
2 changes: 1 addition & 1 deletion src/retasc/models/prerequisites/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def _params(self, context) -> dict:
)
return local_params

def state(self, context) -> ReleaseRuleState:
def update_state(self, context) -> ReleaseRuleState:
"""
Fetch schedule and given task, update templating parameters and
return Completed.
Expand Down
23 changes: 4 additions & 19 deletions src/retasc/models/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from pydantic import BaseModel, Field

from retasc.models.jira_issue import JiraIssueTemplate
from retasc.models.prerequisites import Prerequisite
from retasc.models.release_rule_state import ReleaseRuleState

Expand All @@ -24,9 +23,6 @@ class Config:
prerequisites: list[Prerequisite] = Field(
description="The prerequisites for the rule."
)
jira_issues: list[JiraIssueTemplate] = Field(
description="The jira issues to create and manager for the rule."
)
products: list[str] = Field(
description="Affected Product Pages product short names",
default_factory=lambda: ["rhel"],
Expand All @@ -40,26 +36,15 @@ def __hash__(self):
return hash(self.name)

@cache
def state(self, context) -> ReleaseRuleState:
def update_state(self, context) -> ReleaseRuleState:
"""
Return Completed only if all issues were closed, otherwise returns
Pending if any prerequisites are Pending, and InProgress in other
cases.
"""
pending_issues = [
template.label
for template in self.jira_issues
if template.label not in context.closed_issue_labels
]
if not pending_issues:
return ReleaseRuleState.Completed

context.report.set("pending_issues", pending_issues)

result = ReleaseRuleState.InProgress
for prereq in self.prerequisites:
with context.report.section(prereq.section_name()):
state = prereq.state(context)
state = prereq.update_state(context)
context.report.set("state", state.name)
result = min(result, state)
return result
context.prerequisites_state = min(context.prerequisites_state, state)
return context.prerequisites_state
9 changes: 5 additions & 4 deletions src/retasc/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from retasc.jira_client import JiraClient
from retasc.models.parse_rules import parse_rules
from retasc.models.release_rule_state import ReleaseRuleState
from retasc.models.rule import Rule
from retasc.product_pages_api import ProductPagesApi
from retasc.report import Report
Expand Down Expand Up @@ -39,11 +40,11 @@ def parse_version(release: str) -> tuple[int, int]:
return int(x["major"]), int(x["minor"])


def update_jira_issues(rule: Rule, context: RuntimeContext):
def update_state(rule: Rule, context: RuntimeContext):
with context.report.section(rule.name):
state = rule.state(context)
context.prerequisites_state = ReleaseRuleState.Completed
state = rule.update_state(context)
context.report.set("state", state.name)
# TODO: Update or create Jira issues according to the state.


def iterate_rules(context: RuntimeContext) -> Iterator[tuple[str, str, list[Rule]]]:
Expand Down Expand Up @@ -88,7 +89,7 @@ def run(*, dry_run: bool) -> Report:
"minor": minor,
**rule.params,
}
update_jira_issues(rule, context)
update_state(rule, context)

if dry_run:
logger.warning("To apply changes, run without --dry-run flag")
Expand Down
2 changes: 2 additions & 0 deletions src/retasc/runtime_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from requests import Session

from retasc.jira_client import JiraClient
from retasc.models.release_rule_state import ReleaseRuleState
from retasc.models.rule import Rule
from retasc.product_pages_api import ProductPagesApi
from retasc.report import Report
Expand All @@ -22,6 +23,7 @@ class RuntimeContext:
template: TemplateManager
session: Session
report: Report
prerequisites_state: ReleaseRuleState = ReleaseRuleState.Pending

release: str = ""

Expand Down
3 changes: 3 additions & 0 deletions src/retasc/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-3.0-or-later
def to_comma_separated(items: list) -> str:
return ", ".join(sorted(repr(str(x)) for x in items))
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ def rule_dict():
{"schedule_task": "GA for rhel {{ major }}.{{ minor }}"},
{"condition": "today >= start_date + 5|days"},
],
"jira_issues": [],
}


Expand Down Expand Up @@ -53,8 +52,9 @@ def mock_env(monkeypatch):

@fixture(autouse=True)
def mock_jira():
with patch("retasc.run.JiraClient", autospec=True) as mock:
mock(ANY, token=ANY, session=ANY).search_issues.return_value = []
with patch("retasc.run.JiraClient", autospec=True) as mock_cls:
mock = mock_cls(ANY, token=ANY, session=ANY)
mock.search_issues.return_value = []
yield mock


Expand Down
Loading

0 comments on commit 3c62167

Please sign in to comment.