diff --git a/services/web/docker-compose.ci.yml b/services/web/docker-compose.ci.yml index 877a66ce3d..00c815fa64 100644 --- a/services/web/docker-compose.ci.yml +++ b/services/web/docker-compose.ci.yml @@ -108,6 +108,8 @@ services: volumes: - /dev/shm/overleaf:/overleaf - ./data/reports:/overleaf/services/web/data/reports + tmpfs: + - /overleaf/services/web/node_modules/.cache:uid=1000,gid=1000 entrypoint: yarn command: - "run" diff --git a/services/web/frontend/macros/cypress-cache-variant.js b/services/web/frontend/macros/cypress-cache-variant.js new file mode 100644 index 0000000000..c3fb316e3d --- /dev/null +++ b/services/web/frontend/macros/cypress-cache-variant.js @@ -0,0 +1,23 @@ +const path = require('path') + +// Returns the cache subdirectory name for the current Cypress CT variant, +// or null when CYPRESS_RESULTS is not set. +// Throws if the derived basename is unsafe to prevent path traversal or +// absolute-path escapes. +function cypressCacheVariant() { + if (!process.env.CYPRESS_RESULTS) return null + const variant = path.basename(process.env.CYPRESS_RESULTS) + if ( + !variant || + variant === '.' || + variant === '..' || + path.isAbsolute(variant) + ) { + throw new Error( + `CYPRESS_RESULTS must resolve to a safe basename; got "${process.env.CYPRESS_RESULTS}"` + ) + } + return variant +} + +module.exports = cypressCacheVariant diff --git a/services/web/frontend/macros/invalidate-babel-cache-if-needed.js b/services/web/frontend/macros/invalidate-babel-cache-if-needed.js index 05a1071cfd..e65a8467f7 100644 --- a/services/web/frontend/macros/invalidate-babel-cache-if-needed.js +++ b/services/web/frontend/macros/invalidate-babel-cache-if-needed.js @@ -1,9 +1,13 @@ const fs = require('fs') const Path = require('path') const Settings = require('@overleaf/settings') +const cypressCacheVariant = require('./cypress-cache-variant') module.exports = function invalidateBabelCacheIfNeeded() { - const cacheDir = Path.join(__dirname, '../../node_modules/.cache') + // Use a unique subdirectory per Cypress CT variant to avoid parallel jobs + // racing on the shared babel cache and state file. + const suffix = cypressCacheVariant() ?? '' + const cacheDir = Path.join(__dirname, '../../node_modules/.cache', suffix) const cachePath = Path.join(cacheDir, 'babel-loader') const statePath = Path.join(cacheDir, 'last-overleafModuleImports.json') let lastState = '' diff --git a/services/web/webpack.config.dev.js b/services/web/webpack.config.dev.js index f35123aa5c..aa4c705691 100644 --- a/services/web/webpack.config.dev.js +++ b/services/web/webpack.config.dev.js @@ -5,6 +5,9 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') const base = require('./webpack.config') +const cypressCacheVariant = require('./frontend/macros/cypress-cache-variant') + +const cypressVariant = cypressCacheVariant() // if WEBPACK_ENTRYPOINTS is defined, remove any entrypoints that aren't included if (process.env.WEBPACK_ENTRYPOINTS) { @@ -26,6 +29,17 @@ module.exports = merge(base, { cache: { type: 'filesystem', + // Use a unique cache directory per Cypress CT variant to avoid + // parallel jobs corrupting the shared PackFileCacheStrategy pack files. + ...(cypressVariant + ? { + cacheDirectory: path.resolve( + __dirname, + 'node_modules/.cache/webpack', + cypressVariant + ), + } + : {}), buildDependencies: { config: [ __filename, diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 75a75f663d..6853e1a089 100644 --- a/services/web/webpack.config.js +++ b/services/web/webpack.config.js @@ -10,10 +10,18 @@ const { const PackageVersions = require('./app/src/infrastructure/PackageVersions.js') const invalidateBabelCacheIfNeeded = require('./frontend/macros/invalidate-babel-cache-if-needed') +const cypressCacheVariant = require('./frontend/macros/cypress-cache-variant') // Make sure that babel-macros are re-evaluated after changing the modules config invalidateBabelCacheIfNeeded() +// Use a unique babel-loader cache directory per Cypress CT variant to avoid +// parallel jobs corrupting the shared cache. +const cypressVariant = cypressCacheVariant() +const babelCacheDirectory = cypressVariant + ? path.join(__dirname, 'node_modules/.cache', cypressVariant, 'babel-loader') + : true + // Generate a hash of entry points, including modules const entryPoints = { bootstrap: './frontend/js/bootstrap.ts', @@ -123,7 +131,7 @@ module.exports = { { loader: 'babel-loader', options: { - cacheDirectory: true, + cacheDirectory: babelCacheDirectory, configFile: path.join(__dirname, './babel.config.json'), }, }, @@ -156,7 +164,7 @@ module.exports = { options: { // Configure babel-loader to cache compiled output so that // subsequent compile runs are much faster - cacheDirectory: true, + cacheDirectory: babelCacheDirectory, configFile: path.join(__dirname, './babel.config.json'), plugins: [ process.env.REACT_REFRESH_ENABLED === 'true' &&