Merge "Re-enable requirement reject tests" into feature/zuulv3
diff --git a/README.rst b/README.rst
index 4a3376e..697d994 100644
--- a/README.rst
+++ b/README.rst
@@ -86,9 +86,7 @@
    trying to coordinate and prioritize work) know what you would like
    to work on.
 
-7) TODOv3(jeblair): Coming soon: check storyboard for status of
-   current work items.  We do not have a list of work items yet, but
-   we will soon.
+7) Check storyboard for status of current work items: https://storyboard.openstack.org/#!/board/41
 
 Once you are up to speed on those items, it will be helpful to know
 the following:
@@ -100,9 +98,9 @@
   from something simple such as a test-framework method changing its
   name, to more substantial issues, such as a feature being removed as
   part of the v3 work.  Each test will need to be evaluated
-  individually.  Feel free to, at any time, claim a test name on this
-  etherpad and work on re-enabling it:
-  https://etherpad.openstack.org/p/zuulv3
+  individually.  Feel free to, at any time, claim a test name in this
+  story and work on re-enabling it:
+  https://storyboard.openstack.org/#!/story/2000773
 
 * Because of the importance of external systems, as well as the number
   of internal Zuul components, actually running Zuul in a development
@@ -127,3 +125,20 @@
   simply reflect who to ask to explain the item in more detail if it
   is too cryptic.  In your own work, feel free to leave TODOv3 notes
   if a change would otherwise become too large or unweildy.
+
+Roadmap
+-------
+
+* Implement Zookeeper for Nodepool builders and begin using this in
+  OpenStack Infra
+* Implement Zookeeper for Nodepool launchers
+* Implement a shim to translate Zuul v2 demand into Nodepool Zookeeper
+  launcher requests
+* Begin using Zookeeper based Nodepool launchers with Zuul v2.5 in
+  OpenStack Infra
+* Begin using Zuul v3 to run jobs for Zuul itself
+* Move OpenStack Infra to use Zuul v3
+* Implement Github support
+* Begin using Zuul v3 to run tests on Ansible repos
+* Implement support in Nodepool for non-OpenStack clouds
+* Add native container support to Zuul / Nodepool
diff --git a/tests/base.py b/tests/base.py
index 02953f5..6d2c64d 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -758,6 +758,8 @@
         self.running_builds.remove(build)
         del self.job_builds[job.unique]
         self.lock.release()
+        if build.run_error:
+            result = None
         return result
 
 
diff --git a/tests/fixtures/config/project-template/git/common-config/zuul.yaml b/tests/fixtures/config/merge-modes/git/common-config/zuul.yaml
similarity index 73%
rename from tests/fixtures/config/project-template/git/common-config/zuul.yaml
rename to tests/fixtures/config/merge-modes/git/common-config/zuul.yaml
index c6b237f..a7a4c78 100644
--- a/tests/fixtures/config/project-template/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/merge-modes/git/common-config/zuul.yaml
@@ -40,20 +40,23 @@
     name:
       project-test1
 
-- job:
-    name:
-      project-test2
-
-- project-template:
-    name: test-template
+- project:
+    name: org/project-merge
+    merge-mode: merge
     gate:
       jobs:
-        - project-test2
+        - project-test1
 
 - project:
-    name: org/project
-    templates:
-      - test-template
+    name: org/project-merge-resolve
+    merge-mode: merge-resolve
+    gate:
+      jobs:
+        - project-test1
+
+- project:
+    name: org/project-cherry-pick
+    merge-mode: cherry-pick
     gate:
       jobs:
         - project-test1
diff --git a/tests/fixtures/config/project-template/git/org_project/README b/tests/fixtures/config/merge-modes/git/org_project-cherry-pick/README
similarity index 100%
copy from tests/fixtures/config/project-template/git/org_project/README
copy to tests/fixtures/config/merge-modes/git/org_project-cherry-pick/README
diff --git a/tests/fixtures/config/project-template/git/org_project/README b/tests/fixtures/config/merge-modes/git/org_project-merge-resolve/README
similarity index 100%
copy from tests/fixtures/config/project-template/git/org_project/README
copy to tests/fixtures/config/merge-modes/git/org_project-merge-resolve/README
diff --git a/tests/fixtures/config/project-template/git/org_project/README b/tests/fixtures/config/merge-modes/git/org_project-merge/README
similarity index 100%
rename from tests/fixtures/config/project-template/git/org_project/README
rename to tests/fixtures/config/merge-modes/git/org_project-merge/README
diff --git a/tests/fixtures/config/project-template/main.yaml b/tests/fixtures/config/merge-modes/main.yaml
similarity index 100%
rename from tests/fixtures/config/project-template/main.yaml
rename to tests/fixtures/config/merge-modes/main.yaml
diff --git a/tests/fixtures/config/single-tenant/git/layout-inheritance/zuul.yaml b/tests/fixtures/config/single-tenant/git/layout-inheritance/zuul.yaml
new file mode 100644
index 0000000..3070af0
--- /dev/null
+++ b/tests/fixtures/config/single-tenant/git/layout-inheritance/zuul.yaml
@@ -0,0 +1,46 @@
+- pipeline:
+    name: check
+    manager: independent
+    source:
+      gerrit
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
+
+- job:
+    name: project-test-irrelevant-starts-empty
+
+- job:
+    name: project-test-irrelevant-starts-full
+    irrelevant-files:
+      - ^README$
+      - ^ignoreme$
+
+- job:
+    name: project-test-nomatch-starts-empty
+
+- job:
+    name: project-test-nomatch-starts-full
+    irrelevant-files:
+      - ^README$
+
+- project:
+    name: org/project
+    check:
+      jobs:
+        - project-test-irrelevant-starts-empty:
+            irrelevant-files:
+              - ^README$
+              - ^ignoreme$
+        - project-test-irrelevant-starts-full
+        - project-test-nomatch-starts-empty:
+            irrelevant-files:
+              - ^README$
+        - project-test-nomatch-starts-full
diff --git a/tests/fixtures/config/success-url/git/common-config/zuul.yaml b/tests/fixtures/config/success-url/git/common-config/zuul.yaml
new file mode 100644
index 0000000..7edb340
--- /dev/null
+++ b/tests/fixtures/config/success-url/git/common-config/zuul.yaml
@@ -0,0 +1,35 @@
+- pipeline:
+    name: check
+    manager: independent
+    source:
+      gerrit
+    trigger:
+      gerrit:
+        - event: patchset-created
+    start:
+      smtp:
+        to: alternative_me@example.com
+    success:
+      gerrit:
+        verified: 1
+      smtp:
+        to: alternative_me@example.com
+    failure:
+      gerrit:
+        verified: -1
+
+
+- job:
+    name: docs-draft-test
+    success-url: http://docs-draft.example.org/{build.parameters[LOG_PATH]}/publish-docs/
+
+- job:
+    name: docs-draft-test2
+    success-url: http://docs-draft.example.org/{NOPE}/{build.parameters[BAD]}/publish-docs/
+
+- project:
+    name: org/docs
+    check:
+      jobs:
+        - docs-draft-test
+        - docs-draft-test2
diff --git a/tests/fixtures/config/project-template/git/org_project/README b/tests/fixtures/config/success-url/git/org_docs/README
similarity index 100%
copy from tests/fixtures/config/project-template/git/org_project/README
copy to tests/fixtures/config/success-url/git/org_docs/README
diff --git a/tests/fixtures/config/project-template/main.yaml b/tests/fixtures/config/success-url/main.yaml
similarity index 100%
copy from tests/fixtures/config/project-template/main.yaml
copy to tests/fixtures/config/success-url/main.yaml
diff --git a/tests/fixtures/config/templated-project/git/common-config/zuul.yaml b/tests/fixtures/config/templated-project/git/common-config/zuul.yaml
index b4773f1..22a2d6d 100644
--- a/tests/fixtures/config/templated-project/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/templated-project/git/common-config/zuul.yaml
@@ -81,6 +81,9 @@
 - job:
     name: layered-project-foo-test5
 
+- job:
+    name: project-test6
+
 - project:
     name: org/templated-project
     templates:
diff --git a/tests/test_model.py b/tests/test_model.py
index 0d4c7b6..0189340 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -265,6 +265,51 @@
         self.assertEqual(job.name, 'python27')
         self.assertEqual(job.timeout, 70)
 
+    def test_inheritance_keeps_matchers(self):
+        layout = model.Layout()
+
+        pipeline = model.Pipeline('gate', layout)
+        layout.addPipeline(pipeline)
+        queue = model.ChangeQueue(pipeline)
+        project = model.Project('project')
+
+        base = configloader.JobParser.fromYaml(layout, {
+            '_source_project': project,
+            'name': 'base',
+            'timeout': 30,
+        })
+        layout.addJob(base)
+        python27 = configloader.JobParser.fromYaml(layout, {
+            '_source_project': project,
+            'name': 'python27',
+            'parent': 'base',
+            'timeout': 40,
+            'irrelevant-files': ['^ignored-file$'],
+        })
+        layout.addJob(python27)
+
+        project_config = configloader.ProjectParser.fromYaml(layout, {
+            'name': 'project',
+            'gate': {
+                'jobs': [
+                    'python27',
+                ]
+            }
+        })
+        layout.addProjectConfig(project_config, update_pipeline=False)
+
+        change = model.Change(project)
+        change.branch = 'master'
+        change.files = ['/COMMIT_MSG', 'ignored-file']
+        item = queue.enqueueChange(change)
+        item.current_build_set.layout = layout
+
+        self.assertTrue(base.changeMatches(change))
+        self.assertFalse(python27.changeMatches(change))
+
+        item.freezeJobTree()
+        self.assertEqual([], item.getJobs())
+
     def test_job_source_project(self):
         layout = model.Layout()
         base_project = model.Project('base_project')
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index a25815a..b7cf912 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -924,41 +924,6 @@
         self.assertTrue(source.canMerge(a, mgr.getSubmitAllowNeeds()))
 
     @skip("Disabled for early v3 development")
-    def test_build_configuration(self):
-        "Test that zuul merges the right commits for testing"
-
-        self.gearman_server.hold_jobs_in_queue = True
-        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
-        B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
-        C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
-        A.addApproval('code-review', 2)
-        B.addApproval('code-review', 2)
-        C.addApproval('code-review', 2)
-        self.fake_gerrit.addEvent(A.addApproval('approved', 1))
-        self.fake_gerrit.addEvent(B.addApproval('approved', 1))
-        self.fake_gerrit.addEvent(C.addApproval('approved', 1))
-        self.waitUntilSettled()
-
-        self.gearman_server.release('.*-merge')
-        self.waitUntilSettled()
-        self.gearman_server.release('.*-merge')
-        self.waitUntilSettled()
-        self.gearman_server.release('.*-merge')
-        self.waitUntilSettled()
-        queue = self.gearman_server.getQueue()
-        ref = self.getParameter(queue[-1], 'ZUUL_REF')
-        self.gearman_server.hold_jobs_in_queue = False
-        self.gearman_server.release()
-        self.waitUntilSettled()
-
-        path = os.path.join(self.git_root, "org/project")
-        repo = git.Repo(path)
-        repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
-        repo_messages.reverse()
-        correct_messages = ['initial commit', 'A-1', 'B-1', 'C-1']
-        self.assertEqual(repo_messages, correct_messages)
-
-    @skip("Disabled for early v3 development")
     def test_build_configuration_conflict(self):
         "Test that merge conflicts are handled"
 
@@ -2104,7 +2069,6 @@
         self.assertEqual(D.data['status'], 'MERGED')
         self.assertEqual(D.reported, 2)
 
-    @skip("Disabled for early v3 development")
     def test_rerun_on_error(self):
         "Test that if a worker fails to run a job, it is run again"
         self.launch_server.hold_jobs_in_build = True
@@ -2237,6 +2201,24 @@
     def test_irrelevant_files_no_match_runs_job(self):
         self._test_irrelevant_files_jobs(should_skip=False)
 
+    def test_inherited_jobs_keep_matchers(self):
+        self.updateConfigLayout('layout-inheritance')
+        self.sched.reconfigure(self.config)
+
+        files = {'ignoreme': 'ignored\n'}
+
+        change = self.fake_gerrit.addFakeChange('org/project',
+                                                'master',
+                                                'test irrelevant-files',
+                                                files=files)
+        self.fake_gerrit.addEvent(change.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        run_jobs = set([build.name for build in self.history])
+
+        self.assertEqual(set(['project-test-nomatch-starts-empty',
+                              'project-test-nomatch-starts-full']), run_jobs)
+
     @skip("Disabled for early v3 development")
     def test_test_config(self):
         "Test that we can test the config"
@@ -3728,7 +3710,6 @@
         self.launch_server.release()
         self.waitUntilSettled()
 
-    @skip("Disabled for early v3 development")
     def test_client_get_running_jobs(self):
         "Test that the RPC client can get a list of running jobs"
         self.launch_server.hold_jobs_in_build = True
@@ -3746,7 +3727,7 @@
             if time.time() - start > 10:
                 raise Exception("Timeout waiting for gearman server to report "
                                 + "back to the client")
-            build = self.launcher.builds.values()[0]
+            build = self.launch_client.builds.values()[0]
             if build.worker.name == "My Worker":
                 break
             else:
@@ -4552,72 +4533,36 @@
         # No more messages reported via smtp
         self.assertEqual(3, len(self.smtp_messages))
 
-    @skip("Disabled for early v3 development")
-    def test_success_pattern(self):
-        "Ensure bad build params are ignored"
-
-        # Use SMTP reporter to grab the result message easier
-        self.init_repo("org/docs")
-        self.config.set('zuul', 'layout_config',
-                        'tests/fixtures/layout-success-pattern.yaml')
-        self.sched.reconfigure(self.config)
-        self.launch_server.hold_jobs_in_build = True
-        self.registerJobs()
-
-        A = self.fake_gerrit.addFakeChange('org/docs', 'master', 'A')
-        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
-        self.waitUntilSettled()
-
-        # Grab build id
-        self.assertEqual(len(self.builds), 1)
-        uuid = self.builds[0].unique[:7]
-
-        self.launch_server.hold_jobs_in_build = False
-        self.launch_server.release()
-        self.waitUntilSettled()
-
-        self.assertEqual(len(self.smtp_messages), 1)
-        body = self.smtp_messages[0]['body'].splitlines()
-        self.assertEqual('Build succeeded.', body[0])
-
-        self.assertIn(
-            '- docs-draft-test http://docs-draft.example.org/1/1/1/check/'
-            'docs-draft-test/%s/publish-docs/' % uuid,
-            body[2])
-        self.assertIn(
-            '- docs-draft-test2 https://server/job/docs-draft-test2/1/',
-            body[3])
-
-    @skip("Disabled for early v3 development")
     def test_rerun_on_abort(self):
-        "Test that if a worker fails to run a job, it is run again"
+        "Test that if a launch server fails to run a job, it is run again"
 
         self.config.set('zuul', 'layout_config',
                         'tests/fixtures/layout-abort-attempts.yaml')
         self.sched.reconfigure(self.config)
-        self.worker.hold_jobs_in_build = True
+        self.launch_server.hold_jobs_in_build = True
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
         self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
         self.waitUntilSettled()
 
-        self.worker.release('.*-merge')
+        self.launch_server.release('.*-merge')
         self.waitUntilSettled()
 
         self.assertEqual(len(self.builds), 2)
         self.builds[0].requeue = True
-        self.worker.release('.*-test*')
+        self.launch_server.release('.*-test*')
         self.waitUntilSettled()
 
-        for x in range(3):
-            self.assertEqual(len(self.builds), 1)
+        for x in range(2):
+            self.assertEqual(len(self.builds), 1,
+                             'len of builds at x=%d is wrong' % x)
             self.builds[0].requeue = True
-            self.worker.release('.*-test1')
+            self.launch_server.release('.*-test1')
             self.waitUntilSettled()
 
-        self.worker.hold_jobs_in_build = False
-        self.worker.release()
+        self.launch_server.hold_jobs_in_build = False
+        self.launch_server.release()
         self.waitUntilSettled()
-        self.assertEqual(len(self.history), 6)
+        self.assertEqual(len(self.history), 5)
         self.assertEqual(self.countJobResults(self.history, 'SUCCESS'), 2)
         self.assertEqual(A.reported, 1)
         self.assertIn('RETRY_LIMIT', A.messages[0])
@@ -4715,3 +4660,100 @@
                                                 ).result, 'SUCCESS')
         self.assertEqual(self.getJobFromHistory('project-test6').result,
                          'SUCCESS')
+
+
+class TestSchedulerSuccessURL(ZuulTestCase):
+    tenant_config_file = 'config/success-url/main.yaml'
+
+    def test_success_url(self):
+        "Ensure bad build params are ignored"
+        self.sched.reconfigure(self.config)
+        self.init_repo('org/docs')
+
+        A = self.fake_gerrit.addFakeChange('org/docs', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        # Both builds ran: docs-draft-test + docs-draft-test2
+        self.assertEqual(len(self.history), 2)
+
+        # Grab build id
+        uuid = self.history[0].uuid[:7]
+
+        # Two msgs: 'Starting...'  + results
+        self.assertEqual(len(self.smtp_messages), 2)
+        body = self.smtp_messages[1]['body'].splitlines()
+        self.assertEqual('Build succeeded.', body[0])
+
+        self.assertIn(
+            '- docs-draft-test http://docs-draft.example.org/1/1/1/check/'
+            'docs-draft-test/%s/publish-docs/' % uuid,
+            body[2])
+
+        # NOTE: This default URL is currently hard-coded in launcher/server.py
+        self.assertIn(
+            '- docs-draft-test2 https://server/job',
+            body[3])
+
+
+class TestSchedulerMergeModes(ZuulTestCase):
+    tenant_config_file = 'config/merge-modes/main.yaml'
+
+    def _test_project_merge_mode(self, mode):
+        self.launch_server.keep_jobdir = False
+        project = 'org/project-%s' % mode
+        self.launch_server.hold_jobs_in_build = True
+        A = self.fake_gerrit.addFakeChange(project, 'master', 'A')
+        B = self.fake_gerrit.addFakeChange(project, 'master', 'B')
+        C = self.fake_gerrit.addFakeChange(project, 'master', 'C')
+        A.addApproval('code-review', 2)
+        B.addApproval('code-review', 2)
+        C.addApproval('code-review', 2)
+        self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+        self.fake_gerrit.addEvent(B.addApproval('approved', 1))
+        self.fake_gerrit.addEvent(C.addApproval('approved', 1))
+        self.waitUntilSettled()
+
+        build = self.builds[-1]
+        ref = self.getParameter(build, 'ZUUL_REF')
+
+        path = os.path.join(build.jobdir.git_root, project)
+        repo = git.Repo(path)
+        repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
+        repo_messages.reverse()
+
+        self.launch_server.hold_jobs_in_build = False
+        self.launch_server.release()
+        self.waitUntilSettled()
+
+        return repo_messages
+
+    def _test_merge(self, mode):
+        us_path = os.path.join(
+            self.upstream_root, 'org/project-%s' % mode)
+        expected_messages = [
+            'initial commit',
+            'add content from fixture',
+            # the intermediate commits order is nondeterministic
+            "Merge commit 'refs/changes/1/2/1' of %s into HEAD" % us_path,
+            "Merge commit 'refs/changes/1/3/1' of %s into HEAD" % us_path,
+        ]
+        result = self._test_project_merge_mode(mode)
+        self.assertEqual(result[:2], expected_messages[:2])
+        self.assertEqual(result[-2:], expected_messages[-2:])
+
+    def test_project_merge_mode_merge(self):
+        self._test_merge('merge')
+
+    def test_project_merge_mode_merge_resolve(self):
+        self._test_merge('merge-resolve')
+
+    def test_project_merge_mode_cherrypick(self):
+        expected_messages = [
+            'initial commit',
+            'add content from fixture',
+            'A-1',
+            'B-1',
+            'C-1']
+        result = self._test_project_merge_mode('cherry-pick')
+        self.assertEqual(result, expected_messages)
diff --git a/tests/test_v3.py b/tests/test_v3.py
index 8f4e27e..8853302 100644
--- a/tests/test_v3.py
+++ b/tests/test_v3.py
@@ -110,22 +110,3 @@
                          "A should report start and success")
         self.assertIn('tenant-one-gate', A.messages[1],
                       "A should transit tenant-one gate")
-
-
-class TestProjectTemplate(AnsibleZuulTestCase):
-    tenant_config_file = 'config/project-template/main.yaml'
-
-    def test(self):
-        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
-        A.addApproval('code-review', 2)
-        self.fake_gerrit.addEvent(A.addApproval('approved', 1))
-        self.waitUntilSettled()
-        self.assertEqual(self.getJobFromHistory('project-test1').result,
-                         'SUCCESS')
-        self.assertEqual(self.getJobFromHistory('project-test2').result,
-                         'SUCCESS')
-        self.assertEqual(A.data['status'], 'MERGED')
-        self.assertEqual(A.reported, 2,
-                         "A should report start and success")
-        self.assertIn('gate', A.messages[1],
-                      "A should transit gate")
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 0241191..3f85771 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -185,7 +185,12 @@
 
     @staticmethod
     def getSchema(layout):
-        project_template = {vs.Required('name'): str}
+        project_template = {
+            vs.Required('name'): str,
+            'merge-mode': vs.Any(
+                'merge', 'merge-resolve',
+                'cherry-pick')}
+
         for p in layout.pipelines.values():
             project_template[p.name] = {'queue': str,
                                         'jobs': [vs.Any(str, dict)]}
@@ -240,7 +245,9 @@
     @staticmethod
     def getSchema(layout):
         project = {vs.Required('name'): str,
-                   'templates': [str]}
+                   'templates': [str],
+                   'merge-mode': vs.Any('merge', 'merge-resolve',
+                                        'cherry-pick')}
         for p in layout.pipelines.values():
             project[p.name] = {'queue': str,
                                'jobs': [vs.Any(str, dict)]}
@@ -259,6 +266,8 @@
         configs = [layout.project_templates[name] for name in conf_templates]
         configs.append(project_template)
         project = model.ProjectConfig(conf['name'])
+        mode = conf.get('merge-mode', 'merge-resolve')
+        project.merge_mode = model.MERGER_MAP[mode]
         for pipeline in layout.pipelines.values():
             project_pipeline = model.ProjectPipelineConfig()
             project_pipeline.job_tree = model.JobTree(None)
diff --git a/zuul/launcher/client.py b/zuul/launcher/client.py
index f789e85..9fbf1bb 100644
--- a/zuul/launcher/client.py
+++ b/zuul/launcher/client.py
@@ -43,11 +43,13 @@
         newrev = item.change.newrev
         branch = item.change.ref
     connection_name = item.pipeline.source.connection.connection_name
-    return dict(project=item.change.project.name,
+    project = item.change.project.name
+
+    return dict(project=project,
                 url=item.pipeline.source.getGitUrl(
                     item.change.project),
                 connection_name=connection_name,
-                merge_mode=item.change.project.merge_mode,
+                merge_mode=item.current_build_set.getMergeMode(project),
                 refspec=refspec,
                 branch=branch,
                 ref=item.current_build_set.ref,
diff --git a/zuul/launcher/server.py b/zuul/launcher/server.py
index 489869f..74cc2be 100644
--- a/zuul/launcher/server.py
+++ b/zuul/launcher/server.py
@@ -362,7 +362,8 @@
 
             data = {
                 'manager': self.hostname,
-                'url': 'https://server/job',
+                'url': 'https://server/job/{}/0/'.format(args['job']),
+                'worker_name': 'My Worker',
             }
 
             # TODOv3:
@@ -380,6 +381,9 @@
             job.sendWorkStatus(0, 100)
 
             result = self.runAnsible(jobdir, job)
+            if result is None:
+                job.sendWorkFail()
+                return
             result = dict(result=result)
             job.sendWorkComplete(json.dumps(result))
 
diff --git a/zuul/manager/__init__.py b/zuul/manager/__init__.py
index 70a510e..7a4c7cc 100644
--- a/zuul/manager/__init__.py
+++ b/zuul/manager/__init__.py
@@ -422,11 +422,13 @@
             oldrev = item.change.oldrev
             newrev = item.change.newrev
         connection_name = self.pipeline.source.connection.connection_name
-        return dict(project=item.change.project.name,
+
+        project = item.change.project.name
+        return dict(project=project,
                     url=self.pipeline.source.getGitUrl(
                         item.change.project),
                     connection_name=connection_name,
-                    merge_mode=item.change.project.merge_mode,
+                    merge_mode=item.current_build_set.getMergeMode(project),
                     refspec=item.change.refspec,
                     branch=item.change.branch,
                     ref=item.current_build_set.ref,
diff --git a/zuul/model.py b/zuul/model.py
index 06639f1..6e0176f 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -329,7 +329,6 @@
 
     def __init__(self, name, foreign=False):
         self.name = name
-        self.merge_mode = MERGER_MERGE_RESOLVE
         # foreign projects are those referenced in dependencies
         # of layout projects, this should matter
         # when deciding whether to enqueue their changes
@@ -704,6 +703,11 @@
     def getTries(self, job_name):
         return self.tries.get(job_name)
 
+    def getMergeMode(self, job_name):
+        if not self.layout or job_name not in self.layout.project_configs:
+            return MERGER_MERGE_RESOLVE
+        return self.layout.project_configs[job_name].merge_mode
+
 
 class QueueItem(object):
     """Represents the position of a Change in a ChangeQueue.
@@ -1584,13 +1588,14 @@
     def __init__(self):
         self.job_tree = None
         self.queue_name = None
-        # TODOv3(jeblair): add merge mode
+        self.merge_mode = None
 
 
 class ProjectConfig(object):
     # Represents a project cofiguration
     def __init__(self, name):
         self.name = name
+        self.merge_mode = None
         self.pipelines = {}
 
 
@@ -1771,6 +1776,11 @@
                     if variant not in inherited:
                         frozen_job.inheritFrom(variant)
                         inherited.add(variant)
+            if not inherited:
+                # A change must match at least one defined job variant
+                # (that is to say that it must match more than just
+                # the job that is defined in the tree).
+                continue
             if job not in inherited:
                 # Only update from the job in the tree if it is
                 # unique, otherwise we might unset an attribute we
diff --git a/zuul/rpclistener.py b/zuul/rpclistener.py
index 90e17dc..c780df4 100644
--- a/zuul/rpclistener.py
+++ b/zuul/rpclistener.py
@@ -161,10 +161,11 @@
         # args = json.loads(job.arguments)
         # TODO: use args to filter by pipeline etc
         running_items = []
-        for pipeline_name, pipeline in six.iteritems(
-                self.sched.layout.pipelines):
-            for queue in pipeline.queues:
-                for item in queue.queue:
-                    running_items.append(item.formatJSON())
+        for tenant in self.sched.abide.tenants.values():
+            for pipeline_name, pipeline in six.iteritems(
+                    tenant.layout.pipelines):
+                for queue in pipeline.queues:
+                    for item in queue.queue:
+                        running_items.append(item.formatJSON())
 
         job.sendWorkComplete(json.dumps(running_items))