Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class JiraLabels(Enum):

REBASED = "jotnar_rebased"
BACKPORTED = "jotnar_backported"
MERGED = "jotnar_merged"

REBASE_ERRORED = "jotnar_rebase_errored"
BACKPORT_ERRORED = "jotnar_backport_errored"
Expand Down
3 changes: 3 additions & 0 deletions supervisor/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from datetime import datetime, timezone

# Compares correctly - all our dates are tz-aware
DATETIME_MIN_UTC = datetime.min.replace(tzinfo=timezone.utc)
# Groups within the redhat organization where we can find issues
GITLAB_GROUPS = ["rhel/rpms", "centos-stream/rpms"]
3 changes: 2 additions & 1 deletion supervisor/gitlab_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def search_gitlab_project_mrs(
logger.debug("Searching for MRs for %s in %s", issue_key, project)
path = f"projects/{urlquote(project, safe='')}/merge_requests"

params = {"search": issue_key, "view": "simple"}
params = {"search": issue_key}
if state is not None:
params["state"] = state

Expand All @@ -67,4 +67,5 @@ def search_gitlab_project_mrs(
title=mr["title"],
state=mr["state"],
description=mr["description"],
merged_at=mr["merged_at"],
)
141 changes: 130 additions & 11 deletions supervisor/issue_handler.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
from datetime import datetime, timezone, timedelta
import logging

from common.constants import JiraLabels

from .constants import DATETIME_MIN_UTC, GITLAB_GROUPS
from .errata_utils import get_erratum_for_link
from .gitlab_utils import search_gitlab_project_mrs
from .work_item_handler import WorkItemHandler
from .jira_utils import add_issue_label, change_issue_status
from .supervisor_types import (
FullIssue,
IssueStatus,
MergeRequestState,
PreliminaryTesting,
TestCoverage,
TestingState,
WorkflowResult,
)
from .testing_analyst import analyze_issue


logger = logging.getLogger(__name__)


Expand All @@ -40,29 +44,116 @@ def resolve_set_status(self, status: IssueStatus, why: str):
def resolve_flag_attention(self, why: str):
add_issue_label(
self.issue.key,
"jotnar_needs_attention",
JiraLabels.NEEDS_ATTENTION.value,
why,
dry_run=self.dry_run,
)

return WorkflowResult(status=why, reschedule_in=-1)

async def run(self) -> WorkflowResult:
"""
Runs the workflow for a single issue.
def label_merge_if_needed(self):
"""Add the jotnar_merged label to the issue

This function will only add jotnar_merged label to the issue if it matches
all the following requirements:
1. Issue has either jotnar_backported or jotnar_rebased label.
2. Issue doesn't have jotnar_merged label.
3. A merged MR is found on Gitlab.

Returns:
True if a merge gitlab issue was found and the merged label was added,
otherwise, return False.
"""
issue = self.issue
component = issue.components[0]

if (
JiraLabels.BACKPORTED.value in issue.labels
or JiraLabels.REBASED.value in issue.labels
) and JiraLabels.MERGED.value not in issue.labels:
for group in GITLAB_GROUPS:
merged_mrs = search_gitlab_project_mrs(
f"redhat/{group}/{component}",
issue.key,
state=MergeRequestState.MERGED,
)

logger.info("Running workflow for issue %s", issue.url)
if merged_mr := next(merged_mrs, None):
add_issue_label(
issue.key,
JiraLabels.MERGED.value,
f"A [merge request| {merged_mr.url}]. resolving this issue has been merged; waiting for errata creation and final testing.",
dry_run=self.dry_run,
)

issue.labels.append(JiraLabels.MERGED.value)
return True

if "jotnar_needs_attention" in issue.labels:
return False

def get_latest_merged_timestamp(self):
"""This function will return DATETIME_MIN_UTC if it doesn't find any merged MRs"""
issue = self.issue
component = issue.components[0]

def get_merged_mrs():
for group in GITLAB_GROUPS:
project = f"redhat/{group}/{component}"
yield from search_gitlab_project_mrs(
project,
issue.key,
state=MergeRequestState.MERGED,
)

return max(
(mr.merged_at or DATETIME_MIN_UTC for mr in get_merged_mrs()),
default=DATETIME_MIN_UTC,
)

async def run_before_errata_created(self) -> WorkflowResult:
"""Workflow for issues with no errata link"""
issue = self.issue

if not any(
label
in (
JiraLabels.BACKPORTED.value,
JiraLabels.REBASED.value,
JiraLabels.MERGED.value,
)
for label in issue.labels
):
return self.resolve_remove_work_item(
"Issue has the jotnar_needs_attention label"
f"Issue without target labels: {issue.labels}"
)

if issue.errata_link is None:
return self.resolve_remove_work_item("Issue has no errata_link")
if JiraLabels.MERGED.value not in issue.labels:
self.label_merge_if_needed()

if JiraLabels.MERGED.value not in issue.labels:
return self.resolve_wait(
"No merged MR found, reschedule it for 3 hours",
reschedule_in=60 * 60 * 3,
)

latest_merged_timestamp = self.get_latest_merged_timestamp()
cur_time = datetime.now(tz=timezone.utc)
time_diff = abs(cur_time - latest_merged_timestamp)
if time_diff < timedelta(days=1):
return self.resolve_wait(
"Wait for the associated erratum to be created",
reschedule_in=60 * 60,
)
else:
return self.resolve_flag_attention(
"A merge request was merged for this issue more than 24 hours ago but no errata "
"was created. Please investigate and look for gating failures or other reasons that "
"might have blocked errata creation."
)

async def run_after_errata_created(self) -> WorkflowResult:
issue = self.issue
assert issue.errata_link is not None
if issue.fixed_in_build is None:
return self.resolve_flag_attention(
"Issue has errata_link but no fixed_in_build"
Expand All @@ -80,6 +171,10 @@ async def run(self) -> WorkflowResult:
"happened before the gitlab pull request was merged"
)

# We still want the jotnar_merged label for JIRA dashboards even if we never saw
# the merged merge request in the pre-errata-creation state.
self.label_merge_if_needed()

if issue.status in (
IssueStatus.NEW,
IssueStatus.PLANNING,
Expand Down Expand Up @@ -121,3 +216,27 @@ async def run(self) -> WorkflowResult:
return self.resolve_remove_work_item(f"Issue status is {issue.status}")
else:
raise ValueError(f"Unknown issue status: {issue.status}")

async def run(self) -> WorkflowResult:
"""
Runs the workflow for a single issue.
"""
issue = self.issue

logger.info("Running workflow for issue %s", issue.url)

if JiraLabels.NEEDS_ATTENTION.value in issue.labels:
return self.resolve_remove_work_item(
"Issue has the jotnar_needs_attention label"
)

if len(issue.components) != 1:
return self.resolve_flag_attention(
"This issue has multiple components. "
"Jotnar only handles issues with single component currently."
)

if issue.errata_link is None:
return await self.run_before_errata_created()
else:
return await self.run_after_errata_created()
1 change: 1 addition & 0 deletions supervisor/supervisor_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class MergeRequest(BaseModel):
title: str
description: str
state: MergeRequestState
merged_at: datetime | None


class Issue(BaseModel):
Expand Down
7 changes: 5 additions & 2 deletions supervisor/work_item_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ def __init__(self, dry_run: bool = False):
def resolve_remove_work_item(self, why: str):
return WorkflowResult(status=why, reschedule_in=-1)

def resolve_wait(self, why: str):
return WorkflowResult(status=why, reschedule_in=WAIT_DELAY)
def resolve_wait(self, why: str, *, reschedule_in: float = WAIT_DELAY):
return WorkflowResult(
status=why,
reschedule_in=reschedule_in,
)

@abstractmethod
async def run(self) -> WorkflowResult:
Expand Down