Bug 1415868 - add support for defining actions with kind=hook; r=jonasfj,tomprince

This does not affect any existing actions.

MozReview-Commit-ID: 9j5cT2kA7UU

--HG--
extra : rebase_source : 1191d7ecb05b8083a4923b9dbe97218faf65a088
This commit is contained in:
Dustin J. Mitchell 2018-04-25 17:56:29 +00:00
Родитель 0ba14ea32c
Коммит 0f0fde3dad
1 изменённых файлов: 112 добавлений и 63 удалений

Просмотреть файл

@ -22,9 +22,7 @@ from taskgraph.parameters import Parameters
actions = []
callbacks = {}
Action = namedtuple('Action', [
'name', 'title', 'description', 'order', 'context', 'schema', 'task_template_builder',
])
Action = namedtuple('Action', ['order', 'action_builder'])
def is_json(data):
@ -37,7 +35,8 @@ def is_json(data):
def register_callback_action(name, title, symbol, description, order=10000,
context=[], available=lambda parameters: True, schema=None):
context=[], available=lambda parameters: True,
schema=None, kind='task', generic=True):
"""
Register an action callback that can be triggered from supporting
user interfaces, such as Treeherder.
@ -90,6 +89,11 @@ def register_callback_action(name, title, symbol, description, order=10000,
schema : dict
JSON schema specifying input accepted by the action.
This is optional and can be left ``null`` if no input is taken.
kind : string
The action kind to define - must be one of `task` or `hook`. Only for
transitional purposes.
generic : boolean
For kind=hook, whether this is a generic action or has its own permissions.
Returns
-------
@ -98,11 +102,15 @@ def register_callback_action(name, title, symbol, description, order=10000,
"""
mem = {"registered": False} # workaround nonlocal missing in 2.x
assert isinstance(title, basestring), 'title must be a string'
assert isinstance(description, basestring), 'description must be a string'
title = title.strip()
description = description.strip()
def register_callback(cb):
assert isinstance(name, basestring), 'name must be a string'
assert isinstance(title, basestring), 'title must be a string'
assert isinstance(description, basestring), 'description must be a string'
assert isinstance(order, int), 'order must be an integer'
assert kind in ('task', 'hook'), 'kind must be task or hook'
assert callable(schema) or is_json(schema), 'schema must be a JSON compatible object'
assert isinstance(cb, FunctionType), 'callback must be a function'
# Allow for json-e > 25 chars in the symbol.
@ -113,58 +121,113 @@ def register_callback_action(name, title, symbol, description, order=10000,
assert not mem['registered'], 'register_callback_action must be used as decorator'
assert cb.__name__ not in callbacks, 'callback name {} is not unique'.format(cb.__name__)
def task_template_builder(parameters, graph_config):
def action_builder(parameters, graph_config):
if not available(parameters):
return None
actionPerm = 'generic' if generic else name
# gather up the common decision-task-supplied data for this action
repo_param = '{}head_repository'.format(graph_config['project-repo-param-prefix'])
repository = {
'url': parameters[repo_param],
'project': parameters['project'],
'level': parameters['level'],
}
revision = parameters['{}head_rev'.format(graph_config['project-repo-param-prefix'])]
push = {
'owner': 'mozilla-taskcluster-maintenance@mozilla.com',
'pushlog_id': parameters['pushlog_id'],
'revision': revision,
}
task_group_id = os.environ.get('TASK_ID', slugid())
match = re.match(r'https://(hg.mozilla.org)/(.*?)/?$', parameters[repo_param])
if not match:
raise Exception('Unrecognized {}'.format(repo_param))
repo_scope = 'assume:repo:{}/{}:branch:default'.format(
match.group(1), match.group(2))
action = {
'name': name,
'title': title,
'description': description,
'taskGroupId': task_group_id,
'cb_name': cb.__name__,
'symbol': symbol,
}
task_group_id = os.environ.get('TASK_ID', slugid())
rv = {
'name': name,
'title': title,
'description': description,
'context': context,
}
if schema:
rv['schema'] = schema(graph_config=graph_config) if callable(schema) else schema
template = graph_config.taskcluster_yml
# for kind=task, we embed the task from .taskcluster.yml in the action, with
# suitable context
if kind == 'task':
template = graph_config.taskcluster_yml
with open(template, 'r') as f:
taskcluster_yml = yaml.safe_load(f)
if taskcluster_yml['version'] != 1:
raise Exception('actions.json must be updated to work with .taskcluster.yml')
if not isinstance(taskcluster_yml['tasks'], list):
raise Exception('.taskcluster.yml "tasks" must be a list for action tasks')
# tasks get all of the scopes the original push did, yuck; this is not
# done with kind = hook.
repo_scope = 'assume:repo:{}/{}:branch:default'.format(
match.group(1), match.group(2))
action['repo_scope'] = repo_scope
return {
'$let': {
'tasks_for': 'action',
'repository': {
'url': parameters[repo_param],
'project': parameters['project'],
'level': parameters['level'],
},
'push': {
'owner': 'mozilla-taskcluster-maintenance@mozilla.com',
'pushlog_id': parameters['pushlog_id'],
'revision': revision,
},
'action': {
'name': name,
'title': title,
'description': description,
'taskGroupId': task_group_id,
'repo_scope': repo_scope,
'cb_name': cb.__name__,
'symbol': symbol,
with open(template, 'r') as f:
taskcluster_yml = yaml.safe_load(f)
if taskcluster_yml['version'] != 1:
raise Exception(
'actions.json must be updated to work with .taskcluster.yml')
if not isinstance(taskcluster_yml['tasks'], list):
raise Exception(
'.taskcluster.yml "tasks" must be a list for action tasks')
rv.update({
'kind': 'task',
'task': {
'$let': {
'tasks_for': 'action',
'repository': repository,
'push': push,
'action': action,
},
'in': taskcluster_yml['tasks'][0],
},
'in': taskcluster_yml['tasks'][0]
}
})
actions.append(Action(
name.strip(), title.strip(), description.strip(), order, context,
schema, task_template_builder))
# for kind=hook
elif kind == 'hook':
trustDomain = graph_config['trust-domain']
level = parameters['level']
rv.update({
'kind': 'hook',
'hookGroupId': 'project-{}'.format(trustDomain),
'hookId': 'in-tree-action-{}-{}'.format(level, actionPerm),
'hookPayload': {
# provide the decision-task parameters as context for triggerHook
"decision": {
'action': action,
'repository': repository,
'push': push,
# parameters is long, so fetch it from the actions.json variables
'parameters': {'$eval': 'parameters'},
},
# and pass everything else through from our own context
"user": {
'input': {'$eval': 'input'},
'task': {'$eval': 'task'},
'taskId': {'$eval': 'taskId'},
'taskGroupId': {'$eval': 'taskGroupId'},
}
},
})
return rv
actions.append(Action(order, action_builder))
mem['registered'] = True
callbacks[cb.__name__] = cb
@ -186,32 +249,18 @@ def render_actions_json(parameters, graph_config):
JSON object representation of the ``public/actions.json`` artifact.
"""
assert isinstance(parameters, Parameters), 'requires instance of Parameters'
result = []
actions = []
for action in sorted(_get_actions(graph_config), key=lambda action: action.order):
task = action.task_template_builder(parameters, graph_config)
if task:
assert is_json(task), 'task must be a JSON compatible object'
res = {
'kind': 'task',
'name': action.name,
'title': action.title,
'description': action.description,
'context': action.context,
'schema': (
action.schema(graph_config=graph_config) if callable(action.schema)
else action.schema
),
'task': task,
}
if res['schema'] is None:
res.pop('schema')
result.append(res)
action = action.action_builder(parameters, graph_config)
if action:
assert is_json(action), 'action must be a JSON compatible object'
actions.append(action)
return {
'version': 1,
'variables': {
'parameters': dict(**parameters),
},
'actions': result,
'actions': actions,
}