Initialize config repository
diff --git a/playbooks/base/README b/playbooks/base/README
new file mode 100644
index 0000000..4450121
--- /dev/null
+++ b/playbooks/base/README
@@ -0,0 +1 @@
+Base job playbooks
diff --git a/playbooks/base/post.yaml b/playbooks/base/post.yaml
new file mode 100644
index 0000000..6bbf3d9
--- /dev/null
+++ b/playbooks/base/post.yaml
@@ -0,0 +1,40 @@
+# This file is managed by ansible, do not edit directly
+---
+- hosts: all
+  tasks:
+    - block:
+        - include_role: name=fetch-output
+      when:
+        - "ansible_connection != 'kubectl'"
+        - ansible_user_dir is defined
+    - block:
+        - include_role: name=fetch-output-openshift
+      when:
+        - "ansible_connection == 'kubectl'"
+        - ansible_user_dir is defined
+    - import_role: name=merge-output-to-logs
+      when: ansible_user_dir is defined
+
+- hosts: localhost
+  roles:
+    - role: add-fileserver
+      fileserver: "{{ site_sflogs }}"
+    - role: generate-zuul-manifest
+    - role: ara-report
+      # This depends-on https://review.openstack.org/577675
+      ara_report_run: True
+      ara_report_type: database
+      ara_report_path: "{{ zuul.executor.log_root }}/ara-report"
+
+- hosts: "spfactory.storpool.com"
+  gather_facts: false
+  tasks:
+    # Use a block because play vars doesn't take precedence on roles vars
+    - block:
+        - import_role: name=upload-logs
+        - import_role: name=buildset-artifacts-location
+      vars:
+        zuul_log_compress: true
+        zuul_log_url: "https://spfactory.storpool.com/logs"
+        zuul_logserver_root: "{{ site_sflogs.path }}"
+
diff --git a/playbooks/base/pre.yaml b/playbooks/base/pre.yaml
new file mode 100644
index 0000000..91d29c8
--- /dev/null
+++ b/playbooks/base/pre.yaml
@@ -0,0 +1,26 @@
+# This file is managed by ansible, do not edit directly
+---
+- hosts: localhost
+  tasks:
+    - block:
+        - import_role: name=emit-job-header
+        # This depends-on https://review.openstack.org/578234
+        - import_role: name=log-inventory
+      vars:
+        zuul_log_url: "https://spfactory.storpool.com/logs"
+
+- hosts: all
+  tasks:
+    - include_role: name=start-zuul-console
+    - block:
+        - include_role: name=validate-host
+        - include_role: name=prepare-workspace
+        - include_role: name=add-build-sshkey
+      when: "ansible_connection != 'kubectl'"
+    - block:
+        - include_role: name=prepare-workspace-openshift
+        - include_role: name=remove-zuul-sshkey
+      run_once: true
+      when: "ansible_connection == 'kubectl'"
+    - import_role: name=ensure-output-dirs
+      when: ansible_user_dir is defined
diff --git a/playbooks/config/check-fetch-artifacts.yaml b/playbooks/config/check-fetch-artifacts.yaml
new file mode 100644
index 0000000..288f4e1
--- /dev/null
+++ b/playbooks/config/check-fetch-artifacts.yaml
@@ -0,0 +1,14 @@
+# This file is managed by ansible, do not edit directly
+---
+- hosts: localhost
+  tasks:
+    - name: Set speculative config path
+      set_fact:
+        config_root: "{{ zuul.executor.src_root }}/{{ zuul.project.canonical_name }}"
+
+    - name: Fetch artifacts
+      synchronize:
+        src: "{{ config_root }}/build"
+        dest: "{{ zuul.executor.log_root }}/logs"
+        mode: pull
+      no_log: True
diff --git a/playbooks/config/check.yaml b/playbooks/config/check.yaml
new file mode 100644
index 0000000..53e983d
--- /dev/null
+++ b/playbooks/config/check.yaml
@@ -0,0 +1,192 @@
+# This file is managed by ansible, do not edit directly
+---
+- hosts: localhost
+  tasks:
+    - name: Set speculative config path
+      set_fact:
+        config_root: "{{ zuul.executor.src_root }}/{{ zuul.project.canonical_name }}"
+
+    - name: Fetch default config
+      get_url:
+        url: "{{ gateway_url }}/_defconf.tgz"
+        dest: "{{ config_root }}/"
+      retries: 30
+      delay: 1
+
+    - name: Create defconf directory
+      file:
+        path: "{{ config_root }}/defconf"
+        state: directory
+
+    - name: Extract default config
+      unarchive:
+        src: "{{ config_root }}/_defconf.tgz"
+        dest: "{{ config_root }}/defconf/"
+
+    - name: include arch.yaml
+      include_vars:
+        file: "{{ config_root }}/defconf/arch.yaml"
+        name: arch
+
+    - name: Create build directory to merge configuration
+      file:
+        path: "{{ config_root }}/build"
+        state: directory
+
+    - name: Tenant env config-check preparation
+      block:
+        - name: Create defconf-master directory
+          file:
+            path: "{{ config_root }}/defconf-master"
+            state: directory
+
+        - name: Fetch master SF default config
+          get_url:
+            url: "{{ master_sf_url }}/_defconf.tgz"
+            dest: "{{ config_root }}/_defconf-master.tgz"
+          retries: 30
+          delay: 1
+
+        - name: Extract master SF default config
+          unarchive:
+            src: "{{ config_root }}/_defconf-master.tgz"
+            dest: "{{ config_root }}/defconf-master/"
+
+        - name: Overwrite with master SF fake zuul.conf
+          copy:
+            remote_src: true
+            src: "{{ config_root }}/defconf-master/defconf-zuul.conf"
+            dest: "{{ config_root }}/defconf/defconf-zuul.conf"
+
+        - set_fact:
+            tenant_options: "--tenant --master-sf-url {{ master_sf_url }}"
+      when: tenant_config is defined and tenant_config
+
+    - name: Copy service_user password in workspace
+      copy:
+        content: "{{ service_user.password }}"
+        dest: "{{ config_root }}/.service_user_password"
+      no_log: true
+
+    - name: Check resources changes
+      shell: managesf-resources remote-validate --remote-gateway {{ gateway_url }}
+      args:
+        chdir: "{{ config_root }}"
+
+    - name: Check gerrit replication
+      command: git config -f gerrit/replication.config -l
+      args:
+        chdir: "{{ config_root }}"
+      when: '"gerrit" in arch.roles'
+
+    - name: Check gerrit commentlinks
+      command: python3 -c "import yaml; 'commentlinks' in yaml.safe_load(open('gerrit/commentlinks.yaml'))"
+      args:
+        chdir: "{{ config_root }}"
+      when: '"gerrit" in arch.roles'
+
+    - name: Check policy file
+      command: python3 -c "import yaml; yaml.safe_load(open('policies/policy.yaml'))"
+      args:
+        chdir: "{{ config_root }}"
+
+    - name: Check nodepool dhall configuration
+      shell: |
+        for dhall_conf in $(ls nodepool/static_config/*.dhall 2> /dev/null); do
+          echo ${dhall_conf}
+          dhall-to-yaml --file ${dhall_conf} --output build/$(basename ${dhall_conf} .dhall).yaml || exit 1
+        done
+      args:
+        chdir: "{{ config_root }}"
+
+    - name: Validate nodepool configuration
+      block:
+        - name: Install defconf nodepool.yaml
+          copy:
+            remote_src: true
+            src: "{{ config_root }}/defconf/defconf-nodepool.yaml"
+            dest: "{{ config_root }}/nodepool/_nodepool.yaml"
+
+        - name: Check all launcher-hosts exists (task fail with invalid hostname on stdout)
+          shell: >
+            find {{ config_root }}/nodepool/ -name "*.yaml" | xargs grep '^ *launcher-host: ' | awk '{ print $3 }' |
+            grep -v '^\({{ arch.launcher_hosts | join('\|') }}\)$'
+          register: _unknown_launcher_hosts
+          failed_when: _unknown_launcher_hosts.stdout
+          changed_when: false
+
+        - name: Generate per launcher-hosts configuration
+          block:
+            - name: Generate configuration
+              command: >
+                managesf-configuration nodepool
+                --cache-dir {{ config_root }}/../.cache
+                {% if item != arch.launcher_hosts[0] %}--extra-launcher {% endif %}
+                --hostname {{ item }}
+                --config-dir {{ config_root }} --output build/nodepool-{{ item }}.yaml
+              args:
+                chdir: "{{ config_root }}"
+              loop: "{{ arch.launcher_hosts }}"
+
+            - name: Run nodepool config-validate for nodepool-launchers
+              command: >
+                nodepool -c build/nodepool-{{ item }}.yaml config-validate
+              args:
+                chdir: "{{ config_root }}"
+              loop: "{{ arch.launcher_hosts }}"
+          when: arch.launcher_hosts
+
+        - name: Merge nodepool-builder config repo files
+          command: >
+            managesf-configuration nodepool
+            --cache-dir {{ config_root }}/../.cache --builder
+            --config-dir {{ config_root }} --output build/nodepool-builder.yaml
+          args:
+            chdir: "{{ config_root }}"
+
+        - name: Run nodepool config-validate for nodepool-builder
+          command: >
+            nodepool -c build/nodepool-builder.yaml config-validate
+          args:
+            chdir: "{{ config_root }}"
+
+        - name: Run nodepool config-validate for static configuration
+          command: >
+            find nodepool/static_config/ -name '*.yaml' -exec nodepool -c {} config-validate \;
+          args:
+            chdir: "{{ config_root }}"
+      when:
+        - '"nodepool-launcher" in arch.roles'
+
+    - name: Validate zuul configuration
+      block:
+        - name: Install fake zuul.conf
+          copy:
+            remote_src: true
+            src: "{{ config_root }}/defconf/defconf-zuul.conf"
+            dest: "{{ config_root }}/build/zuul.conf"
+
+        - name: Merge zuul tenant config
+          command: >
+            managesf-configuration zuul
+            --cache-dir {{ config_root }}/../.cache
+            --config-dir {{ config_root }}
+            --gateway-url {{ gateway_url }} {{ tenant_options | default('') }}
+            --output build/main.yaml
+          args:
+            chdir: "{{ config_root }}"
+
+        - name: Validate zuul config syntax
+          command: >
+            env - /usr/local/bin/zuul -c zuul.conf tenant-conf-check
+          args:
+            chdir: "{{ config_root }}/build"
+
+    - name: Validate metrics dashboards
+      block:
+        - name: Check syntax errors in metrics dashboards
+          shell: |
+           find . -regextype posix-egrep -regex '.*.(yaml|yml)$' | xargs -I yaml grafana-dashboard validate yaml
+          args:
+            chdir: "{{ config_root }}/metrics"
+      when: '"grafana" in arch.roles'
diff --git a/playbooks/config/config-update.yaml b/playbooks/config/config-update.yaml
new file mode 100644
index 0000000..86b4e19
--- /dev/null
+++ b/playbooks/config/config-update.yaml
@@ -0,0 +1,5 @@
+# This file is managed by ansible, do not edit directly
+---
+- hosts: localhost
+  tasks:
+    - include_tasks: update_local.yaml
diff --git a/playbooks/config/update_local.yaml b/playbooks/config/update_local.yaml
new file mode 100644
index 0000000..a334e85
--- /dev/null
+++ b/playbooks/config/update_local.yaml
@@ -0,0 +1,26 @@
+# This file is managed by ansible, do not edit directly
+---
+- name: Create SSH private key tempfile
+  tempfile:
+    state: file
+  register: ssh_private_key_tmp
+
+- name: Create SSH private key from secret
+  copy:
+    content: "{{ site_install_server.ssh_private_key }}"
+    dest: "{{ ssh_private_key_tmp.path }}"
+    mode: '0600'
+
+- name: Add zuul ssh key
+  command: "ssh-add {{ ssh_private_key_tmp.path }}"
+
+- name: Remove SSH private key from disk
+  command: "shred {{ ssh_private_key_tmp.path }}"
+
+- name: Add site_install_server server to known hosts
+  known_hosts:
+    name: "{{ site_install_server.fqdn }}"
+    key: "{{ site_install_server.ssh_known_hosts }}"
+
+- name: run config update
+  command: "ssh root@{{ site_install_server.fqdn }} config_update {{ (zuul | zuul_legacy_vars)['ZUUL_NEWREV'] }}"
diff --git a/playbooks/config/update_tenant.yaml b/playbooks/config/update_tenant.yaml
new file mode 100644
index 0000000..9d8c51f
--- /dev/null
+++ b/playbooks/config/update_tenant.yaml
@@ -0,0 +1,40 @@
+# This file is managed by ansible, do not edit directly
+---
+- name: Discover path of config repository
+  command: git rev-parse --show-toplevel
+  register: config_path
+
+- name: Get last change sha
+  command: "git --git-dir={{ config_path.stdout }}/.git log -n1 --pretty=format:'%h' --no-merges"
+  register: git_log
+
+- name: Get last change on resources sha
+  command: "git --git-dir={{ config_path.stdout }}/.git log -n1 --pretty=format:'%h' --no-merges -- resources zuul"
+  register: git_log_resources
+
+- block:
+    - name: Create SSH private key tempfile
+      tempfile:
+        state: file
+      register: ssh_private_key_tmp
+
+    - name: Create SSH private key from secret
+      copy:
+        content: "{{ site_tenant_update.ssh_private_key }}"
+        dest: "{{ ssh_private_key_tmp.path }}"
+        mode: '0600'
+
+    - name: Add zuul ssh key
+      command: "ssh-add {{ ssh_private_key_tmp.path }}"
+
+    - name: Remove SSH private key from disk
+      command: "shred {{ ssh_private_key_tmp.path }}"
+
+    - name: Add site_tenant_update server to known hosts
+      known_hosts:
+        name: "{{ site_tenant_update.fqdn }}"
+        key: "{{ site_tenant_update.ssh_known_hosts }}"
+
+    - name: Run tenant_update
+      command: "ssh root@{{ site_tenant_update.fqdn }} tenant_update"
+  when: git_log_resources.stdout == git_log.stdout
diff --git a/playbooks/openshift/build-project.yaml b/playbooks/openshift/build-project.yaml
new file mode 100644
index 0000000..957c957
--- /dev/null
+++ b/playbooks/openshift/build-project.yaml
@@ -0,0 +1,84 @@
+# This file is managed by ansible, do not edit directly
+---
+- name: prepare dumb bare clone of future state
+  git:
+    repo: "{{ zuul.executor.work_root }}/{{ zuul.project.src_dir }}"
+    dest: "{{ zuul.executor.work_root }}/{{ zuul.project.src_dir }}.git"
+    bare: yes
+
+- name: update server info for dumb http transport
+  command: git update-server-info
+  args:
+    chdir: "{{ zuul.executor.work_root }}/{{ zuul.project.src_dir }}.git"
+
+- name: create project dir on http server
+  command: >
+    {{ oc_command }} exec {{ zm_name }} -- mkdir -p {{ zuul.project.src_dir }}.git
+
+- name: copy project to http server
+  command: >
+    {{ oc_command }} rsync -q --progress=false
+      {{ zuul.executor.work_root }}/{{ zuul.project.src_dir }}.git/
+      {{ zm_name }}:/opt/app-root/src/{{ zuul.project.src_dir }}.git/
+  no_log: true
+
+- name: create project ImageStream spec
+  openshift_raw:
+    state: present
+    namespace: "{{ zuul.resources['project'].namespace }}"
+    context: "{{ zuul.resources['project'].context }}"
+    definition:
+      apiVersion: v1
+      kind: ImageStream
+      metadata:
+        generation: 1
+        labels:
+          app: "{{ zuul.project.short_name }}"
+        name: "{{ zuul.project.short_name }}"
+      spec:
+        lookupPolicy:
+          local: false
+  register: _image_stream
+
+- name: create project BuildConfig spec
+  openshift_raw:
+    state: present
+    namespace: "{{ zuul.resources['project'].namespace }}"
+    context: "{{ zuul.resources['project'].context }}"
+    definition:
+      apiVersion: v1
+      kind: BuildConfig
+      metadata:
+        labels:
+          app: "{{ zuul.project.short_name }}"
+        name: "{{ zuul.project.short_name }}"
+      spec:
+        output:
+          to:
+            kind: ImageStreamTag
+            name: '{{ zuul.project.short_name }}:latest'
+        runPolicy: Serial
+        source:
+          git:
+            ref: master
+            uri: 'http://staging-http-server:8080/{{ zuul.project.src_dir }}.git'
+          type: Git
+        strategy:
+          sourceStrategy:
+            from:
+              kind: ImageStreamTag
+              name: '{{ base_image }}'
+              namespace: openshift
+          type: Source
+        triggers:
+          - type: ImageChange
+          - type: ConfigChange
+
+- name: wait for project image built
+  command: >
+    {{ oc_command }} get builds
+      -o "jsonpath={.items[?(@.metadata.labels.buildconfig!='staging-http-server')].status.phase}"
+  register: _project_build
+  retries: 600
+  delay: 1
+  until: "'Complete' in _project_build.stdout"
diff --git a/playbooks/openshift/deploy-project.yaml b/playbooks/openshift/deploy-project.yaml
new file mode 100644
index 0000000..bc1b63a
--- /dev/null
+++ b/playbooks/openshift/deploy-project.yaml
@@ -0,0 +1,66 @@
+# This file is managed by ansible, do not edit directly
+---
+- name: start the project
+  openshift_raw:
+    state: present
+    namespace: "{{ zuul.resources['project'].namespace }}"
+    context: "{{ zuul.resources['project'].context }}"
+    definition:
+      apiVersion: v1
+      kind: DeploymentConfig
+      metadata:
+        generation: 2
+        labels:
+          app: "{{ zuul.project.short_name }}"
+        name: "{{ zuul.project.short_name }}"
+      spec:
+        replicas: 1
+        selector:
+          deploymentconfig: "{{ zuul.project.short_name }}"
+        strategy:
+          resources: {}
+          type: Rolling
+        template:
+          metadata:
+            labels:
+              app: "{{ zuul.project.short_name }}"
+              deploymentconfig: "{{ zuul.project.short_name }}"
+          spec:
+            containers:
+              - image: "{{ _image_stream.result.status.dockerImageRepository }}"
+                name: "{{ zuul.project.short_name }}"
+                command: [ "/bin/bash", "-c", "--" ]
+                args: [ "while true; do sleep 30; done;" ]
+                ports:
+                  - containerPort: 8080
+                    protocol: TCP
+                  - containerPort: 8443
+                    protocol: TCP
+                resources: {}
+            dnsPolicy: ClusterFirst
+            restartPolicy: Always
+            schedulerName: default-scheduler
+            securityContext: {}
+            terminationGracePeriodSeconds: 30
+        test: false
+
+- name: get project pod name
+  command: >
+    {{ oc_command }} get pods --field-selector=status.phase=Running
+    -o "jsonpath={.items[?(@.metadata.labels.app=='{{ zuul.project.short_name }}')].metadata.name}"
+  register: _pod_name
+  retries: 600
+  delay: 1
+  until: "zuul.project.short_name in _pod_name.stdout"
+
+- name: create pods list
+  set_fact:
+    pods_data:
+      pods:
+        - name: "{{ zuul.project.short_name }}"
+          pod: "{{ _pod_name.stdout }}"
+
+- name: store pods list in work_root
+  copy:
+    content: "{{ pods_data | to_yaml }}"
+    dest: "{{ zuul.executor.work_root }}/pods.yaml"
diff --git a/playbooks/openshift/pre.yaml b/playbooks/openshift/pre.yaml
new file mode 100644
index 0000000..44fff25
--- /dev/null
+++ b/playbooks/openshift/pre.yaml
@@ -0,0 +1,34 @@
+---
+- hosts: localhost
+  tasks:
+    - block:
+        - import_role: name=emit-job-header
+        # We need those tasks to use log-inventory, see: https://review.openstack.org/577674
+        - name: Define zuul_info_dir fact
+          set_fact:
+            zuul_info_dir: "{{ zuul.executor.log_root }}/zuul-info"
+
+        - name: Ensure Zuul Ansible directory exists
+          delegate_to: localhost
+          run_once: true
+          file:
+            path: "{{ zuul_info_dir }}"
+            state: directory
+
+        - name: Define inventory_file fact
+          set_fact:
+            inventory_file: "/tmp/{{ zuul.build }}/ansible/inventory.yaml"
+
+        - import_role: name=log-inventory
+      vars:
+        zuul_log_url: "https://spfactory.storpool.com/logs"
+
+    - name: Set oc_command fact
+      set_fact:
+        oc_command: >
+          oc --context "{{ zuul.resources['project'].context }}"
+             --namespace "{{ zuul.resources['project'].namespace }}"
+
+    - include_tasks: prepare-namespace.yaml
+    - include_tasks: build-project.yaml
+    - include_tasks: deploy-project.yaml
diff --git a/playbooks/openshift/prepare-namespace.yaml b/playbooks/openshift/prepare-namespace.yaml
new file mode 100644
index 0000000..d583469
--- /dev/null
+++ b/playbooks/openshift/prepare-namespace.yaml
@@ -0,0 +1,80 @@
+# This file is managed by ansible, do not edit directly
+---
+- name: create staging-http DeploymentConfig
+  openshift_raw:
+    state: present
+    namespace: "{{ zuul.resources['project'].namespace }}"
+    context: "{{ zuul.resources['project'].context }}"
+    definition:
+      apiVersion: v1
+      kind: DeploymentConfig
+      metadata:
+        generation: 2
+        labels:
+          app: staging-http-server
+        name: staging-http-server
+      spec:
+        replicas: 1
+        selector:
+          deploymentconfig: staging-http-server
+        strategy:
+          resources: {}
+          type: Rolling
+        template:
+          metadata:
+            labels:
+              app: staging-http-server
+              deploymentconfig: staging-http-server
+          spec:
+            containers:
+              - image: "docker.io/softwarefactoryproject/staging-http-server"
+                # imagePullPolicy: Always
+                name: staging-http-server
+                ports:
+                  - containerPort: 8080
+                    protocol: TCP
+                  - containerPort: 8443
+                    protocol: TCP
+                resources: {}
+            dnsPolicy: ClusterFirst
+            restartPolicy: Always
+            schedulerName: default-scheduler
+            terminationGracePeriodSeconds: 30
+
+- name: create staging-http Service spec
+  openshift_raw:
+    state: present
+    namespace: "{{ zuul.resources['project'].namespace }}"
+    context: "{{ zuul.resources['project'].context }}"
+    definition:
+      apiVersion: v1
+      kind: Service
+      metadata:
+        labels:
+          app: staging-http-server
+        name: staging-http-server
+      spec:
+        ports:
+          - name: 8080-tcp
+            port: 8080
+            protocol: TCP
+            targetPort: 8080
+        selector:
+          deploymentconfig: staging-http-server
+        sessionAffinity: None
+        type: ClusterIP
+      status:
+        loadBalancer: {}
+
+- name: get staging-http-server pod name
+  command: >
+    {{ oc_command }} get pods --field-selector=status.phase=Running
+    -o "jsonpath={.items[?(@.metadata.labels.app=='staging-http-server')].metadata.name}"
+  register: _zm_name
+  retries: 600
+  delay: 1
+  until: "'staging-http' in _zm_name.stdout"
+
+- name: register staging-http-server pod name
+  set_fact:
+    zm_name: "{{ _zm_name.stdout }}"
diff --git a/playbooks/openshift/unprivileged-machine.yaml b/playbooks/openshift/unprivileged-machine.yaml
new file mode 100644
index 0000000..431b844
--- /dev/null
+++ b/playbooks/openshift/unprivileged-machine.yaml
@@ -0,0 +1,39 @@
+---
+- hosts: localhost
+  tasks:
+    - block:
+        - import_role: name=emit-job-header
+        # We need those tasks to use log-inventory, see: https://review.openstack.org/577674
+        - name: Define zuul_info_dir fact
+          set_fact:
+            zuul_info_dir: "{{ zuul.executor.log_root }}/zuul-info"
+
+        - name: Ensure Zuul Ansible directory exists
+          delegate_to: localhost
+          run_once: true
+          file:
+            path: "{{ zuul_info_dir }}"
+            state: directory
+
+        - name: Define inventory_file fact
+          set_fact:
+            inventory_file: "/tmp/{{ zuul.build }}/ansible/inventory.yaml"
+
+        - import_role: name=log-inventory
+      vars:
+        zuul_log_url: "https://spfactory.storpool.com/logs"
+
+    - name: Create src directory
+      command: >
+        oc --context "{{ zuul.resources['pod'].context }}"
+           --namespace "{{ zuul.resources['pod'].namespace }}"
+           exec {{ zuul.resources['pod'].pod }} mkdir src
+
+    - name: Copy src repos to the pod
+      command: >
+        oc --context "{{ zuul.resources['pod'].context }}"
+           --namespace "{{ zuul.resources['pod'].namespace }}"
+        rsync -q --progress=false
+          {{ zuul.executor.src_root }}/
+          {{ zuul.resources['pod'].pod }}:src/
+      no_log: true
diff --git a/playbooks/pages/build.yaml b/playbooks/pages/build.yaml
new file mode 100644
index 0000000..b44b7e1
--- /dev/null
+++ b/playbooks/pages/build.yaml
@@ -0,0 +1,5 @@
+# This file is managed by ansible, do not edit directly
+---
+- hosts: all
+  roles:
+    - role: build-pages
diff --git a/playbooks/pages/publish.yaml b/playbooks/pages/publish.yaml
new file mode 100644
index 0000000..602a609
--- /dev/null
+++ b/playbooks/pages/publish.yaml
@@ -0,0 +1,25 @@
+# This file is managed by ansible, do not edit directly
+---
+- hosts: all
+  tasks:
+    - block:
+        - include_role: name=fetch-output
+      when:
+        - "ansible_connection != 'kubectl'"
+        - ansible_user_dir is defined
+    - block:
+        - include_role: name=fetch-output-openshift
+      when:
+        - "ansible_connection == 'kubectl'"
+        - ansible_user_dir is defined
+    - import_role: name=merge-output-to-logs
+      when: ansible_user_dir is defined
+
+- hosts: localhost
+  roles:
+    - role: add-fileserver
+      fileserver: "{{ site_pages }}"
+
+- hosts: "{{ site_pages.fqdn }}"
+  roles:
+    - role: upload-pages
diff --git a/playbooks/wait-for-changes-ahead.yaml b/playbooks/wait-for-changes-ahead.yaml
new file mode 100644
index 0000000..d78a109
--- /dev/null
+++ b/playbooks/wait-for-changes-ahead.yaml
@@ -0,0 +1,4 @@
+---
+- hosts: localhost
+  roles:
+    - wait-for-changes-ahead