2017-02-01 02:34:05 +03:00
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import inspect
|
2017-02-18 01:43:59 +03:00
|
|
|
import re
|
2017-02-01 02:34:05 +03:00
|
|
|
from types import FunctionType
|
|
|
|
from collections import namedtuple
|
|
|
|
from taskgraph.util.docker import docker_image
|
|
|
|
from taskgraph.parameters import Parameters
|
|
|
|
|
|
|
|
|
|
|
|
GECKO = os.path.realpath(os.path.join(__file__, '..', '..', '..'))
|
|
|
|
|
|
|
|
actions = []
|
|
|
|
callbacks = {}
|
|
|
|
|
|
|
|
Action = namedtuple('Action', [
|
|
|
|
'title', 'description', 'order', 'context', 'schema', 'task_template_builder',
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
def is_json(data):
|
|
|
|
""" Return ``True``, if ``data`` is a JSON serializable data structure. """
|
|
|
|
try:
|
|
|
|
json.dumps(data)
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def register_task_action(title, description, order, context, schema):
|
|
|
|
"""
|
|
|
|
Register an action task that can be triggered from supporting
|
|
|
|
user interfaces, such as Treeherder.
|
|
|
|
|
|
|
|
Most actions will create intermediate action tasks that call back into
|
|
|
|
in-tree python code. To write such an action please use
|
|
|
|
:func:`register_callback_action`.
|
|
|
|
|
|
|
|
This function is to be used a decorator for a function that returns a task
|
|
|
|
template, see :doc:`specification <action-spec>` for details on the
|
|
|
|
templating features. The decorated function will be given decision task
|
|
|
|
parameters, which can be embedded in the task template that is returned.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
title : str
|
|
|
|
A human readable title for the action to be used as label on a button
|
|
|
|
or text on a link for triggering the action.
|
|
|
|
description : str
|
|
|
|
A human readable description of the action in **markdown**.
|
|
|
|
This will be display as tooltip and in dialog window when the action
|
|
|
|
is triggered. This is a good place to describe how to use the action.
|
|
|
|
order : int
|
|
|
|
Order of the action in menus, this is relative to the ``order`` of
|
|
|
|
other actions declared.
|
|
|
|
context : list of dict
|
|
|
|
List of tag-sets specifying which tasks the action is can take as input.
|
|
|
|
If no tag-sets is specified as input the action is related to the
|
|
|
|
entire task-group, and won't be triggered with a given task.
|
|
|
|
|
|
|
|
Otherwise, if ``context = [{'k': 'b', 'p': 'l'}, {'k': 't'}]`` will only
|
|
|
|
be displayed in the context menu for tasks that has
|
|
|
|
``task.tags.k == 'b' && task.tags.p = 'l'`` or ``task.tags.k = 't'``.
|
|
|
|
Esentially, this allows filtering on ``task.tags``.
|
|
|
|
schema : dict
|
|
|
|
JSON schema specifying input accepted by the action.
|
|
|
|
This is optional and can be left ``null`` if no input is taken.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
function
|
|
|
|
To be used as decorator for the function that builds the task template.
|
|
|
|
The decorated function will be given decision parameters and may return
|
|
|
|
``None`` instead of a task template, if the action is disabled.
|
|
|
|
"""
|
|
|
|
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 is_json(schema), 'schema must be a JSON compatible object'
|
|
|
|
mem = {"registered": False} # workaround nonlocal missing in 2.x
|
|
|
|
|
|
|
|
def register_task_template_builder(task_template_builder):
|
|
|
|
assert not mem['registered'], 'register_task_action must be used as decorator'
|
|
|
|
actions.append(Action(
|
|
|
|
title.strip(), description.strip(), order, context, schema, task_template_builder,
|
|
|
|
))
|
|
|
|
mem['registered'] = True
|
|
|
|
return register_task_template_builder
|
|
|
|
|
|
|
|
|
|
|
|
def register_callback_action(title, symbol, description, order=10000, context=[],
|
|
|
|
available=lambda parameters: True, schema=None):
|
|
|
|
"""
|
|
|
|
Register an action callback that can be triggered from supporting
|
|
|
|
user interfaces, such as Treeherder.
|
|
|
|
|
|
|
|
This function is to be used as a decorator for a callback that takes
|
|
|
|
parameters as follows:
|
|
|
|
|
|
|
|
``parameters``:
|
|
|
|
Decision task parameters, see ``taskgraph.parameters.Parameters``.
|
|
|
|
``input``:
|
|
|
|
Input matching specified JSON schema, ``None`` if no ``schema``
|
|
|
|
parameter is given to ``register_callback_action``.
|
|
|
|
``task_group_id``:
|
|
|
|
The id of the task-group this was triggered for.
|
|
|
|
``task_id`` and `task``:
|
|
|
|
task identifier and task definition for task the action was triggered
|
|
|
|
for, ``None`` if no ``context`` parameters was given to
|
|
|
|
``register_callback_action``.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
title : str
|
|
|
|
A human readable title for the action to be used as label on a button
|
|
|
|
or text on a link for triggering the action.
|
|
|
|
symbol : str
|
|
|
|
Treeherder symbol for the action callback, this is the symbol that the
|
|
|
|
task calling your callback will be displayed as. This is usually 1-3
|
|
|
|
letters abbreviating the action title.
|
|
|
|
description : str
|
|
|
|
A human readable description of the action in **markdown**.
|
|
|
|
This will be display as tooltip and in dialog window when the action
|
|
|
|
is triggered. This is a good place to describe how to use the action.
|
|
|
|
order : int
|
|
|
|
Order of the action in menus, this is relative to the ``order`` of
|
|
|
|
other actions declared.
|
|
|
|
context : list of dict
|
|
|
|
List of tag-sets specifying which tasks the action is can take as input.
|
|
|
|
If no tag-sets is specified as input the action is related to the
|
|
|
|
entire task-group, and won't be triggered with a given task.
|
|
|
|
|
|
|
|
Otherwise, if ``context = [{'k': 'b', 'p': 'l'}, {'k': 't'}]`` will only
|
|
|
|
be displayed in the context menu for tasks that has
|
|
|
|
``task.tags.k == 'b' && task.tags.p = 'l'`` or ``task.tags.k = 't'``.
|
|
|
|
Esentially, this allows filtering on ``task.tags``.
|
|
|
|
available : function
|
|
|
|
An optional function that given decision parameters decides if the
|
|
|
|
action is available. Defaults to a function that always returns ``True``.
|
|
|
|
schema : dict
|
|
|
|
JSON schema specifying input accepted by the action.
|
|
|
|
This is optional and can be left ``null`` if no input is taken.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
function
|
|
|
|
To be used as decorator for the callback function.
|
|
|
|
"""
|
|
|
|
mem = {"registered": False} # workaround nonlocal missing in 2.x
|
|
|
|
|
|
|
|
def register_callback(cb):
|
|
|
|
assert isinstance(cb, FunctionType), 'callback must be a function'
|
|
|
|
assert isinstance(symbol, basestring), 'symbol must be a string'
|
|
|
|
assert 1 <= len(symbol) <= 25, 'symbol must be between 1 and 25 characters'
|
|
|
|
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__)
|
|
|
|
source_path = os.path.relpath(inspect.stack()[1][1], GECKO)
|
|
|
|
|
|
|
|
@register_task_action(title, description, order, context, schema)
|
|
|
|
def build_callback_action_task(parameters):
|
|
|
|
if not available(parameters):
|
|
|
|
return None
|
2017-02-18 01:43:59 +03:00
|
|
|
|
|
|
|
match = re.match(r'https://(hg.mozilla.org)/(.*?)/?$', parameters['head_repository'])
|
|
|
|
if not match:
|
|
|
|
raise Exception('Unrecognized head_repository')
|
|
|
|
repo_scope = 'assume:repo:{}/{}:*'.format(
|
|
|
|
match.group(1), match.group(2))
|
|
|
|
|
2017-02-01 02:34:05 +03:00
|
|
|
return {
|
|
|
|
'created': {'$fromNow': ''},
|
|
|
|
'deadline': {'$fromNow': '12 hours'},
|
|
|
|
'expires': {'$fromNow': '14 days'},
|
|
|
|
'metadata': {
|
|
|
|
'owner': 'mozilla-taskcluster-maintenance@mozilla.com',
|
|
|
|
'source': '{}raw-file/{}/{}'.format(
|
|
|
|
parameters['head_repository'], parameters['head_rev'], source_path,
|
|
|
|
),
|
|
|
|
'name': 'Action: {}'.format(title),
|
|
|
|
'description': 'Task executing callback for action.\n\n---\n' + description,
|
|
|
|
},
|
|
|
|
'workerType': 'gecko-decision',
|
|
|
|
'provisionerId': 'aws-provisioner-v1',
|
|
|
|
'scopes': [
|
2017-02-18 01:43:59 +03:00
|
|
|
repo_scope,
|
2017-02-01 02:34:05 +03:00
|
|
|
],
|
|
|
|
'tags': {
|
|
|
|
'createdForUser': parameters['owner'],
|
|
|
|
'kind': 'action-callback',
|
|
|
|
},
|
|
|
|
'routes': [
|
|
|
|
'tc-treeherder.v2.{}.{}.{}'.format(
|
|
|
|
parameters['project'], parameters['head_rev'], parameters['pushlog_id']),
|
|
|
|
'tc-treeherder-stage.v2.{}.{}.{}'.format(
|
|
|
|
parameters['project'], parameters['head_rev'], parameters['pushlog_id']),
|
|
|
|
],
|
|
|
|
'payload': {
|
|
|
|
'env': {
|
|
|
|
'GECKO_BASE_REPOSITORY': 'https://hg.mozilla.org/mozilla-unified',
|
|
|
|
'GECKO_HEAD_REPOSITORY': parameters['head_repository'],
|
|
|
|
'GECKO_HEAD_REF': parameters['head_ref'],
|
|
|
|
'GECKO_HEAD_REV': parameters['head_rev'],
|
|
|
|
'HG_STORE_PATH': '/home/worker/checkouts/hg-store',
|
|
|
|
'ACTION_TASK_GROUP_ID': {'$eval': 'taskGroupId'},
|
|
|
|
'ACTION_TASK_ID': {'$dumps': {'$eval': 'taskId'}},
|
|
|
|
'ACTION_TASK': {'$dumps': {'$eval': 'task'}},
|
|
|
|
'ACTION_INPUT': {'$dumps': {'$eval': 'input'}},
|
|
|
|
'ACTION_CALLBACK': cb.__name__,
|
|
|
|
'ACTION_PARAMETERS': {'$dumps': {'$eval': 'parameters'}},
|
|
|
|
},
|
|
|
|
'cache': {
|
|
|
|
'level-{}-checkouts'.format(parameters['level']):
|
|
|
|
'/home/worker/checkouts',
|
|
|
|
},
|
|
|
|
'features': {
|
|
|
|
'taskclusterProxy': True,
|
|
|
|
'chainOfTrust': True,
|
|
|
|
},
|
|
|
|
'image': docker_image('decision'),
|
|
|
|
'maxRunTime': 1800,
|
|
|
|
'command': [
|
|
|
|
'/home/worker/bin/run-task', '--vcs-checkout=/home/worker/checkouts/gecko',
|
|
|
|
'--', 'bash', '-cx',
|
|
|
|
"""\
|
|
|
|
cd /home/worker/checkouts/gecko &&
|
|
|
|
ln -s /home/worker/artifacts artifacts &&
|
2017-02-25 00:27:20 +03:00
|
|
|
./mach --log-no-times taskgraph action-callback""",
|
2017-02-01 02:34:05 +03:00
|
|
|
],
|
|
|
|
},
|
|
|
|
'extra': {
|
|
|
|
'treeherder': {
|
|
|
|
'groupName': 'action-callback',
|
|
|
|
'groupSymbol': 'AC',
|
|
|
|
'symbol': symbol,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
mem['registered'] = True
|
|
|
|
callbacks[cb.__name__] = cb
|
|
|
|
return register_callback
|
|
|
|
|
|
|
|
|
|
|
|
def render_actions_json(parameters):
|
|
|
|
"""
|
|
|
|
Render JSON object for the ``public/actions.json`` artifact.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
parameters : taskgraph.parameters.Parameters
|
|
|
|
Decision task parameters.
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
dict
|
|
|
|
JSON object representation of the ``public/actions.json`` artifact.
|
|
|
|
"""
|
|
|
|
global actions
|
|
|
|
assert isinstance(parameters, Parameters), 'requires instance of Parameters'
|
|
|
|
result = []
|
|
|
|
for action in sorted(actions, key=lambda action: action.order):
|
|
|
|
task = action.task_template_builder(parameters)
|
|
|
|
if task:
|
|
|
|
assert is_json(task), 'task must be a JSON compatible object'
|
|
|
|
result.append({
|
|
|
|
'title': action.title,
|
|
|
|
'description': action.description,
|
|
|
|
'context': action.context,
|
|
|
|
'schema': action.schema,
|
|
|
|
'task': task,
|
|
|
|
})
|
|
|
|
return {
|
|
|
|
'version': 1,
|
|
|
|
'variables': {
|
|
|
|
'parameters': dict(**parameters),
|
|
|
|
},
|
|
|
|
'actions': result,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def trigger_action_callback():
|
|
|
|
"""
|
|
|
|
Trigger action callback using arguments from environment variables.
|
|
|
|
"""
|
|
|
|
global callbacks
|
|
|
|
task_group_id = os.environ.get('ACTION_TASK_GROUP_ID', None)
|
|
|
|
task_id = json.loads(os.environ.get('ACTION_TASK_ID', 'null'))
|
|
|
|
task = json.loads(os.environ.get('ACTION_TASK', 'null'))
|
|
|
|
input = json.loads(os.environ.get('ACTION_INPUT', 'null'))
|
|
|
|
callback = os.environ.get('ACTION_CALLBACK', None)
|
|
|
|
parameters = json.loads(os.environ.get('ACTION_PARAMETERS', 'null'))
|
|
|
|
cb = callbacks.get(callback, None)
|
|
|
|
if not cb:
|
|
|
|
raise Exception('Unknown callback: {}'.format(callback))
|
|
|
|
cb(Parameters(**parameters), input, task_group_id, task_id, task)
|
|
|
|
|
|
|
|
|
|
|
|
# Load all modules from this folder, relying on the side-effects of register_
|
|
|
|
# functions to populate the action registry.
|
|
|
|
for f in os.listdir(os.path.dirname(__file__)):
|
|
|
|
if f.endswith('.py') and f not in ('__init__.py', 'registry.py'):
|
|
|
|
__import__('actions.' + f[:-3])
|