зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
0ba14ea32c
Коммит
0f0fde3dad
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче