Quellcode durchsuchen

[web] add junit integration for Jenkins (#28788)

* [web] add junit integration for Jenkins

* [web] integrate junit test report for writefull into Jenkins

* [web] put all the junit test results into namespaces

GitOrigin-RevId: ba1ff07b5ea7bcfa97bb4d6bf7fa9e5291ab7b0f
Jakob Ackermann vor 10 Monaten
Ursprung
Commit
c3c04acfea

+ 20 - 0
package-lock.json

@@ -37047,6 +37047,23 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/mocha-multi-reporters": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz",
+      "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "lodash": "^4.17.15"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      },
+      "peerDependencies": {
+        "mocha": ">=3.1.2"
+      }
+    },
     "node_modules/mocha/node_modules/brace-expansion": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -52717,6 +52734,7 @@
         "css-loader": "^6.8.1",
         "css-minimizer-webpack-plugin": "^5.0.1",
         "cypress": "13.13.2",
+        "cypress-multi-reporters": "^2.0.5",
         "cypress-plugin-tab": "^1.0.5",
         "d3": "^3.5.16",
         "daterangepicker": "2.1.27",
@@ -52758,6 +52776,8 @@
         "mini-css-extract-plugin": "^2.7.6",
         "mocha": "^11.1.0",
         "mocha-each": "^2.0.1",
+        "mocha-junit-reporter": "^2.2.1",
+        "mocha-multi-reporters": "^1.5.1",
         "mock-fs": "^5.1.2",
         "nock": "^13.5.6",
         "nvd3": "^1.8.6",

+ 8 - 0
services/web/.mocharc.js

@@ -0,0 +1,8 @@
+let reporterOptions = {}
+if (process.env.CI && process.env.MOCHA_ROOT_SUITE_NAME) {
+  reporterOptions = {
+    reporter: '/overleaf/node_modules/mocha-multi-reporters',
+    'reporter-options': ['configFile=./test/mocha-multi-reporters.js'],
+  }
+}
+module.exports = reporterOptions

+ 58 - 253
services/web/Jenkinsfile

@@ -1,17 +1,3 @@
-// Initialize variables to signal that a given stage finished.
-// We use them to build a graph of interconnected steps/dependencies.
-// - Incoming edges use "waitUntil" and reference the given variables of dependencies.
-// - Outgoing edges set the given variable to true.
-def action_test_frontend_ct_build = false
-def action_build_deps = false
-def action_copy_external_pages = false
-def action_build_dev = false
-def action_test_acceptance_app_server_pro = false
-def action_build_webpack = false
-def action_test_acceptance_app_saas = false
-def action_build_pug = false
-def action_build_production = false
-
 pipeline {
   agent {
     node {
@@ -24,8 +10,6 @@ pipeline {
   options {
     // Print timestamp next to each log line.
     timestamps()
-    // Abort build after hitting first failure.
-    parallelsAlwaysFailFast()
     retry(3)
     timeout(time: 15, unit: 'MINUTES')
   }
@@ -45,34 +29,8 @@ pipeline {
     CDN_PROD = "gs://mgcp-1117973-ol-prod-web-assets-1"
   }
   stages {
-    // Retries will use the same pipeline instance. Reset the vars.
-    stage('Reset vars') {
-      steps {
-        script {
-          action_test_frontend_ct_build = false
-          action_build_deps = false
-          action_copy_external_pages = false
-          action_build_dev = false
-          action_test_acceptance_app_server_pro = false
-          action_build_webpack = false
-          action_test_acceptance_app_saas = false
-          action_build_pug = false
-          action_build_production = false
-        }
-      }
-    }
-    stage ('Build, Test and Push') {
+    stage('Stage 1') {
       parallel {
-        stage('Copy external pages') {
-          steps {
-            dir('services/web') {
-              sh 'bin/copy_external_pages'
-              script {
-                action_copy_external_pages = true
-              }
-            }
-          }
-        }
         stage('Prefetch Tests Images') {
           steps {
             dir('services/web') {
@@ -80,50 +38,29 @@ pipeline {
             }
           }
         }
-        stage('Build Deps') {
+        stage('Build') {
           steps {
             dir('services/web') {
+              sh 'bin/copy_external_pages'
               sh 'make build_deps'
-            }
-            script {
-              action_build_deps = true
+              sh 'make build_dev'
+              sh 'make build_test_frontend_ct'
             }
           }
         }
+      }
+    }
+    stage('Stage 2') {
+      parallel {
         stage('Push Deps') {
           steps {
-            script {
-              waitUntil {
-                return action_build_deps
-              }
-            }
             dir('services/web') {
               sh 'docker push ${AR_URL}/${IMAGE_NAME}:${BRANCH_NAME}-deps'
             }
           }
         }
-        stage('Build Dev') {
-          steps {
-            script {
-              waitUntil {
-                return action_build_deps && action_copy_external_pages
-              }
-            }
-            dir('services/web') {
-              sh 'make build_dev'
-            }
-            script {
-              action_build_dev = true
-            }
-          }
-        }
-        stage ('Format') {
+        stage('Format') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh 'make format_in_docker'
             }
@@ -131,15 +68,16 @@ pipeline {
         }
         stage('Lint') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh 'make lint_in_docker'
             }
           }
+          post {
+            always {
+              recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'web-eslint', name: 'Web eslint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [esLint(pattern: 'services/web/data/reports/eslint.json')]
+              recordIssues checksAnnotationScope: 'ALL', enabledForFailure: true, failOnError: true, id: 'web-stylelint', name: 'Web stylelint', qualityGates: [[integerThreshold: 1, threshold: 1.0, type: 'TOTAL']], sourceCodeRetention: 'LAST_BUILD', tools: [styleLint(pattern: 'services/web/data/reports/stylelint.json')]
+            }
+          }
         }
         stage('Shellcheck') {
           steps {
@@ -150,26 +88,13 @@ pipeline {
         }
         stage('Acceptance SaaS') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh 'make test_acceptance_app_saas'
             }
-            script {
-              action_test_acceptance_app_saas = true
-            }
           }
         }
         stage('Acceptance Server CE') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_app_server_ce"
             }
@@ -177,26 +102,13 @@ pipeline {
         }
         stage('Acceptance Server Pro') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_app_server_pro"
             }
-            script {
-              action_test_acceptance_app_server_pro = true
-            }
           }
         }
         stage('test_acceptance_modules_merged_saas_1') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_modules_merged_saas_1"
             }
@@ -204,11 +116,6 @@ pipeline {
         }
         stage('test_acceptance_modules_merged_saas_2') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_modules_merged_saas_2"
             }
@@ -216,11 +123,6 @@ pipeline {
         }
         stage('test_acceptance_modules_merged_saas_3') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_modules_merged_saas_3"
             }
@@ -228,11 +130,6 @@ pipeline {
         }
         stage('test_acceptance_modules_merged_saas_4') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_modules_merged_saas_4"
             }
@@ -240,11 +137,6 @@ pipeline {
         }
         stage('test_acceptance_modules_merged_server_ce') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_modules_merged_server_ce"
             }
@@ -252,11 +144,6 @@ pipeline {
         }
         stage('test_acceptance_modules_merged_server_pro') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_acceptance_modules_merged_server_pro"
             }
@@ -264,11 +151,6 @@ pipeline {
         }
         stage('test_frontend') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_frontend"
             }
@@ -276,41 +158,16 @@ pipeline {
         }
         stage('test_writefull') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_writefull"
             }
           }
         }
-        stage('test_frontend_ct_build') {
-          steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
-            dir('services/web') {
-              sh "make build_test_frontend_ct"
-            }
-            script {
-              action_test_frontend_ct_build = true
-            }
-          }
-        }
         stage('test_frontend_ct_core_other') {
           environment {
             CYPRESS_INTERNAL_BROWSER_CONNECT_TIMEOUT = '120000'
           }
           steps {
-            script {
-              waitUntil {
-                return action_test_frontend_ct_build
-              }
-            }
             dir('services/web') {
               sh "make test_frontend_ct_core_other"
             }
@@ -321,11 +178,6 @@ pipeline {
             CYPRESS_INTERNAL_BROWSER_CONNECT_TIMEOUT = '120000'
           }
           steps {
-            script {
-              waitUntil {
-                return action_test_frontend_ct_build
-              }
-            }
             dir('services/web') {
               sh "make test_frontend_ct_core_features"
             }
@@ -336,11 +188,6 @@ pipeline {
             CYPRESS_INTERNAL_BROWSER_CONNECT_TIMEOUT = '120000'
           }
           steps {
-            script {
-              waitUntil {
-                return action_test_frontend_ct_build
-              }
-            }
             dir('services/web') {
               sh "make test_frontend_ct_modules"
             }
@@ -348,11 +195,6 @@ pipeline {
         }
         stage('test_frontend_ct_editor_visual') {
           steps {
-            script {
-              waitUntil {
-                return action_test_frontend_ct_build
-              }
-            }
             dir('services/web') {
               sh "make test_frontend_ct_editor_visual"
             }
@@ -360,11 +202,6 @@ pipeline {
         }
         stage('test_frontend_ct_editor_other') {
           steps {
-            script {
-              waitUntil {
-                return action_test_frontend_ct_build
-              }
-            }
             dir('services/web') {
               sh "make test_frontend_ct_editor_other"
             }
@@ -372,11 +209,6 @@ pipeline {
         }
         stage('Test Unit ESM') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_unit_esm"
             }
@@ -384,90 +216,60 @@ pipeline {
         }
         stage('Test Unit Mocha') {
           steps {
-            script {
-              waitUntil {
-                return action_build_dev
-              }
-            }
             dir('services/web') {
               sh "make test_unit_mocha"
             }
           }
         }
-        stage('Build Webpack') {
-          steps {
-            script {
-              waitUntil {
-                return action_build_dev && action_test_acceptance_app_server_pro
+        stage('Build webpack + production + cdn upload + sentry upload') {
+          stages {
+            stage('Wait a bit to give tests all the CPU capacity') {
+              steps {
+                sh 'sleep 120'
               }
             }
-            dir('services/web') {
-              sh 'make build_webpack'
-            }
-            script {
-              action_build_webpack = true
-            }
-          }
-        }
-        stage('Build Pug') {
-          steps {
-            script {
-              waitUntil {
-                return action_build_dev && action_test_acceptance_app_server_pro
+            stage('Build Webpack') {
+              steps {
+                dir('services/web') {
+                  sh 'make build_webpack'
+                }
               }
             }
-            dir('services/web') {
-              sh 'make build_pug'
-            }
-            script {
-              action_build_pug = true
-            }
-          }
-        }
-        stage('CDN Upload Image') {
-          steps {
-            script {
-              waitUntil {
-                return action_build_webpack
+            stage('Build Pug') {
+              steps {
+                dir('services/web') {
+                  sh 'make build_pug'
+                }
               }
             }
-            dir('services/web') {
-              sh 'make tar'
-              sh 'bin/cdn_upload'
-            }
-          }
-        }
-        stage('Build Production') {
-          steps {
-            script {
-              waitUntil {
-                return action_build_webpack && action_build_pug
+            stage('CDN Upload Image') {
+              steps {
+                dir('services/web') {
+                  sh 'make tar'
+                  sh 'bin/cdn_upload'
+                }
               }
             }
-            dir('services/web') {
-              sh 'make build'
-            }
-            script {
-              action_build_production = true
-            }
-          }
-        }
-        stage('Sentry Upload') {
-          steps {
-            script {
-              waitUntil {
-                return action_build_webpack && action_build_production
+            stage('Build Production') {
+              steps {
+                dir('services/web') {
+                  sh 'make build'
+                }
               }
             }
-            dir('services/web') {
-              sh 'gcloud secrets versions access latest --secret=web-sentryclirc > .sentryclirc'
-              sh 'make sentry_upload'
-            }
-          }
-          post {
-            cleanup {
-              dir('services/web') {
-                sh 'rm -f .sentryclirc'
+            stage('Sentry Upload') {
+              steps {
+                dir('services/web') {
+                  sh 'gcloud secrets versions access latest --secret=web-sentryclirc > .sentryclirc'
+                  sh 'make sentry_upload'
+                }
+              }
+              post {
+                cleanup {
+                  dir('services/web') {
+                    sh 'rm -f .sentryclirc'
+                  }
+                }
               }
             }
           }
@@ -483,6 +285,9 @@ pipeline {
     }
   }
   post {
+    always {
+      junit checksName: 'Web test results', testResults: 'services/web/data/reports/junit-*.xml'
+    }
     // Ensure tear down of test containers, then run general Jenkins VM cleanup.
     cleanup {
       dir('services/web') {

+ 25 - 3
services/web/Makefile

@@ -6,6 +6,7 @@ export COMMIT_SHA ?= $(shell git rev-parse HEAD)
 PROJECT_NAME = web
 BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
 PWD = $(shell pwd)
+export MONOREPO ?= $(shell cd ../../ && pwd)
 
 export OVERLEAF_CONFIG ?= /overleaf/services/web/test/acceptance/config/settings.test.saas.js
 export BASE_CONFIG ?= ${OVERLEAF_CONFIG}
@@ -60,6 +61,7 @@ SHARD_PROJECT_NAMES = \
 
 CLEAN_SHARDS=$(addprefix clean/,$(SHARD_PROJECT_NAMES))
 clean: $(CLEAN_SHARDS)
+	-docker run --rm --volume /dev/shm:/dev/shm --user root $(IMAGE_CI) rm -rf /dev/shm/overleaf
 	-docker rmi --force $(IMAGE_CI) $(IMAGE_CI)-dev $(IMAGE_CI)-pug $(IMAGE_CI)-webpack $(IMAGE_REPO_FINAL)
 	-docker compose down --remove-orphans --rmi local --timeout 0 --volumes
 	-git clean -dfX data/
@@ -98,10 +100,12 @@ test_unit_app: export COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME)
 test_unit_app: mongo_migrations_for_tests
 	$(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit
 
+test_unit_mocha: export MOCHA_ROOT_SUITE_NAME = Mocha unit tests
 test_unit_mocha: export COMPOSE_PROJECT_NAME=unit_test_mocha_$(BUILD_DIR_NAME)
 test_unit_mocha: mongo_migrations_for_tests
 	$(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:mocha
 
+test_unit_esm: export MOCHA_ROOT_SUITE_NAME = ESM unit tests
 test_unit_esm: export COMPOSE_PROJECT_NAME=unit_test_esm_$(BUILD_DIR_NAME)
 test_unit_esm: mongo_migrations_for_tests
 	$(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:esm
@@ -148,6 +152,7 @@ mongo_migrations_for_tests:
 # Frontend tests
 #
 
+test_frontend: export MOCHA_ROOT_SUITE_NAME = JSDOM frontend tests
 test_frontend:
 	COMPOSE_PROJECT_NAME=frontend_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_frontend
 	COMPOSE_PROJECT_NAME=frontend_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
@@ -169,13 +174,14 @@ TEST_FRONTEND_CT_VARIANTS = \
 # Writefull tests
 #
 
+test_writefull: export MOCHA_ROOT_SUITE_NAME = Writefull tests
 test_writefull:
 	COMPOSE_PROJECT_NAME=writefull_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_writefull
 	COMPOSE_PROJECT_NAME=writefull_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
 
 # Note: The below cypress targets are for CI only
 build_test_frontend_ct:
-	docker run --rm --volume /dev/shm:/dev/shm --user root $(IMAGE_CI) bash -ec 'tar -cC / overleaf | tar -xC /dev/shm'
+	docker run --rm --volume /dev/shm:/dev/shm --user root $(IMAGE_CI) bash -ec 'for path in /overleaf/services/web/cypress/results /overleaf/services/web/node_modules/.cache; do mkdir -p $$path; chown -R node:node $$path; done && tar -cC / overleaf | tar -xC /dev/shm'
 
 test_frontend_ct_core_other: export CYPRESS_RESULTS=./cypress/results/core
 test_frontend_ct_core_other: export CYPRESS_SPEC_PATTERN=./test/frontend/**/*.spec.{js,jsx,ts,tsx}
@@ -214,10 +220,13 @@ TEST_ACCEPTANCE_APP :=  \
 	test_acceptance_app_server_pro \
 
 test_acceptance_app: $(TEST_ACCEPTANCE_APP)
+test_acceptance_app_saas: export MOCHA_ROOT_SUITE_NAME = SaaS app acceptance tests
 test_acceptance_app_saas: export COMPOSE_PROJECT_NAME=acceptance_test_saas_$(BUILD_DIR_NAME)
 test_acceptance_app_saas: export OVERLEAF_CONFIG=$(CFG_SAAS)
+test_acceptance_app_server_ce: export MOCHA_ROOT_SUITE_NAME = Server CE app acceptance tests
 test_acceptance_app_server_ce: export COMPOSE_PROJECT_NAME=acceptance_test_server_ce_$(BUILD_DIR_NAME)
 test_acceptance_app_server_ce: export OVERLEAF_CONFIG=$(CFG_SERVER_CE)
+test_acceptance_app_server_pro: export MOCHA_ROOT_SUITE_NAME = Server Pro app acceptance tests
 test_acceptance_app_server_pro: export COMPOSE_PROJECT_NAME=acceptance_test_server_pro_$(BUILD_DIR_NAME)
 test_acceptance_app_server_pro: export OVERLEAF_CONFIG=$(CFG_SERVER_PRO)
 
@@ -349,14 +358,17 @@ $(TEST_ACCEPTANCE_MODULES_MERGED_INNER_SPLIT):
 	)
 
 # See docs for test_acceptance_server_ce how this works.
+test_acceptance_modules_merged_saas: export MOCHA_ROOT_SUITE_NAME = SaaS modules acceptance tests
 test_acceptance_modules_merged_saas: export COMPOSE_PROJECT_NAME = \
 	acceptance_test_modules_merged_saas_$(BUILD_DIR_NAME)
 test_acceptance_modules_merged_saas: export BASE_CONFIG = $(CFG_SAAS)
 
+test_acceptance_modules_merged_server_ce: export MOCHA_ROOT_SUITE_NAME = Server CE modules acceptance tests
 test_acceptance_modules_merged_server_ce: export COMPOSE_PROJECT_NAME = \
 	acceptance_test_modules_merged_server_ce_$(BUILD_DIR_NAME)
 test_acceptance_modules_merged_server_ce: export BASE_CONFIG = $(CFG_SERVER_CE)
 
+test_acceptance_modules_merged_server_pro: export MOCHA_ROOT_SUITE_NAME = Server Pro modules acceptance tests
 test_acceptance_modules_merged_server_pro: export COMPOSE_PROJECT_NAME = \
 	acceptance_test_modules_merged_server_pro_$(BUILD_DIR_NAME)
 test_acceptance_modules_merged_server_pro: export BASE_CONFIG = $(CFG_SERVER_PRO)
@@ -388,6 +400,7 @@ test_acceptance_modules_merged_saas_3: export COMPOSE_PROJECT_NAME = \
 test_acceptance_modules_merged_saas_4: export COMPOSE_PROJECT_NAME = \
 	acceptance_test_modules_merged_saas_4_$(BUILD_DIR_NAME)
 $(TEST_ACCEPTANCE_MODULES_MERGED_SPLIT_SAAS): export BASE_CONFIG = $(CFG_SAAS)
+$(TEST_ACCEPTANCE_MODULES_MERGED_SPLIT_SAAS): export MOCHA_ROOT_SUITE_NAME = SaaS modules acceptance tests
 
 $(TEST_ACCEPTANCE_MODULES_MERGED_SPLIT_SAAS): test_acceptance_modules_merged_saas_%:
 	$(DOCKER_COMPOSE) run --rm test_acceptance make test_acceptance_modules_merged_inner_$*
@@ -408,7 +421,7 @@ ci:
 #
 ORG_PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
 RUN_LINT_FORMAT ?= \
-	docker run --rm ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
+	docker run --rm --env BRANCH_NAME --env CI --env COMMIT_SHA --env MONOREPO --volume ${PWD}/data/reports:/overleaf/services/web/data/reports ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER)
 
 NODE_MODULES_PATH := ${PATH}:${PWD}/node_modules/.bin:/overleaf/services/web/node_modules/.bin
 WITH_NODE_MODULES_PATH = \
@@ -425,11 +438,21 @@ $(WITH_NODE_MODULES_PATH): export PATH=$(NODE_MODULES_PATH)
 
 lint: lint_eslint
 lint_eslint:
+ifeq ($(CI),true)
+	-npm run lint -- --format json --output-file data/reports/eslint.json
+	sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' data/reports/eslint.json
+else
 	npm run lint
+endif
 
 lint: lint_stylelint
 lint_stylelint:
+ifeq ($(CI),true)
+	-npm run lint:styles -- --formatter json --output-file data/reports/stylelint.json
+	sed -i 's_"source":"/overleaf_"source":"$(MONOREPO)_g' data/reports/stylelint.json
+else
 	npm run lint:styles
+endif
 
 lint: lint_pug
 lint_pug:
@@ -589,7 +612,6 @@ SENTRY_IMAGE=getsentry/sentry-cli:2.16.1
 sentry_prefetch:
 	docker pull $(SENTRY_IMAGE)
 
-sentry_upload: MONOREPO=$(shell cd ../../ && pwd)
 sentry_upload:
 	docker run --rm \
 		--volume $(MONOREPO):$(MONOREPO) --workdir $(PWD) \

+ 0 - 1
services/web/buildscript.txt

@@ -7,4 +7,3 @@ web
 --esmock-loader=False
 --node-version=22.18.0
 --public-repo=False
---script-version=4.7.0

+ 13 - 2
services/web/cypress.config.ts

@@ -1,11 +1,21 @@
 import { defineConfig } from 'cypress'
 import { webpackConfig } from './cypress/support/webpack.cypress'
 
+let reporterOptions = {}
+if (process.env.CI) {
+  reporterOptions = {
+    reporter: '/overleaf/node_modules/cypress-multi-reporters',
+    reporterOptions: {
+      configFile: 'cypress/cypress-multi-reporters.json',
+    },
+  }
+}
+
 export default defineConfig({
   fixturesFolder: 'cypress/fixtures',
   video: process.env.CYPRESS_VIDEO === 'true',
-  screenshotsFolder: 'cypress/results',
-  videosFolder: 'cypress/results',
+  screenshotsFolder: process.env.CYPRESS_RESULTS || 'cypress/results',
+  videosFolder: process.env.CYPRESS_RESULTS || 'cypress/results',
   viewportHeight: 800,
   viewportWidth: 800,
   component: {
@@ -25,4 +35,5 @@ export default defineConfig({
   retries: {
     runMode: 3,
   },
+  ...reporterOptions,
 })

+ 10 - 0
services/web/cypress/cypress-multi-reporters.json

@@ -0,0 +1,10 @@
+{
+  "reporterEnabled": "spec, mocha-junit-reporter",
+  "mochaJunitReporterReporterOptions": {
+    "mochaFile": "data/reports/junit-cypress-[hash].xml",
+    "includePending": true,
+    "jenkinsMode": true,
+    "jenkinsClassnamePrefix": "Web Cypress tests",
+    "rootSuiteTitle": "Web Cypress tests"
+  }
+}

+ 0 - 0
services/web/data/reports/.gitignore


+ 20 - 3
services/web/docker-compose.ci.yml

@@ -13,11 +13,14 @@ services:
     user: node
     volumes:
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+      - ./data/reports:/overleaf/services/web/data/reports
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:unit:app
     working_dir: /overleaf/services/web
     env_file: docker-compose.common.env
     environment:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       BASE_CONFIG:
       OVERLEAF_CONFIG:
       NODE_ENV: test
@@ -38,6 +41,8 @@ services:
     working_dir: /overleaf/services/web
     env_file: docker-compose.common.env
     environment:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       BASE_CONFIG:
       OVERLEAF_CONFIG:
       REDIS_HOST: redis_test
@@ -45,6 +50,7 @@ services:
       - 'www.overleaf.test:127.0.0.1'
     volumes:
       - ../../bin/shared/wait_for_it:/overleaf/bin/shared/wait_for_it
+      - ./data/reports:/overleaf/services/web/data/reports
     entrypoint: /overleaf/bin/shared/wait_for_it mongo:27017 --timeout=60 --
     command: npm run test:acceptance:app
     user: root
@@ -61,6 +67,8 @@ services:
       context: ../..
       dockerfile: services/web/Dockerfile
     image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER
+    volumes:
+      - ./data/reports:/overleaf/services/web/data/reports
     logging:
       driver: local
     user: node
@@ -68,18 +76,23 @@ services:
     command: npm run test:frontend
     environment:
       NODE_OPTIONS: "--unhandled-rejections=strict"
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
 
   test_frontend_ct:
     image: cypress/included:13.13.2
     logging:
       driver: local
     working_dir: /overleaf/services/web
+    user: "${DOCKER_USER:-1000:1000}"
     environment:
-      CYPRESS_SPEC_PATTERN: ${CYPRESS_SPEC_PATTERN:-}
-      CYPRESS_EXCLUDE_SPEC_PATTERN: ${CYPRESS_EXCLUDE_SPEC_PATTERN:-}
+      CI:
+      CYPRESS_RESULTS:
+      CYPRESS_SPEC_PATTERN:
+      CYPRESS_EXCLUDE_SPEC_PATTERN:
     volumes:
-      - ${CYPRESS_RESULTS:-./cypress/results}:/overleaf/services/web/cypress/results/
       - /dev/shm/overleaf:/overleaf
+      - ./data/reports:/overleaf/services/web/data/reports
     entrypoint: npm
     command:
       - "run"
@@ -95,7 +108,11 @@ services:
     user: node
     working_dir: /overleaf/services/web
     command: npm run test:writefull
+    volumes:
+      - ./data/reports:/overleaf/services/web/data/reports
     environment:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       NODE_OPTIONS: "--unhandled-rejections=strict"
 
   tar:

+ 11 - 0
services/web/docker-compose.yml

@@ -15,6 +15,8 @@ services:
     environment:
       BASE_CONFIG:
       OVERLEAF_CONFIG:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       NODE_ENV: test
@@ -40,6 +42,8 @@ services:
     environment:
       BASE_CONFIG:
       OVERLEAF_CONFIG:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       MOCHA_GREP: ${MOCHA_GREP}
       LOG_LEVEL: ${LOG_LEVEL:-}
       MONGO_SERVER_SELECTION_TIMEOUT: 600000
@@ -68,6 +72,8 @@ services:
       - ../../libraries:/overleaf/libraries
     working_dir: /overleaf/services/web
     environment:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       MOCHA_GREP: ${MOCHA_GREP}
       NODE_OPTIONS: "--unhandled-rejections=strict"
       VERBOSE_LOGGING:
@@ -79,8 +85,11 @@ services:
     volumes:
       - ../../:/overleaf
     working_dir: /overleaf/services/web
+    user: "${DOCKER_USER:-1000:1000}"
     environment:
       VERBOSE_LOGGING:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       CYPRESS_SPEC_PATTERN: ${CYPRESS_SPEC_PATTERN:-}
       CYPRESS_EXCLUDE_SPEC_PATTERN: ${CYPRESS_EXCLUDE_SPEC_PATTERN:-}
     entrypoint: npm
@@ -99,6 +108,8 @@ services:
       - ../../libraries:/overleaf/libraries
     working_dir: /overleaf/services/web
     environment:
+      CI:
+      MOCHA_ROOT_SUITE_NAME:
       NODE_OPTIONS: "--unhandled-rejections=strict"
       VERBOSE_LOGGING:
     command: npm run --silent test:writefull

+ 3 - 0
services/web/package.json

@@ -304,6 +304,7 @@
     "css-loader": "^6.8.1",
     "css-minimizer-webpack-plugin": "^5.0.1",
     "cypress": "13.13.2",
+    "cypress-multi-reporters": "^2.0.5",
     "cypress-plugin-tab": "^1.0.5",
     "d3": "^3.5.16",
     "daterangepicker": "2.1.27",
@@ -345,6 +346,8 @@
     "mini-css-extract-plugin": "^2.7.6",
     "mocha": "^11.1.0",
     "mocha-each": "^2.0.1",
+    "mocha-junit-reporter": "^2.2.1",
+    "mocha-multi-reporters": "^1.5.1",
     "mock-fs": "^5.1.2",
     "nock": "^13.5.6",
     "nvd3": "^1.8.6",

+ 10 - 0
services/web/test/mocha-multi-reporters.js

@@ -0,0 +1,10 @@
+module.exports = {
+  reporterEnabled: 'spec, mocha-junit-reporter',
+  mochaJunitReporterReporterOptions: {
+    mochaFile: 'data/reports/junit-mocha-[hash]-[suiteFilename].xml',
+    includePending: true,
+    jenkinsMode: true,
+    jenkinsClassnamePrefix: process.env.MOCHA_ROOT_SUITE_NAME,
+    rootSuiteTitle: process.env.MOCHA_ROOT_SUITE_NAME,
+  },
+}

+ 16 - 0
services/web/vitest.config.js

@@ -1,5 +1,20 @@
 const { defineConfig } = require('vitest/config')
 
+let reporterOptions = {}
+if (process.env.CI && process.env.MOCHA_ROOT_SUITE_NAME) {
+  reporterOptions = {
+    reporters: [
+      'default',
+      [
+        'junit',
+        {
+          classnameTemplate: `${process.env.MOCHA_ROOT_SUITE_NAME}.{filename}`,
+        },
+      ],
+    ],
+    outputFile: 'data/reports/junit-vitest.xml',
+  }
+}
 module.exports = defineConfig({
   test: {
     include: [
@@ -9,5 +24,6 @@ module.exports = defineConfig({
     setupFiles: ['./test/unit/vitest_bootstrap.mjs'],
     globals: true,
     isolate: false,
+    ...reporterOptions,
   },
 })