blob: 457d7c4219f526ff6987a19664c4540f96466f01 [file] [log] [blame]
# 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."""
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
DEPS = [
'fuchsia/gerrit',
'fuchsia/status_check',
'recipe_engine/json',
'recipe_engine/properties',
'recipe_engine/step',
]
PROPERTIES = InputProperties
PYTHON_VERSION_COMPATIBILITY = 'PY3'
def RunSteps(api, props):
assert re.match(r'^[-\w]+$', props.label)
assert re.match(r'^[-\w.@]+$', props.cq_account)
assert re.match(r'^[-\w./_]*$', props.project)
def _summary(change_id, action):
return '[{change_id}](https://{host}/{change_id}): {action}'.format(
change_id=change_id,
host=api.gerrit.normalize_host(props.host),
action=action,
)
project_part = ''
if props.project:
project_part = 'project:{} '.format(props.project)
query_string = (
'is:open '
'-(label:Commit-Queue+1 OR label:Commit-Queue+2) '
'-(label:{label}-1 OR label:{label}+1) '
'(r:{cq_account} OR cc:{cq_account}) '
'{project_part} '
).format(
label=props.label,
cq_account=props.cq_account,
project_part=project_part,
)
changes = api.gerrit.change_query(
name='get unlabeled changes',
query_string=query_string,
host=props.host,
max_attempts=5,
timeout=30,
).json.output
actions = []
for change in changes or ():
change_id = str(change['_number'])
with api.step.nest(change_id) as pres:
pres.links[change_id] = 'https://{}/{}'.format(
api.gerrit.normalize_host(props.host), change_id,
)
details = api.gerrit.change_details(
'details',
change_id,
query_params=['CURRENT_REVISION'],
host=props.host,
).json.output
if props.project and props.project != details['project']:
continue
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']['email'] != props.cq_account:
continue
passed = re.search(
r'Patch Set {}:\s*Dry run: This CL passed'.format(patch),
message['message'],
re.IGNORECASE,
)
if passed:
with api.step.nest('passed'):
value = 1
break
failed = re.search(
r'Patch Set {}:\s*Dry run: Failed builds'.format(patch),
message['message'],
re.IGNORECASE,
)
if failed:
with api.step.nest('failed'):
value = -1
break
if value:
if props.dry_run:
action = 'would set {} to {}'.format(props.label, value)
pres.step_summary_text = action
with api.step.nest(action):
pass
else:
action = 'set {} to {}'.format(props.label, value)
pres.step_summary_text = action
api.gerrit.set_review(
action,
change_id,
labels={props.label: value},
host=props.host,
notify='NONE',
)
actions.append(_summary(change_id, action))
else:
pres.step_summary_text = 'no action'
query_string = (
'is:open '
'(label:Commit-Queue+1 OR label:Commit-Queue+2) '
'(label:{label}-1 OR label:{label}+1) '
'(r:{cq_account} OR cc:{cq_account}) '
'{project_part} '
).format(
label=props.label,
cq_account=props.cq_account,
project_part=project_part,
)
changes = api.gerrit.change_query(
name='get labeled changes',
query_string=query_string,
host=props.host,
max_attempts=5,
timeout=30,
).json.output
for change in changes or ():
change_id = str(change['_number'])
with api.step.nest(change_id) as pres:
pres.links[change_id] = 'https://{}/{}'.format(
api.gerrit.normalize_host(props.host), change_id,
)
if props.dry_run:
action = 'would clear {}'.format(props.label)
pres.step_summary_text = action
with api.step.nest(action):
pass
else:
action = 'clear {}'.format(props.label)
pres.step_summary_text = action
api.gerrit.set_review(
action,
change_id,
labels={props.label: 0},
host=props.host,
notify='NONE',
)
actions.append(_summary(change_id, action))
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(**kwargs):
new_kwargs = {
'host': 'pigweed',
'project': PROJECT,
'cq_account': CQ_BOT_ACCOUNT,
'label': VERIFIED_LABEL,
}
new_kwargs.update(**kwargs)
return api.properties(**new_kwargs)
def unlabeled_query_results(project='pigweed'):
return api.step_data(
'get unlabeled changes',
api.json.output([{'_number': 1, 'project': project}]),
)
def labeled_query_results(project='pigweed'):
return api.step_data(
'get labeled changes',
api.json.output([{'_number': 1, 'project': project}]),
)
def message(patch_set, passed, author=CQ_BOT_ACCOUNT):
return {
'author': {'email': author,},
'message': 'Patch Set {}: Dry run: {}'.format(
patch_set, 'This CL passed' if passed else 'Failed builds',
),
}
def details(*messages, **kwargs):
res = {
'current_revision': 'h3110',
'revisions': {'h3110': {'_number': 2}},
'messages': list(messages),
'project': PROJECT,
}
res.update(kwargs)
return api.step_data('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, '1.{}set {} to 1'.format(dry_run_part, label),
)
def set_failed(label=VERIFIED_LABEL, dry_run=False):
dry_run_part = 'would ' if dry_run else ''
return api.post_process(
post_process.MustRun,
'1.{}set {} to -1'.format(dry_run_part, label),
)
def no_set(label=VERIFIED_LABEL):
return (
api.post_process(
post_process.DoesNotRun, '1.set {} to 1'.format(label),
)
+ api.post_process(
post_process.DoesNotRun, '1.would set {} to 1'.format(label),
)
+ api.post_process(
post_process.DoesNotRun, '1.set {} to -1'.format(label),
)
+ api.post_process(
post_process.DoesNotRun, '1.would set {} to -1'.format(label),
)
)
def does_not_clear(label=VERIFIED_LABEL):
return api.post_process(
post_process.DoesNotRun, '1.clear {}'.format(label),
) + api.post_process(
post_process.DoesNotRun, '1.would clear {}'.format(label),
)
def clear(label=VERIFIED_LABEL, dry_run=False):
dry_run_part = 'would ' if dry_run else ''
return api.post_process(
post_process.MustRun, '1.{}clear {}'.format(dry_run_part, 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('wrong-project')
+ properties()
+ unlabeled_query_results()
+ details(message(2, True), project='chromium')
+ 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)
)