0
0
Fork 0
mirror of https://github.com/renovatebot/renovate.git synced 2025-02-05 09:00:46 +00:00
renovatebot_renovate/tools/test-shards.ts
2025-01-24 09:34:08 +00:00

145 lines
4.2 KiB
TypeScript

import crypto from 'node:crypto';
import { minimatch } from 'minimatch';
import { testShards } from './test/shards';
import type { RunsOn, ShardGroup } from './test/types';
/**
* Given the file list affected by commit, return the list
* of shards that test these changes.
*/
function getMatchingShards(files: string[]): string[] {
const matchingShards = new Set<string>();
for (const file of files) {
for (const [key, { matchPaths }] of Object.entries(testShards)) {
const patterns = matchPaths.map((path) =>
path.endsWith('.spec.ts')
? path.replace(/\.spec\.ts$/, '{.ts,.spec.ts}')
: `${path}/**/*`,
);
if (patterns.some((pattern) => minimatch(file, pattern))) {
matchingShards.add(key);
break;
}
}
}
return Object.keys(testShards).filter((shard) => matchingShards.has(shard));
}
/**
* Distribute items evenly across runner instances.
*/
function scheduleItems<T>(items: T[], availableInstances: number): T[][] {
const numInstances = Math.min(items.length, availableInstances);
const maxPerInstance = Math.ceil(items.length / numInstances);
const lighterInstancesIdx =
items.length % numInstances === 0
? numInstances
: items.length % numInstances;
const partitionSizes = Array.from({ length: numInstances }, (_, idx) =>
idx < lighterInstancesIdx ? maxPerInstance : maxPerInstance - 1,
);
const result: T[][] = Array.from({ length: numInstances }, () => []);
let rest = items.slice();
for (let idx = 0; idx < numInstances; idx += 1) {
const partitionSize = partitionSizes[idx];
const partition = rest.slice(0, partitionSize);
result[idx] = partition;
rest = rest.slice(partitionSize);
}
return result;
}
let shardKeys = Object.keys(testShards);
if (process.env.FILTER_SHARDS === 'true' && process.env.CHANGED_FILES) {
try {
const changedFiles: string[] = JSON.parse(process.env.CHANGED_FILES);
const matchingShards = getMatchingShards(changedFiles);
if (matchingShards.length === 0) {
// eslint-disable-next-line no-console
console.log(`test-matrix-empty=true`);
process.exit(0);
}
shardKeys = shardKeys.filter((key) => matchingShards.includes(key));
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
}
}
/**
* Not all runners are created equal.
* Minutes cost proportion is 1:2:10 for Ubuntu:Windows:MacOS.
*
* Although it's free in our case,
* we can't run as many Windows and MacOS runners as we want.
*
* Because of this, we partition shards into groups, given that:
* - There are 16 shards in total
* - We can't run more than 10 Windows runners
* - We can't run more than 5 MacOS runners
*/
const shardGrouping: Record<string, string[][]> = {
'ubuntu-latest': scheduleItems(shardKeys, 16),
};
if (process.env.ALL_PLATFORMS === 'true') {
// shardGrouping['windows-latest'] = scheduleItems(shardKeys, 8);
shardGrouping['macos-latest'] = scheduleItems(shardKeys, 4);
}
const shardGroups: ShardGroup[] = [];
for (const [os, groups] of Object.entries(shardGrouping)) {
const coverage = os === 'ubuntu-latest';
const total = groups.length;
for (let idx = 0; idx < groups.length; idx += 1) {
const number = idx + 1;
const platform = os.replace(/-latest$/, '');
const name =
platform === 'ubuntu'
? `test (${number}/${total})`
: `test-${platform} (${number}/${total})`;
const shards = groups[idx];
const cacheKey = crypto
.createHash('md5')
.update(shards.join(':'))
.digest('hex');
const runnerTimeoutMinutes =
{
ubuntu: 10,
windows: 20,
macos: 20,
}[platform] ?? 20;
const testTimeoutMilliseconds =
{
windows: 240000,
}[platform] ?? 120000;
shardGroups.push({
os: os as RunsOn,
coverage,
name,
shards: shards.join(' '),
'cache-key': cacheKey,
'runner-timeout-minutes': runnerTimeoutMinutes,
'test-timeout-milliseconds': testTimeoutMilliseconds,
'upload-artifact-name': `coverage-${shards.sort().join('_')}`,
});
}
}
/**
* Output will be consumed by `setup` CI job.
*/
// eslint-disable-next-line no-console
console.log(`test-shard-matrix=${JSON.stringify(shardGroups)}`);