diff --git a/doc/source/js-roles.rst b/doc/source/js-roles.rst
index 449cd5a06..f9fba0813 100644
--- a/doc/source/js-roles.rst
+++ b/doc/source/js-roles.rst
@@ -1,6 +1,7 @@
 Javascript Roles
 ================
 
+.. zuul:autorole:: ensure-javascript-build-tool
 .. zuul:autorole:: ensure-javascript-packages
 .. zuul:autorole:: ensure-nodejs
 .. zuul:autorole:: ensure-yarn
diff --git a/playbooks/javascript/pre.yaml b/playbooks/javascript/pre.yaml
index c80609e42..dd79fe3d4 100644
--- a/playbooks/javascript/pre.yaml
+++ b/playbooks/javascript/pre.yaml
@@ -1,34 +1,3 @@
 - hosts: all
-  tasks:
-    - name: Set node version if not set
-      set_fact:
-        node_version: '14'
-      when: node_version is not defined
-
-    - name: Check for yarn.lock
-      when: js_build_tool is not defined
-      stat:
-        path: "{{ zuul_work_dir }}/yarn.lock"
-        get_checksum: false
-        get_mime: false
-        get_md5: false
-      register: yarn_lock_exists
-
-    - name: Set js_build_tool fact
-      set_fact:
-        js_build_tool: '{{ yarn_lock_exists.stat.exists | ternary("yarn", "npm") }}'
-      when: js_build_tool is not defined
-
-    - name: Ensure yarn if needed
-      include_role:
-        name: ensure-yarn
-      when: js_build_tool == 'yarn'
-
-    - name: Ensure nodejs if needed
-      include_role:
-        name: ensure-nodejs
-      when: js_build_tool == 'npm'
-
-    - name: Install javascript depends
-      include_role:
-        name: ensure-javascript-packages
+  roles:
+    - ensure-javascript-build-tool
diff --git a/roles/ensure-javascript-build-tool/README.rst b/roles/ensure-javascript-build-tool/README.rst
new file mode 100644
index 000000000..ee26bfe09
--- /dev/null
+++ b/roles/ensure-javascript-build-tool/README.rst
@@ -0,0 +1,14 @@
+Install javascript build tool needed for a project
+
+**Role Variables**
+
+.. zuul:rolevar:: js_build_tool
+   :default: autodetected
+
+   What command to use. If the ``zuul_work_dir`` has a ``yarn.lock``
+   file, it will default to ``yarn``, otherwise ``npm``.
+
+.. zuul:rolevar:: zuul_work_dir
+   :default: {{ zuul.project.src_dir }}
+
+   The directory to work in.
diff --git a/roles/ensure-javascript-build-tool/defaults/main.yaml b/roles/ensure-javascript-build-tool/defaults/main.yaml
new file mode 100644
index 000000000..237b94895
--- /dev/null
+++ b/roles/ensure-javascript-build-tool/defaults/main.yaml
@@ -0,0 +1,2 @@
+node_version: '14'
+zuul_work_dir: "{{ zuul.project.src_dir }}"
diff --git a/roles/ensure-javascript-build-tool/tasks/main.yaml b/roles/ensure-javascript-build-tool/tasks/main.yaml
new file mode 100644
index 000000000..9a744e98f
--- /dev/null
+++ b/roles/ensure-javascript-build-tool/tasks/main.yaml
@@ -0,0 +1,24 @@
+- name: Check for yarn.lock
+  when: js_build_tool is not defined
+  stat:
+    path: "{{ zuul_work_dir }}/yarn.lock"
+    get_checksum: false
+    get_mime: false
+    get_md5: false
+  register: yarn_lock_exists
+
+- name: Set js_build_tool fact
+  set_fact:
+    js_build_tool: '{{ yarn_lock_exists.stat.exists | ternary("yarn", "npm") }}'
+    cacheable: true
+  when: js_build_tool is not defined
+
+- name: Ensure yarn if needed
+  include_role:
+    name: ensure-yarn
+  when: js_build_tool == 'yarn'
+
+- name: Ensure nodejs if needed
+  include_role:
+    name: ensure-nodejs
+  when: js_build_tool == 'npm'
diff --git a/roles/js-package-manager/tasks/main.yaml b/roles/js-package-manager/tasks/main.yaml
index 65e82b722..dba2ffbbe 100644
--- a/roles/js-package-manager/tasks/main.yaml
+++ b/roles/js-package-manager/tasks/main.yaml
@@ -28,6 +28,10 @@
     js_build_tool: '{{ yarn_lock_exists.stat.exists | ternary("yarn", "npm") }}'
   when: js_build_tool is not defined
 
+- name: Install javascript depends
+  include_role:
+    name: ensure-javascript-packages
+
 - name: Run js build command
   command: "{{ js_build_tool }} {% if js_build_command not in npm_lifecycle_phases %} run {% endif %} {{ js_build_command }} --verbose"
   # Need to set DISPLAY to the value that will be set when the virtual