1Actions
2=======
3
4This document shows how to define an action in-tree such that it shows up in
5supported user interfaces like Treeherder. For details on interface between
6in-tree logic and external user interfaces, see `the actions.json spec`_.
7
8At a very high level, the process looks like this:
9
10 * The decision task produces an artifact, ``public/actions.json``, indicating
11   what actions are available.
12
13 * A user interface (for example, Treeherder or the Taskcluster tools) consults
14   ``actions.json`` and presents appropriate choices to the user, if necessary
15   gathering additional data from the user, such as the number of times to
16   re-trigger a test case.
17
18 * The user interface follows the action description to carry out the action.
19   In most cases (``action.kind == 'task'``), that entails creating an "action
20   task", including the provided information. That action task is responsible
21   for carrying out the named action, and may create new sub-tasks if necessary
22   (for example, to re-trigger a task).
23
24Defining Action Tasks
25---------------------
26
27There is one options for defining actions: creating a callback action.
28A callback action automatically defines an action task that will invoke a
29Python function of your devising.
30
31Creating a Callback Action
32--------------------------
33
34.. note::
35
36    You can generate ``actions.json`` on the command line with ``./mach taskgraph actions``.
37
38A *callback action* is an action that calls back into in-tree logic. That is,
39you register the action with name, title, description, context, input schema and a
40python callback. When the action is triggered in a user interface,
41input matching the schema is collected, passed to a new task which then calls
42your python callback, enabling it to do pretty much anything it wants to.
43
44To create a new callback action you must create a file
45``taskcluster/gecko_taskgraph/actions/my-action.py``, that at minimum contains::
46
47  from __future__ import absolute_import, print_function, unicode_literals
48
49  from .registry import register_callback_action
50
51  @register_callback_action(
52      name='hello',
53      title='Say Hello',
54      symbol='hw',  # Show the callback task in treeherder as 'hw'
55      description="Simple **proof-of-concept** callback action",
56      order=10000,  # Order in which it should appear relative to other actions
57  )
58  def hello_world_action(parameters, graph_config, input, task_group_id, task_id, task):
59      print("Hello was triggered from taskGroupId: {}".format(task_group_id))
60
61The arguments are:
62
63``parameters``
64  an instance of ``taskgraph.parameters.Parameters``, carrying decision task parameters from the original decision task.
65
66``graph_config``
67  an instance of ``taskgraph.config.GraphConfig``, carrying configuration for this tree
68
69``input``
70  the input from the user triggering the action (if any)
71
72``task_group_id``
73  the target task group on which this action should operate
74
75``task_id``
76  the target task on which this action should operate (or None if it is operating on the whole group)
77
78``task``
79  the definition of the target task (or None, as for ``task_id``)
80
81The example above defines an action that is available in the context-menu for
82the entire task-group (result-set or push in Treeherder terminology). To create
83an action that shows up in the context menu for a task we would specify the
84``context`` parameter.
85
86The ``order`` value is the sort key defining the order of actions in the
87resulting ``actions.json`` file.  If multiple actions have the same name and
88match the same task, the action with the smallest ``order`` will be used.
89
90Setting the Action Context
91..........................
92The context parameter should be a list of tag-sets, such as
93``context=[{"platform": "linux"}]``, which will make the task show up in the
94context-menu for any task with ``task.tags.platform = 'linux'``. Below is
95some examples of context parameters and the resulting conditions on
96``task.tags`` (tags used below are just illustrative).
97
98``context=[{"platform": "linux"}]``:
99  Requires ``task.tags.platform = 'linux'``.
100``context=[{"kind": "test", "platform": "linux"}]``:
101  Requires ``task.tags.platform = 'linux'`` **and** ``task.tags.kind = 'test'``.
102``context=[{"kind": "test"}, {"platform": "linux"}]``:
103  Requires ``task.tags.platform = 'linux'`` **or** ``task.tags.kind = 'test'``.
104``context=[{}]``:
105  Requires nothing and the action will show up in the context menu for all tasks.
106``context=[]``:
107  Is the same as not setting the context parameter, which will make the action
108  show up in the context menu for the task-group.
109  (i.e., the action is not specific to some task)
110
111The example action below will be shown in the context-menu for tasks with
112``task.tags.platform = 'linux'``::
113
114  from registry import register_callback_action
115
116  @register_callback_action(
117      name='retrigger',
118      title='Retrigger',
119      symbol='re-c',  # Show the callback task in treeherder as 're-c'
120      description="Create a clone of the task",
121      order=1,
122      context=[{'platform': 'linux'}]
123  )
124  def retrigger_action(parameters, graph_config, input, task_group_id, task_id, task):
125      # input will be None
126      print "Retriggering: {}".format(task_id)
127      print "task definition: {}".format(task)
128
129When the ``context`` parameter is set, the ``task_id`` and ``task`` parameters
130will provided to the callback. In this case the ``task_id`` and ``task``
131parameters will be the ``taskId`` and *task definition* of the task from whose
132context-menu the action was triggered.
133
134Typically, the ``context`` parameter is used for actions that operate on
135tasks, such as retriggering, running a specific test case, creating a loaner,
136bisection, etc. You can think of the context as a place the action should
137appear, but it's also very much a form of input the action can use.
138
139
140Specifying an Input Schema
141..........................
142In call examples so far the ``input`` parameter for the callbacks has been
143``None``. To make an action that takes input you must specify an input schema.
144This is done by passing a JSON schema as the ``schema`` parameter.
145
146When designing a schema for the input it is important to exploit as many of the
147JSON schema validation features as reasonably possible. Furthermore, it is
148*strongly* encouraged that the ``title`` and ``description`` properties in
149JSON schemas is used to provide a detailed explanation of what the input
150value will do. Authors can reasonably expect JSON schema ``description``
151properties to be rendered as markdown before being presented.
152
153The example below illustrates how to specify an input schema. Notice that while
154this example doesn't specify a ``context`` it is perfectly legal to specify
155both ``input`` and ``context``::
156
157  from registry import register_callback_action
158
159  @register_callback_action(
160      name='run-all',
161      title='Run All Tasks',
162      symbol='ra-c',  # Show the callback task in treeherder as 'ra-c'
163      description="**Run all tasks** that have been _optimized_ away.",
164      order=1,
165      input={
166          'title': 'Action Options',
167          'description': 'Options for how you wish to run all tasks',
168          'properties': {
169              'priority': {
170                  'title': 'priority'
171                  'description': 'Priority that should be given to the tasks',
172                  'type': 'string',
173                  'enum': ['low', 'normal', 'high'],
174                  'default': 'low',
175              },
176              'runTalos': {
177                  'title': 'Run Talos'
178                  'description': 'Do you wish to also include talos tasks?',
179                  'type': 'boolean',
180                  'default': 'false',
181              }
182          },
183          'required': ['priority', 'runTalos'],
184          'additionalProperties': False,
185      },
186  )
187  def retrigger_action(parameters, graph_config, input, task_group_id, task_id, task):
188      print "Create all pruned tasks with priority: {}".format(input['priority'])
189      if input['runTalos']:
190          print "Also running talos jobs..."
191
192When the ``schema`` parameter is given the callback will always be called with
193an ``input`` parameter that satisfies the previously given JSON schema.
194It is encouraged to set ``additionalProperties: false``, as well as specifying
195all properties as ``required`` in the JSON schema. Furthermore, it's good
196practice to provide ``default`` values for properties, as user interface generators
197will often take advantage of such properties.
198
199It is possible to specify the ``schema`` parameter as a callable that returns
200the JSON schema. It will be called with a keyword parameter ``graph_config``
201with the `graph configuration <taskgraph-graph-config>` of the current
202taskgraph.
203
204Once you have specified input and context as applicable for your action you can
205do pretty much anything you want from within your callback. Whether you want
206to create one or more tasks or run a specific piece of code like a test.
207
208Conditional Availability
209........................
210
211The decision parameters ``taskgraph.parameters.Parameters`` passed to
212the callback are also available when the decision task generates the list of
213actions to be displayed in the user interface. When registering an action
214callback the ``availability`` option can be used to specify a callable
215which, given the decision parameters, determines if the action should be available.
216The feature is illustrated below::
217
218  from registry import register_callback_action
219
220  @register_callback_action(
221      name='hello',
222      title='Say Hello',
223      symbol='hw',  # Show the callback task in treeherder as 'hw'
224      description="Simple **proof-of-concept** callback action",
225      order=2,
226      # Define an action that is only included if this is a push to try
227      available=lambda parameters: parameters.get('project', None) == 'try',
228  )
229  def try_only_action(parameters, graph_config, input, task_group_id, task_id, task):
230      print "My try-only action"
231
232Properties of ``parameters``  are documented in the
233:doc:`parameters section <parameters>`. You can also examine the
234``parameters.yml`` artifact created by decisions tasks.
235
236Context can be similarly conditionalized by passing a function which returns
237the appropriate context::
238
239    context=lambda params:
240        [{}] if int(params['level']) < 3 else [{'worker-implementation': 'docker-worker'}],
241
242Creating Tasks
243--------------
244
245The ``create_tasks`` utility function provides a full-featured way to create
246new tasks.  Its features include creating prerequisite tasks, operating in a
247"testing" mode with ``./mach taskgraph test-action-callback``, and generating
248artifacts that can be used by later action tasks to figure out what happened.
249See the source for more detailed docmentation.
250
251The artifacts are:
252
253``task-graph.json`` (or ``task-graph-<suffix>.json``:
254  The graph of all tasks created by the action task. Includes tasks
255  created to satisfy requirements.
256``to-run.json`` (or ``to-run-<suffix>.json``:
257  The set of tasks that the action task requested to build. This does not
258  include the requirements.
259``label-to-taskid.json`` (or ``label-to-taskid-<suffix>.json``:
260  This is the mapping from label to ``taskid`` for all tasks involved in
261  the task-graph. This includes dependencies.
262
263More Information
264----------------
265
266For further details on actions in general, see `the actions.json spec`_.
267The hooks used for in-tree actions are set up by `ci-admin`_ based on configuration in `ci-configuration`_.
268
269.. _the actions.json spec: https://firefox-ci-tc.services.mozilla.com/docs/manual/tasks/actions/spec
270.. _ci-admin: http://hg.mozilla.org/ci/ci-admin/
271.. _ci-configuration: http://hg.mozilla.org/ci/ci-configuration/
272