Merge "Allow merge failures to have unique reporters."
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 24de765..7274342 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -706,6 +706,11 @@
 project-pyflakes are only executed if project-merge succeeds.  This
 can help avoid running unnecessary jobs.
 
+The special job named ``noop`` is internal to Zuul and will always
+return ``SUCCESS`` immediately.  This can be useful if you require
+that all changes be processed by a pipeline but a project has no jobs
+that can be run on it.
+
 .. seealso:: The OpenStack Zuul configuration for a comprehensive example: https://github.com/openstack-infra/config/blob/master/modules/openstack_project/files/zuul/layout.yaml
 
 Project Templates
diff --git a/etc/status/public_html/app.js b/etc/status/public_html/app.js
index 2f9d3b7..b4c82f8 100644
--- a/etc/status/public_html/app.js
+++ b/etc/status/public_html/app.js
@@ -17,8 +17,9 @@
 // under the License.
 
 (function ($) {
-    var $container, $msg, $msgWrap, $indicator, $queueInfo, $queueEventsNum, $queueResultsNum, $pipelines,
-        prevHtml, xhr, zuul, $jq,
+    var $container, $msg, $msgWrap, $indicator, $queueInfo, $queueEventsNum,
+        $queueResultsNum, $pipelines, $jq;
+    var xhr, prevHtml, zuul,
         demo = location.search.match(/[?&]demo=([^?&]*)/),
         source = demo ?
             './status-' + (demo[1] || 'basic') + '.json-sample' :
@@ -67,8 +68,10 @@
                     $('#zuul-version-span').text(data['zuul_version']);
                 }
                 if ('last_reconfigured' in data) {
-                    var last_reconfigured = new Date(data['last_reconfigured']);
-                    $('#last-reconfigured-span').text(last_reconfigured.toString());
+                    var last_reconfigured =
+                        new Date(data['last_reconfigured']);
+                    $('#last-reconfigured-span').text(
+                        last_reconfigured.toString());
                 }
 
                 $.each(data.pipelines, function (i, pipeline) {
@@ -82,10 +85,12 @@
                 }
 
                 $queueEventsNum.text(
-                    data.trigger_event_queue ? data.trigger_event_queue.length : '0'
+                    data.trigger_event_queue ?
+                        data.trigger_event_queue.length : '0'
                 );
                 $queueResultsNum.text(
-                    data.result_event_queue ? data.result_event_queue.length : '0'
+                    data.result_event_queue ?
+                        data.result_event_queue.length : '0'
                 );
             })
             .fail(function (err, jqXHR, errMsg) {
@@ -102,7 +107,8 @@
 
         format: {
             change: function (change) {
-                var html = '<div class="well well-small zuul-change"><ul class="nav nav-list">',
+                var html = '<div class="well well-small zuul-change">' +
+                        '<ul class="nav nav-list">',
                     id = change.id,
                     url = change.url;
 
@@ -140,10 +146,12 @@
                     }
                     html += '<li class="zuul-change-job">';
                     html += job.url !== null ?
-                        '<a href="' + job.url + '" class="zuul-change-job-link">' :
+                        '<a href="' + job.url + '" ' +
+                        'class="zuul-change-job-link">' :
                         '<span class="zuul-change-job-link">';
                     html += job.name;
-                    html += ' <span class="' + resultClass + '">' + result + '</span>';
+                    html += ' <span class="' + resultClass + '">' + result +
+                        '</span>';
                     if (job.voting === false) {
                         html += ' <span class="muted">(non-voting)</span>';
                     }
@@ -159,12 +167,15 @@
                 var html = '<div class="zuul-pipeline span4"><h3>' +
                     pipeline.name + '</h3>';
                 if (typeof pipeline.description === 'string') {
-                    html += '<p><small>' + pipeline.description + '</small></p>';
+                    html += '<p><small>' + pipeline.description +
+                        '</small></p>';
                 }
 
-                $.each(pipeline.change_queues, function (queueNum, changeQueue) {
+                $.each(pipeline.change_queues,
+                       function (queueNum, changeQueue) {
                     $.each(changeQueue.heads, function (headNum, changes) {
-                        if (pipeline.change_queues.length > 1 && headNum === 0) {
+                        if (pipeline.change_queues.length > 1 &&
+                            headNum === 0) {
                             var name = changeQueue.name;
                             html += '<p>Queue: <abbr title="' + name + '">';
                             if (name.length > 32) {
@@ -173,9 +184,11 @@
                             html += name + '</abbr></p>';
                         }
                         $.each(changes, function (changeNum, change) {
-                            // If there are multiple changes in the same head it means they're connected
+                            // If there are multiple changes in the same head
+                            // it means they're connected
                             if (changeNum > 0) {
-                                html += '<div class="zuul-change-arrow">&uarr;</div>';
+                                html += '<div class="zuul-change-arrow">' +
+                                    '&uarr;</div>';
                             }
                             html += zuul.format.change(change);
                         });
@@ -216,25 +229,34 @@
     });
 
     $jq.one('update-end', function () {
-        // Do this asynchronous so that if the first update adds a message, it will not animate
-        // while we fade in the content. Instead it simply appears with the rest of the content.
+        // Do this asynchronous so that if the first update adds a message, it
+        // will not animate while we fade in the content. Instead it simply
+        // appears with the rest of the content.
         setTimeout(function () {
-            $container.addClass('zuul-container-ready'); // Fades in the content
+            // Fade in the content
+            $container.addClass('zuul-container-ready');
         });
     });
 
     $(function ($) {
         $msg = $('<div class="zuul-msg alert alert-error"></div>');
-        $msgWrap = $msg.wrap('<div class="zuul-msg-wrap zuul-msg-wrap-off"></div>').parent();
-        $indicator = $('<span class="btn pull-right zuul-spinner">updating <i class="icon-refresh"></i></span>');
-        $queueInfo = $('<p>Queue lengths: <span>0</span> events, <span>0</span> results.</p>');
+        $msgWrap = $msg.wrap('<div class="zuul-msg-wrap zuul-msg-wrap-off">' +
+                             '</div>').parent();
+        $indicator = $('<span class="btn pull-right zuul-spinner">updating ' +
+                       '<i class="icon-refresh"></i></span>');
+        $queueInfo = $('<p>Queue lengths: <span>0</span> events, ' +
+                       '<span>0</span> results.</p>');
         $queueEventsNum =  $queueInfo.find('span').eq(0);
         $queueResultsNum =  $queueEventsNum.next();
         $pipelines = $('<div class="row"></div>');
-        $zuulVersion = $('<p>Zuul version: <span id="zuul-version-span"></span></p>');
-        $lastReconf = $('<p>Last reconfigured: <span id="last-reconfigured-span"></span></p>');
+        $zuulVersion = $('<p>Zuul version: <span id="zuul-version-span">' +
+                         '</span></p>');
+        $lastReconf = $('<p>Last reconfigured: ' +
+                        '<span id="last-reconfigured-span"></span></p>');
 
-        $container = $('#zuul-container').append($msgWrap, $indicator, $queueInfo, $pipelines, $zuulVersion, $lastReconf);
+        $container = $('#zuul-container').append($msgWrap, $indicator,
+                                                 $queueInfo, $pipelines,
+                                                 $zuulVersion, $lastReconf);
 
         zuul.schedule();
 
diff --git a/requirements.txt b/requirements.txt
index 4e9f29c..f14441b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,5 +12,5 @@
 extras
 statsd>=1.0.0,<3.0
 voluptuous>=0.7
-gear>=0.5.1,<1.0.0
+gear>=0.5.4,<1.0.0
 apscheduler>=2.1.1,<3.0
diff --git a/tests/fixtures/layout-no-jobs.yaml b/tests/fixtures/layout-no-jobs.yaml
index ee8dc62..e860ad5 100644
--- a/tests/fixtures/layout-no-jobs.yaml
+++ b/tests/fixtures/layout-no-jobs.yaml
@@ -38,6 +38,6 @@
   - name: org/project
     merge-mode: cherry-pick
     check:
-      - noop
+      - gate-noop
     gate:
-      - noop
+      - gate-noop
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 496d468..351854d 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -795,6 +795,7 @@
         self.init_repo("org/layered-project")
         self.init_repo("org/node-project")
         self.init_repo("org/conflict-project")
+        self.init_repo("org/noop-project")
 
         self.statsd = FakeStatsd()
         os.environ['STATSD_HOST'] = 'localhost'
@@ -2654,6 +2655,19 @@
         self.assertEqual(len(self.history), 10)
         self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)
 
+    def test_noop_job(self):
+        "Test that the internal noop job works"
+        A = self.fake_gerrit.addFakeChange('org/noop-project', 'master', 'A')
+        A.addApproval('CRVW', 2)
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+        self.waitUntilSettled()
+
+        self.assertEqual(len(self.gearman_server.getQueue()), 0)
+        self.assertTrue(self.sched._areAllBuildsComplete())
+        self.assertEqual(len(self.history), 0)
+        self.assertEqual(A.data['status'], 'MERGED')
+        self.assertEqual(A.reported, 2)
+
     def test_zuul_refs(self):
         "Test that zuul refs exist and have the right changes"
         self.worker.hold_jobs_in_build = True
@@ -2858,6 +2872,11 @@
 
     def test_stuck_job_cleanup(self):
         "Test that pending jobs are cleaned up if removed from layout"
+        # This job won't be registered at startup because it is not in
+        # the standard layout, but we need it to already be registerd
+        # for when we reconfigure, as that is when Zuul will attempt
+        # to run the new job.
+        self.worker.registerFunction('build:gate-noop')
         self.gearman_server.hold_jobs_in_queue = True
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
         A.addApproval('CRVW', 2)
@@ -2870,13 +2889,13 @@
         self.sched.reconfigure(self.config)
         self.waitUntilSettled()
 
-        self.gearman_server.release('noop')
+        self.gearman_server.release('gate-noop')
         self.waitUntilSettled()
         self.assertEqual(len(self.gearman_server.getQueue()), 0)
         self.assertTrue(self.sched._areAllBuildsComplete())
 
         self.assertEqual(len(self.history), 1)
-        self.assertEqual(self.history[0].name, 'noop')
+        self.assertEqual(self.history[0].name, 'gate-noop')
         self.assertEqual(self.history[0].result, 'SUCCESS')
 
     def test_file_jobs(self):
diff --git a/zuul/cmd/server.py b/zuul/cmd/server.py
index 79a2538..13e6283 100755
--- a/zuul/cmd/server.py
+++ b/zuul/cmd/server.py
@@ -137,6 +137,7 @@
         if not os.path.exists(path):
             raise Exception("Unable to find job list: %s" % path)
         jobs = set()
+        jobs.add('noop')
         for line in open(path):
             v = line.strip()
             if v:
diff --git a/zuul/launcher/gearman.py b/zuul/launcher/gearman.py
index 081db2b..9ab1f61 100644
--- a/zuul/launcher/gearman.py
+++ b/zuul/launcher/gearman.py
@@ -290,6 +290,11 @@
         build = Build(job, uuid)
         build.parameters = params
 
+        if job.name == 'noop':
+            build.result = 'SUCCESS'
+            self.sched.onBuildCompleted(build)
+            return build
+
         gearman_job = gear.Job(name, json.dumps(params),
                                unique=uuid)
         build.__gearman_job = gearman_job