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 = [] actions = []
callbacks = {} callbacks = {}
Action = namedtuple('Action', [ Action = namedtuple('Action', ['order', 'action_builder'])
'name', 'title', 'description', 'order', 'context', 'schema', 'task_template_builder',
])
def is_json(data): def is_json(data):
@ -37,7 +35,8 @@ def is_json(data):
def register_callback_action(name, title, symbol, description, order=10000, 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 Register an action callback that can be triggered from supporting
user interfaces, such as Treeherder. user interfaces, such as Treeherder.
@ -90,6 +89,11 @@ def register_callback_action(name, title, symbol, description, order=10000,
schema : dict schema : dict
JSON schema specifying input accepted by the action. JSON schema specifying input accepted by the action.
This is optional and can be left ``null`` if no input is taken. 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 Returns
------- -------
@ -98,11 +102,15 @@ def register_callback_action(name, title, symbol, description, order=10000,
""" """
mem = {"registered": False} # workaround nonlocal missing in 2.x mem = {"registered": False} # workaround nonlocal missing in 2.x
def register_callback(cb):
assert isinstance(name, basestring), 'name must be a string'
assert isinstance(title, basestring), 'title must be a string' assert isinstance(title, basestring), 'title must be a string'
assert isinstance(description, basestring), 'description 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(order, int), 'order must be an integer' 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 callable(schema) or is_json(schema), 'schema must be a JSON compatible object'
assert isinstance(cb, FunctionType), 'callback must be a function' assert isinstance(cb, FunctionType), 'callback must be a function'
# Allow for json-e > 25 chars in the symbol. # 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 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__) 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): if not available(parameters):
return None 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']) repo_param = '{}head_repository'.format(graph_config['project-repo-param-prefix'])
revision = parameters['{}head_rev'.format(graph_config['project-repo-param-prefix'])] repository = {
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))
task_group_id = os.environ.get('TASK_ID', slugid())
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')
return {
'$let': {
'tasks_for': 'action',
'repository': {
'url': parameters[repo_param], 'url': parameters[repo_param],
'project': parameters['project'], 'project': parameters['project'],
'level': parameters['level'], 'level': parameters['level'],
}, }
'push': {
revision = parameters['{}head_rev'.format(graph_config['project-repo-param-prefix'])]
push = {
'owner': 'mozilla-taskcluster-maintenance@mozilla.com', 'owner': 'mozilla-taskcluster-maintenance@mozilla.com',
'pushlog_id': parameters['pushlog_id'], 'pushlog_id': parameters['pushlog_id'],
'revision': revision, 'revision': revision,
}, }
'action': {
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))
action = {
'name': name, 'name': name,
'title': title, 'title': title,
'description': description, 'description': description,
'taskGroupId': task_group_id, 'taskGroupId': task_group_id,
'repo_scope': repo_scope,
'cb_name': cb.__name__, 'cb_name': cb.__name__,
'symbol': symbol, 'symbol': symbol,
},
},
'in': taskcluster_yml['tasks'][0]
} }
actions.append(Action( rv = {
name.strip(), title.strip(), description.strip(), order, context, 'name': name,
schema, task_template_builder)) 'title': title,
'description': description,
'context': context,
}
if schema:
rv['schema'] = schema(graph_config=graph_config) if callable(schema) else schema
# for kind=task, we embed the task from .taskcluster.yml in the action, with
# suitable context
if kind == 'task':
template = graph_config.taskcluster_yml
# 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
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],
},
})
# 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 mem['registered'] = True
callbacks[cb.__name__] = cb callbacks[cb.__name__] = cb
@ -186,32 +249,18 @@ def render_actions_json(parameters, graph_config):
JSON object representation of the ``public/actions.json`` artifact. JSON object representation of the ``public/actions.json`` artifact.
""" """
assert isinstance(parameters, Parameters), 'requires instance of Parameters' assert isinstance(parameters, Parameters), 'requires instance of Parameters'
result = [] actions = []
for action in sorted(_get_actions(graph_config), key=lambda action: action.order): for action in sorted(_get_actions(graph_config), key=lambda action: action.order):
task = action.task_template_builder(parameters, graph_config) action = action.action_builder(parameters, graph_config)
if task: if action:
assert is_json(task), 'task must be a JSON compatible object' assert is_json(action), 'action must be a JSON compatible object'
res = { actions.append(action)
'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)
return { return {
'version': 1, 'version': 1,
'variables': { 'variables': {
'parameters': dict(**parameters), 'parameters': dict(**parameters),
}, },
'actions': result, 'actions': actions,
} }