Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | # Copyright 2018 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """FLT task to be manually triggered to convert launch issues.""" |
| 6 | from __future__ import print_function |
| 7 | from __future__ import division |
| 8 | from __future__ import absolute_import |
| 9 | |
| 10 | import collections |
| 11 | import logging |
| 12 | import re |
| 13 | import settings |
| 14 | import time |
| 15 | |
| 16 | from businesslogic import work_env |
| 17 | from framework import permissions |
| 18 | from framework import exceptions |
| 19 | from framework import jsonfeed |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 20 | from mrproto import tracker_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 21 | from tracker import template_helpers |
| 22 | from tracker import tracker_bizobj |
| 23 | |
| 24 | PM_PREFIX = 'pm-' |
| 25 | TL_PREFIX = 'tl-' |
| 26 | TEST_PREFIX = 'test-' |
| 27 | UX_PREFIX = 'ux-' |
| 28 | |
| 29 | PM_FIELD = 'pm' |
| 30 | TL_FIELD = 'tl' |
| 31 | TE_FIELD = 'te' |
| 32 | UX_FIELD = 'ux' |
| 33 | MTARGET_FIELD = 'm-target' |
| 34 | MAPPROVED_FIELD = 'm-approved' |
| 35 | |
| 36 | CONVERSION_COMMENT = 'Automatic generating of FLT Launch data.' |
| 37 | |
| 38 | BROWSER_APPROVALS_TO_LABELS = { |
| 39 | 'Chrome-Accessibility': 'Launch-Accessibility-', |
| 40 | 'Chrome-Leadership-Exp': 'Launch-Exp-Leadership-', |
| 41 | 'Chrome-Leadership-Full': 'Launch-Leadership-', |
| 42 | 'Chrome-Legal': 'Launch-Legal-', |
| 43 | 'Chrome-Privacy': 'Launch-Privacy-', |
| 44 | 'Chrome-Security': 'Launch-Security-', |
| 45 | 'Chrome-Test': 'Launch-Test-', |
| 46 | 'Chrome-UX': 'Launch-UI-', |
| 47 | } |
| 48 | |
| 49 | OS_APPROVALS_TO_LABELS = { |
| 50 | 'ChromeOS-Accessibility': 'Launch-Accessibility-', |
| 51 | 'ChromeOS-Leadership-Exp': 'Launch-Exp-Leadership-', |
| 52 | 'ChromeOS-Leadership-Full': 'Launch-Leadership-', |
| 53 | 'ChromeOS-Legal': 'Launch-Legal-', |
| 54 | 'ChromeOS-Privacy': 'Launch-Privacy-', |
| 55 | 'ChromeOS-Security': 'Launch-Security-', |
| 56 | 'ChromeOS-Test': 'Launch-Test-', |
| 57 | 'ChromeOS-UX': 'Launch-UI-', |
| 58 | } |
| 59 | |
| 60 | # 'NotReviewed' not included because this should be converted to |
| 61 | # the template approval's default value, eg NOT_SET OR NEEDS_REVIEW |
| 62 | VALUE_TO_STATUS = { |
| 63 | 'ReviewRequested': tracker_pb2.ApprovalStatus.REVIEW_REQUESTED, |
| 64 | 'NeedInfo': tracker_pb2.ApprovalStatus.NEED_INFO, |
| 65 | 'Yes': tracker_pb2.ApprovalStatus.APPROVED, |
| 66 | 'No': tracker_pb2.ApprovalStatus.NOT_APPROVED, |
| 67 | 'NA': tracker_pb2.ApprovalStatus.NA, |
| 68 | # 'Started' is not a valid label value in the chromium project, |
| 69 | # but for some reason, some labels have this value. |
| 70 | 'Started': tracker_pb2.ApprovalStatus.REVIEW_STARTED, |
| 71 | } |
| 72 | |
| 73 | # This works in the Browser and OS process because |
| 74 | # BROWSER_APPROVALS_TO_LABELS and OS_APPROVALS_TO_LABELS have the same values. |
| 75 | # Adding '^' before each label prefix to ensure Blah-Launch-UI-Yes is ignored |
| 76 | REVIEW_LABELS_RE = re.compile('^' + '|^'.join( |
| 77 | list(OS_APPROVALS_TO_LABELS.values()))) |
| 78 | |
| 79 | # Maps template phases to channel names in 'Launch-M-Target-80-[Channel]' labels |
| 80 | BROWSER_PHASE_MAP = { |
| 81 | 'beta': 'beta', |
| 82 | 'stable': 'stable', |
| 83 | 'stable-full': 'stable', |
| 84 | 'stable-exp': 'stable-exp', |
| 85 | } |
| 86 | |
| 87 | PHASE_PAT = '$|'.join(list(BROWSER_PHASE_MAP.values())) |
| 88 | # Matches launch milestone labels, eg. Launch-M-Target-70-Stable-Exp |
| 89 | BROWSER_M_LABELS_RE = re.compile( |
| 90 | r'^Launch-M-(?P<type>Approved|Target)-(?P<m>\d\d)-' |
| 91 | r'(?P<channel>%s$)' % PHASE_PAT, |
| 92 | re.IGNORECASE) |
| 93 | |
| 94 | OS_PHASE_MAP = {'feature freeze': '', |
| 95 | 'branch': '', |
| 96 | 'stable': 'stable', |
| 97 | 'stable-full': 'stable', |
| 98 | 'stable-exp': 'stable-exp',} |
| 99 | # We only care about Launch-M-<type>-<m>-Stable|Stable-Exp labels for OS. |
| 100 | OS_M_LABELS_RE = re.compile( |
| 101 | r'^Launch-M-(?P<type>Approved|Target)-(?P<m>\d\d)-' |
| 102 | r'(?P<channel>Stable$|Stable-Exp$)', re.IGNORECASE) |
| 103 | |
| 104 | CAN = 2 # Query for open issues only |
| 105 | # Ensure empty group_by_spec and sort_spec so issues are sorted by 'ID'. |
| 106 | GROUP_BY_SPEC = '' |
| 107 | SORT_SPEC = '' |
| 108 | |
| 109 | CONVERT_NUM = 20 |
| 110 | CONVERT_START = 0 |
| 111 | VERIFY_NUM = 400 |
| 112 | |
| 113 | # Queries |
| 114 | QUERY_MAP = { |
| 115 | 'default': |
| 116 | 'Type=Launch Rollout-Type=Default OS=Windows,Mac,Linux,Android,iOS', |
| 117 | 'finch': 'Type=Launch Rollout-Type=Finch OS=Windows,Mac,Linux,Android,iOS', |
| 118 | 'os': 'Type=Launch OS=Chrome -OS=Windows,Mac,Linux,Android,iOS' |
| 119 | ' Rollout-Type=Default', |
| 120 | 'os-finch': 'Type=Launch OS=Chrome -OS=Windows,Mac,Linux,Android,iOS' |
| 121 | ' Rollout-Type=Finch'} |
| 122 | |
| 123 | TEMPLATE_MAP = { |
| 124 | 'default': 'Chrome Launch - Default', |
| 125 | 'finch': 'Chrome Launch - Experimental', |
| 126 | 'os': 'Chrome OS Launch - Default', |
| 127 | 'os-finch': 'Chrome OS Launch - Experimental', |
| 128 | } |
| 129 | |
| 130 | ProjectInfo = collections.namedtuple( |
| 131 | 'ProjectInfo', 'config, q, approval_values, phases, ' |
| 132 | 'pm_fid, tl_fid, te_fid, ux_fid, m_target_id, m_approved_id, ' |
| 133 | 'phase_map, approvals_to_labels, labels_re') |
| 134 | |
| 135 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 136 | class FLTConvertTask(jsonfeed.InternalTask): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 137 | """FLTConvert converts current Type=Launch issues into Type=FLT-Launch.""" |
| 138 | |
| 139 | def AssertBasePermission(self, mr): |
| 140 | super(FLTConvertTask, self).AssertBasePermission(mr) |
| 141 | if not mr.auth.user_pb.is_site_admin: |
| 142 | raise permissions.PermissionException( |
| 143 | 'Only site admins may trigger conversion job') |
| 144 | |
| 145 | def UndoConversion(self, mr): |
| 146 | with work_env.WorkEnv(mr, self.services) as we: |
| 147 | pipeline = we.ListIssues( |
| 148 | 'Type=FLT-Launch FLT=Conversion', ['chromium'], mr.auth.user_id, |
| 149 | CONVERT_NUM, CONVERT_START, 2, GROUP_BY_SPEC, SORT_SPEC, False) |
| 150 | |
| 151 | project = self.services.project.GetProjectByName(mr.cnxn, 'chromium') |
| 152 | config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id) |
| 153 | pm_id = tracker_bizobj.FindFieldDef('PM', config).field_id |
| 154 | tl_id = tracker_bizobj.FindFieldDef('TL', config).field_id |
| 155 | te_id = tracker_bizobj.FindFieldDef('TE', config).field_id |
| 156 | ux_id = tracker_bizobj.FindFieldDef('UX', config).field_id |
| 157 | for possible_stale_issue in pipeline.visible_results: |
| 158 | issue = self.services.issue.GetIssue( |
| 159 | mr.cnxn, possible_stale_issue.issue_id, use_cache=False) |
| 160 | |
| 161 | issue.approval_values = [] |
| 162 | issue.phases = [] |
| 163 | issue.field_values = [fv for fv in issue.field_values |
| 164 | if fv.phase_id is None] |
| 165 | issue.field_values = [fv for fv in issue.field_values |
| 166 | if fv.field_id not in |
| 167 | [pm_id, tl_id, te_id, ux_id]] |
| 168 | issue.labels.remove('Type-FLT-Launch') |
| 169 | issue.labels.remove('FLT-Conversion') |
| 170 | issue.labels.append('Type-Launch') |
| 171 | |
| 172 | self.services.issue._UpdateIssuesApprovals(mr.cnxn, issue) |
| 173 | self.services.issue.UpdateIssue(mr.cnxn, issue) |
| 174 | return {'deleting': [issue.local_id for issue in pipeline.visible_results], |
| 175 | 'num': len(pipeline.visible_results), |
| 176 | } |
| 177 | |
| 178 | def VerifyConversion(self, mr): |
| 179 | """Verify that all FLT-Conversion issues were converted correctly.""" |
| 180 | with work_env.WorkEnv(mr, self.services) as we: |
| 181 | pipeline = we.ListIssues( |
| 182 | 'FLT=Conversion', ['chromium'], mr.auth.user_id, VERIFY_NUM, |
| 183 | CONVERT_START, 2, GROUP_BY_SPEC, SORT_SPEC, False) |
| 184 | |
| 185 | project = self.services.project.GetProjectByName(mr.cnxn, 'chromium') |
| 186 | config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id) |
| 187 | browser_approval_names = {fd.field_id: fd.field_name for fd |
| 188 | in config.field_defs if fd.field_name in |
| 189 | BROWSER_APPROVALS_TO_LABELS.keys()} |
| 190 | os_approval_names = {fd.field_id: fd.field_name for fd in config.field_defs |
| 191 | if (fd.field_name in OS_APPROVALS_TO_LABELS.keys()) |
| 192 | or fd.field_name == 'ChromeOS-Enterprise'} |
| 193 | pm_id = tracker_bizobj.FindFieldDef('PM', config).field_id |
| 194 | tl_id = tracker_bizobj.FindFieldDef('TL', config).field_id |
| 195 | te_id = tracker_bizobj.FindFieldDef('TE', config).field_id |
| 196 | ux_id = tracker_bizobj.FindFieldDef('UX', config).field_id |
| 197 | mapproved_id = tracker_bizobj.FindFieldDef('M-Approved', config).field_id |
| 198 | mtarget_id = tracker_bizobj.FindFieldDef('M-Target', config).field_id |
| 199 | |
| 200 | problems = [] |
| 201 | for possible_stale_issue in pipeline.allowed_results: |
| 202 | issue = self.services.issue.GetIssue( |
| 203 | mr.cnxn, possible_stale_issue.issue_id, use_cache=False) |
| 204 | # Check correct template used |
| 205 | approval_names = browser_approval_names |
| 206 | approvals_to_labels = BROWSER_APPROVALS_TO_LABELS |
| 207 | m_labels_re = BROWSER_M_LABELS_RE |
| 208 | label_channel_to_phase_id = { |
| 209 | phase.name.lower(): phase.phase_id for phase in issue.phases} |
| 210 | if [l for l in issue.labels if l.startswith('OS-')] == ['OS-Chrome']: |
| 211 | approval_names = os_approval_names |
| 212 | m_labels_re = OS_M_LABELS_RE |
| 213 | approvals_to_labels = OS_APPROVALS_TO_LABELS |
| 214 | # OS default launch |
| 215 | if 'Rollout-Type-Default' in issue.labels: |
| 216 | if not all(phase.name in ['Feature Freeze', 'Branch', 'Stable'] |
| 217 | for phase in issue.phases): |
| 218 | problems.append(( |
| 219 | issue.local_id, 'incorrect phases for OS default launch.')) |
| 220 | # OS finch launch |
| 221 | elif 'Rollout-Type-Finch' in issue.labels: |
| 222 | if not all(phase.name in ( |
| 223 | 'Feature Freeze', 'Branch', 'Stable-Exp', 'Stable-Full') |
| 224 | for phase in issue.phases): |
| 225 | problems.append(( |
| 226 | issue.local_id, 'incorrect phases for OS finch launch.')) |
| 227 | else: |
| 228 | problems.append(( |
| 229 | issue.local_id, |
| 230 | 'no rollout-type; should not have been converted')) |
| 231 | # Browser default launch |
| 232 | elif 'Rollout-Type-Default' in issue.labels: |
| 233 | if not all(phase.name.lower() in ['beta', 'stable'] |
| 234 | for phase in issue.phases): |
| 235 | problems.append(( |
| 236 | issue.local_id, 'incorrect phases for Default rollout')) |
| 237 | # Browser finch launch |
| 238 | elif 'Rollout-Type-Finch' in issue.labels: |
| 239 | if not all(phase.name.lower() in ['beta', 'stable-exp', 'stable-full'] |
| 240 | for phase in issue.phases): |
| 241 | problems.append(( |
| 242 | issue.local_id, 'incorrect phases for Finch rollout')) |
| 243 | else: |
| 244 | problems.append(( |
| 245 | issue.local_id, |
| 246 | 'no rollout-type; should not have been converted')) |
| 247 | |
| 248 | # Check approval_values |
| 249 | for av in issue.approval_values: |
| 250 | name = approval_names.get(av.approval_id) |
| 251 | if name == 'ChromeOS-Enterprise': |
| 252 | if av.status != tracker_pb2.ApprovalStatus.NEEDS_REVIEW: |
| 253 | problems.append((issue.local_id, 'bad ChromeOS-Enterprise status')) |
| 254 | continue |
| 255 | label_pre = approvals_to_labels.get(name) |
| 256 | if not label_pre: |
| 257 | # either name was None or not found in APPROVALS_TO_LABELS |
| 258 | problems.append((issue.local_id, 'approval %s not recognized' % name)) |
| 259 | continue |
| 260 | label_value = next((l[len(label_pre):] for l in issue.labels |
| 261 | if l.startswith(label_pre)), None) |
| 262 | if (not label_value or label_value == 'NotReviewed') and av.status in [ |
| 263 | tracker_pb2.ApprovalStatus.NOT_SET, |
| 264 | tracker_pb2.ApprovalStatus.NEEDS_REVIEW]: |
| 265 | continue |
| 266 | if av.status is VALUE_TO_STATUS.get(label_value): |
| 267 | continue |
| 268 | # neither of the above ifs passed |
| 269 | problems.append((issue.local_id, |
| 270 | 'approval %s has status %r for label value %s' % ( |
| 271 | name, av.status.name, label_value))) |
| 272 | |
| 273 | # Check people field_values |
| 274 | expected_people_fvs = self.ConvertPeopleLabels( |
| 275 | mr, issue.labels, pm_id, tl_id, te_id, ux_id) |
| 276 | for people_fv in expected_people_fvs: |
| 277 | if people_fv not in issue.field_values: |
| 278 | if people_fv.field_id == tl_id: |
| 279 | role = 'TL' |
| 280 | elif people_fv.field_id == pm_id: |
| 281 | role = 'PM' |
| 282 | elif people_fv.field_id == ux_id: |
| 283 | role = 'UX' |
| 284 | else: |
| 285 | role = 'TE' |
| 286 | problems.append((issue.local_id, 'missing a field for %s' % role)) |
| 287 | |
| 288 | # Check M phase field_values |
| 289 | for label in issue.labels: |
| 290 | match = re.match(m_labels_re, label) |
| 291 | if match: |
| 292 | channel = match.group('channel') |
| 293 | if (channel.lower() == 'stable-exp' |
| 294 | and 'Rollout-Type-Default' in issue.labels): |
| 295 | # ignore stable-exp for default rollouts. |
| 296 | continue |
| 297 | milestone = match.group('m') |
| 298 | m_type = match.group('type') |
| 299 | m_id = mapproved_id if m_type == 'Approved' else mtarget_id |
| 300 | phase_id = label_channel_to_phase_id.get( |
| 301 | channel.lower(), label_channel_to_phase_id.get('stable-full')) |
| 302 | if not next(( |
| 303 | fv for fv in issue.field_values |
| 304 | if fv.phase_id == phase_id and fv.field_id == m_id and |
| 305 | fv.int_value == int(milestone)), None): |
| 306 | problems.append(( |
| 307 | issue.local_id, 'no phase field for label %s' % label)) |
| 308 | |
| 309 | return { |
| 310 | 'problems found': ['issue %d: %s' % problem for problem in problems], |
| 311 | 'issues verified': ['issue %d' % issue.local_id for |
| 312 | issue in pipeline.allowed_results], |
| 313 | 'num': len(pipeline.allowed_results), |
| 314 | } |
| 315 | |
| 316 | def HandleRequest(self, mr): |
| 317 | """Convert Type=Launch issues to new Type=FLT-Launch issues.""" |
| 318 | launch = mr.GetParam('launch') |
| 319 | if launch == 'delete': |
| 320 | return self.UndoConversion(mr) |
| 321 | if launch == 'verify': |
| 322 | return self.VerifyConversion(mr) |
| 323 | project_info = self.FetchAndAssertProjectInfo(mr) |
| 324 | |
| 325 | # Search for issues: |
| 326 | with work_env.WorkEnv(mr, self.services) as we: |
| 327 | pipeline = we.ListIssues( |
| 328 | project_info.q, ['chromium'], mr.auth.user_id, CONVERT_NUM, |
| 329 | CONVERT_START, 2, GROUP_BY_SPEC, SORT_SPEC, False) |
| 330 | |
| 331 | # Convert issues: |
| 332 | for possible_stale_issue in pipeline.visible_results: |
| 333 | # Note: These approval values and phases from templates will be used |
| 334 | # and modified to create approval values and phases for each issue. |
| 335 | # We need to create copies for each issue so changes are not carried |
| 336 | # over to the conversion of the next issue in the loop. |
| 337 | template_avs = self.CreateApprovalCopies(project_info.approval_values) |
| 338 | template_phases = self.CreatePhasesCopies(project_info.phases) |
| 339 | issue = self.services.issue.GetIssue( |
| 340 | mr.cnxn, possible_stale_issue.issue_id, use_cache=False) |
| 341 | new_approvals = ConvertLaunchLabels( |
| 342 | issue.labels, template_avs, |
| 343 | project_info.config.field_defs, project_info.approvals_to_labels) |
| 344 | m_fvs = ConvertMLabels( |
| 345 | issue.labels, template_phases, |
| 346 | project_info.m_target_id, project_info.m_approved_id, |
| 347 | project_info.labels_re, project_info.phase_map) |
| 348 | people_fvs = self.ConvertPeopleLabels( |
| 349 | mr, issue.labels, |
| 350 | project_info.pm_fid, project_info.tl_fid, project_info.te_fid, |
| 351 | project_info.ux_fid) |
| 352 | amendments = self.ExecuteIssueChanges( |
| 353 | project_info.config, issue, new_approvals, |
| 354 | template_phases, m_fvs + people_fvs) |
| 355 | logging.info(amendments) |
| 356 | |
| 357 | return { |
| 358 | 'converted_issues': [ |
| 359 | issue.local_id for issue in pipeline.visible_results], |
| 360 | 'num': len(pipeline.visible_results), |
| 361 | } |
| 362 | |
| 363 | def CreateApprovalCopies(self, avs): |
| 364 | return [ |
| 365 | tracker_pb2.ApprovalValue( |
| 366 | approval_id=av.approval_id, |
| 367 | status=av.status, |
| 368 | setter_id=av.setter_id, |
| 369 | set_on=av.set_on, |
| 370 | phase_id=av.phase_id) for av in avs |
| 371 | ] |
| 372 | |
| 373 | def CreatePhasesCopies(self, phases): |
| 374 | return [ |
| 375 | tracker_pb2.Phase( |
| 376 | phase_id=phase.phase_id, |
| 377 | name=phase.name, |
| 378 | rank=phase.rank) for phase in phases |
| 379 | ] |
| 380 | |
| 381 | def FetchAndAssertProjectInfo(self, mr): |
| 382 | # Get request details |
| 383 | launch = mr.GetParam('launch') |
| 384 | logging.info(launch) |
| 385 | q = QUERY_MAP.get(launch) |
| 386 | template_name = TEMPLATE_MAP.get(launch) |
| 387 | assert q and template_name, 'bad launch type: %s' % launch |
| 388 | |
| 389 | phase_map = ( |
| 390 | OS_PHASE_MAP if launch in ['os', 'os-finch'] else BROWSER_PHASE_MAP) |
| 391 | approvals_to_labels = ( |
| 392 | OS_APPROVALS_TO_LABELS if launch in ['os', 'os-finch'] |
| 393 | else BROWSER_APPROVALS_TO_LABELS) |
| 394 | m_labels_re = ( |
| 395 | OS_M_LABELS_RE if launch in ['os', 'os-finch'] else BROWSER_M_LABELS_RE) |
| 396 | |
| 397 | # Get project, config, template, assert template in project |
| 398 | project = self.services.project.GetProjectByName(mr.cnxn, 'chromium') |
| 399 | config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id) |
| 400 | template = self.services.template.GetTemplateByName( |
| 401 | mr.cnxn, template_name, project.project_id) |
| 402 | assert template, 'template %s not found in chromium project' % template_name |
| 403 | |
| 404 | # Get template approval_values/phases and assert they are expected |
| 405 | approval_values, phases = template_helpers.FilterApprovalsAndPhases( |
| 406 | template.approval_values, template.phases, config) |
| 407 | assert approval_values and phases, ( |
| 408 | 'no approvals or phases in %s' % template_name) |
| 409 | assert all(phase.name.lower() in list( |
| 410 | phase_map.keys()) for phase in phases), ( |
| 411 | 'one or more phases not recognized') |
| 412 | if launch in ['finch', 'os', 'os-finch']: |
| 413 | assert all( |
| 414 | av.status is tracker_pb2.ApprovalStatus.NEEDS_REVIEW |
| 415 | for av in approval_values |
| 416 | ), '%s template not set up correctly' % launch |
| 417 | |
| 418 | approval_fds = {fd.field_id: fd.field_name for fd in config.field_defs |
| 419 | if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE} |
| 420 | assert all( |
| 421 | approval_fds.get(av.approval_id) in list(approvals_to_labels.keys()) |
| 422 | for av in approval_values |
| 423 | if approval_fds.get(av.approval_id) != 'ChromeOS-Enterprise'), ( |
| 424 | 'one or more approvals not recognized') |
| 425 | approval_def_ids = [ad.approval_id for ad in config.approval_defs] |
| 426 | assert all(av.approval_id in approval_def_ids for av in approval_values), ( |
| 427 | 'one or more approvals not in config.approval_defs') |
| 428 | |
| 429 | # Get relevant USER_TYPE FieldDef ids and assert they exist |
| 430 | user_fds = {fd.field_name.lower(): fd.field_id for fd in config.field_defs |
| 431 | if fd.field_type is tracker_pb2.FieldTypes.USER_TYPE} |
| 432 | logging.info('project USER_TYPE FieldDefs: %s' % user_fds) |
| 433 | pm_fid = user_fds.get(PM_FIELD) |
| 434 | assert pm_fid, 'project has no FieldDef %s' % PM_FIELD |
| 435 | tl_fid = user_fds.get(TL_FIELD) |
| 436 | assert tl_fid, 'project has no FieldDef %s' % TL_FIELD |
| 437 | te_fid = user_fds.get(TE_FIELD) |
| 438 | assert te_fid, 'project has no FieldDef %s' % TE_FIELD |
| 439 | ux_fid = user_fds.get(UX_FIELD) |
| 440 | assert ux_fid, 'project has no FieldDef %s' % UX_FIELD |
| 441 | |
| 442 | # Get relevant M Phase INT_TYPE FieldDef ids and assert they exist |
| 443 | phase_int_fds = {fd.field_name.lower(): fd.field_id |
| 444 | for fd in config.field_defs |
| 445 | if fd.field_type is tracker_pb2.FieldTypes.INT_TYPE |
| 446 | and fd.is_phase_field and fd.is_multivalued} |
| 447 | logging.info( |
| 448 | 'project Phase INT_TYPE multivalued FieldDefs: %s' % phase_int_fds) |
| 449 | m_target_id = phase_int_fds.get(MTARGET_FIELD) |
| 450 | assert m_target_id, 'project has no FieldDef %s' % MTARGET_FIELD |
| 451 | m_approved_id = phase_int_fds.get(MAPPROVED_FIELD) |
| 452 | assert m_approved_id, 'project has no FieldDef %s' % MAPPROVED_FIELD |
| 453 | |
| 454 | return ProjectInfo(config, q, approval_values, phases, pm_fid, tl_fid, |
| 455 | te_fid, ux_fid, m_target_id, m_approved_id, phase_map, |
| 456 | approvals_to_labels, m_labels_re) |
| 457 | |
| 458 | # TODO(jojwang): mr needs to be passed in as arg and |
| 459 | # all self.mr should be changed to mr |
| 460 | def ExecuteIssueChanges(self, config, issue, new_approvals, phases, new_fvs): |
| 461 | # Apply Approval and phase changes |
| 462 | approval_defs_by_id = {ad.approval_id: ad for ad in config.approval_defs} |
| 463 | for av in new_approvals: |
| 464 | ad = approval_defs_by_id.get(av.approval_id) |
| 465 | if ad: |
| 466 | av.approver_ids = ad.approver_ids |
| 467 | survey = '' |
| 468 | if ad.survey: |
| 469 | questions = ad.survey.split('\n') |
| 470 | survey = '\n'.join(['<b>' + q + '</b>' for q in questions]) |
| 471 | self.services.issue.InsertComment( |
| 472 | self.mr.cnxn, tracker_pb2.IssueComment( |
| 473 | issue_id=issue.issue_id, project_id=issue.project_id, |
| 474 | user_id=self.mr.auth.user_id, content=survey, |
| 475 | is_description=True, approval_id=av.approval_id, |
| 476 | timestamp=int(time.time()))) |
| 477 | else: |
| 478 | logging.info( |
| 479 | 'ERROR: ApprovalDef %r for ApprovalValue %r not valid', ad, av) |
| 480 | issue.approval_values = new_approvals |
| 481 | self.services.issue._UpdateIssuesApprovals(self.mr.cnxn, issue) |
| 482 | |
| 483 | # Apply field value changes |
| 484 | issue.phases = phases |
| 485 | delta = tracker_bizobj.MakeIssueDelta( |
| 486 | None, None, [], [], [], [], ['Type-FLT-Launch', 'FLT-Conversion'], |
| 487 | ['Type-Launch'], new_fvs, [], [], [], [], [], [], None, None) |
| 488 | amendments, _ = self.services.issue.DeltaUpdateIssue( |
| 489 | self.mr.cnxn, self.services, self.mr.auth.user_id, issue.project_id, |
| 490 | config, issue, delta, comment=CONVERSION_COMMENT) |
| 491 | |
| 492 | return amendments |
| 493 | |
| 494 | def ConvertPeopleLabels( |
| 495 | self, mr, labels, pm_field_id, tl_field_id, te_field_id, ux_field_id): |
| 496 | field_values = [] |
| 497 | pm_ldap, tl_ldap, test_ldaps, ux_ldaps = ExtractLabelLDAPs(labels) |
| 498 | |
| 499 | pm_fv = self.CreateUserFieldValue(mr, pm_ldap, pm_field_id) |
| 500 | if pm_fv: |
| 501 | field_values.append(pm_fv) |
| 502 | |
| 503 | tl_fv = self.CreateUserFieldValue(mr, tl_ldap, tl_field_id) |
| 504 | if tl_fv: |
| 505 | field_values.append(tl_fv) |
| 506 | |
| 507 | for test_ldap in test_ldaps: |
| 508 | te_fv = self.CreateUserFieldValue(mr, test_ldap, te_field_id) |
| 509 | if te_fv: |
| 510 | field_values.append(te_fv) |
| 511 | |
| 512 | for ux_ldap in ux_ldaps: |
| 513 | ux_fv = self.CreateUserFieldValue(mr, ux_ldap, ux_field_id) |
| 514 | if ux_fv: |
| 515 | field_values.append(ux_fv) |
| 516 | return field_values |
| 517 | |
| 518 | def CreateUserFieldValue(self, mr, ldap, field_id): |
| 519 | if ldap is None: |
| 520 | return None |
| 521 | try: |
| 522 | user_id = self.services.user.LookupUserID(mr.cnxn, ldap+'@chromium.org') |
| 523 | except exceptions.NoSuchUserException: |
| 524 | try: |
| 525 | user_id = self.services.user.LookupUserID(mr.cnxn, ldap+'@google.com') |
| 526 | except exceptions.NoSuchUserException: |
| 527 | logging.info('No chromium.org or google.com accound found for %s', ldap) |
| 528 | return None |
| 529 | return tracker_bizobj.MakeFieldValue( |
| 530 | field_id, None, None, user_id, None, None, False) |
| 531 | |
Adrià Vilanova Martínez | 9f9ade5 | 2022-10-10 23:20:11 +0200 | [diff] [blame] | 532 | def PostFLTConvertTask(self, **kwargs): |
| 533 | return self.handler(**kwargs) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 534 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 535 | |
| 536 | def ConvertMLabels( |
| 537 | labels, phases, m_target_id, m_approved_id, labels_re, phase_map): |
| 538 | field_values = [] |
| 539 | for label in labels: |
| 540 | match = re.match(labels_re, label) |
| 541 | if match: |
| 542 | milestone = match.group('m') |
| 543 | m_type = match.group('type') |
| 544 | channel = match.group('channel') |
| 545 | for phase in phases: |
| 546 | # We know get(phase) will return something because |
| 547 | # we're checking before ConvertMLabels, that all phases |
| 548 | # exist in BROWSER_PHASE_MAP or OS_PHASE_MAP |
| 549 | if phase_map.get(phase.name.lower()) == channel.lower(): |
| 550 | field_id = m_target_id if ( |
| 551 | m_type.lower() == 'target') else m_approved_id |
| 552 | field_values.append(tracker_bizobj.MakeFieldValue( |
| 553 | field_id, int(milestone), None, None, None, None, False, |
| 554 | phase_id=phase.phase_id)) |
| 555 | break # exit phase loop if match is found. |
| 556 | return field_values |
| 557 | |
| 558 | |
| 559 | def ConvertLaunchLabels(labels, approvals, project_fds, approvals_to_labels): |
| 560 | """Converts 'Launch-[Review]' values into statuses for given approvals.""" |
| 561 | label_values = {} |
| 562 | for label in labels: |
| 563 | launch_match = REVIEW_LABELS_RE.match(label) |
| 564 | if launch_match: |
| 565 | prefix = launch_match.group() |
| 566 | value = label[len(prefix):] # returns 'Yes' from 'Launch-UI-Yes' |
| 567 | label_values[prefix] = value |
| 568 | |
| 569 | field_names_dict = {fd.field_id: fd.field_name for fd in project_fds} |
| 570 | for approval in approvals: |
| 571 | approval_name = field_names_dict.get(approval.approval_id, '') |
| 572 | old_prefix = approvals_to_labels.get(approval_name) |
| 573 | label_value = label_values.get(old_prefix, '') |
| 574 | # if label_value not found in VALUE_TO_STATUS, use current status. |
| 575 | approval.status = VALUE_TO_STATUS.get(label_value, approval.status) |
| 576 | |
| 577 | return approvals |
| 578 | |
| 579 | |
| 580 | def ExtractLabelLDAPs(labels): |
| 581 | """Extracts LDAPs from labels 'PM-', 'TL-', 'UX-', and 'test-'""" |
| 582 | |
| 583 | pm_ldap = None |
| 584 | tl_ldap = None |
| 585 | test_ldaps = [] |
| 586 | ux_ldaps = [] |
| 587 | for label in labels: |
| 588 | label = label.lower() |
| 589 | if label.startswith(PM_PREFIX): |
| 590 | pm_ldap = label[len(PM_PREFIX):] |
| 591 | elif label.startswith(TL_PREFIX): |
| 592 | tl_ldap = label[len(TL_PREFIX):] |
| 593 | elif label.startswith(TEST_PREFIX): |
| 594 | ldap = label[len(TEST_PREFIX):] |
| 595 | if ldap: |
| 596 | test_ldaps.append(ldap) |
| 597 | elif label.startswith(UX_PREFIX): |
| 598 | ldap = label[len(UX_PREFIX):] |
| 599 | if ldap: |
| 600 | ux_ldaps.append(ldap) |
| 601 | return pm_ldap, tl_ldap, test_ldaps, ux_ldaps |