diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0ef5d8305488090ed82992a1990379018119eb26..bc305a18156b193e47309f3e2d65c79491d49bc1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,7 +5,6 @@ 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 e04e61fd921ec1cb86cfe29a845377d6ff07accb..5cc3c9984091ff5f6e9ec64cb31ac6083a1c5c37 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -1,11 +1,3 @@
-.with-fixtures-needs:
-  needs:
-    - "rspec-all frontend_fixture"
-
-.with-graphql-schema-dump-needs:
-  needs:
-    - "graphql-schema-dump"
-
 .compile-assets-base:
   extends:
     - .default-retry
@@ -91,6 +83,7 @@ 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:
@@ -99,6 +92,7 @@ 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:
@@ -142,14 +136,8 @@ 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"
-    # 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]
+  needs: ["setup-test-env", "retrieve-tests-metadata", "retrieve-frontend-fixtures"]
   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`.
@@ -190,21 +178,18 @@ rspec-all frontend_fixture:
 # Uploads FOSS fixtures in the FOSS project.
 upload-frontend-fixtures:
   extends:
-    - .default-retry
-    - .default-before_script
-    - .repo-from-artifacts
+    - .frontend-fixtures-base
     - .frontend:rules:upload-frontend-fixtures
   stage: fixtures
-  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]
+  needs: ["rspec-all frontend_fixture"]
   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:
@@ -238,7 +223,18 @@ graphql-schema-dump:
   before_script:
     - !reference [.default-before_script, before_script]
     - yarn_install_script
-  stage: test-frontend
+  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"
 
 jest-build-cache:
   extends:
@@ -262,28 +258,18 @@ 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:
+jest-with-fixtures:
   extends:
-    - .frontend-test-base
+    - .jest-base
     - .frontend:rules:jest
   needs:
+    - "rspec-all frontend_fixture"
     - job: jest-build-cache
       optional: true
   artifacts:
@@ -296,42 +282,47 @@ jest:
       - tmp/tests/frontend/
     reports:
       junit: junit_jest.xml
-  parallel: 11
+  parallel: 2
   script:
-    - run_timed_command "yarn jest:ci:without-fixtures"
+    - run_timed_command "yarn jest:ci:with-fixtures"
 
-jest-with-fixtures:
+jest:
   extends:
-    - jest
-    - .repo-from-artifacts
+    - .jest-base
     - .frontend:rules:jest
   needs:
-    - !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"
+    - 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
 
-jest vue3:
+jest-with-fixtures vue3:
   extends:
-    - jest
+    - jest-with-fixtures
     - .frontend:rules:jest-vue3
     - .vue3
   needs:
-    - !reference [.with-jest-build-cache-vue3-needs, needs]
+    - "rspec-all frontend_fixture"
+    - job: jest-build-cache-vue3
+      optional: true
 
-jest-with-fixtures vue3:
+jest vue3:
   extends:
-    - jest-with-fixtures
+    - jest
     - .frontend:rules:jest-vue3
     - .vue3
   needs:
-    - !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]
+    - job: jest-build-cache-vue3
+      optional: true
 
 jest predictive:
   extends:
@@ -356,25 +347,16 @@ 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:
-    # 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]
+  needs: ["rspec-all frontend_fixture", "graphql-schema-dump"]
 
 jest-snapshot-vue3:
   extends:
-    - .frontend-test-base
-    - .repo-from-artifacts
+    - .jest-base
     - .frontend:rules:jest-snapshot-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]
+  needs: ["rspec-all frontend_fixture"]
   variables:
     VUE_VERSION: 3
     JEST_REPORT: jest-test-report.json
@@ -397,6 +379,7 @@ jest-snapshot-vue3:
         echo 'All snapshot tests passed! Exiting 0...'
         exit 0
       fi
+
   artifacts:
     name: snapshot_tests
     expire_in: 31d
@@ -410,10 +393,8 @@ 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"
@@ -444,9 +425,9 @@ webpack-dev-server:
     - .default-retry
     - .default-utils-before_script
     - .yarn-cache
-    - .repo-from-artifacts
     - .frontend:rules:default-frontend-jobs
-  stage: test-frontend
+  stage: test
+  needs: []
   variables:
     WEBPACK_MEMORY_TEST: "true"
     WEBPACK_VENDOR_DLL: "true"
@@ -460,24 +441,44 @@ webpack-dev-server:
     paths:
       - webpack-dev-server.json
 
-compile-storybook:
+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:
   extends:
     - .frontend-test-base
     - .storybook-yarn-cache
-    - .repo-from-artifacts
+  script:
+    - yarn_install_script_storybook
+    - run_timed_command "yarn run storybook:build"
+  needs: ["graphql-schema-dump"]
+
+compile-storybook:
+  extends:
+    - .compile-storybook-base
     - .frontend:rules:compile-storybook
-  stage: pages
   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]
+    - !reference [.compile-storybook-base, needs]
+    - job: "rspec-all frontend_fixture"
   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 761a8a3071828e636964a14efbda3606620e2d68..73a0e7926ecd5953cbb6145cc4f9a56697d363bc 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -28,8 +28,7 @@
   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.
-    - job: clone-gitlab-repo
-      optional: true # Optional so easier to switch in between
+    - clone-gitlab-repo
 
 .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 15d50bb88c907c9535685caa9341f1a752368bb7..6453936b6cbdb2c8593a5100afa69ff5d14adef5 100644
--- a/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml
+++ b/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml
@@ -2,7 +2,6 @@ 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 e2132d3eeecb5f750a7575b107993c4df1051479..ad8c3047396a11c07a205485489919ae892db4dd 100644
--- a/.gitlab/ci/preflight.gitlab-ci.yml
+++ b/.gitlab/ci/preflight.gitlab-ci.yml
@@ -115,21 +115,3 @@ 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 2dea48c8c3e37eeaaa489bac9a09f5ea308aaf30..9468d958ea1b2866617db744f7961a5a643141e5 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -103,7 +103,6 @@ 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 b5153ae668c3219dacd0829119e842a7ab850161..1d0adfa2ade7efbf693b3147fddedfed6e6d29c3 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -1367,6 +1367,7 @@ 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 9221edba9545cedd343ee94dc44181f52cf1dc3e..e9c976db7fce97ea02d86ab3b4d7018ccaf6bf86 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:
-    - !reference [.repo-from-artifacts, needs]
+    - job: "clone-gitlab-repo"
+      optional: true  # Optional so easier to switch in between
     - job: "setup-test-env"
     - job: "retrieve-tests-metadata"
-    - job: "compile-test-assets"
 
 .rspec-base:
   extends:
@@ -89,6 +89,7 @@ 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 0ebe3c586193ad94b49028725f3c3ae1cbf68897..0acec43ca31a4003b276935930aadc73e7cdef3d 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: preflight
+  stage: test
   needs: []
   script:
     - source scripts/utils.sh