Merge "Report layout config errors for config repos" into feature/zuulv3
diff --git a/tests/fixtures/config/in-repo/git/common-config/playbooks/common-config-test.yaml b/tests/fixtures/config/in-repo/git/common-config/playbooks/common-config-test.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/in-repo/git/common-config/playbooks/common-config-test.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/in-repo/git/common-config/zuul.yaml b/tests/fixtures/config/in-repo/git/common-config/zuul.yaml
index 58b2051..d8b7200 100644
--- a/tests/fixtures/config/in-repo/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/in-repo/git/common-config/zuul.yaml
@@ -35,3 +35,12 @@
       gerrit:
         verified: 0
     precedence: high
+
+- job:
+    name: common-config-test
+
+- project:
+    name: common-config
+    tenant-one-gate:
+      jobs:
+        - common-config-test
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index cf88265..ea7e85a 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -185,7 +185,7 @@
             dict(name='project-test1', result='SUCCESS', changes='2,1'),
             dict(name='project-test2', result='SUCCESS', changes='3,1')])
 
-    def test_dynamic_syntax_error(self):
+    def test_untrusted_syntax_error(self):
         in_repo_conf = textwrap.dedent(
             """
             - job:
@@ -206,6 +206,27 @@
         self.assertIn('syntax error', A.messages[1],
                       "A should have a syntax error reported")
 
+    def test_trusted_syntax_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: project-test2
+                foo: error
+            """)
+
+        file_dict = {'zuul.yaml': in_repo_conf}
+        A = self.fake_gerrit.addFakeChange('common-config', 'master', 'A',
+                                           files=file_dict)
+        A.addApproval('code-review', 2)
+        self.fake_gerrit.addEvent(A.addApproval('approved', 1))
+        self.waitUntilSettled()
+
+        self.assertEqual(A.data['status'], 'NEW')
+        self.assertEqual(A.reported, 2,
+                         "A should report start and failure")
+        self.assertIn('syntax error', A.messages[1],
+                      "A should have a syntax error reported")
+
 
 class TestAnsible(AnsibleZuulTestCase):
     # A temporary class to hold new tests while others are disabled
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 42616a8..50d6143 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -841,21 +841,41 @@
         new_abide.tenants[tenant.name] = new_tenant
         return new_abide
 
-    def createDynamicLayout(self, tenant, files):
-        config = tenant.config_repos_config.copy()
-        for source, project in tenant.project_repos:
-            for branch in source.getProjectBranches(project):
+    def _loadDynamicProjectData(self, config, source, project, files,
+                                config_repo):
+        for branch in source.getProjectBranches(project):
+            data = None
+            if config_repo:
+                data = files.getFile(project.name, branch, 'zuul.yaml')
+            if not data:
                 data = files.getFile(project.name, branch, '.zuul.yaml')
-                if data:
-                    source_context = model.SourceContext(project,
-                                                         branch, False)
-                    incdata = TenantParser._parseProjectRepoLayout(
+            if data:
+                source_context = model.SourceContext(project, branch,
+                                                     config_repo)
+                if config_repo:
+                    incdata = TenantParser._parseConfigRepoLayout(
                         data, source_context)
                 else:
-                    incdata = project.unparsed_branch_config[branch]
-                if not incdata:
-                    continue
-                config.extend(incdata)
+                    incdata = TenantParser._parseProjectRepoLayout(
+                        data, source_context)
+            else:
+                incdata = project.unparsed_branch_config.get(branch)
+            if not incdata:
+                continue
+            config.extend(incdata)
+
+    def createDynamicLayout(self, tenant, files, include_config_repos=False):
+        if include_config_repos:
+            config = model.UnparsedTenantConfig()
+            for source, project in tenant.config_repos:
+                self._loadDynamicProjectData(config, source, project,
+                                             files, True)
+        else:
+            config = tenant.config_repos_config.copy()
+        for source, project in tenant.project_repos:
+            self._loadDynamicProjectData(config, source, project,
+                                         files, False)
+
         layout = model.Layout()
         # TODOv3(jeblair): copying the pipelines could be dangerous/confusing.
         layout.pipelines = tenant.layout.pipelines
diff --git a/zuul/manager/__init__.py b/zuul/manager/__init__.py
index 4447615..f0250cf 100644
--- a/zuul/manager/__init__.py
+++ b/zuul/manager/__init__.py
@@ -466,6 +466,43 @@
                     newrev=newrev,
                     )
 
+    def _loadDynamicLayout(self, item):
+        # Load layout
+        # Late import to break an import loop
+        import zuul.configloader
+        loader = zuul.configloader.ConfigLoader()
+
+        build_set = item.current_build_set
+        self.log.debug("Load dynamic layout with %s" % build_set.files)
+        try:
+            # First parse the config with as it will land with the
+            # full set of config and project repos.  This lets us
+            # catch syntax errors in config repos even though we won't
+            # actually run with that config.
+            loader.createDynamicLayout(
+                item.pipeline.layout.tenant,
+                build_set.files,
+                include_config_repos=True)
+
+            # Then create the config a second time but without changes
+            # to config repos so that we actually use this config.
+            layout = loader.createDynamicLayout(
+                item.pipeline.layout.tenant,
+                build_set.files,
+                include_config_repos=False)
+        except zuul.configloader.ConfigurationSyntaxError as e:
+            self.log.info("Configuration syntax error "
+                          "in dynamic layout %s" %
+                          build_set.files)
+            item.setConfigError(str(e))
+            return None
+        except Exception:
+            self.log.exception("Error in dynamic layout %s" %
+                               build_set.files)
+            item.setConfigError("Unknown configuration error")
+            return None
+        return layout
+
     def getLayout(self, item):
         if not item.change.updatesConfig():
             if item.item_ahead:
@@ -479,27 +516,7 @@
         if build_set.merge_state == build_set.COMPLETE:
             if build_set.unable_to_merge:
                 return None
-            # Load layout
-            # Late import to break an import loop
-            import zuul.configloader
-            loader = zuul.configloader.ConfigLoader()
-            self.log.debug("Load dynamic layout with %s" % build_set.files)
-            try:
-                layout = loader.createDynamicLayout(
-                    item.pipeline.layout.tenant,
-                    build_set.files)
-            except zuul.configloader.ConfigurationSyntaxError as e:
-                self.log.info("Configuration syntax error "
-                              "in dynamic layout %s" %
-                              build_set.files)
-                item.setConfigError(str(e))
-                return None
-            except Exception:
-                self.log.exception("Error in dynamic layout %s" %
-                                   build_set.files)
-                item.setConfigError("Unknown configuration error")
-                return None
-            return layout
+            return self._loadDynamicLayout(item)
         build_set.merge_state = build_set.PENDING
         self.log.debug("Preparing dynamic layout for: %s" % item.change)
         dependent_items = self.getDependentItems(item)
@@ -508,7 +525,7 @@
         merger_items = map(self._makeMergerItem, all_items)
         self.sched.merger.mergeChanges(merger_items,
                                        item.current_build_set,
-                                       ['.zuul.yaml'],
+                                       ['zuul.yaml', '.zuul.yaml'],
                                        self.pipeline.precedence)
 
     def prepareLayout(self, item):