Bug 1380306 - Create a new 'try_task_config' method for scheduling tasks on try, r?dustin draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 27 Jun 2017 13:33:20 -0700
changeset 608457 4393379810f48d46f24fbe5ccb48a908f24cce1f
parent 608415 d4e6f45db73333f693ac0cf53cab1f8ab4798373
child 637306 0cb3991748f032252e1495979ad8ca2f3b1d359a
push id68280
push userahalberstadt@mozilla.com
push dateThu, 13 Jul 2017 18:10:27 +0000
reviewersdustin
bugs1380306, 1380357
milestone56.0a1
Bug 1380306 - Create a new 'try_task_config' method for scheduling tasks on try, r?dustin This introduces a 'try_task_config' method of scheduling. En lieu of (or in addition to) try syntax, you can now check in a file called 'try_task_config.json' to the root of the source tree. The format is either a list of task labels, or dict where task labels are the keys. Taskcluster will simply schedule any tasks that are listed there. This file is primarily meant to be generated by tools (which don't exist yet), as the json format is much easier for tools to generate or consume. These tools should use an in-memory commit to add the file so it is automatically removed again after the push. A server-side hook will be added in bug 1380357 to prevent this file from accidentally landing on non-try trees. MozReview-Commit-ID: 2zKfZXuuDhH
taskcluster/docs/how-tos.rst
taskcluster/taskgraph/decision.py
taskcluster/taskgraph/target_tasks.py
taskcluster/taskgraph/test/test_target_tasks.py
--- a/taskcluster/docs/how-tos.rst
+++ b/taskcluster/docs/how-tos.rst
@@ -217,8 +217,59 @@ mach, instead.  If this is not possible,
 ``taskcluster/taskgraph/transforms/jobs`` with a structure similar to its peers
 will make the new run-using option available for job descriptions.
 
 Something Else?
 ...............
 
 If you make another change not described here that turns out to be simple or
 common, please include an update to this file in your patch.
+
+
+Schedule a Task on Try
+----------------------
+
+There are two methods for scheduling a task on try.
+
+The first method is a command line string called ``try syntax`` which is passed
+into the decision task via the commit message. An example try syntax might look
+like:
+
+.. parsed-literal::
+
+    try: -b o -p linux64 -u mochitest-1 -t none
+
+This gets parsed by ``taskgraph.try_option_syntax:TryOptionSyntax`` and returns
+a list of matching task labels. For more information see the
+`TryServer wiki page <https://wiki.mozilla.org/Try>`_.
+
+The second method uses a checked-in file called ``try_task_config.json`` which
+lives at the root of the source dir. The format of this file is either a list
+of task labels, or a JSON object where task labels make up the keys. For
+example, the ``try_task_config.json`` file might look like:
+
+.. parsed-literal::
+
+    [
+      "test-windows10-64/opt-web-platform-tests-12",
+      "test-windows7-32/opt-reftest-1",
+      "test-windows7-32/opt-reftest-2",
+      "test-windows7-32/opt-reftest-3",
+      "build-linux64/debug",
+      "source-test-mozlint-eslint"
+    ]
+
+Very simply, this will run any task label that gets passed in as well as their
+dependencies. While it is possible to manually commit this file and push to
+try, it is mainly meant to be a generation target for various trychooser tools.
+
+A list of all possible task labels can be obtained by running:
+
+.. parsed-literal::
+
+    $ ./mach taskgraph tasks
+
+A list of task labels relevant to a tree (defaults to mozilla-central) can be
+obtained with:
+
+.. parsed-literal::
+
+    $ ./mach taskgraph target
--- a/taskcluster/taskgraph/decision.py
+++ b/taskcluster/taskgraph/decision.py
@@ -29,17 +29,17 @@ from taskgraph.util.time import (
 logger = logging.getLogger(__name__)
 
 ARTIFACTS_DIR = 'artifacts'
 
 # For each project, this gives a set of parameters specific to the project.
 # See `taskcluster/docs/parameters.rst` for information on parameters.
 PER_PROJECT_PARAMETERS = {
     'try': {
-        'target_tasks_method': 'try_option_syntax',
+        'target_tasks_method': 'try_tasks',
         # Always perform optimization.  This makes it difficult to use try
         # pushes to run a task that would otherwise be optimized, but is a
         # compromise to avoid essentially disabling optimization in try.
         'optimize_target_tasks': True,
         # By default, the `try_option_syntax` `target_task_method` ignores this
         # parameter, and enables/disables nightlies depending whether
         # `--include-nightly` is specified in the commit message.
         # We're setting the `include_nightly` parameter to True here for when
--- a/taskcluster/taskgraph/target_tasks.py
+++ b/taskcluster/taskgraph/target_tasks.py
@@ -1,15 +1,19 @@
 # -*- 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 os
+import json
+
 from taskgraph import try_option_syntax
 from taskgraph.util.attributes import match_run_on_projects
 
 _target_task_methods = {}
 
 
 def _target_task(name):
     def wrap(func):
@@ -43,18 +47,34 @@ def filter_upload_symbols(task, paramete
 
 def standard_filter(task, parameters):
     return all(
         filter_func(task, parameters) for filter_func in
         (filter_on_nightly, filter_for_project, filter_upload_symbols)
     )
 
 
-@_target_task('try_option_syntax')
-def target_tasks_try_option_syntax(full_task_graph, parameters):
+def _try_task_config(full_task_graph, parameters):
+    task_config_file = os.path.join(os.getcwd(), 'try_task_config.json')
+
+    if not os.path.isfile(task_config_file):
+        return []
+
+    with open(task_config_file, 'r') as fh:
+        task_config = json.load(fh)
+
+    target_task_labels = []
+    for task in full_task_graph.tasks.itervalues():
+        if task.label in task_config:
+            target_task_labels.append(task.label)
+
+    return target_task_labels
+
+
+def _try_option_syntax(full_task_graph, parameters):
     """Generate a list of target tasks based on try syntax in
     parameters['message'] and, for context, the full task graph."""
     options = try_option_syntax.TryOptionSyntax(parameters['message'], full_task_graph)
     target_tasks_labels = [t.label for t in full_task_graph.tasks.itervalues()
                            if options.task_matches(t)]
 
     attributes = {
         k: getattr(options, k) for k in [
@@ -92,16 +112,26 @@ def target_tasks_try_option_syntax(full_
                 routes.append("notify.email.{}.on-any".format(owner))
             elif options.notifications == 'failure':
                 routes.append("notify.email.{}.on-failed".format(owner))
                 routes.append("notify.email.{}.on-exception".format(owner))
 
     return target_tasks_labels
 
 
+@_target_task('try_tasks')
+def target_tasks_try(full_task_graph, parameters):
+    labels = _try_task_config(full_task_graph, parameters)
+
+    if 'try:' in parameters['message'] or not labels:
+        labels.extend(_try_option_syntax(full_task_graph, parameters))
+
+    return labels
+
+
 @_target_task('default')
 def target_tasks_default(full_task_graph, parameters):
     """Target the tasks which have indicated they should be run on this project
     via the `run_on_projects` attributes."""
 
     return [l for l, t in full_task_graph.tasks.iteritems()
             if standard_filter(t, parameters)]
 
--- a/taskcluster/taskgraph/test/test_target_tasks.py
+++ b/taskcluster/taskgraph/test/test_target_tasks.py
@@ -1,14 +1,15 @@
 # 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 os
 import unittest
 
 from taskgraph import target_tasks
 from taskgraph import try_option_syntax
 from taskgraph.graph import Graph
 from taskgraph.taskgraph import TaskGraph
 from taskgraph.task import Task
 from mozunit import main
@@ -48,40 +49,61 @@ class TestTargetTasks(unittest.TestCase)
         self.assertTrue(self.default_matches(['all'], 'baobab'))
 
     def test_default_integration(self):
         """run_on_projects=[integration] includes integration projects"""
         self.assertFalse(self.default_matches(['integration'], 'mozilla-central'))
         self.assertTrue(self.default_matches(['integration'], 'mozilla-inbound'))
         self.assertFalse(self.default_matches(['integration'], 'baobab'))
 
-    def test_default_relesae(self):
+    def test_default_release(self):
         """run_on_projects=[release] includes release projects"""
         self.assertTrue(self.default_matches(['release'], 'mozilla-central'))
         self.assertFalse(self.default_matches(['release'], 'mozilla-inbound'))
         self.assertFalse(self.default_matches(['release'], 'baobab'))
 
     def test_default_nothing(self):
         """run_on_projects=[] includes nothing"""
         self.assertFalse(self.default_matches([], 'mozilla-central'))
         self.assertFalse(self.default_matches([], 'mozilla-inbound'))
         self.assertFalse(self.default_matches([], 'baobab'))
 
-    def test_try_option_syntax(self):
+    def test_try_tasks(self):
         tasks = {
             'a': Task(kind=None, label='a', attributes={}, task={}),
             'b': Task(kind=None, label='b', attributes={'at-at': 'yep'}, task={}),
+            'c': Task(kind=None, label='c', attributes={}, task={}),
         }
-        graph = Graph(nodes=set('ab'), edges=set())
+        graph = Graph(nodes=set('abc'), edges=set())
         tg = TaskGraph(tasks, graph)
-        params = {'message': 'try me'}
+
+        method = target_tasks.get_method('try_tasks')
+        config = os.path.join(os.getcwd(), 'try_task_config.json')
 
         orig_TryOptionSyntax = try_option_syntax.TryOptionSyntax
         try:
             try_option_syntax.TryOptionSyntax = FakeTryOptionSyntax
-            method = target_tasks.get_method('try_option_syntax')
-            self.assertEqual(method(tg, params), ['b'])
+
+            # no try specifier
+            self.assertEqual(method(tg, {'message': ''}), ['b'])
+
+            # try syntax only
+            self.assertEqual(method(tg, {'message': 'try: me'}), ['b'])
+
+            # try task config only
+            with open(config, 'w') as fh:
+                fh.write('["c"]')
+            self.assertEqual(method(tg, {'message': ''}), ['c'])
+
+            with open(config, 'w') as fh:
+                fh.write('{"c": {}}')
+            self.assertEqual(method(tg, {'message': ''}), ['c'])
+
+            # both syntax and config
+            self.assertEqual(set(method(tg, {'message': 'try: me'})), set(['b', 'c']))
         finally:
             try_option_syntax.TryOptionSyntax = orig_TryOptionSyntax
+            if os.path.isfile(config):
+                os.remove(config)
 
 
 if __name__ == '__main__':
     main()