Skip to content

Commit

Permalink
Refactor managing issues
Browse files Browse the repository at this point in the history
Fetches the unique issue by single identifying label from Jira for each
prerequisite. This means there are multiple Jira API queries, but it is
much easier to understand then fetching group of issues using a common
label.
  • Loading branch information
hluk committed Jan 30, 2025
1 parent 4a8e41a commit e59c213
Show file tree
Hide file tree
Showing 16 changed files with 129 additions and 213 deletions.
8 changes: 4 additions & 4 deletions examples/rules/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
- target_date: "start_date - 7|days"
- rule: "Dependent Rule 1"
- rule: "Dependent Rule 2"
- jira_issue_id: main
- jira_issue_id: main_{{ release }}
template: "main.yaml.j2"
subtasks:
- id: add_beta_repos
- id: add_beta_repos_{{ release }}
template: "add_beta_repos.yaml.j2"
- id: notify_team
- id: notify_team_{{ release }}
template: "notify_team.yaml.j2"
- jira_issue_id: secondary
- jira_issue_id: secondary_{{ release }}
template: "secondary.yaml.j2"

- version: 1
Expand Down
31 changes: 4 additions & 27 deletions src/retasc/models/inputs/jira_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,25 @@

from retasc.models.inputs.base import InputBase

JIRA_REQUIRED_FIELDS = frozenset(["labels", "resolution"])


def get_issue_id(issue, label_prefix):
for label in issue["fields"]["labels"]:
if label.startswith(label_prefix):
return label[len(label_prefix) :]
return f"retasc-no-id-{issue['key']}"


def get_issues(jql: str, context) -> dict[str, dict]:
supported_fields = list(
JIRA_REQUIRED_FIELDS.union(context.config.jira_fields.values())
)
issues = context.jira.search_issues(jql=jql, fields=sorted(supported_fields))
return {
get_issue_id(issue, context.config.jira_label_prefix): issue for issue in issues
}


class JiraIssues(InputBase):
"""
Jira issues.
Adds the following template parameters if iterate_issues is true:
- jira_issue - issue data
- jira_issue_id - ReTaSC ID of the issue, based on label prefixed with
Config.jira_label_prefix
- jira_issues - all issues matching the JQL query
"""

jql: str = Field(description="JQL query for searching the issues")
fields: list = Field(description="Jira issues fields to fetch")

def values(self, context) -> Iterator[dict]:
issues = get_issues(self.jql, context)
for issue_id, data in issues.items():
issues = context.jira.search_issues(jql=self.jql, fields=self.fields)
for issue in issues:
yield {
"jira_issue_id": issue_id,
"jira_issue": data,
"jira_issue": issue,
"jira_issues": issues,
"jql": self.jql,
"managed_jira_issues": issues,
}

def section_name(self, values: dict) -> str:
Expand Down
19 changes: 0 additions & 19 deletions src/retasc/models/inputs/product_pages_releases.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import json
import re
from collections.abc import Iterator

from pydantic import Field

from retasc.models.inputs.base import InputBase
from retasc.models.inputs.jira_issues import get_issues

RE_VERSION = re.compile(r"^\w+-(?P<major>\d+)(?:[-.](?P<minor>\d+))?")

Expand All @@ -32,18 +30,9 @@ class ProductPagesReleases(InputBase):
- release - release short name
- major - major version number
- minor - minor version number
- jira_labels - common issue labels from jira_label_templates
- jira_issues - all issues matching the jira_labels
"""

product: str = Field(description="Product short name in Product Pages")
jira_label_templates: list[str] = Field(
description=(
"Label templates that need to be set for the managed issues in Jira."
'\nExample: ["retasc-managed", "retasc-managed-{{ release }}"]'
),
default_factory=list,
)

def values(self, context) -> Iterator[dict]:
releases = context.pp.active_releases(self.product)
Expand All @@ -55,14 +44,6 @@ def values(self, context) -> Iterator[dict]:
"major": major,
"minor": minor,
}
labels = [
context.template.render(template, **data)
for template in self.jira_label_templates
]
labels = [label for label in labels if label]
data["jira_labels"] = labels
jql = " AND ".join(f"labels={json.dumps(label)}" for label in labels)
data["jira_issues"] = get_issues(jql, context) if jql else {}
yield data

def section_name(self, values: dict) -> str:
Expand Down
10 changes: 8 additions & 2 deletions src/retasc/models/prerequisites/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ def validation_errors(
return []

def update_state(self, context) -> ReleaseRuleState:
"""Update template variables if needed and returns current state."""
"""
Called by parent rule to update any state and template variables.
Called only if no previous prerequisite returned Pending state.
Returns new prerequisite state.
"""
raise NotImplementedError()

def section_name(self) -> str:
def section_name(self, context) -> str:
"""Section name in report."""
raise NotImplementedError()
2 changes: 1 addition & 1 deletion src/retasc/models/prerequisites/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ def update_state(self, context) -> ReleaseRuleState:
context.report.set("result", is_completed)
return ReleaseRuleState.Completed if is_completed else ReleaseRuleState.Pending

def section_name(self) -> str:
def section_name(self, context) -> str:
return f"Condition({self.condition!r})"
78 changes: 50 additions & 28 deletions src/retasc/models/prerequisites/jira_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
from collections.abc import Iterator
from itertools import takewhile
from textwrap import dedent

from pydantic import BaseModel, Field

Expand All @@ -12,11 +13,19 @@

from .base import PrerequisiteBase

ISSUE_ID_DESCRIPTION = "Unique identifier for the issue."
ISSUE_ID_DESCRIPTION = dedent("""
Template for unique label name to identify the issue.
Note: The label in Jira will be prefixed with "jira_label_prefix"
configuration.
Example: "add_beta_repos_for_{{ release }}"
""").strip()
TEMPLATE_PATH_DESCRIPTION = (
"Path to the Jira issue template YAML file"
' relative to the "jira_template_path" configuration.'
)
JIRA_REQUIRED_FIELDS = frozenset(["labels", "resolution"])


def _is_resolved(issue: dict) -> bool:
Expand All @@ -35,8 +44,7 @@ def _edit_issue(
k: v for k, v in fields.items() if issue["fields"][k] != v and k != "labels"
}

required_labels = {label, *context.jira_labels}
labels = required_labels.union(fields.get("labels", []))
labels = {label, *fields.get("labels", [])}
current_labels = set(issue["fields"]["labels"])
if labels != current_labels:
to_update["labels"] = sorted(labels)
Expand All @@ -50,7 +58,8 @@ def _edit_issue(


def _report_jira_issue(issue: dict, jira_issue_id: str, context):
context.managed_jira_issues[jira_issue_id] = issue
jira_issues = context.template.params.setdefault("jira_issues", {})
jira_issues[jira_issue_id] = issue
context.report.set("issue", issue["key"])


Expand All @@ -59,7 +68,7 @@ def _create_issue(
) -> dict:
context.report.set("create", json.dumps(fields))
_set_parent_issue(fields, parent_issue_key)
fields.setdefault("labels", []).extend([label, *context.jira_labels])
fields.setdefault("labels", []).append(label)
return context.jira.create_issue(fields)


Expand Down Expand Up @@ -92,6 +101,15 @@ def _template_to_issue_data(template_data: dict, context, template: str) -> dict
return fields


def _render_issue_template(template: str, context) -> dict:
with open(context.config.jira_template_path / template) as f:
template_content = f.read()

content = context.template.render(template_content)
template_data = yaml().load(content)
return _template_to_issue_data(template_data, context, template)


def _update_issue(
jira_issue_id: str, template: str, context, parent_issue_key: str | None = None
) -> dict:
Expand All @@ -102,30 +120,31 @@ def _update_issue(
Returns the managed Jira issue.
"""
issue = context.jira_issues.get(jira_issue_id, None)
fields = _render_issue_template(template, context)

if issue:
_report_jira_issue(issue, jira_issue_id, context)
if _is_resolved(issue):
return issue
supported_fields = JIRA_REQUIRED_FIELDS.union(fields.keys())
label = f"{context.config.jira_label_prefix}{jira_issue_id}"
jql = f"labels={json.dumps(label)}"
issues = context.jira.search_issues(jql=jql, fields=sorted(supported_fields))

if issues:
if len(issues) > 1:
keys = to_comma_separated(issue["key"] for issue in issues)
raise RuntimeError(
f"Found multiple issues with the same ID label {label!r}: {keys}"
)

with open(context.config.jira_template_path / template) as f:
template_content = f.read()
issue = issues[0]

content = context.template.render(template_content)
template_data = yaml().load(content)
fields = _template_to_issue_data(template_data, context, template)

label = f"{context.config.jira_label_prefix}{jira_issue_id}"
if issue:
_edit_issue(
issue, fields, context, label=label, parent_issue_key=parent_issue_key
if not _is_resolved(issue):
_edit_issue(
issue, fields, context, label=label, parent_issue_key=parent_issue_key
)
else:
issue = _create_issue(
fields, context, label=label, parent_issue_key=parent_issue_key
)
return issue

issue = _create_issue(
fields, context, label=label, parent_issue_key=parent_issue_key
)
_report_jira_issue(issue, jira_issue_id, context)
return issue

Expand Down Expand Up @@ -188,20 +207,23 @@ def update_state(self, context) -> ReleaseRuleState:
If the issue exists or is created, it is added into "issues" dict
template parameter (dict key is jira_issue_id).
"""
issue = _update_issue(self.jira_issue_id, self.template, context)
jira_issue_id = context.template.render(self.jira_issue_id)
issue = _update_issue(jira_issue_id, self.template, context)
if _is_resolved(issue):
return ReleaseRuleState.Completed

for subtask in self.subtasks:
with context.report.section(f"Subtask({subtask.id!r})"):
subtask_id = context.template.render(subtask.id)
with context.report.section(f"Subtask({subtask_id!r})"):
_update_issue(
subtask.id, subtask.template, context, parent_issue_key=issue["key"]
)

return ReleaseRuleState.InProgress

def section_name(self) -> str:
return f"Jira({self.jira_issue_id!r})"
def section_name(self, context) -> str:
jira_issue_id = context.template.render(self.jira_issue_id)
return f"Jira({jira_issue_id!r})"


def templates_root() -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/retasc/models/prerequisites/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ def update_state(self, context) -> ReleaseRuleState:
rule = context.template.render(self.rule)
return context.rules[rule].update_state(context)

def section_name(self) -> str:
def section_name(self, context) -> 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 @@ -57,5 +57,5 @@ def update_state(self, context) -> ReleaseRuleState:
context.template.params.update(local_params)
return ReleaseRuleState.Completed

def section_name(self) -> str:
def section_name(self, context) -> str:
return f"Schedule({self.schedule_task!r})"
2 changes: 1 addition & 1 deletion src/retasc/models/prerequisites/target_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ def update_state(self, context) -> ReleaseRuleState:
return ReleaseRuleState.Pending
return ReleaseRuleState.Completed

def section_name(self) -> str:
def section_name(self, context) -> str:
return f"TargetDate({self.target_date!r})"
12 changes: 2 additions & 10 deletions src/retasc/models/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,7 @@


def default_inputs() -> list[Input]:
return [
ProductPagesReleases(
product="rhel",
jira_label_templates=[
"retasc-managed",
"retasc-release-{{ release }}",
],
)
]
return [ProductPagesReleases(product="rhel")]


class Rule(BaseModel):
Expand Down Expand Up @@ -68,7 +60,7 @@ def update_state(self, context) -> ReleaseRuleState:
if rule_state == ReleaseRuleState.Pending:
break

with context.report.section(prereq.section_name()):
with context.report.section(prereq.section_name(context)):
state = prereq.update_state(context)
if state != ReleaseRuleState.Completed:
context.report.set("state", state.name)
Expand Down
15 changes: 0 additions & 15 deletions src/retasc/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,6 @@ def iterate_rules(context: RuntimeContext) -> Iterator[tuple[dict, list[Rule]]]:
yield values, rules


def drop_issues(context: RuntimeContext):
to_drop = [
issue["key"]
for issue_id, issue in context.jira_issues.items()
if not issue["fields"]["resolution"]
and issue_id not in context.managed_jira_issues
]
if not to_drop:
return

context.report.set("dropped_issues", to_drop)


def run(*, config: Config, jira_token: str, dry_run: bool) -> Report:
session = requests_session()

Expand Down Expand Up @@ -92,8 +79,6 @@ def run(*, config: Config, jira_token: str, dry_run: bool) -> Report:
context.template.params = input.copy()
update_state(rule, context)

drop_issues(context)

if dry_run:
logger.warning("To apply changes, run without --dry-run flag")

Expand Down
12 changes: 0 additions & 12 deletions src/retasc/runtime_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,3 @@ class RuntimeContext:
config: Config

rules_states: dict[str, ReleaseRuleState] = field(default_factory=dict)

@property
def jira_issues(self) -> dict[str, dict]:
return self.template.params.setdefault("jira_issues", {})

@property
def managed_jira_issues(self) -> dict[str, dict]:
return self.template.params.setdefault("managed_jira_issues", {})

@property
def jira_labels(self) -> list[str]:
return self.template.params.setdefault("jira_labels", [])
Loading

0 comments on commit e59c213

Please sign in to comment.