зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1389248 - Move actions spec to taskcluster docs r=dustin
MozReview-Commit-ID: BsYRmivUZZ7 --HG-- extra : rebase_source : 05d3c67f06585f43b224fce9785dbf8f97edd3e5
This commit is contained in:
Родитель
795d1f97fc
Коммит
d1e7547331
|
@ -1,244 +0,0 @@
|
|||
Action Task Implementation
|
||||
==========================
|
||||
|
||||
This document shows how to define an action in-tree such that it shows up in
|
||||
supported user interfaces like Treeherder. For details on interface between
|
||||
in-tree logic and external user interfaces, see
|
||||
:doc:`the specification for actions.json <action-spec>`.
|
||||
|
||||
There are two options for defining actions: creating a callback action, or
|
||||
creating a custom action task. A callback action automatically defines an
|
||||
action task that will invoke a Python function of your devising.
|
||||
|
||||
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 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.
|
||||
# input, task_id, and task should all be None
|
||||
print "Hello was triggered from taskGroupId: " + taskGroupId
|
||||
|
||||
The example above defines an action that is available in the context-menu for
|
||||
the entire task-group (result-set or push in Treeherder terminology). To create
|
||||
an action that shows up in the context menu for a task we would specify the
|
||||
``context`` parameter.
|
||||
|
||||
|
||||
Setting the Action Context
|
||||
..........................
|
||||
The context parameter should be a list of tag-sets, such as
|
||||
``context=[{"platform": "linux"}]``, which will make the task show up in the
|
||||
context-menu for any task with ``task.tags.platform = 'linux'``. Below is
|
||||
some examples of context parameters and the resulting conditions on
|
||||
``task.tags`` (tags used below are just illustrative).
|
||||
|
||||
``context=[{"platform": "linux"}]``:
|
||||
Requires ``task.tags.platform = 'linux'``.
|
||||
``context=[{"kind": "test", "platform": "linux"}]``:
|
||||
Requires ``task.tags.platform = 'linux'`` **and** ``task.tags.kind = 'test'``.
|
||||
``context=[{"kind": "test"}, {"platform": "linux"}]``:
|
||||
Requires ``task.tags.platform = 'linux'`` **or** ``task.tags.kind = 'test'``.
|
||||
``context=[{}]``:
|
||||
Requires nothing and the action will show up in the context menu for all tasks.
|
||||
``context=[]``:
|
||||
Is the same as not setting the context parameter, which will make the action
|
||||
show up in the context menu for the task-group.
|
||||
(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
|
||||
print "Retriggering: {}".format(task_id)
|
||||
print "task definition: {}".format(task)
|
||||
|
||||
When the ``context`` parameter is set, the ``task_id`` and ``task`` parameters
|
||||
will provided to the callback. In this case the ``task_id`` and ``task``
|
||||
parameters will be the ``taskId`` and *task definition* of the task from whose
|
||||
context-menu the action was triggered.
|
||||
|
||||
Typically, the ``context`` parameter is used for actions that operate on
|
||||
tasks, such as retriggering, running a specific test case, creating a loaner,
|
||||
bisection, etc. You can think of the context as a place the action should
|
||||
appear, but it's also very much a form of input the action can use.
|
||||
|
||||
|
||||
Specifying an Input Schema
|
||||
..........................
|
||||
In call examples so far the ``input`` parameter for the callbacks has been
|
||||
``None``. To make an action that takes input you must specify an input schema.
|
||||
This is done by passing a JSON schema as the ``schema`` parameter.
|
||||
|
||||
When designing a schema for the input it is important to exploit as many of the
|
||||
JSON schema validation features as reasonably possible. Furthermore, it is
|
||||
*strongly* encouraged that the ``title`` and ``description`` properties in
|
||||
JSON schemas is used to provide a detailed explanation of what the input
|
||||
value will do. Authors can reasonably expect JSON schema ``description``
|
||||
properties to be rendered as markdown before being presented.
|
||||
|
||||
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': {
|
||||
'priority': {
|
||||
'title': 'priority'
|
||||
'description': 'Priority that should be given to the tasks',
|
||||
'type': 'string',
|
||||
'enum': ['low', 'normal', 'high'],
|
||||
'default': 'low',
|
||||
},
|
||||
'runTalos': {
|
||||
'title': 'Run Talos'
|
||||
'description': 'Do you wish to also include talos tasks?',
|
||||
'type': 'boolean',
|
||||
'default': 'false',
|
||||
}
|
||||
},
|
||||
'required': ['priority', 'runTalos'],
|
||||
'additionalProperties': False,
|
||||
},
|
||||
)
|
||||
def retrigger_action(parameters, input, task_group_id, task_id, task):
|
||||
print "Create all pruned tasks with priority: {}".format(input['priority'])
|
||||
if input['runTalos']:
|
||||
print "Also running talos jobs..."
|
||||
|
||||
When the ``schema`` parameter is given the callback will always be called with
|
||||
an ``input`` parameter that satisfies the previously given JSON schema.
|
||||
It is encouraged to set ``additionalProperties: false``, as well as specifying
|
||||
all properties as ``required`` in the JSON schema. Furthermore, it's good
|
||||
practice to provide ``default`` values for properties, as user interface generators
|
||||
will often take advantage of such properties.
|
||||
|
||||
Once you have specified input and context as applicable for your action you can
|
||||
do pretty much anything you want from within your callback. Whether you want
|
||||
to create one or more tasks or run a specific piece of code like a test.
|
||||
|
||||
Conditional Availability
|
||||
........................
|
||||
The decision parameters ``taskgraph.parameters.Parameters`` passed to
|
||||
the callback are also available when the decision task generates the list of
|
||||
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):
|
||||
print "My try-only action"
|
||||
|
||||
Properties of ``parameters`` are documented in the
|
||||
:doc:`parameters section <parameters>`. You can also examine the
|
||||
``parameters.yml`` artifact created by decisions tasks.
|
||||
|
||||
|
||||
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',
|
||||
'enum': ['low', 'normal', 'high'],
|
||||
'default': 'low',
|
||||
},
|
||||
)
|
||||
def task_template_builder(parameters):
|
||||
# The task template builder may return None to signal that the action
|
||||
# isn't available.
|
||||
if parameters.get('project', None) != 'try':
|
||||
return None
|
||||
return {
|
||||
'created': {'$fromNow': ''},
|
||||
'deadline': {'$fromNow': '1 hour'},
|
||||
'expires': {'$fromNow': '14 days'},
|
||||
'provisionerId': '...',
|
||||
'workerType': '...',
|
||||
'priority': '${input}',
|
||||
'payload': {
|
||||
'command': '...',
|
||||
'env': {
|
||||
'TASK_DEFINITION': {'$json': {'eval': 'task'}}
|
||||
},
|
||||
...
|
||||
},
|
||||
# It's now your responsibility to include treeherder routes, as well
|
||||
# additional metadata for treeherder in task.extra.treeherder.
|
||||
...
|
||||
},
|
||||
|
||||
This kind of action is useful for creating simple derivative tasks, but is
|
||||
limited by the expressiveness of the template language. On the other hand, it
|
||||
is more efficient than an action callback as it does not involve an
|
||||
intermediate action task before creating the task the user requested.
|
||||
|
||||
For further details on the template language, see :doc:`the specification for
|
||||
actions.json <action-spec>`.
|
|
@ -1,242 +0,0 @@
|
|||
Action Specification
|
||||
====================
|
||||
This document specifies how actions exposed by the *decision task* are to be
|
||||
presented and triggered from Treeherder, or similar user interfaces.
|
||||
|
||||
The *decision task* creates an artifact ``public/actions.json`` to be consumed
|
||||
by a user interface such as Treeherder. The ``public/actions.json`` file
|
||||
specifies actions that can be triggered such as:
|
||||
|
||||
* Retrigger a task,
|
||||
* Retry specific test cases many times,
|
||||
* Obtain a loaner machine,
|
||||
* Back-fill missing tasks,
|
||||
* ...
|
||||
|
||||
Through the ``public/actions.json`` file it is possible expose actions defined
|
||||
in-tree such that the actions can be conveniently triggered in Treeherder.
|
||||
This has two purposes:
|
||||
|
||||
1. Facilitate development of utility actions/tools in-tree, and,
|
||||
2. Strongly decouple build/test configuration from Treeherder.
|
||||
|
||||
For details on how define custom actions in-tree, refer to
|
||||
:doc:`the in-tree actions section <action-details>`. This document merely
|
||||
specifies how ``actions.json`` shall be interpreted.
|
||||
|
||||
Actions
|
||||
-------
|
||||
|
||||
The content of ``actions.json`` is a list of actions (and variables, to be
|
||||
described later). Each action has a ``kind`` describing how a user interface
|
||||
should trigger it. There is currently only one kind defined: ``task``.
|
||||
|
||||
An action with ``kind: 'task'`` specifies a task that the user interface should
|
||||
create. That is, when the action is triggered, the user interface calls the
|
||||
Taskcluster API to create a new task, with the content of that task determined
|
||||
from ``actions.json``.
|
||||
|
||||
The task created by the action may be useful in its own right (for example,
|
||||
running a test with additional debugging), or it may simplify trigger in-tree
|
||||
scripts that create new tasks. The latter form is called an *action task*, and
|
||||
is similar to a decision task. This allows in-tree scripts to execute
|
||||
complicated actions such as backfilling.
|
||||
|
||||
Actions of the ``'task'`` *kind* **must** have a ``task`` property. This
|
||||
property specifies the task template to be parameterized and created in order
|
||||
to trigger the action.
|
||||
|
||||
The template is parameterized using `JSON-e
|
||||
<https://github.com/taskcluster/json-e>`_, with the following context entries
|
||||
available:
|
||||
|
||||
``taskGroupId``
|
||||
the ``taskGroupId`` of task-group this is triggerd from,
|
||||
``taskId``
|
||||
the ``taskId`` of the selected task, ``null`` if no task is
|
||||
selected (this is the case if the action has ``context: []``),
|
||||
``task``
|
||||
the task definition of the selected task, ``null`` if no task is
|
||||
selected (this is the case if the action has ``context: []``), and,
|
||||
``input``
|
||||
the input matching the ``schema`` property, ``null`` if the action
|
||||
doesn't have a ``schema`` property. See "Action Input" below.
|
||||
``<key>``
|
||||
Any ``<key>`` defined in the ``variables`` property may also be referenced.
|
||||
See "Variables" below.
|
||||
|
||||
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"},
|
||||
"image": "my-docker-image",
|
||||
"env": {
|
||||
"TASKID_TRIGGERED_FOR": "${taskId}",
|
||||
"INPUT_JSON": {"$json": {"$eval": "input"}}
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables: {},
|
||||
}
|
||||
|
||||
|
||||
MetaData
|
||||
........
|
||||
|
||||
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
|
||||
simple formatting to explain what the action does.
|
||||
|
||||
|
||||
Action Context
|
||||
..............
|
||||
|
||||
Few actions are relevant in all contexts. For this reason each action specifies
|
||||
a ``context`` property. This property specifies when an action is relevant.
|
||||
Actions *relevant* for a task should be displayed in a context menu for the
|
||||
given task. Similarly actions *not relevant* for a given task should not be
|
||||
displayed in the context menu for the given task.
|
||||
|
||||
As a special case we say that actions for which *no relevant* contexts can
|
||||
exist, are *relevant* for the task-group. This could for example be an action
|
||||
to create tasks that was optimized away.
|
||||
|
||||
The ``context`` property is specified as a list of *tag-sets*. A *tag-set* is a
|
||||
set of key-value pairs. A task is said to *match* a *tag-set* if ``task.tags``
|
||||
is a super-set of the *tag-set*. An action is said to be *relevant* for a given
|
||||
task, if ``task.tags`` *match* one of the *tag-sets* given in the ``context``
|
||||
property for the action.
|
||||
|
||||
Naturally, it follows that an action with an empty list of *tag-sets* in its
|
||||
``context`` property cannot possibly be *relevant* for any task. Hence, by
|
||||
previously declared special case such an action is *relevant* for the
|
||||
task-group.
|
||||
|
||||
**Examples**::
|
||||
|
||||
// Example task definitions (everything but tags eclipsed)
|
||||
TaskA = {..., tags: {kind: 'test', platform: 'linux'}}
|
||||
TaskB = {..., tags: {kind: 'test', platform: 'windows'}}
|
||||
TaskC = {..., tags: {kind: 'build', platform: 'linux'}}
|
||||
|
||||
Action1 = {..., context: [{kind: 'test'}]}
|
||||
// Action1 is relevant to: TaskA, TaskB
|
||||
|
||||
Action2 = {..., context: [{kind: 'test', platform: 'linux'}]}
|
||||
// Action2 is relevant to: TaskA
|
||||
|
||||
Action3 = {..., context: [{platform: 'linux'}]}
|
||||
// Action3 is relevant to: TaskA, TaskC
|
||||
|
||||
Action4 = {..., context: [{kind: 'test'}, {kind: 'build'}]}
|
||||
// Action4 is relevant to: TaskA, TaskB, TaskC
|
||||
|
||||
Action5 = {..., context: [{}]}
|
||||
// Action5 is relevant to: TaskA, TaskB, TaskC (all tasks in fact)
|
||||
|
||||
Action6 = {..., context: []}
|
||||
// Action6 is relevant to the task-group
|
||||
|
||||
|
||||
Action Input
|
||||
............
|
||||
|
||||
An action can take JSON input, the input format accepted by an action is
|
||||
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
|
||||
},
|
||||
"task": {
|
||||
"payload": {
|
||||
"env": {
|
||||
"INPUT_JSON": {"$json": {"$eval": "input"}}
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables: {},
|
||||
}
|
||||
|
||||
User interfaces for triggering actions, like Treeherder, are expected to provide
|
||||
JSON input that satisfies this schema. These interfaces are also expected to
|
||||
validate the input against the schema before attempting to trigger the action.
|
||||
|
||||
It is perfectly legal to reference external schemas using
|
||||
constructs like ``{"$ref": "https://example.com/my-schema.json"}``, in this case
|
||||
it however strongly recommended that the external resource is available over
|
||||
HTTPS and allows CORS requests from any source.
|
||||
|
||||
When writing schemas it is strongly encouraged that the JSON schema
|
||||
``description`` properties are used to provide detailed descriptions. It is
|
||||
assumed that consumers will render these ``description`` properties as markdown.
|
||||
|
||||
|
||||
Variables
|
||||
---------
|
||||
|
||||
The ``public/actions.json`` artifact has a ``variables`` property that is a
|
||||
mapping from variable names to JSON values to be used as constants.
|
||||
These variables can be referenced from task templates, but beware that they
|
||||
may overshadow builtin variables. This is mainly useful to deduplicate commonly
|
||||
used values, in order to reduce template size. This feature does not
|
||||
introduce further expressiveness.
|
||||
|
||||
Formal Specification
|
||||
--------------------
|
||||
|
||||
The following is the JSON schema for ``actions.json``.
|
||||
|
||||
.. literalinclude:: actions-schema.yml
|
||||
:language: YAML
|
|
@ -1,87 +0,0 @@
|
|||
User Interface Considerations
|
||||
=============================
|
||||
|
||||
The actions system decouples in-tree changes from user interface changes by
|
||||
taking advantage of graceful degradation. User interfaces, when presented with
|
||||
an unfamiliar action, fall back to a usable default behavior, and can later be
|
||||
upgraded to handle that action with a more refined approach.
|
||||
|
||||
Default Behavior
|
||||
----------------
|
||||
|
||||
Every user interface should support the following:
|
||||
|
||||
* Displaying a list of actions relevant to each task, and displaying
|
||||
task-group tasks for the associated task-group.
|
||||
|
||||
* Providing an opportuntity for the user to enter input for an action. This
|
||||
might be in JSON or YAML, or using a form auto-generated from the action's
|
||||
JSON-schema. If the action has no schema, this step should be skipped.
|
||||
The user's input should be validated against the schema.
|
||||
|
||||
* For ``action.kind = 'task'``, rendering the template using the JSON-e
|
||||
library, using the variables described in :doc:`action-spec`.
|
||||
|
||||
* Calling ``Queue.createTask`` with the resulting task, using the user's
|
||||
Taskcluster credentials. See the next section for some important
|
||||
security-related concerns.
|
||||
|
||||
Creating Tasks
|
||||
--------------
|
||||
|
||||
When executing an action, a UI must ensure that the user is authorized to
|
||||
perform the action, and that the user is not being "tricked" into executing
|
||||
an unexpected action.
|
||||
|
||||
To accomplish the first, the UI should create tasks with the user's Taskcluster
|
||||
credentials. Do not use credentials configured as part of the service itself!
|
||||
|
||||
To accomplish the second, use the decision tasks's ``scopes`` property as the
|
||||
`authorizedScopes
|
||||
<https://docs.taskcluster.net/manual/design/apis/hawk/authorized-scopes>`_ for
|
||||
the ``Queue.createTask`` call. This prevents action tasks from doing anything
|
||||
the original decision task couldn't do.
|
||||
|
||||
Specialized Behavior
|
||||
--------------------
|
||||
|
||||
The default behavior is too clumsy for day-to-day use for common actions. User
|
||||
interfaces may want to provide a more natural interface that still takes advantage
|
||||
of the actions system.
|
||||
|
||||
Specialized Input
|
||||
.................
|
||||
|
||||
A user interface may provide specialized input forms for specific schemas. The
|
||||
input generated from the form must conform to the schema.
|
||||
|
||||
To ensure that the schema has not changed, implementers should do a deep
|
||||
comparison between a schema for which a hand-written form exists, and the
|
||||
schema required by the action. If the two differ, the default behavior should
|
||||
be used instead.
|
||||
|
||||
Specialized Triggering
|
||||
......................
|
||||
|
||||
A user interface may want to trigger a specific action using a dedicated UI
|
||||
element. For example, an "start interactive session" button might be placed
|
||||
next to each failing test in a list of tests.
|
||||
|
||||
User interfaces should look for the desired action by name. The UI should check
|
||||
that there is exactly one matching action available for the given task or
|
||||
task-graph. If multiple actions match, the UI should treat that as an error
|
||||
(helping to avoid actions being surreptitiously replaced by similarly-named,
|
||||
malicious actions).
|
||||
|
||||
Having discovered the task, the user interface has a choice in how to provide
|
||||
its input. It can use the "specialized input" approach outlined above, providing
|
||||
a customized form if the action's schema is recognized and gracefully degrading
|
||||
if not.
|
||||
|
||||
But if the user interface is generating the input internally, it may instead
|
||||
validate that generated input against the action's schema as given, proceeding
|
||||
if validation succeeds. In this alternative, there is no need to do a deep
|
||||
comparison of the schema. This approach allows in-tree changes that introduce
|
||||
backward-compatible changes to the schema, without breaking support in user
|
||||
interfaces. Of course, if the changes are not backward-compatibile, breakage
|
||||
will ensue.
|
|
@ -1,197 +0,0 @@
|
|||
$schema: http://json-schema.org/draft-04/schema#
|
||||
id: https://hg.mozilla.org/mozilla-central/raw-file/tip/taskcluster/docs/actions-schema.yml
|
||||
title: Schema for Exposing Actions
|
||||
description: |
|
||||
This document specifies the schema for the `public/actions.json` used by
|
||||
_decision tasks_ to expose actions that can be triggered by end-users.
|
||||
|
||||
For the purpose of this document the _consumer_ is the user-interface that
|
||||
displays task results to the end-user and allows end-users to trigger actions
|
||||
defined by `public/actions.json`. A _consumer_ might be Treeherder.
|
||||
The _end-user_ is a developer who is inspecting the results, and wish to
|
||||
trigger actions.
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
enum: [1]
|
||||
type: integer
|
||||
variables:
|
||||
type: object
|
||||
description: |
|
||||
Mapping from variable name to constants that can be referenced using
|
||||
`{$eval: '<variable>'}` within the task templates defined for each action.
|
||||
|
||||
This is useful for commonly used constants that are used in many task
|
||||
templates. Whether it's to reduce the size of the `public/actions.json`
|
||||
artifact by reuseing large constants, or simply to make it easier to
|
||||
write task templates by exposing additional variables.
|
||||
|
||||
These will overwrite any builtin variables, such as `taskGroupId`,
|
||||
`input`, `taskId`, `task`, and any further variables that future
|
||||
backwards compatible iterations of this specifcation adds. Hence, you
|
||||
should avoid declaring variables such as `input`, as it will shadow the
|
||||
builtin `input` variable.
|
||||
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
|
||||
description: |
|
||||
Human readable description of the action in markdown.
|
||||
Can be displayed in tooltip, popup and/or dialog when triggering
|
||||
the action.
|
||||
kind:
|
||||
enum:
|
||||
- task
|
||||
description: |
|
||||
Specifies the kind of action this is.
|
||||
|
||||
The `task` _action kind_ is triggered by creating a task, following
|
||||
a task template.
|
||||
|
||||
Other kinds might be added in the future. Consumers should ignore
|
||||
all entries featuring a `kind` property they don't recognize.
|
||||
context:
|
||||
type: array
|
||||
default: []
|
||||
items:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
maxLength: 4096
|
||||
title: tag-set
|
||||
description: |
|
||||
A set of key-value pairs specifying a _tag-set_.
|
||||
description: |
|
||||
The `context` property determines in what context the action is
|
||||
relevant. Thus, what context the action should be presented to the
|
||||
end-user.
|
||||
|
||||
The `context` property contains a set of tag-sets. A _tag-set_ is a
|
||||
set of key-value pairs. A task is said satisfy a tag-set if
|
||||
`task.tags` is a super-set of the given tag-set. An action is
|
||||
relevant for a task if the task satisfies at-least one of
|
||||
the tag-sets.
|
||||
|
||||
Hence, an action with `context: [{a: '1'}, {b: '2'}]` is relevant
|
||||
for any task with `task.tags.a = '1'` or `task.tags.b = '2'`.
|
||||
An action with `context: [{a: '1', b: '2'}]` is only relevant for
|
||||
tasks with `task.tags.a = '1'` and `task.tags.b = '2'`.
|
||||
|
||||
This allows restrictions of what tasks an action is relevant for.
|
||||
For example some tasks might not support running under a debugger.
|
||||
|
||||
The keen reader observes that actions with `context: [{}]` are
|
||||
relevant for all tasks. Conversely, we have that tasks with
|
||||
`context: []` are irrelevant for all tasks. We abuse this property
|
||||
and define actions with `context: []` to be relevant for the
|
||||
_task-group_ only.
|
||||
|
||||
That is an action with `context: []` should not be display in the
|
||||
context-sensitive menu for a task, rather it should be display when
|
||||
selecting the entire task-group. Presentation details are left for
|
||||
consumer to decide.
|
||||
|
||||
Notice that the `context` property is optional, but defined to have
|
||||
a default value `context: []`. Hence, if the `context` is not
|
||||
specified consumer should take this to mean `context: []` implying
|
||||
that the action is relevant to the task-group, rather than any
|
||||
subset of tasks.
|
||||
schema:
|
||||
$ref: http://json-schema.org/schema
|
||||
description: |
|
||||
JSON schema for input parameters to the `task` template property.
|
||||
Consumers shall offer a user-interface where end-users can enter
|
||||
values that satisfy this schema. Furthermore, consumers **must**
|
||||
validate enter values against the given schema before parameterizing
|
||||
the `task` template property and triggering the action.
|
||||
|
||||
In practice it's encourage that consumers employ a facility that
|
||||
can generate HTML forms from JSON schemas. However, if certain
|
||||
schemas are particularly complicated or common, consumers may also
|
||||
hand-write a user-interface for collecting the input. In this case
|
||||
the consumer **must** do a deep comparison between the schema given
|
||||
in the action, and the schema for which a custom user-interface have
|
||||
been written, and fall-back to an auto-generated form if the schema
|
||||
doesn't match.
|
||||
|
||||
It is assumed that the JSON schema `description` property will be
|
||||
rendered as markdown when displayed as documentation for end-users.
|
||||
Producers of `public/actions.json` is encouraged to provide a
|
||||
detailed explanation of the input parameters using these
|
||||
`description` properties. And consumers are *strongly* encouraged
|
||||
to render `description` values as markdown.
|
||||
|
||||
The `schema` property is optional, and if not given the input for
|
||||
`task` template parameterization shall be `null`.
|
||||
task:
|
||||
type: object
|
||||
title: task template
|
||||
description: |
|
||||
Task template for triggering the action.
|
||||
|
||||
When an action have been selected in the appropriate context and
|
||||
input satisfying the `schema` (if any) has been collected. The
|
||||
action is triggered by parameterizing the task template given in
|
||||
this property, and creating the resulting task.
|
||||
|
||||
The template is an object that is parameterized using
|
||||
[JSON-e](https://github.com/taskcluster/json-e), with the above
|
||||
variables supplied as context.
|
||||
|
||||
This allows for dumping `input` and `taskId` into environment
|
||||
variables for the task to be created. The following task template
|
||||
injects `input` and `taskId` as environment variables:
|
||||
```json
|
||||
{
|
||||
"workerType": "my-worker",
|
||||
"payload": {
|
||||
"created": {"$fromNow": ""},
|
||||
"deadline": {"$fromNow": "1 hour 15 minutes"},
|
||||
"expiration": {"$fromNow": "14 days"},
|
||||
"image": "my-docker-image",
|
||||
"env": {
|
||||
"TASKID_TRIGGERED_FOR": "${taskId}",
|
||||
"INPUT_JSON": {"$json": {"$eval": "input"}}
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
additionalProperties: false
|
||||
required:
|
||||
- title
|
||||
- description
|
||||
- kind
|
||||
- task
|
||||
additionalProperties: false
|
||||
required:
|
||||
- version
|
||||
- actions
|
||||
- variables
|
|
@ -1,8 +1,9 @@
|
|||
Actions
|
||||
=======
|
||||
|
||||
This section shows how to define an action in-tree such that it shows up in
|
||||
supported user interfaces like Treeherder.
|
||||
This document shows how to define an action in-tree such that it shows up in
|
||||
supported user interfaces like Treeherder. For details on interface between
|
||||
in-tree logic and external user interfaces, see `the actions.json spec`_.
|
||||
|
||||
At a very high level, the process looks like this:
|
||||
|
||||
|
@ -20,9 +21,243 @@ At a very high level, the process looks like this:
|
|||
for carrying out the named action, and may create new sub-tasks if necessary
|
||||
(for example, to re-trigger a task).
|
||||
|
||||
Action Task Implementation
|
||||
==========================
|
||||
|
||||
.. toctree::
|
||||
There are two options for defining actions: creating a callback action, or
|
||||
creating a custom action task. A callback action automatically defines an
|
||||
action task that will invoke a Python function of your devising.
|
||||
|
||||
action-spec
|
||||
action-uis
|
||||
action-implementation
|
||||
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 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.
|
||||
# input, task_id, and task should all be None
|
||||
print "Hello was triggered from taskGroupId: " + taskGroupId
|
||||
|
||||
The example above defines an action that is available in the context-menu for
|
||||
the entire task-group (result-set or push in Treeherder terminology). To create
|
||||
an action that shows up in the context menu for a task we would specify the
|
||||
``context`` parameter.
|
||||
|
||||
|
||||
Setting the Action Context
|
||||
..........................
|
||||
The context parameter should be a list of tag-sets, such as
|
||||
``context=[{"platform": "linux"}]``, which will make the task show up in the
|
||||
context-menu for any task with ``task.tags.platform = 'linux'``. Below is
|
||||
some examples of context parameters and the resulting conditions on
|
||||
``task.tags`` (tags used below are just illustrative).
|
||||
|
||||
``context=[{"platform": "linux"}]``:
|
||||
Requires ``task.tags.platform = 'linux'``.
|
||||
``context=[{"kind": "test", "platform": "linux"}]``:
|
||||
Requires ``task.tags.platform = 'linux'`` **and** ``task.tags.kind = 'test'``.
|
||||
``context=[{"kind": "test"}, {"platform": "linux"}]``:
|
||||
Requires ``task.tags.platform = 'linux'`` **or** ``task.tags.kind = 'test'``.
|
||||
``context=[{}]``:
|
||||
Requires nothing and the action will show up in the context menu for all tasks.
|
||||
``context=[]``:
|
||||
Is the same as not setting the context parameter, which will make the action
|
||||
show up in the context menu for the task-group.
|
||||
(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
|
||||
print "Retriggering: {}".format(task_id)
|
||||
print "task definition: {}".format(task)
|
||||
|
||||
When the ``context`` parameter is set, the ``task_id`` and ``task`` parameters
|
||||
will provided to the callback. In this case the ``task_id`` and ``task``
|
||||
parameters will be the ``taskId`` and *task definition* of the task from whose
|
||||
context-menu the action was triggered.
|
||||
|
||||
Typically, the ``context`` parameter is used for actions that operate on
|
||||
tasks, such as retriggering, running a specific test case, creating a loaner,
|
||||
bisection, etc. You can think of the context as a place the action should
|
||||
appear, but it's also very much a form of input the action can use.
|
||||
|
||||
|
||||
Specifying an Input Schema
|
||||
..........................
|
||||
In call examples so far the ``input`` parameter for the callbacks has been
|
||||
``None``. To make an action that takes input you must specify an input schema.
|
||||
This is done by passing a JSON schema as the ``schema`` parameter.
|
||||
|
||||
When designing a schema for the input it is important to exploit as many of the
|
||||
JSON schema validation features as reasonably possible. Furthermore, it is
|
||||
*strongly* encouraged that the ``title`` and ``description`` properties in
|
||||
JSON schemas is used to provide a detailed explanation of what the input
|
||||
value will do. Authors can reasonably expect JSON schema ``description``
|
||||
properties to be rendered as markdown before being presented.
|
||||
|
||||
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': {
|
||||
'priority': {
|
||||
'title': 'priority'
|
||||
'description': 'Priority that should be given to the tasks',
|
||||
'type': 'string',
|
||||
'enum': ['low', 'normal', 'high'],
|
||||
'default': 'low',
|
||||
},
|
||||
'runTalos': {
|
||||
'title': 'Run Talos'
|
||||
'description': 'Do you wish to also include talos tasks?',
|
||||
'type': 'boolean',
|
||||
'default': 'false',
|
||||
}
|
||||
},
|
||||
'required': ['priority', 'runTalos'],
|
||||
'additionalProperties': False,
|
||||
},
|
||||
)
|
||||
def retrigger_action(parameters, input, task_group_id, task_id, task):
|
||||
print "Create all pruned tasks with priority: {}".format(input['priority'])
|
||||
if input['runTalos']:
|
||||
print "Also running talos jobs..."
|
||||
|
||||
When the ``schema`` parameter is given the callback will always be called with
|
||||
an ``input`` parameter that satisfies the previously given JSON schema.
|
||||
It is encouraged to set ``additionalProperties: false``, as well as specifying
|
||||
all properties as ``required`` in the JSON schema. Furthermore, it's good
|
||||
practice to provide ``default`` values for properties, as user interface generators
|
||||
will often take advantage of such properties.
|
||||
|
||||
Once you have specified input and context as applicable for your action you can
|
||||
do pretty much anything you want from within your callback. Whether you want
|
||||
to create one or more tasks or run a specific piece of code like a test.
|
||||
|
||||
Conditional Availability
|
||||
........................
|
||||
The decision parameters ``taskgraph.parameters.Parameters`` passed to
|
||||
the callback are also available when the decision task generates the list of
|
||||
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):
|
||||
print "My try-only action"
|
||||
|
||||
Properties of ``parameters`` are documented in the
|
||||
:doc:`parameters section <parameters>`. You can also examine the
|
||||
``parameters.yml`` artifact created by decisions tasks.
|
||||
|
||||
|
||||
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 `the actions.json 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',
|
||||
'enum': ['low', 'normal', 'high'],
|
||||
'default': 'low',
|
||||
},
|
||||
)
|
||||
def task_template_builder(parameters):
|
||||
# The task template builder may return None to signal that the action
|
||||
# isn't available.
|
||||
if parameters.get('project', None) != 'try':
|
||||
return None
|
||||
return {
|
||||
'created': {'$fromNow': ''},
|
||||
'deadline': {'$fromNow': '1 hour'},
|
||||
'expires': {'$fromNow': '14 days'},
|
||||
'provisionerId': '...',
|
||||
'workerType': '...',
|
||||
'priority': '${input}',
|
||||
'payload': {
|
||||
'command': '...',
|
||||
'env': {
|
||||
'TASK_DEFINITION': {'$json': {'eval': 'task'}}
|
||||
},
|
||||
...
|
||||
},
|
||||
# It's now your responsibility to include treeherder routes, as well
|
||||
# additional metadata for treeherder in task.extra.treeherder.
|
||||
...
|
||||
},
|
||||
|
||||
This kind of action is useful for creating simple derivative tasks, but is
|
||||
limited by the expressiveness of the template language. On the other hand, it
|
||||
is more efficient than an action callback as it does not involve an
|
||||
intermediate action task before creating the task the user requested.
|
||||
|
||||
For further details on the template language, see `the actions.json spec`_.
|
||||
|
||||
.. _the actions.json spec: https://docs.taskcluster.net/manual/tasks/actions/spec
|
||||
|
|
Загрузка…
Ссылка в новой задаче