| # Copyright 2021 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Set a "Verified" label with the results of CQ. |
| |
| Find CLs where CQ has completed and set a label to +1 or -1 based on the status |
| of CQ. Also find CLs where CQ is running and clear that +1 or -1. |
| """ |
| |
| import collections |
| import re |
| |
| from recipe_engine import post_process, recipe_test_api |
| from PB.go.chromium.org.luci.buildbucket.proto import common |
| from PB.recipe_engine import result |
| from PB.recipes.pigweed.cq_label import InputProperties |
| from RECIPE_MODULES.fuchsia.auto_roller.api import ( |
| FAILED_DRY_RUN_MESSAGES, |
| PASSED_DRY_RUN_MESSAGES, |
| ) |
| |
| DEPS = [ |
| 'fuchsia/auto_roller', |
| 'fuchsia/gerrit', |
| 'fuchsia/status_check', |
| 'recipe_engine/json', |
| 'recipe_engine/properties', |
| 'recipe_engine/step', |
| ] |
| |
| PROPERTIES = InputProperties |
| |
| |
| def _summary(api, host, change_id, action): |
| return '[{change_id}](https://{host}/{change_id}): {action}'.format( |
| change_id=change_id, |
| host=api.gerrit.normalize_host(host), |
| action=action, |
| ) |
| |
| |
| def _label_completed_unlabelled( |
| api, host, projects, label, cq_account, dry_run, |
| ): |
| project_part = ' OR '.join(f'project:{p}' for p in projects) |
| query_string = ( |
| f'is:open ' |
| f'-(label:Commit-Queue+1 OR label:Commit-Queue+2) ' |
| f'-(label:{label}-1 OR label:{label}+1) ' |
| f'(r:{cq_account} OR cc:{cq_account}) ' |
| f'({project_part}) ' |
| ) |
| |
| changes = ( |
| api.gerrit.change_query( |
| name='get unlabeled changes', |
| query_string=query_string, |
| host=host, |
| max_attempts=5, |
| timeout=30, |
| ) |
| .get_result() |
| .json.output |
| ) |
| |
| for change in changes or (): |
| change_id = str(change['_number']) |
| |
| with api.step.nest(change_id) as pres: |
| pres.links[ |
| change_id |
| ] = f'https://{api.gerrit.normalize_host(host)}/{change_id}' |
| |
| details = ( |
| api.gerrit.change_details( |
| 'details', |
| change_id, |
| query_params=['CURRENT_REVISION'], |
| host=host, |
| ) |
| .get_result() |
| .json.output |
| ) |
| |
| patch = details['revisions'][details['current_revision']]['_number'] |
| value = 0 |
| # Look at messages in reverse order so we always set based on the |
| # most recent CQ results. |
| for message in reversed(details['messages']): |
| if message['author'].get('email') != cq_account: |
| continue |
| |
| if not message['message'].startswith(f'Patch Set {patch}:'): |
| continue |
| |
| for msg in PASSED_DRY_RUN_MESSAGES: |
| if msg in message['message']: |
| with api.step.nest('passed'): |
| value = 1 |
| break |
| |
| for msg in FAILED_DRY_RUN_MESSAGES: |
| if msg in message['message']: |
| with api.step.nest('failed'): |
| value = -1 |
| break |
| |
| if value: |
| break |
| |
| if value: |
| if dry_run: |
| action = 'would set {} to {}'.format(label, value) |
| pres.step_summary_text = action |
| with api.step.nest(action): |
| pass |
| |
| else: |
| action = 'set {} to {}'.format(label, value) |
| pres.step_summary_text = action |
| api.gerrit.set_review( |
| action, |
| change_id, |
| labels={label: value}, |
| host=host, |
| notify='NONE', |
| ) |
| |
| yield _summary(api, host, change_id, action) |
| |
| else: |
| pres.step_summary_text = 'no action' |
| |
| |
| def _remove_running_labelled(api, host, projects, label, cq_account, dry_run): |
| project_part = ' OR '.join('project:{}'.format(p) for p in projects) |
| query_string = ( |
| f'is:open ' |
| f'(label:Commit-Queue+1 OR label:Commit-Queue+2) ' |
| f'(label:{label}-1 OR label:{label}+1) ' |
| f'(r:{cq_account} OR cc:{cq_account}) ' |
| f'({project_part}) ' |
| ) |
| |
| changes = ( |
| api.gerrit.change_query( |
| name='get labeled changes', |
| query_string=query_string, |
| host=host, |
| max_attempts=5, |
| timeout=30, |
| ) |
| .get_result() |
| .json.output |
| ) |
| |
| for change in changes or (): |
| change_id = str(change['_number']) |
| |
| with api.step.nest(change_id) as pres: |
| pres.links[ |
| change_id |
| ] = f'https://{api.gerrit.normalize_host(host)}/{change_id}' |
| |
| if dry_run: |
| action = f'would clear {label}' |
| pres.step_summary_text = action |
| with api.step.nest(action): |
| pass |
| |
| else: |
| action = f'clear {label}' |
| pres.step_summary_text = action |
| api.gerrit.set_review( |
| action, |
| change_id, |
| labels={label: 0}, |
| host=host, |
| notify='NONE', |
| ) |
| |
| yield _summary(api, host, change_id, action) |
| |
| |
| def RunSteps(api, props): |
| assert re.match(r'^[-\w]+$', props.label) |
| assert re.match(r'^[-\w.@]+$', props.cq_account) |
| |
| projects_by_host = collections.defaultdict(list) |
| for repo in props.repo: |
| match = re.search( |
| r'^https://(?P<host>.*).googlesource.com/(?P<project>.*)$', repo, |
| ) |
| assert match, repo |
| host = match.group('host') |
| project = match.group('project') |
| projects_by_host[host].append(project) |
| |
| actions = [] |
| |
| with api.step.defer_results(): |
| for host, projects in projects_by_host.items(): |
| with api.step.nest(host): |
| actions.extend( |
| _label_completed_unlabelled( |
| api=api, |
| host=host, |
| projects=projects, |
| label=props.label, |
| cq_account=props.cq_account, |
| dry_run=props.dry_run, |
| ) |
| ) |
| |
| actions.extend( |
| _remove_running_labelled( |
| api=api, |
| host=host, |
| projects=projects, |
| label=props.label, |
| cq_account=props.cq_account, |
| dry_run=props.dry_run, |
| ) |
| ) |
| |
| return result.RawResult( |
| summary_markdown='\n\n'.join(actions), status=common.SUCCESS, |
| ) |
| |
| |
| def GenTests(api): |
| CQ_BOT_ACCOUNT = 'cq-bot-account@gserviceaccount.com' |
| VERIFIED_LABEL = 'CQ-Verified' |
| PROJECT = 'pigweed' |
| |
| def properties( |
| repo=(f'https://pigweed.googlesource.com/{PROJECT}'), |
| cq_account=CQ_BOT_ACCOUNT, |
| label=VERIFIED_LABEL, |
| dry_run=False, |
| ): |
| if isinstance(repo, str): |
| repo = [repo] |
| |
| return api.properties( |
| repo=list(repo), |
| cq_account=cq_account, |
| label=label, |
| dry_run=dry_run, |
| ) |
| |
| def unlabeled_query_results(project='pigweed'): |
| return api.step_data( |
| 'pigweed.get unlabeled changes', |
| api.json.output([{'_number': 1, 'project': project}]), |
| ) |
| |
| def labeled_query_results(project='pigweed'): |
| return api.step_data( |
| 'pigweed.get labeled changes', |
| api.json.output([{'_number': 1, 'project': project}]), |
| ) |
| |
| def message(patch_set, passed, author=CQ_BOT_ACCOUNT): |
| passfail = FAILED_DRY_RUN_MESSAGES[0] |
| if passed: |
| passfail = PASSED_DRY_RUN_MESSAGES[0] |
| |
| return { |
| 'author': {'email': author,}, |
| 'message': f'Patch Set {patch_set}: {passfail}', |
| } |
| |
| def details(*messages, **kwargs): |
| res = { |
| 'current_revision': 'h3110', |
| 'revisions': {'h3110': {'_number': 2}}, |
| 'messages': list(messages), |
| 'project': PROJECT, |
| } |
| res.update(kwargs) |
| return api.step_data('pigweed.1.details', api.json.output(res)) |
| |
| def set_passed(label=VERIFIED_LABEL, dry_run=False): |
| dry_run_part = 'would ' if dry_run else '' |
| return api.post_process( |
| post_process.MustRun, f'pigweed.1.{dry_run_part}set {label} to 1', |
| ) |
| |
| def set_failed(label=VERIFIED_LABEL, dry_run=False): |
| dry_run_part = 'would ' if dry_run else '' |
| return api.post_process( |
| post_process.MustRun, f'pigweed.1.{dry_run_part}set {label} to -1', |
| ) |
| |
| def no_set(label=VERIFIED_LABEL): |
| return ( |
| api.post_process( |
| post_process.DoesNotRun, f'pigweed.1.set {label} to 1', |
| ) |
| + api.post_process( |
| post_process.DoesNotRun, f'pigweed.1.would set {label} to 1', |
| ) |
| + api.post_process( |
| post_process.DoesNotRun, f'pigweed.1.set {label} to -1', |
| ) |
| + api.post_process( |
| post_process.DoesNotRun, f'pigweed.1.would set {label} to -1', |
| ) |
| ) |
| |
| def does_not_clear(label=VERIFIED_LABEL): |
| return api.post_process( |
| post_process.DoesNotRun, f'pigweed.1.clear {label}', |
| ) + api.post_process( |
| post_process.DoesNotRun, f'pigweed.1.would clear {label}' |
| ) |
| |
| def clear(label=VERIFIED_LABEL, dry_run=False): |
| dry_run_part = 'would ' if dry_run else '' |
| return api.post_process( |
| post_process.MustRun, f'pigweed.1.{dry_run_part}clear {label}', |
| ) |
| |
| yield ( |
| api.status_check.test('passed') |
| + properties() |
| + unlabeled_query_results() |
| + details(message(2, True)) |
| + set_passed() |
| + does_not_clear() |
| ) |
| |
| yield ( |
| api.status_check.test('failed') |
| + properties() |
| + unlabeled_query_results() |
| + details(message(2, False)) |
| + set_failed() |
| + does_not_clear() |
| ) |
| |
| yield ( |
| api.status_check.test('wrong-author') |
| + properties() |
| + unlabeled_query_results() |
| + details(message(2, True, author='test@example.com')) |
| + no_set() |
| + does_not_clear() |
| ) |
| |
| yield ( |
| api.status_check.test('wrong-patchset') |
| + properties() |
| + unlabeled_query_results() |
| + details(message(1, True)) |
| + no_set() |
| + does_not_clear() |
| ) |
| |
| yield ( |
| api.status_check.test('dry-run') |
| + properties(dry_run=True) |
| + unlabeled_query_results() |
| + details(message(2, True)) |
| + set_passed(dry_run=True) |
| + does_not_clear() |
| ) |
| |
| yield ( |
| api.status_check.test('clear') |
| + properties() |
| + labeled_query_results() |
| + no_set() |
| + clear() |
| ) |
| |
| yield ( |
| api.status_check.test('clear-dryrun') |
| + properties(dry_run=True) |
| + labeled_query_results() |
| + no_set() |
| + clear(dry_run=True) |
| ) |