Merge "Add trigger capability on github pr review" into feature/zuulv3
diff --git a/.zuul.yaml b/.zuul.yaml
index 50223fa..0ae5beb 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,49 +1,3 @@
-- job:
-    name: base
-    pre-run: base/pre
-    post-run: base/post
-    success-url: http://zuulv3-dev.openstack.org/logs/{build.uuid}/
-    failure-url: http://zuulv3-dev.openstack.org/logs/{build.uuid}/
-    timeout: 1800
-    vars:
-      zuul_workspace_root: /home/zuul
-    nodes:
-      - name: ubuntu-xenial
-        image: ubuntu-xenial
-
-- job:
-    name: tox
-    parent: base
-    pre-run: tox/pre
-    post-run: tox/post
-
-- job:
-    name: tox-cover
-    parent: tox
-    run: tox/cover
-    voting: false
-
-- job:
-    name: tox-docs
-    parent: tox
-    run: tox/docs
-
-- job:
-    name: tox-linters
-    parent: tox
-    run: tox/linters
-
-- job:
-    name: tox-py27
-    parent: tox
-    run: tox/py27
-
-- job:
-    name: tox-tarball
-    parent: tox
-    run: tox/tarball
-    post-run: tox/tarball-post
-
 - project:
     name: openstack-infra/zuul
     check:
diff --git a/playbooks/base/post.yaml b/playbooks/base/post.yaml
deleted file mode 100644
index ed3f7b8..0000000
--- a/playbooks/base/post.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-- hosts: all
-  tasks:
-    - name: Collect console log.
-      synchronize:
-        dest: "{{ zuul.executor.log_root }}"
-        mode: pull
-        src: "/tmp/console.log"
-
-    - name: Publish logs.
-      copy:
-        dest: "/opt/zuul-logs/{{ zuul.uuid}}"
-        src: "{{ zuul.executor.log_root }}/"
-      delegate_to: 127.0.0.1
diff --git a/playbooks/base/pre.yaml b/playbooks/base/pre.yaml
deleted file mode 100644
index 1a2e699..0000000
--- a/playbooks/base/pre.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-- hosts: all
-  roles:
-    - prepare-workspace
diff --git a/playbooks/base/roles b/playbooks/base/roles
deleted file mode 120000
index 7b9ade8..0000000
--- a/playbooks/base/roles
+++ /dev/null
@@ -1 +0,0 @@
-../roles/
\ No newline at end of file
diff --git a/playbooks/roles/extra-test-setup/tasks/main.yaml b/playbooks/roles/extra-test-setup/tasks/main.yaml
deleted file mode 100644
index da4259e..0000000
--- a/playbooks/roles/extra-test-setup/tasks/main.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
----
-- name: Check if projects tools/test-setup.sh exists.
-  stat:
-    path: "{{ zuul_workspace_root }}/src/{{ zuul.project }}/tools/test-setup.sh"
-  register: p
-
-- name: Run tools/test-setup.sh.
-  shell: tools/test-setup.sh
-  args:
-    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
-  when:
-    - p.stat.exists
-    - p.stat.executable
diff --git a/playbooks/roles/prepare-workspace/tasks/main.yaml b/playbooks/roles/prepare-workspace/tasks/main.yaml
deleted file mode 100644
index 4d42b2d..0000000
--- a/playbooks/roles/prepare-workspace/tasks/main.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-- name: Ensure console.log does not exist.
-  file:
-    path: /tmp/console.log
-    state: absent
-
-- name: Start zuul_console daemon.
-  zuul_console:
-    path: /tmp/console.log
-    port: 19885
-
-- name: Create workspace directory.
-  file:
-    path: "{{ zuul_workspace_root }}"
-    owner: zuul
-    group: zuul
-    state: directory
-
-- name: Synchronize src repos to workspace directory.
-  synchronize:
-    dest: "{{ zuul_workspace_root }}"
-    src: "{{ zuul.executor.src_root }}"
-  no_log: true
diff --git a/playbooks/roles/revoke-sudo/tasks/main.yaml b/playbooks/roles/revoke-sudo/tasks/main.yaml
deleted file mode 100644
index 1c18187..0000000
--- a/playbooks/roles/revoke-sudo/tasks/main.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-- name: Remove sudo access for zuul user.
-  become: yes
-  file:
-    path: /etc/sudoers.d/zuul-sudo
-    state: absent
-
-- name: Prove that general sudo access is actually revoked.
-  shell: ! sudo -n true
diff --git a/playbooks/roles/run-bindep/tasks/main.yaml b/playbooks/roles/run-bindep/tasks/main.yaml
deleted file mode 100644
index 5a9d33e..0000000
--- a/playbooks/roles/run-bindep/tasks/main.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-- name: Run install-distro-packages.sh
-  shell: /usr/local/jenkins/slave_scripts/install-distro-packages.sh
-  args:
-    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-cover/defaults/main.yaml b/playbooks/roles/run-cover/defaults/main.yaml
deleted file mode 100644
index 2e32efe..0000000
--- a/playbooks/roles/run-cover/defaults/main.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
----
-run_cover_envlist: cover
diff --git a/playbooks/roles/run-cover/tasks/main.yaml b/playbooks/roles/run-cover/tasks/main.yaml
deleted file mode 100644
index caed13c..0000000
--- a/playbooks/roles/run-cover/tasks/main.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-- name: Execute run-cover.sh.
-  shell: "/usr/local/jenkins/slave_scripts/run-cover.sh {{ run_cover_envlist }}"
-  args:
-    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-docs/defaults/main.yaml b/playbooks/roles/run-docs/defaults/main.yaml
deleted file mode 100644
index 5855a3d..0000000
--- a/playbooks/roles/run-docs/defaults/main.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
----
-run_docs_envlist: venv
diff --git a/playbooks/roles/run-docs/tasks/main.yaml b/playbooks/roles/run-docs/tasks/main.yaml
deleted file mode 100644
index 2250593..0000000
--- a/playbooks/roles/run-docs/tasks/main.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-- name: Execute run-docs.sh.
-  shell: "/usr/local/jenkins/slave_scripts/run-docs.sh {{ run_docs_envlist }}"
-  args:
-    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-tarball/defaults/main.yaml b/playbooks/roles/run-tarball/defaults/main.yaml
deleted file mode 100644
index 072828a..0000000
--- a/playbooks/roles/run-tarball/defaults/main.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
----
-run_tarball_envlist: venv
diff --git a/playbooks/roles/run-tarball/tasks/main.yaml b/playbooks/roles/run-tarball/tasks/main.yaml
deleted file mode 100644
index e21c4c8..0000000
--- a/playbooks/roles/run-tarball/tasks/main.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-- name: Execute run-tarball.sh.
-  shell: "/usr/local/jenkins/slave_scripts/run-tarball.sh {{ run_tarball_envlist }}"
-  args:
-    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-tox/defaults/main.yaml b/playbooks/roles/run-tox/defaults/main.yaml
deleted file mode 100644
index 9cb1477..0000000
--- a/playbooks/roles/run-tox/defaults/main.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
----
-run_tox_envlist:
diff --git a/playbooks/roles/run-tox/tasks/main.yaml b/playbooks/roles/run-tox/tasks/main.yaml
deleted file mode 100644
index 29a4cc4..0000000
--- a/playbooks/roles/run-tox/tasks/main.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-- name: Run tox
-  shell: "/usr/local/jenkins/slave_scripts/run-tox.sh {{ run_tox_envlist }}"
-  args:
-    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/roles/run-wheel/defaults/main.yaml b/playbooks/roles/run-wheel/defaults/main.yaml
deleted file mode 100644
index 8645d33..0000000
--- a/playbooks/roles/run-wheel/defaults/main.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
----
-run_wheel_envlist: venv
diff --git a/playbooks/roles/run-wheel/tasks/main.yaml b/playbooks/roles/run-wheel/tasks/main.yaml
deleted file mode 100644
index f5aaf54..0000000
--- a/playbooks/roles/run-wheel/tasks/main.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-- name: Execute run-wheel.sh.
-  shell: "/usr/local/jenkins/slave_scripts/run-wheel.sh {{ run_wheel_envlist }}"
-  args:
-    chdir: "{{ zuul_workspace_root }}/src/{{ zuul.project }}"
diff --git a/playbooks/tox/cover.yaml b/playbooks/tox/cover.yaml
deleted file mode 100644
index 642eb4e..0000000
--- a/playbooks/tox/cover.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-- hosts: all
-  roles:
-    - extra-test-setup
-    - revoke-sudo
-    - run-cover
diff --git a/playbooks/tox/docs.yaml b/playbooks/tox/docs.yaml
deleted file mode 100644
index 028e1c5..0000000
--- a/playbooks/tox/docs.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-- hosts: all
-  roles:
-    - revoke-sudo
-    - run-docs
diff --git a/playbooks/tox/linters.yaml b/playbooks/tox/linters.yaml
deleted file mode 100644
index d1e7f13..0000000
--- a/playbooks/tox/linters.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-- hosts: all
-  vars:
-    run_tox_envlist: pep8
-  roles:
-    - revoke-sudo
-    - run-tox
diff --git a/playbooks/tox/post.yaml b/playbooks/tox/post.yaml
deleted file mode 100644
index 3b035f8..0000000
--- a/playbooks/tox/post.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-- hosts: all
-  tasks:
-    - name: Find tox directories to synchrionize.
-      find:
-        file_type: directory
-        paths: "{{ zuul_workspace_root }}/src/{{ zuul.project }}/.tox"
-        # NOTE(pabelanger): The .tox/log folder is empty, ignore it.
-        patterns: ^(?!log).*$
-        use_regex: yes
-      register: result
-
-    - name: Collect tox logs.
-      synchronize:
-        dest: "{{ zuul.executor.log_root }}/tox"
-        mode: pull
-        src: "{{ item.path }}/log/"
-      with_items: "{{ result.files }}"
diff --git a/playbooks/tox/pre.yaml b/playbooks/tox/pre.yaml
deleted file mode 100644
index 0bf9b3c..0000000
--- a/playbooks/tox/pre.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-- hosts: all
-  roles:
-    - run-bindep
diff --git a/playbooks/tox/py27.yaml b/playbooks/tox/py27.yaml
deleted file mode 100644
index fd45f27..0000000
--- a/playbooks/tox/py27.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-- hosts: all
-  vars:
-    run_tox_envlist: py27
-  roles:
-    - extra-test-setup
-    - revoke-sudo
-    - run-tox
diff --git a/playbooks/tox/roles b/playbooks/tox/roles
deleted file mode 120000
index 7b9ade8..0000000
--- a/playbooks/tox/roles
+++ /dev/null
@@ -1 +0,0 @@
-../roles/
\ No newline at end of file
diff --git a/playbooks/tox/tarball-post.yaml b/playbooks/tox/tarball-post.yaml
deleted file mode 100644
index fb41707..0000000
--- a/playbooks/tox/tarball-post.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-- hosts: all
-  tasks:
-    - name: Collect tarball artifacts.
-      synchronize:
-        dest: "{{ zuul.executor.src_root }}/tarballs"
-        mode: pull
-        src: "{{ zuul_workspace_root }}/src/{{ zuul.project }}/dist/{{ item }}"
-      with_items:
-        - "*.tar.gz"
-        - "*.whl"
diff --git a/playbooks/tox/tarball.yaml b/playbooks/tox/tarball.yaml
deleted file mode 100644
index 4d5a8f6..0000000
--- a/playbooks/tox/tarball.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-- hosts: all
-  roles:
-    - revoke-sudo
-    - run-tarball
-    - run-wheel
diff --git a/test-requirements.txt b/test-requirements.txt
index 6262a02..735b4dd 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,7 +1,7 @@
 hacking>=0.12.0,!=0.13.0,<0.14  # Apache-2.0
 
 coverage>=3.6
-sphinx>=1.5.1
+sphinx>=1.5.1,<1.6
 sphinxcontrib-blockdiag>=1.1.0
 fixtures>=0.3.14
 python-keystoneclient>=0.4.2
diff --git a/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml b/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
index 92c66d1..1f8fdf3 100644
--- a/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
+++ b/tests/fixtures/config/ansible/git/common-config/playbooks/check-vars.yaml
@@ -13,3 +13,10 @@
           - zuul.executor.hostname is defined
           - zuul.executor.src_root is defined
           - zuul.executor.log_root is defined
+
+    - name: Assert zuul.project variables are valid.
+      assert:
+        that:
+          - zuul.project.name == 'org/project'
+          - zuul.project.canonical_hostname == 'review.example.com'
+          - zuul.project.canonical_name == 'review.example.com/org/project'
diff --git a/tests/fixtures/config/in-repo/git/org_project1/README b/tests/fixtures/config/in-repo/git/org_project1/README
new file mode 100644
index 0000000..9daeafb
--- /dev/null
+++ b/tests/fixtures/config/in-repo/git/org_project1/README
@@ -0,0 +1 @@
+test
diff --git a/tests/fixtures/config/in-repo/main.yaml b/tests/fixtures/config/in-repo/main.yaml
index 208e274..5f57245 100644
--- a/tests/fixtures/config/in-repo/main.yaml
+++ b/tests/fixtures/config/in-repo/main.yaml
@@ -6,3 +6,4 @@
           - common-config
         untrusted-projects:
           - org/project
+          - org/project1
diff --git a/tests/fixtures/layout-live-reconfiguration-add-job.yaml b/tests/fixtures/layout-live-reconfiguration-add-job.yaml
deleted file mode 100644
index e4aea6f..0000000
--- a/tests/fixtures/layout-live-reconfiguration-add-job.yaml
+++ /dev/null
@@ -1,38 +0,0 @@
-pipelines:
-  - name: gate
-    manager: DependentPipelineManager
-    failure-message: Build failed.  For information on how to proceed, see http://wiki.example.org/Test_Failures
-    trigger:
-      gerrit:
-        - event: comment-added
-          approval:
-            - approved: 1
-    success:
-      gerrit:
-        verified: 2
-        submit: true
-    failure:
-      gerrit:
-        verified: -2
-    start:
-      gerrit:
-        verified: 0
-    precedence: high
-
-jobs:
-  - name: ^.*-merge$
-    failure-message: Unable to merge change
-    hold-following-changes: true
-  - name: project-testfile
-    files:
-      - '.*-requires'
-
-projects:
-  - name: org/project
-    merge-mode: cherry-pick
-    gate:
-      - project-merge:
-        - project-test1
-        - project-test2
-        - project-test3
-        - project-testfile
diff --git a/tests/fixtures/layout-live-reconfiguration-failed-job.yaml b/tests/fixtures/layout-live-reconfiguration-failed-job.yaml
deleted file mode 100644
index e811af1..0000000
--- a/tests/fixtures/layout-live-reconfiguration-failed-job.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-pipelines:
-  - name: check
-    manager: IndependentPipelineManager
-    trigger:
-      gerrit:
-        - event: patchset-created
-    success:
-      gerrit:
-        verified: 1
-    failure:
-      gerrit:
-        verified: -1
-
-jobs:
-  - name: ^.*-merge$
-    failure-message: Unable to merge change
-    hold-following-changes: true
-
-projects:
-  - name: org/project
-    merge-mode: cherry-pick
-    check:
-      - project-merge:
-        - project-test2
-        - project-testfile
diff --git a/tests/fixtures/layout-live-reconfiguration-shared-queue.yaml b/tests/fixtures/layout-live-reconfiguration-shared-queue.yaml
deleted file mode 100644
index ad3f666..0000000
--- a/tests/fixtures/layout-live-reconfiguration-shared-queue.yaml
+++ /dev/null
@@ -1,62 +0,0 @@
-pipelines:
-  - name: check
-    manager: IndependentPipelineManager
-    trigger:
-      gerrit:
-        - event: patchset-created
-    success:
-      gerrit:
-        verified: 1
-    failure:
-      gerrit:
-        verified: -1
-
-  - name: gate
-    manager: DependentPipelineManager
-    failure-message: Build failed.  For information on how to proceed, see http://wiki.example.org/Test_Failures
-    trigger:
-      gerrit:
-        - event: comment-added
-          approval:
-            - approved: 1
-    success:
-      gerrit:
-        verified: 2
-        submit: true
-    failure:
-      gerrit:
-        verified: -2
-    start:
-      gerrit:
-        verified: 0
-    precedence: high
-
-jobs:
-  - name: ^.*-merge$
-    failure-message: Unable to merge change
-    hold-following-changes: true
-  - name: project1-project2-integration
-    queue-name: integration
-
-projects:
-  - name: org/project1
-    check:
-      - project1-merge:
-        - project1-test1
-        - project1-test2
-    gate:
-      - project1-merge:
-        - project1-test1
-        - project1-test2
-
-  - name: org/project2
-    check:
-      - project2-merge:
-        - project2-test1
-        - project2-test2
-        - project1-project2-integration
-    gate:
-      - project2-merge:
-        - project2-test1
-        - project2-test2
-        - project1-project2-integration
diff --git a/tests/fixtures/layouts/live-reconfiguration-add-job.yaml b/tests/fixtures/layouts/live-reconfiguration-add-job.yaml
new file mode 100644
index 0000000..5916282
--- /dev/null
+++ b/tests/fixtures/layouts/live-reconfiguration-add-job.yaml
@@ -0,0 +1,57 @@
+- pipeline:
+    name: gate
+    manager: dependent
+    failure-message: Build failed.  For information on how to proceed, see http://wiki.example.org/Test_Failures
+    trigger:
+      gerrit:
+        - event: comment-added
+          approval:
+            - approved: 1
+    success:
+      gerrit:
+        verified: 2
+        submit: true
+    failure:
+      gerrit:
+        verified: -2
+    start:
+      gerrit:
+        verified: 0
+    precedence: high
+
+- job:
+    name: project-merge
+    hold-following-changes: true
+
+- job:
+    name: project-test1
+
+- job:
+    name: project-test2
+
+- job:
+    name: project-test3
+
+- job:
+    name: project-testfile
+    files:
+      - '.*-requires'
+
+- project:
+    name: org/project
+    merge-mode: cherry-pick
+    gate:
+      jobs:
+      - project-merge
+      - project-test1:
+          dependencies:
+            - project-merge
+      - project-test2:
+          dependencies:
+            - project-merge
+      - project-test3:
+          dependencies:
+            - project-merge
+      - project-testfile:
+          dependencies:
+            - project-merge
diff --git a/tests/fixtures/layouts/live-reconfiguration-failed-job.yaml b/tests/fixtures/layouts/live-reconfiguration-failed-job.yaml
new file mode 100644
index 0000000..0907880
--- /dev/null
+++ b/tests/fixtures/layouts/live-reconfiguration-failed-job.yaml
@@ -0,0 +1,35 @@
+- pipeline:
+    name: check
+    manager: independent
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
+- job:
+    name: project-merge
+    hold-following-changes: true
+
+- job:
+    name: project-test2
+
+- job:
+    name: project-testfile
+
+- project:
+    name: org/project
+    merge-mode: cherry-pick
+    check:
+      jobs:
+      - project-merge
+      - project-test2:
+          dependencies:
+            - project-merge
+      - project-testfile:
+          dependencies:
+            - project-merge
diff --git a/tests/fixtures/layouts/live-reconfiguration-shared-queue.yaml b/tests/fixtures/layouts/live-reconfiguration-shared-queue.yaml
new file mode 100644
index 0000000..bf4416a
--- /dev/null
+++ b/tests/fixtures/layouts/live-reconfiguration-shared-queue.yaml
@@ -0,0 +1,86 @@
+- pipeline:
+    name: check
+    manager: independent
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
+- pipeline:
+    name: gate
+    manager: dependent
+    failure-message: Build failed.  For information on how to proceed, see http://wiki.example.org/Test_Failures
+    trigger:
+      gerrit:
+        - event: comment-added
+          approval:
+            - approved: 1
+    success:
+      gerrit:
+        verified: 2
+        submit: true
+    failure:
+      gerrit:
+        verified: -2
+    start:
+      gerrit:
+        verified: 0
+    precedence: high
+
+- job:
+    name: project-merge
+    hold-following-changes: true
+
+- job:
+    name: project-test1
+
+- job:
+    name: project-test2
+
+- job:
+    name: project1-project2-integration
+
+- project:
+    name: org/project1
+    check:
+      jobs:
+        - project-merge
+        - project-test1:
+            dependencies: project-merge
+        - project-test2:
+            dependencies: project-merge
+    gate:
+      queue: integrated
+      jobs:
+        - project-merge
+        - project-test1:
+            dependencies: project-merge
+        - project-test2:
+            dependencies: project-merge
+
+- project:
+    name: org/project2
+    check:
+      jobs:
+        - project-merge
+        - project-test1:
+            dependencies: project-merge
+        - project-test2:
+            dependencies: project-merge
+        - project1-project2-integration:
+            dependencies: project-merge
+    gate:
+      queue: integrated
+      jobs:
+        - project-merge
+        - project-test1:
+            dependencies: project-merge
+        - project-test2:
+            dependencies: project-merge
+        - project1-project2-integration:
+            dependencies: project-merge
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index f67318d..bc827b3 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -2164,12 +2164,6 @@
         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"
-        self.sched.testConfig(self.config.get('zuul', 'tenant_config'),
-                              self.connections)
-
     def test_queue_names(self):
         "Test shared change queue names"
         tenant = self.sched.abide.tenants.get('tenant-one')
@@ -2293,13 +2287,11 @@
         self.assertEqual(A.data['status'], 'MERGED')
         self.assertEqual(A.reported, 2)
 
-    @skip("Disabled for early v3 development")
     def test_live_reconfiguration_merge_conflict(self):
         # A real-world bug: a change in a gate queue has a merge
         # conflict and a job is added to its project while it's
         # sitting in the queue.  The job gets added to the change and
         # enqueued and the change gets stuck.
-        self.worker.registerFunction('build:project-test3')
         self.executor_server.hold_jobs_in_build = True
 
         # This change is fine.  It's here to stop the queue long
@@ -2307,14 +2299,14 @@
         # reconfiguration, as well as to provide a conflict for the
         # next change.  This change will succeed and merge.
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
-        A.addPatchset(['conflict'])
+        A.addPatchset({'conflict': 'A'})
         A.addApproval('code-review', 2)
 
         # This change will be in merge conflict.  During the
         # reconfiguration, we will add a job.  We want to make sure
         # that doesn't cause it to get stuck.
         B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
-        B.addPatchset(['conflict'])
+        B.addPatchset({'conflict': 'B'})
         B.addApproval('code-review', 2)
 
         self.fake_gerrit.addEvent(A.addApproval('approved', 1))
@@ -2330,8 +2322,8 @@
         self.assertEqual(len(self.history), 0)
 
         # Add the "project-test3" job.
-        self.updateConfigLayout(
-            'tests/fixtures/layout-live-reconfiguration-add-job.yaml')
+        self.commitConfigUpdate('common-config',
+                                'layouts/live-reconfiguration-add-job.yaml')
         self.sched.reconfigure(self.config)
         self.waitUntilSettled()
 
@@ -2353,19 +2345,17 @@
                          'SUCCESS')
         self.assertEqual(len(self.history), 4)
 
-    @skip("Disabled for early v3 development")
     def test_live_reconfiguration_failed_root(self):
         # An extrapolation of test_live_reconfiguration_merge_conflict
         # that tests a job added to a job tree with a failed root does
         # not run.
-        self.worker.registerFunction('build:project-test3')
         self.executor_server.hold_jobs_in_build = True
 
         # This change is fine.  It's here to stop the queue long
         # enough for the next change to be subject to the
         # reconfiguration.  This change will succeed and merge.
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
-        A.addPatchset(['conflict'])
+        A.addPatchset({'conflict': 'A'})
         A.addApproval('code-review', 2)
         self.fake_gerrit.addEvent(A.addApproval('approved', 1))
         self.waitUntilSettled()
@@ -2393,8 +2383,8 @@
         self.assertEqual(len(self.history), 2)
 
         # Add the "project-test3" job.
-        self.updateConfigLayout(
-            'tests/fixtures/layout-live-reconfiguration-add-job.yaml')
+        self.commitConfigUpdate('common-config',
+                                'layouts/live-reconfiguration-add-job.yaml')
         self.sched.reconfigure(self.config)
         self.waitUntilSettled()
 
@@ -2415,7 +2405,6 @@
         self.assertEqual(self.history[4].result, 'SUCCESS')
         self.assertEqual(len(self.history), 5)
 
-    @skip("Disabled for early v3 development")
     def test_live_reconfiguration_failed_job(self):
         # Test that a change with a removed failing job does not
         # disrupt reconfiguration.  If a change has a failed job and
@@ -2447,8 +2436,8 @@
         self.assertEqual(len(self.history), 2)
 
         # Remove the test1 job.
-        self.updateConfigLayout(
-            'tests/fixtures/layout-live-reconfiguration-failed-job.yaml')
+        self.commitConfigUpdate('common-config',
+                                'layouts/live-reconfiguration-failed-job.yaml')
         self.sched.reconfigure(self.config)
         self.waitUntilSettled()
 
@@ -2468,7 +2457,6 @@
         # Ensure the removed job was not included in the report.
         self.assertNotIn('project-test1', A.messages[0])
 
-    @skip("Disabled for early v3 development")
     def test_live_reconfiguration_shared_queue(self):
         # Test that a change with a failing job which was removed from
         # this project but otherwise still exists in the system does
@@ -2490,15 +2478,16 @@
         self.assertEqual(A.data['status'], 'NEW')
         self.assertEqual(A.reported, 0)
 
-        self.assertEqual(self.getJobFromHistory('project1-merge').result,
+        self.assertEqual(self.getJobFromHistory('project-merge').result,
                          'SUCCESS')
         self.assertEqual(self.getJobFromHistory(
             'project1-project2-integration').result, 'FAILURE')
         self.assertEqual(len(self.history), 2)
 
         # Remove the integration job.
-        self.updateConfigLayout(
-            'tests/fixtures/layout-live-reconfiguration-shared-queue.yaml')
+        self.commitConfigUpdate(
+            'common-config',
+            'layouts/live-reconfiguration-shared-queue.yaml')
         self.sched.reconfigure(self.config)
         self.waitUntilSettled()
 
@@ -2506,11 +2495,11 @@
         self.executor_server.release()
         self.waitUntilSettled()
 
-        self.assertEqual(self.getJobFromHistory('project1-merge').result,
+        self.assertEqual(self.getJobFromHistory('project-merge').result,
                          'SUCCESS')
-        self.assertEqual(self.getJobFromHistory('project1-test1').result,
+        self.assertEqual(self.getJobFromHistory('project-test1').result,
                          'SUCCESS')
-        self.assertEqual(self.getJobFromHistory('project1-test2').result,
+        self.assertEqual(self.getJobFromHistory('project-test2').result,
                          'SUCCESS')
         self.assertEqual(self.getJobFromHistory(
             'project1-project2-integration').result, 'FAILURE')
@@ -2522,7 +2511,6 @@
         # Ensure the removed job was not included in the report.
         self.assertNotIn('project1-project2-integration', A.messages[0])
 
-    @skip("Disabled for early v3 development")
     def test_double_live_reconfiguration_shared_queue(self):
         # This was a real-world regression.  A change is added to
         # gate; a reconfigure happens, a second change which depends
diff --git a/tests/unit/test_scheduler_cmd.py b/tests/unit/test_scheduler_cmd.py
new file mode 100644
index 0000000..ee6200f
--- /dev/null
+++ b/tests/unit/test_scheduler_cmd.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+
+import testtools
+import zuul.cmd.scheduler
+
+from tests import base
+
+
+class TestSchedulerCmdArguments(testtools.TestCase):
+
+    def setUp(self):
+        super(TestSchedulerCmdArguments, self).setUp()
+        self.app = zuul.cmd.scheduler.Scheduler()
+
+    def test_test_config(self):
+        conf_path = os.path.join(base.FIXTURE_DIR, 'zuul.conf')
+        self.app.parse_arguments(['-t', '-c', conf_path])
+        self.assertTrue(self.app.args.validate)
+        self.app.read_config()
+        self.assertEqual(0, self.app.test_config())
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 3919418..2168a7f 100644
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -191,6 +191,61 @@
             dict(name='project-test1', result='SUCCESS', changes='2,1'),
             dict(name='project-test2', result='SUCCESS', changes='3,1')])
 
+    def test_crd_dynamic_config_branch(self):
+        # Test that we can create a job in one repo and be able to use
+        # it from a different branch on a different repo.
+
+        self.create_branch('org/project1', 'stable')
+
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: project-test2
+
+            - project:
+                name: org/project
+                check:
+                  jobs:
+                    - project-test2
+            """)
+
+        in_repo_playbook = textwrap.dedent(
+            """
+            - hosts: all
+              tasks: []
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf,
+                     'playbooks/project-test2.yaml': in_repo_playbook}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+
+        second_repo_conf = textwrap.dedent(
+            """
+            - project:
+                name: org/project1
+                check:
+                  jobs:
+                    - project-test2
+            """)
+
+        second_file_dict = {'.zuul.yaml': second_repo_conf}
+        B = self.fake_gerrit.addFakeChange('org/project1', 'stable', 'B',
+                                           files=second_file_dict)
+        B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
+            B.subject, A.data['id'])
+
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        self.assertEqual(A.reported, 1, "A should report")
+        self.assertHistory([
+            dict(name='project-test2', result='SUCCESS', changes='1,1'),
+            dict(name='project-test2', result='SUCCESS', changes='1,1 2,1'),
+        ])
+
     def test_untrusted_syntax_error(self):
         in_repo_conf = textwrap.dedent(
             """
@@ -253,6 +308,26 @@
         self.assertIn('syntax error', A.messages[1],
                       "A should have a syntax error reported")
 
+    def test_untrusted_shadow_error(self):
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: common-config-test
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf}
+        A = self.fake_gerrit.addFakeChange('org/project', '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('not permitted to shadow', 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/cmd/scheduler.py b/zuul/cmd/scheduler.py
index f1d1015..5328bba 100755
--- a/zuul/cmd/scheduler.py
+++ b/zuul/cmd/scheduler.py
@@ -40,7 +40,7 @@
         super(Scheduler, self).__init__()
         self.gear_server_pid = None
 
-    def parse_arguments(self):
+    def parse_arguments(self, args=None):
         parser = argparse.ArgumentParser(description='Project gating system.')
         parser.add_argument('-c', dest='config',
                             help='specify the config file')
@@ -52,7 +52,7 @@
         parser.add_argument('--version', dest='version', action='version',
                             version=self._get_version(),
                             help='show zuul version')
-        self.args = parser.parse_args()
+        self.args = parser.parse_args(args)
 
     def reconfigure_handler(self, signum, frame):
         signal.signal(signal.SIGHUP, signal.SIG_IGN)
diff --git a/zuul/configloader.py b/zuul/configloader.py
index d7cef94..070e731 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -65,6 +65,8 @@
 def configuration_exceptions(stanza, conf):
     try:
         yield
+    except ConfigurationSyntaxError:
+        raise
     except Exception as e:
         conf = copy.deepcopy(conf)
         context = conf.pop('_source_context')
@@ -271,7 +273,29 @@
     ]
 
     @staticmethod
-    def fromYaml(tenant, layout, conf):
+    def _getImpliedBranches(reference, job, project_pipeline):
+        # If the current job definition is not in the same branch as
+        # the reference definition of this job, and this is a project
+        # repo, add an implicit branch matcher for this branch
+        # (assuming there are no explicit branch matchers).  But only
+        # for top-level job definitions and variants.
+        # Project-pipeline job variants should more closely attach to
+        # their branch if they appear in a project-repo.
+        if (reference and
+            reference.source_context and
+            reference.source_context.branch != job.source_context.branch):
+            same_context = False
+        else:
+            same_context = True
+
+        if (job.source_context and
+            (not job.source_context.trusted) and
+            ((not same_context) or project_pipeline)):
+            return [job.source_context.branch]
+        return None
+
+    @staticmethod
+    def fromYaml(tenant, layout, conf, project_pipeline=False):
         with configuration_exceptions('job', conf):
             JobParser.getSchema()(conf)
 
@@ -280,6 +304,8 @@
         # them (e.g., "job.run = ..." rather than
         # "job.run.append(...)").
 
+        reference = layout.jobs.get(conf['name'], [None])[0]
+
         job = model.Job(conf['name'])
         job.source_context = conf.get('_source_context')
         if 'auth' in conf:
@@ -316,9 +342,10 @@
             run = model.PlaybookContext(job.source_context, run_name)
             job.run = (run,)
         else:
-            run_name = os.path.join('playbooks', job.name)
-            run = model.PlaybookContext(job.source_context, run_name)
-            job.implied_run = (run,) + job.implied_run
+            if not project_pipeline:
+                run_name = os.path.join('playbooks', job.name)
+                run = model.PlaybookContext(job.source_context, run_name)
+                job.implied_run = (run,) + job.implied_run
 
         for k in JobParser.simple_attributes:
             a = k.replace('-', '_')
@@ -350,13 +377,14 @@
 
         job.dependencies = frozenset(as_list(conf.get('dependencies')))
 
-        roles = []
-        for role in conf.get('roles', []):
-            if 'zuul' in role:
-                r = JobParser._makeZuulRole(tenant, job, role)
-                if r:
-                    roles.append(r)
-        job.roles = job.roles.union(set(roles))
+        if 'roles' in conf:
+            roles = []
+            for role in conf.get('roles', []):
+                if 'zuul' in role:
+                    r = JobParser._makeZuulRole(tenant, job, role)
+                    if r:
+                        roles.append(r)
+            job.roles = job.roles.union(set(roles))
 
         variables = conf.get('vars', None)
         if variables:
@@ -372,14 +400,20 @@
                 allowed.append(project.name)
             job.allowed_projects = frozenset(allowed)
 
-        # If the definition for this job came from a project repo,
-        # implicitly apply a branch matcher for the branch it was on.
-        if (not job.source_context.trusted):
-            branches = [job.source_context.branch]
-        elif 'branches' in conf:
+        # If the current job definition is not in the same branch as
+        # the reference definition of this job, and this is a project
+        # repo, add an implicit branch matcher for this branch
+        # (assuming there are no explicit branch matchers).  But only
+        # for top-level job definitions and variants.
+        # Project-pipeline job variants should more closely attach to
+        # their branch if they appear in a project-repo.
+
+        branches = None
+        if (project_pipeline or 'branches' not in conf):
+            branches = JobParser._getImpliedBranches(
+                reference, job, project_pipeline)
+        if (not branches) and ('branches' in conf):
             branches = as_list(conf['branches'])
-        else:
-            branches = None
         if branches:
             matchers = []
             for branch in branches:
@@ -408,7 +442,7 @@
 
         return model.ZuulRole(role.get('name', name),
                               project.connection_name,
-                              project.name, trusted)
+                              project.name)
 
 
 class ProjectTemplateParser(object):
@@ -456,23 +490,22 @@
                       start_mark, job_list):
         for conf_job in conf:
             if isinstance(conf_job, six.string_types):
-                job = model.Job(conf_job)
-                job_list.addJob(job)
+                attrs = dict(name=conf_job)
             elif isinstance(conf_job, dict):
                 # A dictionary in a job tree may override params
                 jobname, attrs = conf_job.items()[0]
                 if attrs:
                     # We are overriding params, so make a new job def
                     attrs['name'] = jobname
-                    attrs['_source_context'] = source_context
-                    attrs['_start_mark'] = start_mark
-                    job_list.addJob(JobParser.fromYaml(tenant, layout, attrs))
                 else:
                     # Not overriding, so add a blank job
-                    job = model.Job(jobname)
-                    job_list.addJob(job)
+                    attrs = dict(name=jobname)
             else:
                 raise Exception("Job must be a string or dictionary")
+            attrs['_source_context'] = source_context
+            attrs['_start_mark'] = start_mark
+            job_list.addJob(JobParser.fromYaml(tenant, layout, attrs,
+                                               project_pipeline=True))
 
 
 class ProjectParser(object):
@@ -993,7 +1026,8 @@
             layout.addSecret(SecretParser.fromYaml(layout, config_secret))
 
         for config_job in data.jobs:
-            layout.addJob(JobParser.fromYaml(tenant, layout, config_job))
+            with configuration_exceptions('job', config_job):
+                layout.addJob(JobParser.fromYaml(tenant, layout, config_job))
 
         for config_semaphore in data.semaphores:
             layout.addSemaphore(SemaphoreParser.fromYaml(config_semaphore))
@@ -1122,7 +1156,8 @@
             layout.addSecret(SecretParser.fromYaml(layout, config_secret))
 
         for config_job in config.jobs:
-            layout.addJob(JobParser.fromYaml(tenant, layout, config_job))
+            with configuration_exceptions('job', config_job):
+                layout.addJob(JobParser.fromYaml(tenant, layout, config_job))
 
         for config_template in config.project_templates:
             layout.addProjectTemplate(ProjectTemplateParser.fromYaml(
diff --git a/zuul/executor/client.py b/zuul/executor/client.py
index 7e2d296..9f234e9 100644
--- a/zuul/executor/client.py
+++ b/zuul/executor/client.py
@@ -183,10 +183,15 @@
         dependent_items.reverse()
         # TODOv3(jeblair): This ansible vars data structure will
         # replace the environment variables below.
+        project = dict(
+            name=item.change.project.name,
+            canonical_hostname=item.change.project.canonical_hostname,
+            canonical_name=item.change.project.canonical_name)
+
         zuul_params = dict(uuid=uuid,
                            pipeline=pipeline.name,
                            job=job.name,
-                           project=item.change.project.name,
+                           project=project,
                            tags=' '.join(sorted(job.tags)))
 
         if hasattr(item.change, 'branch'):
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index fa0f4d5..71b643a 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -108,7 +108,8 @@
         self.pre_playbooks = []
         self.post_playbooks = []
         self.roles = []
-        self.roles_path = []
+        self.trusted_roles_path = []
+        self.untrusted_roles_path = []
         self.untrusted_config = os.path.join(
             self.ansible_root, 'untrusted.cfg')
         self.trusted_config = os.path.join(self.ansible_root, 'trusted.cfg')
@@ -142,6 +143,10 @@
         count = len(self.roles)
         root = os.path.join(self.ansible_root, 'role_%i' % (count,))
         os.makedirs(root)
+        trusted = os.path.join(root, 'trusted')
+        os.makedirs(trusted)
+        untrusted = os.path.join(root, 'untrusted')
+        os.makedirs(untrusted)
         self.roles.append(root)
         return root
 
@@ -284,22 +289,37 @@
         library_path = os.path.dirname(os.path.abspath(
             zuul.ansible.library.__file__))
         for fn in os.listdir(library_path):
-            shutil.copy(os.path.join(library_path, fn), self.library_dir)
-
+            full_path = os.path.join(library_path, fn)
+            if os.path.isdir(full_path):
+                shutil.copytree(full_path, os.path.join(self.library_dir, fn))
+            else:
+                shutil.copy(os.path.join(library_path, fn), self.library_dir)
         action_path = os.path.dirname(os.path.abspath(
-            zuul.ansible.action.__file__))
+                                      zuul.ansible.action.__file__))
         for fn in os.listdir(action_path):
-            shutil.copy(os.path.join(action_path, fn), self.action_dir)
+            full_path = os.path.join(action_path, fn)
+            if os.path.isdir(full_path):
+                shutil.copytree(full_path, os.path.join(self.action_dir, fn))
+            else:
+                shutil.copy(full_path, self.action_dir)
 
         callback_path = os.path.dirname(os.path.abspath(
             zuul.ansible.callback.__file__))
         for fn in os.listdir(callback_path):
-            shutil.copy(os.path.join(callback_path, fn), self.callback_dir)
+            full_path = os.path.join(callback_path, fn)
+            if os.path.isdir(full_path):
+                shutil.copytree(full_path, os.path.join(self.callback_dir, fn))
+            else:
+                shutil.copy(os.path.join(callback_path, fn), self.callback_dir)
 
         lookup_path = os.path.dirname(os.path.abspath(
             zuul.ansible.lookup.__file__))
         for fn in os.listdir(lookup_path):
-            shutil.copy(os.path.join(lookup_path, fn), self.lookup_dir)
+            full_path = os.path.join(lookup_path, fn)
+            if os.path.isdir(full_path):
+                shutil.copytree(full_path, os.path.join(self.lookup_dir, fn))
+            else:
+                shutil.copy(os.path.join(lookup_path, fn), self.lookup_dir)
 
         self.job_workers = {}
 
@@ -601,9 +621,9 @@
             repo.delete_remote(repo.remotes.origin)
 
         # is the playbook in a repo that we have already prepared?
-        self.preparePlaybookRepos(args)
+        trusted, untrusted = self.preparePlaybookRepos(args)
 
-        self.prepareRoles(args)
+        self.prepareRoles(args, trusted, untrusted)
 
         # TODOv3: Ansible the ansible thing here.
         self.prepareAnsibleFiles(args)
@@ -737,15 +757,24 @@
         return None
 
     def preparePlaybookRepos(self, args):
+        trusted = untrusted = False
         for playbook in args['pre_playbooks']:
             jobdir_playbook = self.jobdir.addPrePlaybook()
             self.preparePlaybookRepo(jobdir_playbook, playbook,
                                      args, required=True)
+            if playbook['trusted']:
+                trusted = True
+            else:
+                untrusted = True
 
         for playbook in args['playbooks']:
             jobdir_playbook = self.jobdir.addPlaybook()
             self.preparePlaybookRepo(jobdir_playbook, playbook,
                                      args, required=False)
+            if playbook['trusted']:
+                trusted = True
+            else:
+                untrusted = True
             if jobdir_playbook.path is not None:
                 self.jobdir.playbook = jobdir_playbook
                 break
@@ -756,6 +785,11 @@
             jobdir_playbook = self.jobdir.addPostPlaybook()
             self.preparePlaybookRepo(jobdir_playbook, playbook,
                                      args, required=True)
+            if playbook['trusted']:
+                trusted = True
+            else:
+                untrusted = True
+        return (trusted, untrusted)
 
     def preparePlaybookRepo(self, jobdir_playbook, playbook, args, required):
         self.log.debug("Prepare playbook repo for %s" % (playbook,))
@@ -799,11 +833,11 @@
             required=required,
             trusted=playbook['trusted'])
 
-    def prepareRoles(self, args):
+    def prepareRoles(self, args, trusted, untrusted):
         for role in args['roles']:
             if role['type'] == 'zuul':
                 root = self.jobdir.addRole()
-                self.prepareZuulRole(args, role, root)
+                self.prepareZuulRole(args, role, root, trusted, untrusted)
 
     def findRole(self, path, trusted=False):
         d = os.path.join(path, 'tasks')
@@ -826,17 +860,22 @@
                 self._blockPluginDirs(os.path.join(path, entry))
         return path
 
-    def prepareZuulRole(self, args, role, root):
+    def prepareZuulRole(self, args, role, root, trusted, untrusted):
         self.log.debug("Prepare zuul role for %s" % (role,))
         # Check out the role repo if needed
         source = self.executor_server.connections.getSource(
             role['connection'])
         project = source.getProject(role['project'])
-        role_repo = None
-        if not role['trusted']:
-            # This is a project repo, so it is safe to use the already
-            # checked out version (from speculative merging) of the
-            # role
+        untrusted_role_repo = None
+        trusted_role_repo = None
+        trusted_root = os.path.join(root, 'trusted')
+        untrusted_root = os.path.join(root, 'untrusted')
+        name = role['target_name']
+
+        if untrusted:
+            # There is at least one untrusted playbook.  For that
+            # case, use the already checked out version (from
+            # speculative merging) of the role.
 
             for i in args['items']:
                 if (i['connection'] == role['connection'] and
@@ -847,27 +886,70 @@
                     path = os.path.join(self.jobdir.src_root,
                                         project.canonical_hostname,
                                         project.name)
-                    link = os.path.join(root, role['name'])
+                    # The name of the symlink is the requested name of
+                    # the role (which may be the repo name or may be
+                    # something else; this can come into play if this
+                    # is a bare role).
+                    link = os.path.join(untrusted_root, name)
+                    link = os.path.realpath(link)
+                    if not link.startswith(os.path.realpath(untrusted_root)):
+                        raise Exception("Invalid role name %s", name)
                     os.symlink(path, link)
-                    role_repo = link
+                    untrusted_role_repo = link
                     break
 
-        # The role repo is either a config repo, or it isn't in
-        # the stack of changes we are testing, so check out the branch
-        # tip into a dedicated space.
-
-        if not role_repo:
-            merger = self.executor_server._getMerger(root)
+        if trusted or not untrusted_role_repo:
+            # There is at least one trusted playbook which will need a
+            # trusted checkout of the role, or the role did not appear
+            # in the dependency chain for the change (in which case,
+            # there is no existing untrusted checkout of it).  Check
+            # out the branch tip into a dedicated space.
+            merger = self.executor_server._getMerger(trusted_root)
             merger.checkoutBranch(role['connection'], project.name,
                                   'master')
-            role_repo = os.path.join(root, project.canonical_hostname,
-                                     project.name)
+            orig_repo_path = os.path.join(trusted_root,
+                                          project.canonical_hostname,
+                                          project.name)
+            if name != project.name:
+                # The requested name of the role is not the same as
+                # the project name, so rename the git repo as the
+                # requested name.  It is the only item in this
+                # directory, so we don't need to worry about
+                # collisions.
+                target = os.path.join(trusted_root,
+                                      project.canonical_hostname,
+                                      name)
+                target = os.path.realpath(target)
+                if not target.startswith(os.path.realpath(trusted_root)):
+                    raise Exception("Invalid role name %s", name)
+                os.rename(orig_repo_path, target)
+                trusted_role_repo = target
+            else:
+                trusted_role_repo = orig_repo_path
 
-        role_path = self.findRole(role_repo, trusted=role['trusted'])
-        if role_path is None:
-            # In the case of a bare role, add the containing directory
-            role_path = os.path.join(root, project.canonical_hostname)
-        self.jobdir.roles_path.append(role_path)
+            if not untrusted_role_repo:
+                # In the case that there was no untrusted checkout,
+                # use the trusted checkout.
+                untrusted_role_repo = trusted_role_repo
+                untrusted_root = trusted_root
+
+        if untrusted:
+            untrusted_role_path = self.findRole(untrusted_role_repo,
+                                                trusted=False)
+            if untrusted_role_path is None:
+                # In the case of a bare role, add the containing directory
+                untrusted_role_path = os.path.join(untrusted_root,
+                                                   project.canonical_hostname)
+            self.jobdir.untrusted_roles_path.append(untrusted_role_path)
+
+        if trusted:
+            trusted_role_path = self.findRole(trusted_role_repo,
+                                              trusted=True)
+            if trusted_role_path is None:
+                # In the case of a bare role, add the containing directory
+                trusted_role_path = os.path.join(trusted_root,
+                                                 project.canonical_hostname)
+            self.jobdir.trusted_roles_path.append(trusted_role_path)
 
     def prepareAnsibleFiles(self, args):
         keys = []
@@ -909,9 +991,6 @@
             config.write('gathering = explicit\n')
             config.write('library = %s\n'
                          % self.executor_server.library_dir)
-            if self.jobdir.roles_path:
-                config.write('roles_path = %s\n' %
-                             ':'.join(self.jobdir.roles_path))
             config.write('command_warnings = False\n')
             config.write('callback_plugins = %s\n'
                          % self.executor_server.callback_dir)
@@ -924,6 +1003,12 @@
                              % self.executor_server.action_dir)
                 config.write('lookup_plugins = %s\n'
                              % self.executor_server.lookup_dir)
+                roles_path = self.jobdir.untrusted_roles_path
+            else:
+                roles_path = self.jobdir.trusted_roles_path
+
+            if roles_path:
+                config.write('roles_path = %s\n' % ':'.join(roles_path))
 
             # On trusted jobs, we want to prevent the printing of args,
             # since trusted jobs might have access to secrets that they may
diff --git a/zuul/model.py b/zuul/model.py
index fdccc26..8002f16 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -673,11 +673,10 @@
 class ZuulRole(Role):
     """A reference to an ansible role in a Zuul project."""
 
-    def __init__(self, target_name, connection_name, project_name, trusted):
+    def __init__(self, target_name, connection_name, project_name):
         super(ZuulRole, self).__init__(target_name)
         self.connection_name = connection_name
         self.project_name = project_name
-        self.trusted = trusted
 
     def __repr__(self):
         return '<ZuulRole %s %s>' % (self.project_name, self.target_name)
@@ -687,8 +686,7 @@
             return False
         return (super(ZuulRole, self).__eq__(other) and
                 self.connection_name == other.connection_name,
-                self.project_name == other.project_name,
-                self.trusted == other.trusted)
+                self.project_name == other.project_name)
 
     def toDict(self):
         # Render to a dict to use in passing json to the executor
@@ -696,7 +694,6 @@
         d['type'] = 'zuul'
         d['connection'] = self.connection_name
         d['project'] = self.project_name
-        d['trusted'] = self.trusted
         return d