Bug 1381622: add a 'name' property to actions; r?jonasfj draft
authorDustin J. Mitchell <dustin@mozilla.com>
Tue, 18 Jul 2017 15:57:12 +0000
changeset 610658 d4329c2b24ea163c84be9f61c7c583f8437df49e
parent 610657 c9888183e83564cd8043c3b4962b07964ef8f2a4
child 610659 d6d9133607e339f2727e3a5c6faddfea7d9012e9
push id68972
push userdmitchell@mozilla.com
push dateTue, 18 Jul 2017 17:03:15 +0000
reviewersjonasfj
bugs1381622
milestone56.0a1
Bug 1381622: add a 'name' property to actions; r?jonasfj This name can be used by user interfaces to find the action corresponding to a dedicaetd UI element. MozReview-Commit-ID: HFBDlX30J31
taskcluster/actions/hello-action.py
taskcluster/actions/registry.py
taskcluster/actions/test-retrigger-action.py
taskcluster/docs/action-implementation.rst
taskcluster/docs/action-spec.rst
taskcluster/docs/actions-schema.yml
--- a/taskcluster/actions/hello-action.py
+++ b/taskcluster/actions/hello-action.py
@@ -1,12 +1,13 @@
 from .registry import register_callback_action
 
 
 @register_callback_action(
+    name='hello',
     title='Say Hello',
     symbol='hw',
     description="""
     Simple **proof-of-concept** action that prints a hello action.
     """,
     order=10000,  # Put this at the very bottom/end of any menu (default)
     context=[{}],  # Applies to any task
     available=lambda parameters: True,  # available regardless decision parameters (default)
--- a/taskcluster/actions/registry.py
+++ b/taskcluster/actions/registry.py
@@ -9,45 +9,47 @@ from taskgraph.parameters import Paramet
 
 
 GECKO = os.path.realpath(os.path.join(__file__, '..', '..', '..'))
 
 actions = []
 callbacks = {}
 
 Action = namedtuple('Action', [
-    'title', 'description', 'order', 'context', 'schema', 'task_template_builder',
+    'name', '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):
+def register_task_action(name, 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
     ----------
+    name : str
+        An identifier for this action, used by UIs to find the action.
     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
@@ -68,33 +70,35 @@ def register_task_action(title, descript
 
     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(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 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,
+            name.strip(), 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):
+def register_callback_action(name, 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``:
@@ -106,16 +110,18 @@ def register_callback_action(title, symb
         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
     ----------
+    name : str
+        An identifier for this action, used by UIs to find the action.
     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
@@ -151,17 +157,17 @@ def register_callback_action(title, symb
     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)
+        @register_task_action(name, title, description, order, context, schema)
         def build_callback_action_task(parameters):
             if not available(parameters):
                 return None
 
             match = re.match(r'https://(hg.mozilla.org)/(.*?)/?$', parameters['head_repository'])
             if not match:
                 raise Exception('Unrecognized head_repository')
             repo_scope = 'assume:repo:{}/{}:*'.format(
@@ -258,16 +264,17 @@ def render_actions_json(parameters):
     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({
                 'kind': 'task',
+                'name': action.name,
                 'title': action.title,
                 'description': action.description,
                 'context': action.context,
                 'schema': action.schema,
                 'task': task,
             })
     return {
         'version': 1,
--- a/taskcluster/actions/test-retrigger-action.py
+++ b/taskcluster/actions/test-retrigger-action.py
@@ -13,16 +13,17 @@ from taskgraph.util.time import (
 )
 
 TASKCLUSTER_QUEUE_URL = "https://queue.taskcluster.net/v1/task"
 
 logger = logging.getLogger(__name__)
 
 
 @register_callback_action(
+    name='run-with-options',
     title='Schedule test retrigger',
     symbol='tr',
     description="Retriggers the specified test job with additional options",
     context=[{'test-type': 'mochitest'},
              {'test-type': 'reftest'}],
     order=0,
     schema={
         'type': 'object',
--- a/taskcluster/docs/action-implementation.rst
+++ b/taskcluster/docs/action-implementation.rst
@@ -12,27 +12,28 @@ action task that will invoke a Python fu
 
 A custom action task is an arbitrary task definition that will be created
 directly.  In cases where the callback would simply call ``queue.createTask``,
 a custom action task can be more efficient.
 
 Creating a Callback Action
 --------------------------
 A *callback action* is an action that calls back into in-tree logic. That is,
-you register the action with title, description, context, input schema and a
+you register the action with name, title, description, context, input schema and a
 python callback. When the action is triggered in a user interface,
 input matching the schema is collected, passed to a new task which then calls
 your python callback, enabling it to do pretty much anything it wants to.
 
 To create a new action you must create a file
 ``taskcluster/taskgraph/actions/my-action.py``, that at minimum contains::
 
   from registry import register_callback_action
 
   @register_callback_action(
+      name='hello',
       title='Say Hello',
       symbol='hw',  # Show the callback task in treeherder as 'hw'
       description="Simple **proof-of-concept** callback action",
       order=10000,  # Order in which it should appear relative to other actions
   )
   def hello_world_action(parameters, input, task_group_id, task_id, task):
       # parameters is an instance of taskgraph.parameters.Parameters
       # it carries decision task parameters from the original decision task.
@@ -67,16 +68,17 @@ some examples of context parameters and 
   (i.e., the action is not specific to some task)
 
 The example action below will be shown in the context-menu for tasks with
 ``task.tags.platform = 'linux'``::
 
   from registry import register_callback_action
 
   @register_callback_action(
+      name='retrigger',
       title='Retrigger',
       symbol='re-c',  # Show the callback task in treeherder as 're-c'
       description="Create a clone of the task",
       order=1,
       context=[{'platform': 'linux'}]
   )
   def retrigger_action(parameters, input, task_group_id, task_id, task):
       # input will be None
@@ -109,16 +111,17 @@ properties to be rendered as markdown be
 
 The example below illustrates how to specify an input schema. Notice that while
 this example doesn't specify a ``context`` it is perfectly legal to specify
 both ``input`` and ``context``::
 
   from registry import register_callback_action
 
   @register_callback_action(
+      name='run-all',
       title='Run All Tasks',
       symbol='ra-c',  # Show the callback task in treeherder as 'ra-c'
       description="**Run all tasks** that have been _optimized_ away.",
       order=1,
       input={
           'title': 'Action Options',
           'description': 'Options for how you wish to run all tasks',
           'properties': {
@@ -163,16 +166,17 @@ the callback are also available when the
 actions to be displayed in the user interface. When registering an action
 callback the ``availability`` option can be used to specify a callable
 which, given the decision parameters, determines if the action should be available.
 The feature is illustrated below::
 
   from registry import register_callback_action
 
   @register_callback_action(
+      name='hello',
       title='Say Hello',
       symbol='hw',  # Show the callback task in treeherder as 'hw'
       description="Simple **proof-of-concept** callback action",
       order=2,
       # Define an action that is only included if this is a push to try
       available=lambda parameters: parameters.get('project', None) == 'try',
   )
   def try_only_action(parameters, input, task_group_id, task_id, task):
@@ -189,16 +193,17 @@ Creating a Custom Action Task
 It is possible to define an action that doesn't take a callback. Instead, you'll
 then have to provide a task template. For details on how the task template
 language works refer to :doc:`the specification for actions.json <action-spec>`,
 the example below illustrates how to create such an action::
 
   from registry import register_task_action
 
   @register_task_action(
+      name='retrigger',
       title='Retrigger',
       description="Create a clone of the task",
       order=1,
       context=[{'platform': 'linux'}],
       input={
           'title': 'priority'
           'description': 'Priority that should be given to the tasks',
           'type': 'string',
--- a/taskcluster/docs/action-spec.rst
+++ b/taskcluster/docs/action-spec.rst
@@ -68,16 +68,17 @@ available:
 The following **example** demonstrates how a task template can specify
 timestamps and dump input JSON into environment variables::
 
   {
     "version": 1,
     "actions": [
       {
         "kind": "task",
+        "name": "thing",
         "title: "Do A Thing",
         "description": "Do something",
         "task": {
           "workerType": "my-worker",
           "payload": {
             "created": {"$fromNow": ""},
             "deadline": {"$fromNow": "1 hour 15 minutes"},
             "expiration": {"$fromNow": "14 days"},
@@ -94,19 +95,28 @@ timestamps and dump input JSON into envi
     ],
     "variables: {},
   }
 
 
 MetaData
 ........
 
-Each action entry must define a ``title`` and ``description``.  furthermore,
-the list of actions should be sorted by the order in which actions should
-appear in a menu.
+Each action entry must define a ``name``, ``title`` and ``description``.
+furthermore, the list of actions should be sorted by the order in which actions
+should appear in a menu.
+
+The ``name`` is used by user interfaces to identify the action. For example, a
+retrigger button might look for an action with `name = "retrigger"`.
+
+Action names must be unique for a given task, or for a taskgroup, but the same
+name may be used for actions applying to disjoint sets of tasks. For example,
+it may be helpful to define different "retrigger" actions for build tasks
+`[{jobKind: 'build'}]` and test tasks `[{jobKind: 'test'}]`, and in this case
+only one such action would apply to any given task.
 
 The ``title`` is a human readable string intended to be used as label on the
 button, link or menu entry that triggers the action. This should be short and
 concise.  Ideally, you'll want to avoid duplicates.
 
 The ``description`` property contains a human readable string describing the
 action, such as what it does, how it does it, what it is useful for. This string
 is to be render as **markdown**, allowing for bullet points, links and other
@@ -170,16 +180,17 @@ An action can take JSON input, the input
 specified using a `JSON schema <http://json-schema.org/>`_. This schema is
 specified with by the action's ``schema`` property.  For example::
 
   {
     "version": 1,
     "actions": [
       {
         "kind": "task",
+        "name": "thing",
         "title: "Do A Thing",
         "description": "Do something",
         "schema": {
           "description": "The thing to do",
           "title": "Thing",
           "default": "something",
           "type": "string"
           "maxLength": 255
--- a/taskcluster/docs/actions-schema.yml
+++ b/taskcluster/docs/actions-schema.yml
@@ -34,16 +34,30 @@ properties:
     additionalProperties: true
   actions:
     type: array
     description: |
       List of actions that can be triggered.
     items:
       type: object
       properties:
+        name:
+          type: string
+          maxLength: 255
+          description: |
+            The name of this action.  This is used by user interfaces to
+            identify the action. For example, a retrigger button might look for
+            an action with `name = "retrigger"`.
+
+            Action names must be unique for a given task, or for a taskgroup,
+            but the same name may be used for actions applying to disjoint sets
+            of tasks. For example, it may be helpful to define different
+            "retrigger" actions for build tasks `[{jobKind: 'build'}]` and test
+            tasks `[{jobKind: 'test'}]`, and in this case only one such action
+            would apply to any given task.
         title:
           type: string
           maxLength: 255
           description: |
             Title text to be displayed on the button or link triggering the action.
         description:
           type: string
           maxLength: 4096