• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / relman-auto-nag / #4704

pending completion
#4704

push

coveralls-python

web-flow
[tracked_attention] Fix the reminder interval (#2200)

716 of 3574 branches covered (20.03%)

13 of 13 new or added lines in 1 file covered. (100.0%)

1925 of 8741 relevant lines covered (22.02%)

0.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/bugbot/rules/tracked_attention.py
1
# This Source Code Form is subject to the terms of the Mozilla Public
2
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
# You can obtain one at http://mozilla.org/MPL/2.0/.
4

5
from typing import Optional
×
6

7
import humanize
×
8
from libmozdata import utils as lmdutils
×
9
from libmozdata.fx_trains import FirefoxTrains
×
10

11
from bugbot import utils
×
12
from bugbot.bzcleaner import BzCleaner
×
13
from bugbot.constants import LOW_PRIORITY, LOW_SEVERITY
×
14
from bugbot.history import History
×
15
from bugbot.team_managers import TeamManagers
×
16

17

18
class TrackedAttention(BzCleaner):
×
19
    """Tracked bugs that need attention"""
20

21
    def __init__(
×
22
        self,
23
        target_channels: tuple = ("esr", "release", "beta", "nightly"),
24
        show_soft_freeze_days: int = 14,
25
        reminder_interval: int = 5,
26
    ):
27
        """Constructor
28

29
        Args:
30
            target_channels: the list of channels that we target to find tracked
31
                and unassigned bugs.
32
            show_soft_freeze_days: number of days before the soft freeze date to
33
                start showing the soft freeze comment in the needinfo requests.
34
            reminder_interval: number of days to wait before posting a reminder
35
                comment. We remind only if the bug is not assigned, it is not a
36
                weekend, and we are close to the soft-freeze date.
37
        """
38
        super().__init__()
×
39
        if not self.init_versions():
×
40
            return
×
41

42
        self.team_managers = TeamManagers()
×
43

44
        schedule = FirefoxTrains().get_release_schedule("nightly")
×
45
        soft_freeze_date = lmdutils.get_date_ymd(schedule["soft_code_freeze"])
×
46
        today = lmdutils.get_date_ymd("today")
×
47
        soft_freeze_delta = soft_freeze_date - today
×
48

49
        self.is_soft_freeze_soon = 0 < soft_freeze_delta.days <= show_soft_freeze_days
×
50
        self.soft_freeze_delta = (
×
51
            "today"
52
            if soft_freeze_delta.days == 0
53
            else f"in { humanize.naturaldelta(soft_freeze_delta)}"
54
        )
55
        self.extra_ni = {
×
56
            "soft_freeze_delta": self.soft_freeze_delta,
57
        }
58

59
        # Determine the date to decide if a bug will receive a reminder comment
60
        self.reminder_comment_date = lmdutils.get_date(today, reminder_interval)
×
61
        self.is_weekend = utils.is_weekend(today)
×
62

63
        self.version_flags = [
×
64
            {
65
                "version": self.versions[channel],
66
                "channel": channel,
67
                "tracking_field": utils.get_flag(
68
                    self.versions[channel], "tracking", channel
69
                ),
70
                "status_field": utils.get_flag(
71
                    self.versions[channel], "status", channel
72
                ),
73
            }
74
            for channel in target_channels
75
        ]
76

77
    def description(self):
×
78
        return "Tracked bugs that need attention"
×
79

80
    def get_extra_for_needinfo_template(self):
×
81
        return self.extra_ni
×
82

83
    def columns(self):
×
84
        return [
×
85
            "id",
86
            "summary",
87
            "tracking_statuses",
88
            "is_regression",
89
            "reasons",
90
            "action",
91
        ]
92

93
    def handle_bug(self, bug, data):
×
94
        is_no_assignee = utils.is_no_assignee(bug["assigned_to"])
×
95
        last_comment = self._get_last_comment(bug)
×
96

97
        # If we commented before, we want to send reminders when we are close to
98
        # the soft freeze.
99
        is_reminder = bool(last_comment)
×
100
        if is_reminder:
×
101
            if self.is_weekend or not is_no_assignee or not self.is_soft_freeze_soon:
×
102
                return None
×
103

104
            # Post reminders based on the configured interval
105
            last_reminder_comment = self._get_last_reminder_comment(bug)
×
106
            last_comment_time = (
×
107
                last_reminder_comment["time"]
108
                if last_reminder_comment
109
                else last_comment["time"]
110
            )
111
            if last_comment_time > self.reminder_comment_date:
×
112
                return None
×
113

114
        bugid = str(bug["id"])
×
115

116
        def format_flag(flag: dict) -> str:
×
117
            tracking_type = (
×
118
                "tracked for" if bug[flag["tracking_field"]] == "+" else "blocking"
119
            )
120
            version = flag["version"]
×
121
            channel = flag["channel"]
×
122
            return f"{tracking_type} firefox{version} ({channel})"
×
123

124
        tracking_statuses = [
×
125
            format_flag(flag)
126
            for flag in self.version_flags
127
            if bug.get(flag["tracking_field"]) in ("blocking", "+")
128
            and bug.get(flag["status_field"]) in ("affected", "---")
129
        ]
130
        assert tracking_statuses
×
131

132
        reasons = []
×
133
        solutions = []
×
134
        if is_no_assignee:
×
135
            reasons.append("isn't assigned")
×
136
            solutions.append("find an assignee")
×
137
        if bug["priority"] in LOW_PRIORITY:
×
138
            reasons.append("has low priority")
×
139
            solutions.append("increase the priority")
×
140
        if not is_reminder and bug["severity"] in LOW_SEVERITY:
×
141
            reasons.append("has low severity")
×
142
            solutions.append("increase the severity")
×
143
        assert reasons and solutions
×
144

145
        # We are using the regressed_by field to identify regression instead of
146
        # using the regression keyword because we want to suggesting backout. We
147
        # can only suggest backout if we know the exact cause of the regression.
148
        is_regression = bool(bug["regressed_by"])
×
149

150
        # This is a workaround to pass the information to get_mail_to_auto_ni()
151
        bug["is_reminder"] = is_reminder
×
152

153
        data[bugid] = {
×
154
            "tracking_statuses": tracking_statuses,
155
            "reasons": reasons,
156
            "is_regression": is_regression,
157
            "action": "Reminder comment" if is_reminder else "Needinfo",
158
        }
159

160
        if is_reminder:
×
161
            assert self.is_soft_freeze_soon
×
162
            comment_num = last_comment["count"]
×
163
            self.autofix_changes[bugid] = {
×
164
                "comment": {
165
                    "body": (
166
                        f"This is a reminder regarding comment #{comment_num}!\n\n"
167
                        f"The bug is marked as { utils.english_list(tracking_statuses) }. "
168
                        "We have limited time to fix this, "
169
                        f"the soft freeze is { self.soft_freeze_delta }. "
170
                        f"However, the bug still { utils.english_list(reasons) }."
171
                    )
172
                },
173
            }
174
        else:
175
            need_action = is_no_assignee or bug["priority"] in LOW_PRIORITY
×
176
            self.extra_ni[bugid] = {
×
177
                "tracking_statuses": utils.english_list(tracking_statuses),
178
                "reasons": utils.english_list(reasons),
179
                "solutions": utils.english_list(solutions),
180
                "show_soft_freeze_comment": self.is_soft_freeze_soon and need_action,
181
                "show_regression_comment": is_regression and need_action,
182
            }
183

184
        return bug
×
185

186
    def get_bz_params(self, date):
×
187
        fields = [
×
188
            "regressed_by",
189
            "product",
190
            "component",
191
            "triage_owner",
192
            "assigned_to",
193
            "comments",
194
            "severity",
195
            "priority",
196
        ]
197
        for flag in self.version_flags:
×
198
            fields.extend((flag["tracking_field"], flag["status_field"]))
×
199

200
        params = {
×
201
            "include_fields": fields,
202
            "resolution": "---",
203
            "f1": "keywords",
204
            "o1": "nowords",
205
            "v1": "intermittent-failure",
206
            "f2": "status_whiteboard",
207
            "o2": "notsubstring",
208
            "v2": "[stockwell]",
209
            "f3": "short_desc",
210
            "o3": "notregexp",
211
            "v3": r"^Perma |Intermittent ",
212
            "j4": "OR",
213
            "f4": "OP",
214
            "f5": "bug_severity",
215
            "o5": "anyexact",
216
            "v5": list(LOW_SEVERITY),
217
            "f6": "priority",
218
            "o6": "anyexact",
219
            "v6": list(LOW_PRIORITY),
220
        }
221
        utils.get_empty_assignees(params)
×
222
        n = utils.get_last_field_num(params)
×
223
        params[f"f{n}"] = "CP"
×
224

225
        self._amend_tracking_params(params)
×
226

227
        return params
×
228

229
    def _amend_tracking_params(self, params: dict) -> None:
×
230
        n = utils.get_last_field_num(params)
×
231
        params.update(
×
232
            {
233
                f"j{n}": "OR",
234
                f"f{n}": "OP",
235
            }
236
        )
237

238
        for flag in self.version_flags:
×
239
            n = int(utils.get_last_field_num(params))
×
240
            params.update(
×
241
                {
242
                    f"f{n}": "OP",
243
                    f"f{n+1}": flag["tracking_field"],
244
                    f"o{n+1}": "anyexact",
245
                    f"v{n+1}": ["+", "blocking"],
246
                    f"f{n+2}": flag["status_field"],
247
                    f"o{n+2}": "anyexact",
248
                    f"v{n+2}": ["---", "affected"],
249
                    f"f{n+3}": "CP",
250
                }
251
            )
252

253
        n = utils.get_last_field_num(params)
×
254
        params[f"f{n}"] = "CP"
×
255

256
    def get_mail_to_auto_ni(self, bug):
×
257
        # If this is not the first time, we will needinfo no body
258
        if bug["is_reminder"]:
×
259
            return None
×
260

261
        manager = self.team_managers.get_component_manager(
×
262
            bug["product"], bug["component"], False
263
        )
264
        if manager and "bz_email" in manager:
×
265
            return {
×
266
                "mail": manager["bz_email"],
267
                "nickname": manager["nick"],
268
            }
269

270
        if not bug["triage_owner"]:
×
271
            return None
×
272

273
        return {
×
274
            "mail": bug["triage_owner"],
275
            "nickname": bug["triage_owner_detail"]["nick"],
276
        }
277

278
    @staticmethod
×
279
    def _get_last_comment(bug: dict) -> Optional[dict]:
×
280
        """Get the the last comment generated by this rule"""
281
        for comment in reversed(bug["comments"]):
×
282
            if comment["author"] == History.BOT and comment["text"].startswith(
×
283
                "The bug is marked as"
284
            ):
285
                return comment
×
286

287
        return None
×
288

289
    @staticmethod
×
290
    def _get_last_reminder_comment(bug: dict) -> Optional[dict]:
×
291
        """Get the the last comment generated by this rule"""
292
        for comment in reversed(bug["comments"]):
×
293
            if comment["author"] == History.BOT and comment["text"].startswith(
×
294
                "This is a reminder regarding"
295
            ):
296
                return comment
×
297

298
        return None
×
299

300

301
if __name__ == "__main__":
×
302
    TrackedAttention().run()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc