diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bc305a18156b193e47309f3e2d65c79491d49bc1..0ef5d8305488090ed82992a1990379018119eb26 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,6 +5,7 @@ stages:
   - build-images
   - fixtures
   - lint
+  - test-frontend
   - test
   - post-test
   - review
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 5cc3c9984091ff5f6e9ec64cb31ac6083a1c5c37..e04e61fd921ec1cb86cfe29a845377d6ff07accb 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -1,3 +1,11 @@
+.with-fixtures-needs:
+  needs:
+    - "rspec-all frontend_fixture"
+
+.with-graphql-schema-dump-needs:
+  needs:
+    - "graphql-schema-dump"
+
 .compile-assets-base:
   extends:
     - .default-retry
@@ -83,7 +91,6 @@ update-assets-compile-production-cache:
     - .update-cache-base
     - .assets-compile-cache-push
     - .shared:rules:update-cache
-  stage: prepare
   artifacts: {}  # This job's purpose is only to update the cache.
 
 update-assets-compile-test-cache:
@@ -92,7 +99,6 @@ update-assets-compile-test-cache:
     - .update-cache-base
     - .assets-compile-cache-push
     - .shared:rules:update-cache
-  stage: prepare
   artifacts: {}  # This job's purpose is only to update the cache.
 
 update-storybook-yarn-cache:
@@ -136,8 +142,14 @@ retrieve-frontend-fixtures:
     - .default-before_script
     - .ruby-cache
     - .use-pg14
+    - .repo-from-artifacts
   stage: fixtures
-  needs: ["setup-test-env", "retrieve-tests-metadata", "retrieve-frontend-fixtures"]
+  needs:
+    - "setup-test-env"
+    - "retrieve-tests-metadata"
+    - "retrieve-frontend-fixtures"
+    # it's ok to wait for the repo artifact as we're waiting for setup-test-env (which takes longer than clone-gitlab-repo) anyway
+    - !reference [.repo-from-artifacts, needs]
   variables:
     # Don't add `CRYSTALBALL: "false"` here as we're enabling Crystalball for scheduled pipelines (in `.gitlab-ci.yml`), so that we get coverage data
     # for the `frontend fixture RSpec files` that will be added to the Crystalball mapping in `update-tests-metadata`.
@@ -178,18 +190,21 @@ rspec-all frontend_fixture:
 # Uploads FOSS fixtures in the FOSS project.
 upload-frontend-fixtures:
   extends:
-    - .frontend-fixtures-base
+    - .default-retry
+    - .default-before_script
+    - .repo-from-artifacts
     - .frontend:rules:upload-frontend-fixtures
   stage: fixtures
-  needs: ["rspec-all frontend_fixture"]
+  needs:
+    # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway
+    - !reference [.repo-from-artifacts, needs]
+    - !reference [.with-fixtures-needs, needs]
   script:
-    - source scripts/utils.sh
     - source scripts/gitlab_component_helpers.sh
     - export_fixtures_sha_for_upload
     - 'fixtures_archive_doesnt_exist || { echoinfo "INFO: Exiting early as package exists."; exit 0; }'
     - run_timed_command "create_fixtures_package"
     - run_timed_command "upload_fixtures_package"
-  artifacts: {}
 
 graphql-schema-dump:
   variables:
@@ -223,18 +238,7 @@ graphql-schema-dump:
   before_script:
     - !reference [.default-before_script, before_script]
     - yarn_install_script
-  stage: test
-
-.vue3:
-  variables:
-    VUE_VERSION: 3
-    NODE_OPTIONS: --max-old-space-size=7680
-  allow_failure: true
-
-.jest-base:
-  extends: .frontend-test-base
-  script:
-    - run_timed_command "yarn jest:ci:without-fixtures"
+  stage: test-frontend
 
 jest-build-cache:
   extends:
@@ -258,18 +262,28 @@ jest-build-cache:
     # they exit with 1, so as not to break master and other pipelines.
     exit_codes: 1
 
+.vue3:
+  variables:
+    VUE_VERSION: 3
+    NODE_OPTIONS: --max-old-space-size=7680
+  allow_failure: true
+
+.with-jest-build-cache-vue3-needs:
+  needs:
+    - job: jest-build-cache-vue3
+      optional: true
+
 jest-build-cache-vue3:
   extends:
     - jest-build-cache
     - .frontend:rules:jest-vue3
     - .vue3
 
-jest-with-fixtures:
+jest:
   extends:
-    - .jest-base
+    - .frontend-test-base
     - .frontend:rules:jest
   needs:
-    - "rspec-all frontend_fixture"
     - job: jest-build-cache
       optional: true
   artifacts:
@@ -282,47 +296,42 @@ jest-with-fixtures:
       - tmp/tests/frontend/
     reports:
       junit: junit_jest.xml
-  parallel: 2
+  parallel: 11
   script:
-    - run_timed_command "yarn jest:ci:with-fixtures"
+    - run_timed_command "yarn jest:ci:without-fixtures"
 
-jest:
+jest-with-fixtures:
   extends:
-    - .jest-base
+    - jest
+    - .repo-from-artifacts
     - .frontend:rules:jest
   needs:
-    - job: jest-build-cache
-      optional: true
-  artifacts:
-    name: coverage-frontend
-    expire_in: 31d
-    when: always
-    paths:
-      - coverage-frontend/
-      - junit_jest.xml
-      - tmp/tests/frontend/
-    reports:
-      junit: junit_jest.xml
-  parallel: 11
+    - !reference [jest, needs]
+    # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway
+    - !reference [.repo-from-artifacts, needs]
+    - !reference [.with-fixtures-needs, needs]
+  parallel: 2
+  script:
+    - run_timed_command "yarn jest:ci:with-fixtures"
 
-jest-with-fixtures vue3:
+jest vue3:
   extends:
-    - jest-with-fixtures
+    - jest
     - .frontend:rules:jest-vue3
     - .vue3
   needs:
-    - "rspec-all frontend_fixture"
-    - job: jest-build-cache-vue3
-      optional: true
+    - !reference [.with-jest-build-cache-vue3-needs, needs]
 
-jest vue3:
+jest-with-fixtures vue3:
   extends:
-    - jest
+    - jest-with-fixtures
     - .frontend:rules:jest-vue3
     - .vue3
   needs:
-    - job: jest-build-cache-vue3
-      optional: true
+    - !reference ["jest vue3", needs]
+    # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway
+    - !reference [.repo-from-artifacts, needs]
+    - !reference [.with-fixtures-needs, needs]
 
 jest predictive:
   extends:
@@ -347,16 +356,25 @@ jest-with-fixtures predictive:
 jest-integration:
   extends:
     - .frontend-test-base
+    - .repo-from-artifacts
     - .frontend:rules:jest-integration
   script:
     - run_timed_command "yarn jest:integration --ci"
-  needs: ["rspec-all frontend_fixture", "graphql-schema-dump"]
+  needs:
+    # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway
+    - !reference [.repo-from-artifacts, needs]
+    - !reference [.with-fixtures-needs, needs]
+    - !reference [.with-graphql-schema-dump-needs, needs]
 
 jest-snapshot-vue3:
   extends:
-    - .jest-base
+    - .frontend-test-base
+    - .repo-from-artifacts
     - .frontend:rules:jest-snapshot-vue3
-  needs: ["rspec-all frontend_fixture"]
+  needs:
+    # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway
+    - !reference [.repo-from-artifacts, needs]
+    - !reference [.with-fixtures-needs, needs]
   variables:
     VUE_VERSION: 3
     JEST_REPORT: jest-test-report.json
@@ -379,7 +397,6 @@ jest-snapshot-vue3:
         echo 'All snapshot tests passed! Exiting 0...'
         exit 0
       fi
-
   artifacts:
     name: snapshot_tests
     expire_in: 31d
@@ -393,8 +410,10 @@ coverage-frontend:
     - .default-retry
     - .default-utils-before_script
     - .yarn-cache
+    - .repo-from-artifacts
     - .frontend:rules:coverage-frontend
   needs:
+    - !reference [.repo-from-artifacts, needs]
     - job: "jest"
       optional: true
     - job: "jest-with-fixtures"
@@ -425,9 +444,9 @@ webpack-dev-server:
     - .default-retry
     - .default-utils-before_script
     - .yarn-cache
+    - .repo-from-artifacts
     - .frontend:rules:default-frontend-jobs
-  stage: test
-  needs: []
+  stage: test-frontend
   variables:
     WEBPACK_MEMORY_TEST: "true"
     WEBPACK_VENDOR_DLL: "true"
@@ -441,44 +460,24 @@ webpack-dev-server:
     paths:
       - webpack-dev-server.json
 
-bundle-size-review:
-  extends:
-    - .default-retry
-    - .default-utils-before_script
-    - .assets-compile-cache
-    - .frontend:rules:bundle-size-review
-  image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:danger
-  stage: test
-  needs: []
-  script:
-    - yarn_install_script
-    - scripts/bundle_size_review
-  artifacts:
-    when: always
-    name: bundle-size-review
-    expire_in: 31d
-    paths:
-      - bundle-size-review/
-
-.compile-storybook-base:
+compile-storybook:
   extends:
     - .frontend-test-base
     - .storybook-yarn-cache
-  script:
-    - yarn_install_script_storybook
-    - run_timed_command "yarn run storybook:build"
-  needs: ["graphql-schema-dump"]
-
-compile-storybook:
-  extends:
-    - .compile-storybook-base
+    - .repo-from-artifacts
     - .frontend:rules:compile-storybook
+  stage: pages
   needs:
-    - !reference [.compile-storybook-base, needs]
-    - job: "rspec-all frontend_fixture"
+    # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway
+    - !reference [.repo-from-artifacts, needs]
+    - !reference [.with-fixtures-needs, needs]
+    - !reference [.with-graphql-schema-dump-needs, needs]
   artifacts:
     name: storybook
     expire_in: 31d
     when: always
     paths:
       - storybook/public
+  script:
+    - yarn_install_script_storybook
+    - run_timed_command "yarn run storybook:build"
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 73a0e7926ecd5953cbb6145cc4f9a56697d363bc..761a8a3071828e636964a14efbda3606620e2d68 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -28,7 +28,8 @@
   needs:
     # If the job extending this also defines `needs`, make sure to update
     # its `needs` to include `clone-gitlab-repo` because it'll be overridden.
-    - clone-gitlab-repo
+    - job: clone-gitlab-repo
+      optional: true # Optional so easier to switch in between
 
 .production:
   variables:
diff --git a/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml b/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml
index 6453936b6cbdb2c8593a5100afa69ff5d14adef5..15d50bb88c907c9535685caa9341f1a752368bb7 100644
--- a/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml
+++ b/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml
@@ -2,6 +2,7 @@ include:
   - component: ${CI_SERVER_FQDN}/gitlab-org/components/danger-review/danger-review@1.4.1
     inputs:
       job_image: "${DEFAULT_CI_IMAGE}"
+      job_stage: "preflight"
       # By default DANGER_DANGERFILE_PREFIX is not defined but allows JiHu to
       # use a different prefix.
       # See https://jihulab.com/gitlab-cn/gitlab/-/blob/main-jh/jh/.gitlab-ci.yml
diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml
index ad8c3047396a11c07a205485489919ae892db4dd..e2132d3eeecb5f750a7575b107993c4df1051479 100644
--- a/.gitlab/ci/preflight.gitlab-ci.yml
+++ b/.gitlab/ci/preflight.gitlab-ci.yml
@@ -115,3 +115,21 @@ pipeline-tier-3:
   extends:
     - .pipeline-tier-base
     - .preflight:rules:pipeline-tier-3
+
+bundle-size-review:
+  extends:
+    - .default-retry
+    - .default-utils-before_script
+    - .assets-compile-cache
+    - .repo-from-artifacts
+    - .frontend:rules:bundle-size-review
+  stage: preflight
+  script:
+    - yarn_install_script
+    - scripts/bundle_size_review
+  artifacts:
+    when: always
+    name: bundle-size-review
+    expire_in: 31d
+    paths:
+      - bundle-size-review/
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 9468d958ea1b2866617db744f7961a5a643141e5..2dea48c8c3e37eeaaa489bac9a09f5ea308aaf30 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -103,6 +103,7 @@ qa:metadata-lint:
   extends:
     - .qa-job-base
     - .qa:rules:metadata-lint
+  stage: lint
   variables:
     QA_EXPORT_TEST_METRICS: "false"
     # Disable warnings in browserslist which can break on backports
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 1d0adfa2ade7efbf693b3147fddedfed6e6d29c3..b5153ae668c3219dacd0829119e842a7ab850161 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -1367,7 +1367,6 @@ fail-pipeline-early:
   stage: test
   needs:
     - !reference [.rspec-base-needs, needs]
-    - job: "compile-test-assets"
     - job: "detect-previous-failed-tests"
   script:
     - !reference [.base-script, script]
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index e9c976db7fce97ea02d86ab3b4d7018ccaf6bf86..9221edba9545cedd343ee94dc44181f52cf1dc3e 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -68,10 +68,10 @@ include:
 
 .rspec-base-needs:
   needs:
-    - job: "clone-gitlab-repo"
-      optional: true  # Optional so easier to switch in between
+    - !reference [.repo-from-artifacts, needs]
     - job: "setup-test-env"
     - job: "retrieve-tests-metadata"
+    - job: "compile-test-assets"
 
 .rspec-base:
   extends:
@@ -89,7 +89,6 @@ include:
     EVENT_PROF: "sql.active_record"
   needs:
     - !reference [.rspec-base-needs, needs]
-    - job: "compile-test-assets"
     - job: "detect-tests"
       optional: true
   script:
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index 0acec43ca31a4003b276935930aadc73e7cdef3d..0ebe3c586193ad94b49028725f3c3ae1cbf68897 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -91,7 +91,7 @@ verify-tests-yml:
   extends:
     - .setup:rules:verify-tests-yml
   image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine3.20
-  stage: test
+  stage: preflight
   needs: []
   script:
     - source scripts/utils.sh