All these answers seem fine, but I wanted something very simple to use with GitLab that does not requiring additional external services. I came up with this very simple wrapper around Cypress:
const path = require('path');
const fs = require('fs');
const childProcess = require('child_process');
const glob = require('glob-promise');
async function getIntegrationDirectory() {
let integrationFolder = 'cypress/integration';
let cypressJson = path.join(process.cwd(), 'cypress.json');
try {
await fs.promises.access(cypressJson, fs.constants.F_OK);
let cypressConfig = require(cypressJson);
integrationFolder = cypressConfig.integrationFolder ?? integrationFolder;
}
catch (err) {
// Ignore if file does not exist
}
return integrationFolder;
}
async function main() {
let nodeIndex = parseInt(process.env.CI_NODE_INDEX ?? 1, 10);
let totalNodes = parseInt(process.env.CI_NODE_TOTAL ?? 1, 10);
let integrationFolder = await getIntegrationDirectory();
let specs = await glob(integrationFolder + '/**/*.js');
let start = (nodeIndex - 1) * specs.length / totalNodes | 0;
let end = nodeIndex * specs.length / totalNodes | 0;
let specsToRun = specs.slice(start, end);
let prefix = `Parallel Cypress: Worker ${nodeIndex} of ${totalNodes}`;
if (!specsToRun.length) {
console.log(`${prefix}, no specs to run, ${specs.length} total`);
return;
}
console.log(`${prefix}, running specs ${start + 1} to ${end} of ${specs.length} total`);
let args = process.argv.slice(2)
.concat(['--spec', specsToRun.join(',')]);
let child = childProcess.spawn('cypress', args, {stdio: 'inherit', shell: true});
await new Promise((resolve, reject) => {
child.on('exit', exitCode => {
if (exitCode) {
reject(new Error(`Child process exited with code ${exitCode}`));
}
resolve();
});
});
}
main().catch(err => {
console.error(err);
process.exit(1);
});
It requires glob-promise and glob to be installed.
It integrates nicely with GitLab's parallel keyword and would work with other CI runners with minimal modification.
To use it, simply add parallel: 4 to the GitLab CI job, and then call node parallelCypress.js <some options>. It will then scan for tests and split them up to the available parallel jobs, then invoke cypress with the --spec option, so that each job only runs a part of the tests.
There's a lot of room for improvements though, especially since not all spec files contain the same number of tests and not all tests have similar runtimes.