Merge "Require a base job" into feature/zuulv3
diff --git a/doc/source/admin/tenants.rst b/doc/source/admin/tenants.rst
index b518c91..a1709a6 100644
--- a/doc/source/admin/tenants.rst
+++ b/doc/source/admin/tenants.rst
@@ -165,3 +165,12 @@
       can be limited to the protected branches which are gated. This
       is a tenant wide setting and can be overridden per project.
       This currently only affects GitHub projects.
+
+   .. attr:: default-parent
+      :default: base
+
+      If a job is defined without an explicit :attr:`job.parent`
+      attribute, this job will be configured as the job's parent.
+      This allows an administrator to configure a default base job to
+      implement local policies such as node setup and artifact
+      publishing.
diff --git a/doc/source/glossary.rst b/doc/source/glossary.rst
index 6c8a4b4..d4dbf01 100644
--- a/doc/source/glossary.rst
+++ b/doc/source/glossary.rst
@@ -6,6 +6,13 @@
 .. glossary::
    :sorted:
 
+   base job
+
+      A job with no parent.  A base job may only be defined in a
+      :term:`config-project`.  Multiple base jobs may be defined, but
+      each tenant has a single default job which will be used as the
+      parent of any job which does not specify one explicitly.
+
    check
 
       By convention, the name of a pipeline which performs pre-merge
diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst
index 446718b..b2e4be2 100644
--- a/doc/source/user/config.rst
+++ b/doc/source/user/config.rst
@@ -17,9 +17,10 @@
 *config-project* is one which is primarily tasked with holding
 configuration information and job content for Zuul.  Jobs which are
 defined in a config-project are run with elevated privileges, and all
-Zuul configuration items are available for use.  It is expected that
-changes to config-projects will undergo careful scrutiny before being
-merged.
+Zuul configuration items are available for use.  Base jobs (that is,
+jobs without a parent) may only be defined in config-projects.  It is
+expected that changes to config-projects will undergo careful scrutiny
+before being merged.
 
 An *untrusted-project* is a project whose primary focus is not to
 operate Zuul, but rather it is one of the projects being tested or
@@ -439,6 +440,12 @@
 from any other job in any project (however, if the other job is marked
 as ``final``, some attributes may not be overidden).
 
+A job with no parent is called a *base job* and may only be defined in
+a :term:`config-project`.  Every other job must have a parent, and so
+ultimately, all jobs must have an inheritance path which terminates at
+a base job.  Each tenant has a default parent job which will be used
+if no explicit parent is specified.
+
 Jobs also support a concept called variance.  The first time a job
 definition appears is called the reference definition of the job.
 Subsequent job definitions with the same name are called variants.
@@ -503,11 +510,17 @@
       this name to use as the main playbook for the job.  This name is
       also referenced later in a project pipeline configuration.
 
+   .. TODO: figure out how to link the parent default to tenant.default.parent
+
    .. attr:: parent
+      :default: Tenant default-parent
 
       Specifies a job to inherit from.  The parent job can be defined
-      in this or any other project.  Any attributes not specified on
-      a job will be collected from its parent.
+      in this or any other project.  Any attributes not specified on a
+      job will be collected from its parent.  If no value is supplied
+      here, the job specified by :attr:`tenant.default-parent` will be
+      used.  If **parent** is set to ``null`` (which is only valid in
+      a :term:`config-project`), this is a :term:`base job`.
 
    .. attr:: description
 
diff --git a/tests/fixtures/config/ansible/git/common-config/zuul.yaml b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
index 7a144fb..c70191f 100644
--- a/tests/fixtures/config/ansible/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/zuul.yaml
@@ -70,6 +70,10 @@
       value: vartest_secret
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: base-urls
     success-url: https://success.example.com/zuul-logs/{build.uuid}/
     failure-url: https://failure.example.com/zuul-logs/{build.uuid}/
diff --git a/tests/fixtures/config/base-jobs/git/common-config/zuul.yaml b/tests/fixtures/config/base-jobs/git/common-config/zuul.yaml
new file mode 100644
index 0000000..0603d23
--- /dev/null
+++ b/tests/fixtures/config/base-jobs/git/common-config/zuul.yaml
@@ -0,0 +1,24 @@
+- pipeline:
+    name: check
+    manager: independent
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        Verified: 1
+    failure:
+      gerrit:
+        Verified: -1
+
+- job:
+    name: my-base
+    parent: null
+    tags:
+      - mybase
+    
+- job:
+    name: other-base
+    parent: null
+    tags:
+      - otherbase
diff --git a/tests/fixtures/config/base-jobs/git/org_project/.zuul.yaml b/tests/fixtures/config/base-jobs/git/org_project/.zuul.yaml
new file mode 100644
index 0000000..9844c14
--- /dev/null
+++ b/tests/fixtures/config/base-jobs/git/org_project/.zuul.yaml
@@ -0,0 +1,13 @@
+- job:
+    name: my-job
+
+- job:
+    name: other-job
+    parent: other-base
+
+- project:
+    name: org/project
+    check:
+      jobs:
+        - my-job
+        - other-job
diff --git a/tests/fixtures/config/base-jobs/git/org_project/README b/tests/fixtures/config/base-jobs/git/org_project/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/base-jobs/git/org_project/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/base-jobs/git/org_project/playbooks/my-job.yaml b/tests/fixtures/config/base-jobs/git/org_project/playbooks/my-job.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/base-jobs/git/org_project/playbooks/my-job.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/base-jobs/git/org_project/playbooks/other-job.yaml b/tests/fixtures/config/base-jobs/git/org_project/playbooks/other-job.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/base-jobs/git/org_project/playbooks/other-job.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/base-jobs/main.yaml b/tests/fixtures/config/base-jobs/main.yaml
new file mode 100644
index 0000000..3ab6dca
--- /dev/null
+++ b/tests/fixtures/config/base-jobs/main.yaml
@@ -0,0 +1,9 @@
+- tenant:
+    name: tenant-one
+    default-parent: my-base
+    source:
+      gerrit:
+        config-projects:
+          - common-config
+        untrusted-projects:
+          - org/project
diff --git a/tests/fixtures/config/conflict-config/git/common-config/zuul.yaml b/tests/fixtures/config/conflict-config/git/common-config/zuul.yaml
index 792fc8f..469dd7e 100644
--- a/tests/fixtures/config/conflict-config/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/conflict-config/git/common-config/zuul.yaml
@@ -1,2 +1,6 @@
 - job:
+    name: base
+    parent: null
+
+- job:
     name: trusted-zuul.yaml-job
diff --git a/tests/fixtures/config/data-return/git/common-config/zuul.yaml b/tests/fixtures/config/data-return/git/common-config/zuul.yaml
index 38107e5..4db7eb6 100644
--- a/tests/fixtures/config/data-return/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/data-return/git/common-config/zuul.yaml
@@ -13,6 +13,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: data-return
 
 - job:
diff --git a/tests/fixtures/config/dependency-graph/git/common-config/zuul.yaml b/tests/fixtures/config/dependency-graph/git/common-config/zuul.yaml
index 975d04e..4179226 100644
--- a/tests/fixtures/config/dependency-graph/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/dependency-graph/git/common-config/zuul.yaml
@@ -20,6 +20,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: A
 
 - job:
diff --git a/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml b/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml
index 83a5158..4a13e73 100644
--- a/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/disk-accountant/git/common-config/zuul.yaml
@@ -13,6 +13,10 @@
         verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: dd-big-empty-file
 
 - project:
diff --git a/tests/fixtures/config/duplicate-pipeline/git/common-config/zuul.yaml b/tests/fixtures/config/duplicate-pipeline/git/common-config/zuul.yaml
index dabe58c..117e381 100755
--- a/tests/fixtures/config/duplicate-pipeline/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/duplicate-pipeline/git/common-config/zuul.yaml
@@ -27,6 +27,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - project:
diff --git a/tests/fixtures/config/git-driver/git/common-config/zuul.yaml b/tests/fixtures/config/git-driver/git/common-config/zuul.yaml
index 128bbb5..34d1136 100644
--- a/tests/fixtures/config/git-driver/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/git-driver/git/common-config/zuul.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - project:
diff --git a/tests/fixtures/config/implicit-roles/git/common-config/zuul.yaml b/tests/fixtures/config/implicit-roles/git/common-config/zuul.yaml
index 5b2636f..c941573 100644
--- a/tests/fixtures/config/implicit-roles/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/implicit-roles/git/common-config/zuul.yaml
@@ -10,3 +10,7 @@
     failure:
       gerrit:
         Verified: -1
+
+- job:
+    name: base
+    parent: null
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 bfd6199..ff4268b 100644
--- a/tests/fixtures/config/in-repo/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/in-repo/git/common-config/zuul.yaml
@@ -70,6 +70,10 @@
     trigger: {}
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: common-config-test
 
 - project:
diff --git a/tests/fixtures/config/inventory/git/common-config/zuul.yaml b/tests/fixtures/config/inventory/git/common-config/zuul.yaml
index 177633d..d2179b7 100644
--- a/tests/fixtures/config/inventory/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/inventory/git/common-config/zuul.yaml
@@ -32,6 +32,10 @@
           - compute2
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: single-inventory
     nodes:
       - name: ubuntu-xenial
diff --git a/tests/fixtures/config/merges/git/common-config/zuul.yaml b/tests/fixtures/config/merges/git/common-config/zuul.yaml
index 7ae2ef0..1ea5048 100644
--- a/tests/fixtures/config/merges/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/merges/git/common-config/zuul.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - job:
diff --git a/tests/fixtures/config/multi-driver/git/org_common-config/zuul.yaml b/tests/fixtures/config/multi-driver/git/org_common-config/zuul.yaml
index 7b5a77c..7a5c190 100644
--- a/tests/fixtures/config/multi-driver/git/org_common-config/zuul.yaml
+++ b/tests/fixtures/config/multi-driver/git/org_common-config/zuul.yaml
@@ -27,7 +27,12 @@
         Verified: 0
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-gerrit
+
 - job:
     name: project1-github
 
diff --git a/tests/fixtures/config/multi-tenant-semaphore/git/common-config/zuul.yaml b/tests/fixtures/config/multi-tenant-semaphore/git/common-config/zuul.yaml
index 5b2636f..4abe532 100644
--- a/tests/fixtures/config/multi-tenant-semaphore/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/multi-tenant-semaphore/git/common-config/zuul.yaml
@@ -10,3 +10,7 @@
     failure:
       gerrit:
         Verified: -1
+
+- job:
+    name: base
+    parent: null
\ No newline at end of file
diff --git a/tests/fixtures/config/multi-tenant/git/common-config/zuul.yaml b/tests/fixtures/config/multi-tenant/git/common-config/zuul.yaml
index 8e29d3b..27f2fd5 100644
--- a/tests/fixtures/config/multi-tenant/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/multi-tenant/git/common-config/zuul.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: python27
     nodes:
       - name: controller
diff --git a/tests/fixtures/config/openstack/git/project-config/zuul.yaml b/tests/fixtures/config/openstack/git/project-config/zuul.yaml
index b3a8b0c..2506db0 100644
--- a/tests/fixtures/config/openstack/git/project-config/zuul.yaml
+++ b/tests/fixtures/config/openstack/git/project-config/zuul.yaml
@@ -35,6 +35,7 @@
 
 - job:
     name: base
+    parent: null
     timeout: 30
     nodes:
       - name: controller
diff --git a/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml b/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml
index cfece30..0a6c557 100644
--- a/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/pre-playbook/git/common-config/zuul.yaml
@@ -13,6 +13,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: python27
     pre-run: playbooks/pre
     post-run: playbooks/post
diff --git a/tests/fixtures/config/push-reqs/git/org_common-config/zuul.yaml b/tests/fixtures/config/push-reqs/git/org_common-config/zuul.yaml
index 6569966..63af1c9 100644
--- a/tests/fixtures/config/push-reqs/git/org_common-config/zuul.yaml
+++ b/tests/fixtures/config/push-reqs/git/org_common-config/zuul.yaml
@@ -77,6 +77,10 @@
         - event: ref-updated
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: job1
 
 - project:
diff --git a/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
index 1626168..90c9ac2 100644
--- a/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/email/git/common-config/zuul.yaml
@@ -31,6 +31,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
index 2980ef1..5f266a4 100644
--- a/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/newer-than/git/common-config/zuul.yaml
@@ -33,6 +33,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
index 9e2f5d7..4287a94 100644
--- a/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/older-than/git/common-config/zuul.yaml
@@ -33,6 +33,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
index 333a4c7..aabfb6a 100644
--- a/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/reject-username/git/common-config/zuul.yaml
@@ -31,6 +31,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
index 9aef0c5..2661eed 100644
--- a/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/reject/git/common-config/zuul.yaml
@@ -47,6 +47,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
index 081b655..715b89f 100644
--- a/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/state/git/common-config/zuul.yaml
@@ -50,6 +50,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-job
 
 - project:
diff --git a/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
index d5f3553..778ac16 100644
--- a/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/username/git/common-config/zuul.yaml
@@ -31,6 +31,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
index 787cf0d..b5d7498 100644
--- a/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/vote1/git/common-config/zuul.yaml
@@ -33,6 +33,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml b/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
index b91a216..3f41868 100644
--- a/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/requirements/vote2/git/common-config/zuul.yaml
@@ -37,6 +37,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-job
 
 - job:
diff --git a/tests/fixtures/config/roles/git/common-config/zuul.yaml b/tests/fixtures/config/roles/git/common-config/zuul.yaml
index a1b6e42..7ae6263 100644
--- a/tests/fixtures/config/roles/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/roles/git/common-config/zuul.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: common-config-test
 
 - project:
diff --git a/tests/fixtures/config/semaphore/git/common-config/zuul.yaml b/tests/fixtures/config/semaphore/git/common-config/zuul.yaml
index 5eeee21..c8bd322 100644
--- a/tests/fixtures/config/semaphore/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/semaphore/git/common-config/zuul.yaml
@@ -20,6 +20,10 @@
     max: 2
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - job:
diff --git a/tests/fixtures/config/semaphore/zuul-reconfiguration.yaml b/tests/fixtures/config/semaphore/zuul-reconfiguration.yaml
index 128bbb5..34d1136 100644
--- a/tests/fixtures/config/semaphore/zuul-reconfiguration.yaml
+++ b/tests/fixtures/config/semaphore/zuul-reconfiguration.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - project:
diff --git a/tests/fixtures/config/shadow/git/local-config/zuul.yaml b/tests/fixtures/config/shadow/git/local-config/zuul.yaml
index dc2133f..87f46b7 100644
--- a/tests/fixtures/config/shadow/git/local-config/zuul.yaml
+++ b/tests/fixtures/config/shadow/git/local-config/zuul.yaml
@@ -13,6 +13,7 @@
 
 - job:
     name: base
+    parent: null
 
 - job:
     name: test2
diff --git a/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml b/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
index 710d2b4..9796fe2 100644
--- a/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/single-tenant/git/common-config/zuul.yaml
@@ -41,6 +41,10 @@
           ref: ^(?!refs/).*$
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
     nodes:
diff --git a/tests/fixtures/config/split-config/git/common-config/zuul.d/jobs.yaml b/tests/fixtures/config/split-config/git/common-config/zuul.d/jobs.yaml
index 280342c..9d15599 100644
--- a/tests/fixtures/config/split-config/git/common-config/zuul.d/jobs.yaml
+++ b/tests/fixtures/config/split-config/git/common-config/zuul.d/jobs.yaml
@@ -1,2 +1,6 @@
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
diff --git a/tests/fixtures/config/sql-driver/git/common-config/zuul.yaml b/tests/fixtures/config/sql-driver/git/common-config/zuul.yaml
index 01a8d0f..b8f4d67 100644
--- a/tests/fixtures/config/sql-driver/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/sql-driver/git/common-config/zuul.yaml
@@ -15,6 +15,10 @@
       resultsdb_failures:
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
 
 - job:
diff --git a/tests/fixtures/config/streamer/git/common-config/zuul.yaml b/tests/fixtures/config/streamer/git/common-config/zuul.yaml
index 5a653b4..f9925fe 100644
--- a/tests/fixtures/config/streamer/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/streamer/git/common-config/zuul.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: python27
     vars:
       waitpath: '{{zuul._test.test_root}}/{{zuul.build}}/test_wait'
diff --git a/tests/fixtures/config/success-url/git/common-config/zuul.yaml b/tests/fixtures/config/success-url/git/common-config/zuul.yaml
index 6fbad0d..8929240 100644
--- a/tests/fixtures/config/success-url/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/success-url/git/common-config/zuul.yaml
@@ -17,6 +17,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: docs-draft-test
     success-url: http://docs-draft.example.org/{change.number:.2}/{change.number}/{change.patchset}/{pipeline.name}/{job.name}/{build.uuid:.7}/publish-docs/
 
diff --git a/tests/fixtures/config/templated-project/git/common-config/zuul.d/jobs.yaml b/tests/fixtures/config/templated-project/git/common-config/zuul.d/jobs.yaml
index e051871..f9de1ad 100644
--- a/tests/fixtures/config/templated-project/git/common-config/zuul.d/jobs.yaml
+++ b/tests/fixtures/config/templated-project/git/common-config/zuul.d/jobs.yaml
@@ -1,4 +1,8 @@
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - job:
diff --git a/tests/fixtures/config/tenant-parser/git/common-config/zuul.yaml b/tests/fixtures/config/tenant-parser/git/common-config/zuul.yaml
index ec371e8..e21f967 100644
--- a/tests/fixtures/config/tenant-parser/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/tenant-parser/git/common-config/zuul.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: common-config-job
 
 - project:
diff --git a/tests/fixtures/config/unprotected-branches/git/org_common-config/zuul.yaml b/tests/fixtures/config/unprotected-branches/git/org_common-config/zuul.yaml
index c0fbf0d..b16683f 100644
--- a/tests/fixtures/config/unprotected-branches/git/org_common-config/zuul.yaml
+++ b/tests/fixtures/config/unprotected-branches/git/org_common-config/zuul.yaml
@@ -17,3 +17,7 @@
     start:
       github:
         comment: true
+
+- job:
+    name: base
+    parent: null
diff --git a/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml b/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml
index 87182b0..d70a384 100644
--- a/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/zuul-connections-multiple-gerrits/git/common-config/zuul.yaml
@@ -44,6 +44,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - job:
diff --git a/tests/fixtures/config/zuul-connections-same-gerrit/git/common-config/zuul.yaml b/tests/fixtures/config/zuul-connections-same-gerrit/git/common-config/zuul.yaml
index e16bb80..eb65279 100644
--- a/tests/fixtures/config/zuul-connections-same-gerrit/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/zuul-connections-same-gerrit/git/common-config/zuul.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - job:
diff --git a/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml b/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
index aae8b8d..3dd8324 100644
--- a/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/zuultrigger/parent-change-enqueued/git/common-config/zuul.yaml
@@ -46,6 +46,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-check
 
 - job:
diff --git a/tests/fixtures/config/zuultrigger/project-change-merged/git/common-config/zuul.yaml b/tests/fixtures/config/zuultrigger/project-change-merged/git/common-config/zuul.yaml
index f0f3584..a5c5a1c 100644
--- a/tests/fixtures/config/zuultrigger/project-change-merged/git/common-config/zuul.yaml
+++ b/tests/fixtures/config/zuultrigger/project-change-merged/git/common-config/zuul.yaml
@@ -44,6 +44,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-check
 
 - job:
diff --git a/tests/fixtures/layouts/autohold.yaml b/tests/fixtures/layouts/autohold.yaml
index 015e562..515f79d 100644
--- a/tests/fixtures/layouts/autohold.yaml
+++ b/tests/fixtures/layouts/autohold.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test2
     nodes:
       - name: controller
diff --git a/tests/fixtures/layouts/basic-github.yaml b/tests/fixtures/layouts/basic-github.yaml
index 709fd02..d7b323a 100644
--- a/tests/fixtures/layouts/basic-github.yaml
+++ b/tests/fixtures/layouts/basic-github.yaml
@@ -18,7 +18,12 @@
       github: {}
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
+
 - job:
     name: project-test2
 
diff --git a/tests/fixtures/layouts/crd-github.yaml b/tests/fixtures/layouts/crd-github.yaml
index 11bdf76..9696226 100644
--- a/tests/fixtures/layouts/crd-github.yaml
+++ b/tests/fixtures/layouts/crd-github.yaml
@@ -28,15 +28,24 @@
       github: {}
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-test
+
 - job:
     name: project2-test
+
 - job:
     name: project3-test
+
 - job:
     name: project4-test
+
 - job:
     name: project5-test
+
 - job:
     name: project6-test
 
diff --git a/tests/fixtures/layouts/dependent-github.yaml b/tests/fixtures/layouts/dependent-github.yaml
index 46cc7b3..eb74163 100644
--- a/tests/fixtures/layouts/dependent-github.yaml
+++ b/tests/fixtures/layouts/dependent-github.yaml
@@ -16,9 +16,15 @@
         unlabel: 'merge'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
+
 - job:
     name: project-test2
+
 - job:
     name: project-merge
     failure-message: Unable to merge change
diff --git a/tests/fixtures/layouts/dequeue-github.yaml b/tests/fixtures/layouts/dequeue-github.yaml
index 25e92c9..ae61cd5 100644
--- a/tests/fixtures/layouts/dequeue-github.yaml
+++ b/tests/fixtures/layouts/dequeue-github.yaml
@@ -9,6 +9,10 @@
             - changed
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: one-job-project-merge
 
 - project:
diff --git a/tests/fixtures/layouts/disable_at.yaml b/tests/fixtures/layouts/disable_at.yaml
index 09082a4..7b1b8c8 100644
--- a/tests/fixtures/layouts/disable_at.yaml
+++ b/tests/fixtures/layouts/disable_at.yaml
@@ -16,6 +16,10 @@
     disable-after-consecutive-failures: 3
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
     nodes:
       - name: controller
diff --git a/tests/fixtures/layouts/dont-ignore-ref-deletes.yaml b/tests/fixtures/layouts/dont-ignore-ref-deletes.yaml
index 6a05fe6..6a92deb 100644
--- a/tests/fixtures/layouts/dont-ignore-ref-deletes.yaml
+++ b/tests/fixtures/layouts/dont-ignore-ref-deletes.yaml
@@ -8,6 +8,10 @@
           ignore-deletes: false
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-post
     nodes:
       - name: static
diff --git a/tests/fixtures/layouts/files-github.yaml b/tests/fixtures/layouts/files-github.yaml
index 734b945..ec35259 100644
--- a/tests/fixtures/layouts/files-github.yaml
+++ b/tests/fixtures/layouts/files-github.yaml
@@ -7,6 +7,10 @@
           action: opened
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
     files:
       - '.*-requires'
diff --git a/tests/fixtures/layouts/footer-message.yaml b/tests/fixtures/layouts/footer-message.yaml
index e500371..4ee25f6 100644
--- a/tests/fixtures/layouts/footer-message.yaml
+++ b/tests/fixtures/layouts/footer-message.yaml
@@ -26,6 +26,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 #    success-url: http://logs.exxxample.com/{change.number}/{change.patchset}/{pipeline.name}/{job.name}
 
diff --git a/tests/fixtures/layouts/idle.yaml b/tests/fixtures/layouts/idle.yaml
index 49c45ac..ec31408 100644
--- a/tests/fixtures/layouts/idle.yaml
+++ b/tests/fixtures/layouts/idle.yaml
@@ -6,6 +6,10 @@
         - time: '* * * * * */1'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-bitrot
     nodes:
       - name: static
diff --git a/tests/fixtures/layouts/ignore-dependencies.yaml b/tests/fixtures/layouts/ignore-dependencies.yaml
index 9eab9f4..89a82b3 100644
--- a/tests/fixtures/layouts/ignore-dependencies.yaml
+++ b/tests/fixtures/layouts/ignore-dependencies.yaml
@@ -13,6 +13,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-merge
 
 - job:
diff --git a/tests/fixtures/layouts/inheritance.yaml b/tests/fixtures/layouts/inheritance.yaml
index 146ca4d..3fe7fd4 100644
--- a/tests/fixtures/layouts/inheritance.yaml
+++ b/tests/fixtures/layouts/inheritance.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test-irrelevant-starts-empty
 
 - job:
diff --git a/tests/fixtures/layouts/irrelevant-files.yaml b/tests/fixtures/layouts/irrelevant-files.yaml
index fbcca8f..97f58e7 100644
--- a/tests/fixtures/layouts/irrelevant-files.yaml
+++ b/tests/fixtures/layouts/irrelevant-files.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test-irrelevant-files
 
 - project:
diff --git a/tests/fixtures/layouts/labeling-github.yaml b/tests/fixtures/layouts/labeling-github.yaml
index 33ce993..2441a9c 100644
--- a/tests/fixtures/layouts/labeling-github.yaml
+++ b/tests/fixtures/layouts/labeling-github.yaml
@@ -20,6 +20,10 @@
           - 'test'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-labels
 
 - project:
diff --git a/tests/fixtures/layouts/live-reconfiguration-add-job.yaml b/tests/fixtures/layouts/live-reconfiguration-add-job.yaml
index 1616dcb..57d2a5f 100644
--- a/tests/fixtures/layouts/live-reconfiguration-add-job.yaml
+++ b/tests/fixtures/layouts/live-reconfiguration-add-job.yaml
@@ -20,6 +20,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/live-reconfiguration-del-project.yaml b/tests/fixtures/layouts/live-reconfiguration-del-project.yaml
index 1eada5c..b149af0 100644
--- a/tests/fixtures/layouts/live-reconfiguration-del-project.yaml
+++ b/tests/fixtures/layouts/live-reconfiguration-del-project.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/live-reconfiguration-failed-job.yaml b/tests/fixtures/layouts/live-reconfiguration-failed-job.yaml
index 2b97419..c4719f4 100644
--- a/tests/fixtures/layouts/live-reconfiguration-failed-job.yaml
+++ b/tests/fixtures/layouts/live-reconfiguration-failed-job.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/live-reconfiguration-shared-queue.yaml b/tests/fixtures/layouts/live-reconfiguration-shared-queue.yaml
index 3cbed68..e363b4c 100644
--- a/tests/fixtures/layouts/live-reconfiguration-shared-queue.yaml
+++ b/tests/fixtures/layouts/live-reconfiguration-shared-queue.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/merge-failure.yaml b/tests/fixtures/layouts/merge-failure.yaml
index cc683a9..7c5121c 100644
--- a/tests/fixtures/layouts/merge-failure.yaml
+++ b/tests/fixtures/layouts/merge-failure.yaml
@@ -47,6 +47,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/merging-github.yaml b/tests/fixtures/layouts/merging-github.yaml
index 9f43f75..c9673b9 100644
--- a/tests/fixtures/layouts/merging-github.yaml
+++ b/tests/fixtures/layouts/merging-github.yaml
@@ -13,6 +13,10 @@
         merge: true
         comment: false
 
+- job:
+    name: base
+    parent: null
+
 - project:
     name: org/project
     merge:
diff --git a/tests/fixtures/layouts/no-jobs-project.yaml b/tests/fixtures/layouts/no-jobs-project.yaml
index 593a8d7..8f965e2 100644
--- a/tests/fixtures/layouts/no-jobs-project.yaml
+++ b/tests/fixtures/layouts/no-jobs-project.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-testfile
     files:
       - .*-requires
diff --git a/tests/fixtures/layouts/no-jobs.yaml b/tests/fixtures/layouts/no-jobs.yaml
index facae0a..301b27a 100644
--- a/tests/fixtures/layouts/no-jobs.yaml
+++ b/tests/fixtures/layouts/no-jobs.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: gate-noop
 
 - project:
diff --git a/tests/fixtures/layouts/no-timer.yaml b/tests/fixtures/layouts/no-timer.yaml
index fda7b8b..3790ea7 100644
--- a/tests/fixtures/layouts/no-timer.yaml
+++ b/tests/fixtures/layouts/no-timer.yaml
@@ -21,6 +21,10 @@
         - event: ref-updated
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - job:
diff --git a/tests/fixtures/layouts/nonvoting-job.yaml b/tests/fixtures/layouts/nonvoting-job.yaml
index c536e11..6a912bf 100644
--- a/tests/fixtures/layouts/nonvoting-job.yaml
+++ b/tests/fixtures/layouts/nonvoting-job.yaml
@@ -20,6 +20,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: nonvoting-project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/nonvoting-pipeline.yaml b/tests/fixtures/layouts/nonvoting-pipeline.yaml
index be5d5af..d8468dd 100644
--- a/tests/fixtures/layouts/nonvoting-pipeline.yaml
+++ b/tests/fixtures/layouts/nonvoting-pipeline.yaml
@@ -10,6 +10,10 @@
       gerrit: {}
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/one-job-project.yaml b/tests/fixtures/layouts/one-job-project.yaml
index db117b1..4b682d3 100644
--- a/tests/fixtures/layouts/one-job-project.yaml
+++ b/tests/fixtures/layouts/one-job-project.yaml
@@ -41,6 +41,10 @@
           ref: ^(?!refs/).*$
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: one-job-project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/push-tag-github.yaml b/tests/fixtures/layouts/push-tag-github.yaml
index 54683e9..5805127 100644
--- a/tests/fixtures/layouts/push-tag-github.yaml
+++ b/tests/fixtures/layouts/push-tag-github.yaml
@@ -15,7 +15,12 @@
           ref: ^refs/tags/.*$
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-post
+
 - job:
     name: project-tag
 
diff --git a/tests/fixtures/layouts/rate-limit.yaml b/tests/fixtures/layouts/rate-limit.yaml
index 9392f5e..1f32dbf 100644
--- a/tests/fixtures/layouts/rate-limit.yaml
+++ b/tests/fixtures/layouts/rate-limit.yaml
@@ -25,6 +25,10 @@
     window-decrease-factor: 2
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
 
 - job:
diff --git a/tests/fixtures/layouts/repo-checkout-four-project.yaml b/tests/fixtures/layouts/repo-checkout-four-project.yaml
index 36937e9..17303f5 100644
--- a/tests/fixtures/layouts/repo-checkout-four-project.yaml
+++ b/tests/fixtures/layouts/repo-checkout-four-project.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     required-projects:
       - org/project1
diff --git a/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml b/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml
index de3bfe0..4680869 100644
--- a/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml
+++ b/tests/fixtures/layouts/repo-checkout-no-timer-override.yaml
@@ -8,6 +8,10 @@
         - event: ref-updated
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     branches: master
     override-branch: stable/havana
diff --git a/tests/fixtures/layouts/repo-checkout-no-timer.yaml b/tests/fixtures/layouts/repo-checkout-no-timer.yaml
index 2b65850..ed20bb1 100644
--- a/tests/fixtures/layouts/repo-checkout-no-timer.yaml
+++ b/tests/fixtures/layouts/repo-checkout-no-timer.yaml
@@ -8,6 +8,10 @@
         - event: ref-updated
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     override-branch: stable/havana
     required-projects:
diff --git a/tests/fixtures/layouts/repo-checkout-post.yaml b/tests/fixtures/layouts/repo-checkout-post.yaml
index 9698289..191569c 100644
--- a/tests/fixtures/layouts/repo-checkout-post.yaml
+++ b/tests/fixtures/layouts/repo-checkout-post.yaml
@@ -7,6 +7,10 @@
           ref: ^(?!refs/).*$
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     required-projects:
       - org/project1
diff --git a/tests/fixtures/layouts/repo-checkout-six-project.yaml b/tests/fixtures/layouts/repo-checkout-six-project.yaml
index c0de47d..9a81eae 100644
--- a/tests/fixtures/layouts/repo-checkout-six-project.yaml
+++ b/tests/fixtures/layouts/repo-checkout-six-project.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     required-projects:
       - org/project1
diff --git a/tests/fixtures/layouts/repo-checkout-timer-override.yaml b/tests/fixtures/layouts/repo-checkout-timer-override.yaml
index 594d74c..99fc4f5 100644
--- a/tests/fixtures/layouts/repo-checkout-timer-override.yaml
+++ b/tests/fixtures/layouts/repo-checkout-timer-override.yaml
@@ -6,6 +6,10 @@
         - time: '* * * * * */1'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     branches: master
     override-branch: stable/havana
diff --git a/tests/fixtures/layouts/repo-checkout-timer.yaml b/tests/fixtures/layouts/repo-checkout-timer.yaml
index 3c4d030..e707732 100644
--- a/tests/fixtures/layouts/repo-checkout-timer.yaml
+++ b/tests/fixtures/layouts/repo-checkout-timer.yaml
@@ -6,6 +6,10 @@
         - time: '* * * * * */1'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     required-projects:
       - org/project1
diff --git a/tests/fixtures/layouts/repo-checkout-two-project.yaml b/tests/fixtures/layouts/repo-checkout-two-project.yaml
index e25554b..7910ae7 100644
--- a/tests/fixtures/layouts/repo-checkout-two-project.yaml
+++ b/tests/fixtures/layouts/repo-checkout-two-project.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: integration
     required-projects:
       - org/project1
diff --git a/tests/fixtures/layouts/repo-deleted.yaml b/tests/fixtures/layouts/repo-deleted.yaml
index cea3dda..6e6c301 100644
--- a/tests/fixtures/layouts/repo-deleted.yaml
+++ b/tests/fixtures/layouts/repo-deleted.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/reporting-github.yaml b/tests/fixtures/layouts/reporting-github.yaml
index ddb0588..159f205 100644
--- a/tests/fixtures/layouts/reporting-github.yaml
+++ b/tests/fixtures/layouts/reporting-github.yaml
@@ -79,6 +79,10 @@
         status: 'failure'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - project:
diff --git a/tests/fixtures/layouts/reporting-multiple-github.yaml b/tests/fixtures/layouts/reporting-multiple-github.yaml
index 22fa1e7..0126ec5 100644
--- a/tests/fixtures/layouts/reporting-multiple-github.yaml
+++ b/tests/fixtures/layouts/reporting-multiple-github.yaml
@@ -23,7 +23,12 @@
         status: 'success'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-test1
+
 - job:
     name: project2-test2
 
diff --git a/tests/fixtures/layouts/requirements-github.yaml b/tests/fixtures/layouts/requirements-github.yaml
index 891a366..f2ecd16 100644
--- a/tests/fixtures/layouts/requirements-github.yaml
+++ b/tests/fixtures/layouts/requirements-github.yaml
@@ -184,23 +184,36 @@
         comment: true
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project1-pipeline
+
 - job:
     name: project2-trigger
+
 - job:
     name: project3-reviewusername
+
 - job:
     name: project4-reviewreq
+
 - job:
     name: project5-reviewuserstate
+
 - job:
     name: project6-newerthan
+
 - job:
     name: project7-olderthan
+
 - job:
     name: project8-requireopen
+
 - job:
     name: project9-requirecurrent
+
 - job:
     name: project10-label
 
diff --git a/tests/fixtures/layouts/reviews-github.yaml b/tests/fixtures/layouts/reviews-github.yaml
index 1cc887a..f186fbe 100644
--- a/tests/fixtures/layouts/reviews-github.yaml
+++ b/tests/fixtures/layouts/reviews-github.yaml
@@ -12,6 +12,10 @@
           - 'tests passed'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-reviews
 
 - project:
diff --git a/tests/fixtures/layouts/smtp.yaml b/tests/fixtures/layouts/smtp.yaml
index bf1d273..5ea75ce 100644
--- a/tests/fixtures/layouts/smtp.yaml
+++ b/tests/fixtures/layouts/smtp.yaml
@@ -39,6 +39,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/tags.yaml b/tests/fixtures/layouts/tags.yaml
index 646160e..f86f5ab 100644
--- a/tests/fixtures/layouts/tags.yaml
+++ b/tests/fixtures/layouts/tags.yaml
@@ -12,6 +12,10 @@
         Verified: -1
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: merge
     tags:
       - merge
diff --git a/tests/fixtures/layouts/three-projects.yaml b/tests/fixtures/layouts/three-projects.yaml
index 6481fb9..51cd406 100644
--- a/tests/fixtures/layouts/three-projects.yaml
+++ b/tests/fixtures/layouts/three-projects.yaml
@@ -33,6 +33,10 @@
     precedence: high
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-merge
     hold-following-changes: true
 
diff --git a/tests/fixtures/layouts/timer-smtp.yaml b/tests/fixtures/layouts/timer-smtp.yaml
index 66e9aaf..a27b183 100644
--- a/tests/fixtures/layouts/timer-smtp.yaml
+++ b/tests/fixtures/layouts/timer-smtp.yaml
@@ -11,6 +11,10 @@
         subject: Periodic check for {change.project} succeeded
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-bitrot-stable-old
     success-url: http://logs.example.com/{job.name}/{build.number}
 
diff --git a/tests/fixtures/layouts/timer.yaml b/tests/fixtures/layouts/timer.yaml
index 45f137c..e1c4e77 100644
--- a/tests/fixtures/layouts/timer.yaml
+++ b/tests/fixtures/layouts/timer.yaml
@@ -19,6 +19,10 @@
         - time: '* * * * * */1'
 
 - job:
+    name: base
+    parent: null
+
+- job:
     name: project-test1
 
 - job:
diff --git a/tests/unit/test_configloader.py b/tests/unit/test_configloader.py
index 1ba4ed9..3b5c206 100644
--- a/tests/unit/test_configloader.py
+++ b/tests/unit/test_configloader.py
@@ -305,5 +305,6 @@
         tenant = self.sched.abide.tenants.get('tenant-one')
         jobs = sorted(tenant.layout.jobs.keys())
         self.assertEquals(
-            ['noop', 'trusted-zuul.yaml-job', 'untrusted-zuul.yaml-job'],
+            ['base', 'noop', 'trusted-zuul.yaml-job',
+             'untrusted-zuul.yaml-job'],
             jobs)
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index 3538555..6a63125 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -65,6 +65,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'job',
+            'parent': None,
             'irrelevant-files': [
                 '^docs/.*$'
             ]})
@@ -184,6 +185,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
             'pre-run': 'base-pre',
             'post-run': 'base-post',
@@ -389,6 +391,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
         })
         layout.addJob(base)
@@ -487,6 +490,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
         })
         layout.addJob(base)
@@ -565,6 +569,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
         })
         layout.addJob(base)
@@ -614,6 +619,7 @@
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': base_context,
             '_start_mark': self.start_mark,
+            'parent': None,
             'name': 'base',
         })
         layout.addJob(base)
@@ -639,6 +645,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'job',
+            'parent': None,
             'allowed-projects': ['project'],
         })
         self.layout.addJob(job)
@@ -679,6 +686,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'job',
+            'parent': None,
         })
         auth = model.AuthContext()
         auth.secrets.append('foo')
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 7c36cc4..74d72e7 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -1122,3 +1122,43 @@
         self.waitUntilSettled()
         self.assertNotIn("exceeds tenant max-nodes", B.messages[0],
                          "B should not fail because of nodes limit")
+
+
+class TestBaseJobs(ZuulTestCase):
+    tenant_config_file = 'config/base-jobs/main.yaml'
+
+    def test_multiple_base_jobs(self):
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertHistory([
+            dict(name='my-job', result='SUCCESS', changes='1,1'),
+            dict(name='other-job', result='SUCCESS', changes='1,1'),
+        ], ordered=False)
+        self.assertEqual(self.getJobFromHistory('my-job').
+                         parameters['zuul']['jobtags'],
+                         ['mybase'])
+        self.assertEqual(self.getJobFromHistory('other-job').
+                         parameters['zuul']['jobtags'],
+                         ['otherbase'])
+
+    def test_untrusted_base_job(self):
+        """Test that a base job may not be defined in an untrusted repo"""
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: fail-base
+                parent: null
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertEqual(A.reported, 1,
+                         "A should report failure")
+        self.assertEqual(A.patchsets[0]['approvals'][0]['value'], "-1")
+        self.assertIn('Base jobs must be defined in config projects',
+                      A.messages[0])
+        self.assertHistory([])
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 1036a2c..0ecbc14 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -344,7 +344,7 @@
                        'override-branch': str}
 
         job = {vs.Required('name'): str,
-               'parent': str,
+               'parent': vs.Any(str, None),
                'failure-message': str,
                'success-message': str,
                'failure-url': str,
@@ -441,9 +441,19 @@
                 job.auth.secrets.append(secret.decrypt(
                     job.source_context.project.private_key))
 
+        is_variant = layout.hasJob(conf['name'])
         if 'parent' in conf:
-            parent = layout.getJob(conf['parent'])
-            job.inheritFrom(parent)
+            if conf['parent'] is not None:
+                parent = layout.getJob(conf['parent'])
+                job.inheritFrom(parent)
+            else:
+                if not conf['_source_context'].trusted:
+                    raise Exception(
+                        "Base jobs must be defined in config projects")
+        else:
+            if not is_variant:
+                parent = layout.getJob(tenant.default_base_job)
+                job.inheritFrom(parent)
 
         # Roles are part of the playbook context so we must establish
         # them earlier than playbooks.
@@ -984,6 +994,7 @@
                   'max-nodes-per-job': int,
                   'source': TenantParser.validateTenantSources(connections),
                   'exclude-unprotected-branches': bool,
+                  'default-parent': str,
                   }
         return vs.Schema(tenant)
 
@@ -997,6 +1008,7 @@
         if conf.get('exclude-unprotected-branches') is not None:
             tenant.exclude_unprotected_branches = \
                 conf['exclude-unprotected-branches']
+        tenant.default_base_job = conf.get('default-parent', 'base')
 
         tenant.unparsed_config = conf
         unparsed_config = model.UnparsedTenantConfig()
diff --git a/zuul/model.py b/zuul/model.py
index 26a7963..9a89f75 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -2238,6 +2238,9 @@
             return self.jobs[name][0]
         raise Exception("Job %s not defined" % (name,))
 
+    def hasJob(self, name):
+        return name in self.jobs
+
     def getJobs(self, name):
         return self.jobs.get(name, [])
 
@@ -2456,6 +2459,7 @@
         self.name = name
         self.max_nodes_per_job = 5
         self.exclude_unprotected_branches = False
+        self.default_base_job = None
         self.layout = None
         # The unparsed configuration from the main zuul config for
         # this tenant.