diff --git a/buildscripts/monitor_build_status/cli.py b/buildscripts/monitor_build_status/cli.py index ce2015ef9fb..36cb551d437 100644 --- a/buildscripts/monitor_build_status/cli.py +++ b/buildscripts/monitor_build_status/cli.py @@ -218,7 +218,9 @@ class MonitorBuildStatusOrchestrator: for group_name in sorted(self.code_lockdown_config.get_all_group_names()): group_teams = self.code_lockdown_config.get_group_teams(group_name) - group_thresholds = notification_config.thresholds.group + group_thresholds = self.code_lockdown_config.get_group_thresholds( + group_name, notification_config.thresholds.group + ) group_slack_tags = self.code_lockdown_config.get_group_slack_tags(group_name) _process_thresholds( f"[Group] {group_name}", diff --git a/buildscripts/monitor_build_status/code_lockdown_config.py b/buildscripts/monitor_build_status/code_lockdown_config.py index 16a6531f562..dc1b2a91c41 100644 --- a/buildscripts/monitor_build_status/code_lockdown_config.py +++ b/buildscripts/monitor_build_status/code_lockdown_config.py @@ -60,6 +60,7 @@ class GroupConfig(BaseModel): name: str teams: list[str] slack_tags: Optional[list[str]] + thresholds: Optional[ThresholdOverride] = None class CodeLockdownConfig(BaseModel): @@ -121,6 +122,22 @@ class CodeLockdownConfig(BaseModel): return [] + def get_group_thresholds(self, group_name: str, defaults: IssueThresholds) -> IssueThresholds: + """ + Get group thresholds (or defaults if none set) + """ + + for group in self.groups: + if group.name == group_name and group.thresholds: + thresholds = deepcopy(defaults) + if group.thresholds.hot is not None: + thresholds.hot.count = group.thresholds.hot + if group.thresholds.cold is not None: + thresholds.cold.count = group.thresholds.cold + return thresholds + + return defaults + def get_team_thresholds(self, team_name: str, defaults: IssueThresholds) -> IssueThresholds: """ Get team thresholds (or defaults if none set) diff --git a/buildscripts/tests/monitor_build_status/test_cli.py b/buildscripts/tests/monitor_build_status/test_cli.py index 1b50f143dba..01c21a1a753 100644 --- a/buildscripts/tests/monitor_build_status/test_cli.py +++ b/buildscripts/tests/monitor_build_status/test_cli.py @@ -1,6 +1,21 @@ import unittest import buildscripts.monitor_build_status.cli as under_test +from buildscripts.monitor_build_status.code_lockdown_config import ( + CodeLockdownConfig, + GroupConfig, + IssueThresholds, + JiraQueriesConfig, + NotificationsConfig, + ScopesConfig, + SlackConfig, + TeamConfig, + ThresholdConfig, + ThresholdOverride, + ThresholdsConfig, +) +from buildscripts.monitor_build_status.issue_report import IssueCategory, IssueReport +from buildscripts.monitor_build_status.jira_service import IssueTuple class TestSummarize(unittest.TestCase): @@ -109,5 +124,97 @@ class TestSummarize(unittest.TestCase): self.assertEqual(summary, expected_summary) +def _make_notification_config(group_hot: int = 20, group_cold: int = 16) -> NotificationsConfig: + return NotificationsConfig( + scopes=[ScopesConfig(name="test", jira_queries=JiraQueriesConfig(hot="", cold=""))], + thresholds=ThresholdsConfig( + overall=IssueThresholds( + hot=ThresholdConfig(count=110, grace_period_days=0), + cold=ThresholdConfig(count=80, grace_period_days=0), + ), + group=IssueThresholds( + hot=ThresholdConfig(count=group_hot, grace_period_days=0), + cold=ThresholdConfig(count=group_cold, grace_period_days=0), + ), + team=IssueThresholds( + hot=ThresholdConfig(count=5, grace_period_days=0), + cold=ThresholdConfig(count=6, grace_period_days=0), + ), + ), + slack=SlackConfig(overall_scope_tags=[], message_footer=""), + ) + + +def _make_issue(key: str, team: str) -> IssueTuple: + return IssueTuple(key=key, assigned_team=team, team_assignment_duration_hours=999) + + +class TestGetIssueCountsStatusGroupThreshold(unittest.TestCase): + def _run( + self, + config: CodeLockdownConfig, + report: IssueReport, + notification_config: NotificationsConfig, + ): + orchestrator = under_test.MonitorBuildStatusOrchestrator( + jira_service=None, + code_lockdown_config=config, + slack_webhook_url=None, + ) + _, percentages, _ = orchestrator._get_issue_counts_status( + "test", report, notification_config + ) + return percentages + + def test_group_override_used_for_percentage_calculation(self): + # DTA group has override hot=50; 25 hot issues → 50%, not 125% (which would be 25/20) + config = CodeLockdownConfig( + notifications=[], + teams=[TeamConfig(name="Replication", slack_tags=None, thresholds=None)], + groups=[ + GroupConfig( + name="DTA", + teams=["Replication"], + slack_tags=None, + thresholds=ThresholdOverride(hot=50, cold=25), + ) + ], + ) + report = IssueReport.empty() + for i in range(25): + report.add_issue(IssueCategory.HOT, _make_issue(f"BF-{i}", "Replication")) + + percentages = self._run(config, report, _make_notification_config()) + + group_key = next(k for k in percentages if "[Group] DTA" in k) + hot_pct, cold_pct = percentages[group_key] + self.assertEqual(hot_pct, 50.0) # 25/50 * 100 + self.assertEqual(cold_pct, 0.0) + + def test_group_without_override_uses_default(self): + # Group has no override; 25 hot issues against default hot=20 → 125% + config = CodeLockdownConfig( + notifications=[], + teams=[TeamConfig(name="Query Execution", slack_tags=None, thresholds=None)], + groups=[ + GroupConfig( + name="Query", + teams=["Query Execution"], + slack_tags=None, + thresholds=None, + ) + ], + ) + report = IssueReport.empty() + for i in range(25): + report.add_issue(IssueCategory.HOT, _make_issue(f"BF-{i}", "Query Execution")) + + percentages = self._run(config, report, _make_notification_config(group_hot=20)) + + group_key = next(k for k in percentages if "[Group] Query" in k) + hot_pct, _ = percentages[group_key] + self.assertEqual(hot_pct, 125.0) # 25/20 * 100 + + if __name__ == "__main__": unittest.main() diff --git a/etc/code_lockdown.yml b/etc/code_lockdown.yml index e0b8272bd25..8c2ce8a6b29 100644 --- a/etc/code_lockdown.yml +++ b/etc/code_lockdown.yml @@ -110,6 +110,9 @@ groups: - "<@U0V2RMB1N>" # judah.schvimer@mongodb.com - Director - "<@U01ALSK50HH>" # sergi.mateo-bellido@mongodb.com - Director - name: "Durable Transactions & Availability" + thresholds: + hot: 50 + cold: 25 teams: - "RSSD" - "Replication"