Bug 1472777: add create-interactive action; r=bstack
authorDustin J. Mitchell <dustin@mozilla.com>
Mon, 02 Jul 2018 20:24:16 +0000
changeset 424715 fd9f1da215425382cf28cce5ce9c373fc803333d
parent 424714 87bb8b686a7e913611ef9c02928cadf5708d98b1
child 424716 021b2f3683cfa311166050cb84aff780fca60d73
push id34222
push useraiakab@mozilla.com
push dateTue, 03 Jul 2018 08:54:46 +0000
treeherdermozilla-central@f6e1ff7b57e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbstack
bugs1472777
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1472777: add create-interactive action; r=bstack The resulting action task isn't useful to the user, so instead we send an email containing a link to the interaction console. MozReview-Commit-ID: 5uHnQo9WTF6
taskcluster/taskgraph/actions/create_interactive.py
taskcluster/taskgraph/actions/util.py
taskcluster/taskgraph/util/taskcluster.py
new file mode 100644
--- /dev/null
+++ b/taskcluster/taskgraph/actions/create_interactive.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import logging
+
+from .util import (
+    create_tasks,
+    fetch_graph_and_labels
+)
+from taskgraph.util.taskcluster import send_email
+from .registry import register_callback_action
+
+logger = logging.getLogger(__name__)
+
+EMAIL_SUBJECT = 'Your Interactive Task for {label}'
+EMAIL_CONTENT = '''\
+As you requested, Firefox CI has created an interactive task to run {label}
+on revision {revision} in {repo}. Click the button below to connect to the
+task. You may need to wait for it to begin running.
+'''
+
+@register_callback_action(
+    title='Create Interactive Task',
+    name='create-interactive',
+    symbol='create-inter',
+    kind='hook',
+    generic=True,
+    description=(
+        'Create a a copy of the task that you can interact with'
+    ),
+    order=1,
+    context=[{'worker-implementation': 'docker-worker'}],
+    schema={
+        'type': 'object',
+        'properties': {
+            'notify': {
+                'type': 'string',
+                'format': 'email',
+                'title': 'Who to notify of the pending interactive task',
+                'description': 'Enter your email here to get an email containing a link to interact with the task',
+                # include a default for ease of users' editing
+                'default': 'noreply@noreply.mozilla.org',
+            },
+        },
+        'additionalProperties': False,
+    },
+)
+def create_interactive_action(parameters, graph_config, input, task_group_id, task_id, task):
+    # fetch the original task definition from the taskgraph, to avoid
+    # creating interactive copies of unexpected tasks.  Note that this only applies
+    # to docker-worker tasks, so we can assume the docker-worker payload format.
+    decision_task_id, full_task_graph, label_to_taskid = fetch_graph_and_labels(
+        parameters, graph_config)
+
+    label = task['metadata']['name']
+    def edit(task):
+        if task.label != label:
+            return task
+        task_def = task.task
+
+        # drop task routes (don't index this!)
+        task_def['routes'] = []
+
+        # only try this once
+        task_def['retries'] = 0
+
+        # short expirations, at least 3 hour maxRunTime
+        task_def['deadline'] = {'relative-datestamp': '12 hours'}
+        task_def['created'] = {'relative-datestamp': '0 hours'}
+        task_def['expires'] = {'relative-datestamp': '1 day'}
+        task_def['payload']['maxRunTime'] = max(3600 * 3, task_def['payload'].get('maxRunTime', 0))
+
+        # no caches
+        task_def['scopes'] = [s for s in task_def['scopes'] if not s.startswith('docker-worker:cache:')]
+        task_def['payload']['cache'] = {}
+
+        # no artifacts
+        task_def['payload']['artifacts'] = {}
+
+        # enable interactive mode
+        task_def['payload'].setdefault('features', {})['interactive'] = True
+        task_def['payload'].setdefault('env', {})['TASKCLUSTER_INTERACTIVE'] = 'true'
+
+        return task
+
+    # Create the task and any of its dependencies. This uses a new taskGroupId to avoid
+    # polluting the existing taskGroup with interactive tasks.
+    label_to_taskid = create_tasks([label], full_task_graph, label_to_taskid, parameters, modifier=edit)
+
+    taskId = label_to_taskid[label]
+
+    if input and 'notify' in input:
+        email = input['notify']
+        # no point sending to a noreply address!
+        if email == 'noreply@noreply.mozilla.org':
+            return
+
+        info = {
+            'url': 'https://tools.taskcluster.net/tasks/{}/connect'.format(taskId),
+            'label': label,
+            'revision': parameters['head_rev'],
+            'repo': parameters['head_repository'],
+        }
+        send_email(
+            email,
+            subject=EMAIL_SUBJECT.format(**info),
+            content=EMAIL_CONTENT.format(**info),
+            link={
+                'text': 'Connect',
+                'href': info['url'],
+            },
+            use_proxy=True)
+
--- a/taskcluster/taskgraph/actions/util.py
+++ b/taskcluster/taskgraph/actions/util.py
@@ -118,17 +118,19 @@ def create_tasks(to_run, full_task_graph
     allowing easy debugging with `mach taskgraph action-callback --test`.
     This builds up all required tasks to run in order to run the tasks requested.
 
     Optionally this function takes a `modifier` function that is passed in each
     task before it is put into a new graph. It should return a valid task. Note
     that this is passed _all_ tasks in the graph, not just the set in to_run. You
     may want to skip modifying tasks not in your to_run list.
 
-    If you wish to create the tasks in a new group, leave out decision_task_id."""
+    If you wish to create the tasks in a new group, leave out decision_task_id.
+
+    Returns an updated label_to_taskid containing the new tasks"""
     if suffix != '':
         suffix = '-{}'.format(suffix)
     to_run = set(to_run)
 
     #  Copy to avoid side-effects later
     full_task_graph = copy.deepcopy(full_task_graph)
     label_to_taskid = label_to_taskid.copy()
 
@@ -140,8 +142,9 @@ def create_tasks(to_run, full_task_graph
     optimized_task_graph, label_to_taskid = optimize_task_graph(target_task_graph,
                                                                 params,
                                                                 to_run,
                                                                 label_to_taskid)
     write_artifact('task-graph{}.json'.format(suffix), optimized_task_graph.to_json())
     write_artifact('label-to-taskid{}.json'.format(suffix), label_to_taskid)
     write_artifact('to-run{}.json'.format(suffix), list(to_run))
     create.create_tasks(optimized_task_graph, label_to_taskid, params, decision_task_id)
+    return label_to_taskid
--- a/taskcluster/taskgraph/util/taskcluster.py
+++ b/taskcluster/taskgraph/util/taskcluster.py
@@ -223,8 +223,23 @@ def get_taskcluster_artifact_prefix(task
     if artifact_prefix == 'public/build' and not force_private:
         tmpl = _PUBLIC_TC_ARTIFACT_LOCATION
     else:
         tmpl = _PRIVATE_TC_ARTIFACT_LOCATION
 
     return tmpl.format(
         task_id=task_id, postfix=postfix, artifact_prefix=artifact_prefix
     )
+
+
+def send_email(address, subject, content, link, use_proxy=False):
+    """Sends an email using the notify service"""
+    logger.info('Sending email to {}.'.format(address))
+    if use_proxy:
+        url = 'http://taskcluster/notify/v1/email'
+    else:
+        url = 'https://notify.taskcluster.net/v1/email'
+    _do_request(url, json={
+        'address': address,
+        'subject': subject,
+        'content': content,
+        'link': link,
+    })