diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4ea5ad1b401776786ec32f7a9471e4b8a295b6e6..5e54f9cde3536f21be96d5712192eae777ff4b43 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -109,3 +109,4 @@ include:
   - local: .gitlab/ci/releases.gitlab-ci.yml
   - local: .gitlab/ci/notify.gitlab-ci.yml
   - local: .gitlab/ci/dast.gitlab-ci.yml
+  - local: .gitlab/ci/workhorse.gitlab-ci.yml
diff --git a/.gitlab/ci/workhorse.gitlab-ci.yml b/.gitlab/ci/workhorse.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9b52b1f00207c005d5d2f166841f4da0cc96af74
--- /dev/null
+++ b/.gitlab/ci/workhorse.gitlab-ci.yml
@@ -0,0 +1,9 @@
+workhorse:
+  image: golang:1.14
+  stage: test
+  needs: []
+  script:
+    - rm .git/hooks/post-checkout
+    - git checkout .
+    - scripts/update-workhorse check
+    - make -C workhorse
diff --git a/.rubocop.yml b/.rubocop.yml
index 125b2db5cf8211fb1d2c4f3be633413921b94a37..2609b9a1dede78ea1744974bcac21247bea75d39 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -32,6 +32,7 @@ AllCops:
     - 'builds/**/*'
     - 'plugins/**/*'
     - 'file_hooks/**/*'
+    - 'workhorse/**/*'
   CacheRootDirectory: tmp
   MaxFilesInCache: 18000
 
diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile
index 4e17db604710a99ee9d084dbc567834ab876888e..1ac77af473660a862d218b60d1f511ae12d2f920 100644
--- a/danger/commit_messages/Dangerfile
+++ b/danger/commit_messages/Dangerfile
@@ -139,4 +139,12 @@ def warn_or_fail_commits(failed_linters, default_to_fail: true)
   end
 end
 
-lint_commits(git.commits)
+# As part of https://gitlab.com/groups/gitlab-org/-/epics/4826 we are
+# vendoring workhorse commits from the stand-alone gitlab-workhorse
+# repo. There is no point in linting commits that we want to vendor as
+# is.
+def workhorse_changes?
+  git.diff.any? { |file| file.path.start_with?('workhorse/') }
+end
+
+lint_commits(git.commits) unless workhorse_changes?
diff --git a/scripts/update-workhorse b/scripts/update-workhorse
new file mode 100755
index 0000000000000000000000000000000000000000..0955f6a671a0fcac967e35d94f2de8db4a73e264
--- /dev/null
+++ b/scripts/update-workhorse
@@ -0,0 +1,53 @@
+#!/bin/sh
+set -e
+WORKHORSE_DIR=workhorse/
+WORKHORSE_REF="v$(cat GITLAB_WORKHORSE_VERSION)"
+
+if [ $# -gt 1 ] || ([ $# = 1 ] && [ x$1 != xcheck ]); then
+  echo "Usage: update-workhorse [check]"
+  exit 1
+fi
+
+clean="$(git status --porcelain)"
+if [ -n "$clean" ] ; then
+  echo 'error: working directory is not clean:'
+  echo "$clean"
+  exit 1
+fi
+
+git fetch https://gitlab.com/gitlab-org/gitlab-workhorse.git "$WORKHORSE_REF"
+git rm -rf --quiet -- "$WORKHORSE_DIR"
+git read-tree --prefix="$WORKHORSE_DIR" -u FETCH_HEAD
+
+status="$(git status --porcelain)"
+
+if [ x$1 = xcheck ]; then
+  if [ -n "$status" ]; then
+    cat <<MSG
+error: $WORKHORSE_DIR does not match $WORKHORSE_REF
+
+During the transition period of https://gitlab.com/groups/gitlab-org/-/epics/4826,
+the workhorse/ directory in this repository is read-only. To make changes:
+
+1. Submit a MR to https://gitlab.com/gitlab-org/gitlab-workhorse
+2. Once your MR is merged, have a new gitlab-workhorse tag made
+   by a maintainer
+3. Update the GITLAB_WORKHORSE_VERSION file in this repository
+4. Run scripts/update-workhorse to update the workhorse/ directory
+
+MSG
+    exit 1
+  fi
+  exit 0
+fi
+
+if [ -z "$status" ]; then
+  echo "warn: $WORKHORSE_DIR is already up to date, exiting without commit"
+  exit 0
+fi
+
+tree=$(git write-tree)
+msg="Update vendored workhorse to $WORKHORSE_REF"
+commit=$(git commit-tree -p HEAD -p FETCH_HEAD^{commit} -m "$msg" "$tree")
+git update-ref HEAD "$commit"
+git log -1
diff --git a/workhorse/.gitkeep b/workhorse/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391