Skip to content

Commit b1278cc

Browse files
kibanamachinedelanniCopilot
authored
[8.19] [ci] push script/eslint calls through run() to add timing data (#225306) (#226140)
# Backport This will backport the following commits from `main` to `8.19`: - [[ci] push script/eslint calls through run() to add timing data (#225306)](#225306) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Alex Szabo","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-07-02T10:24:57Z","message":"[ci] push script/eslint calls through run() to add timing data (#225306)\n\n## Summary\nThis PR adds a new script, `node scripts/node scripts/eslint_all_files`\n- this would do the equivalent of `git ls-files | grep -E\n'\\.(js|mjs|ts|tsx) BACKPORT--> | xargs -n 250 -P 8 node scripts/eslint\n--no-cache`, but wrapped in our dev-cli runner, so the total run will be\nmeasured and reported.\n\nIt also adds the runner wrapper around the previous eslint script -\nhowever, the calls from the above script (eslint_all_files) would run\npreventing reporting so as not to overload ci-stats.\n\n---------\n\nCo-authored-by: Copilot <[email protected]>","sha":"2a97f913ba02c5a4c1086190b71b3d402a2cbf28","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Operations","release_note:skip","backport:all-open","v9.2.0"],"title":"[ci] push script/eslint calls through run() to add timing data","number":225306,"url":"https://github.com/elastic/kibana/pull/225306","mergeCommit":{"message":"[ci] push script/eslint calls through run() to add timing data (#225306)\n\n## Summary\nThis PR adds a new script, `node scripts/node scripts/eslint_all_files`\n- this would do the equivalent of `git ls-files | grep -E\n'\\.(js|mjs|ts|tsx) BACKPORT--> | xargs -n 250 -P 8 node scripts/eslint\n--no-cache`, but wrapped in our dev-cli runner, so the total run will be\nmeasured and reported.\n\nIt also adds the runner wrapper around the previous eslint script -\nhowever, the calls from the above script (eslint_all_files) would run\npreventing reporting so as not to overload ci-stats.\n\n---------\n\nCo-authored-by: Copilot <[email protected]>","sha":"2a97f913ba02c5a4c1086190b71b3d402a2cbf28"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/225306","number":225306,"mergeCommit":{"message":"[ci] push script/eslint calls through run() to add timing data (#225306)\n\n## Summary\nThis PR adds a new script, `node scripts/node scripts/eslint_all_files`\n- this would do the equivalent of `git ls-files | grep -E\n'\\.(js|mjs|ts|tsx) BACKPORT--> | xargs -n 250 -P 8 node scripts/eslint\n--no-cache`, but wrapped in our dev-cli runner, so the total run will be\nmeasured and reported.\n\nIt also adds the runner wrapper around the previous eslint script -\nhowever, the calls from the above script (eslint_all_files) would run\npreventing reporting so as not to overload ci-stats.\n\n---------\n\nCo-authored-by: Copilot <[email protected]>","sha":"2a97f913ba02c5a4c1086190b71b3d402a2cbf28"}}]}] BACKPORT--> Co-authored-by: Alex Szabo <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 4322e2d commit b1278cc

File tree

4 files changed

+218
-36
lines changed

4 files changed

+218
-36
lines changed

.buildkite/scripts/steps/lint.sh

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,17 @@ echo '--- Lint: eslint'
1515
# after possibly commiting fixed files to the repo
1616
set +e;
1717
if is_pr && ! is_auto_commit_disabled; then
18-
git ls-files | grep -E '\.(js|mjs|ts|tsx)$' | xargs -n 250 -P 8 node scripts/eslint --no-cache --fix
18+
desc="node scripts/eslint_all_files --no-cache --fix"
19+
node scripts/eslint_all_files --no-cache --fix
1920
else
20-
git ls-files | grep -E '\.(js|mjs|ts|tsx)$' | xargs -n 250 -P 8 node scripts/eslint --no-cache
21+
desc="node scripts/eslint_all_files --no-cache"
22+
node scripts/eslint_all_files --no-cache
2123
fi
2224

2325
eslint_exit=$?
2426
# re-enable "Exit immediately" mode
2527
set -e;
2628

27-
desc="node scripts/eslint --no-cache"
28-
if is_pr && ! is_auto_commit_disabled; then
29-
desc="$desc --fix"
30-
fi
31-
3229
check_for_changed_files "$desc" true
3330

3431
if [[ "${eslint_exit}" != "0" ]]; then

scripts/eslint_all_files.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
require('../src/setup_node_env');
11+
require('../src/dev/eslint/run_eslint_full');

src/dev/eslint/run_eslint_full.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { run } from '@kbn/dev-cli-runner';
11+
import { REPO_ROOT } from '@kbn/repo-info';
12+
import { ToolingLog } from '@kbn/tooling-log';
13+
import execa from 'execa';
14+
15+
const batchSize = 250;
16+
const maxParallelism = 8;
17+
18+
run(
19+
async ({ log, flags }) => {
20+
const bail = !!(flags.bail || false);
21+
22+
const { batches, files } = getLintableFileBatches();
23+
log.info(`Found ${files.length} files in ${batches.length} batches to lint.`);
24+
25+
const eslintArgs =
26+
// Unexpected will contain anything meant for ESLint directly, like `--fix`
27+
flags.unexpected
28+
// ESLint has no cache by default
29+
.concat([flags.cache ? '--cache' : '--no-cache']);
30+
log.info(
31+
`Running ESLint with args: ${pretty({
32+
args: eslintArgs,
33+
batchSize,
34+
maxParallelism,
35+
})}`
36+
);
37+
38+
const lintPromiseThunks = batches.map(
39+
(batch, idx) => () =>
40+
lintFileBatch({ batch, idx, eslintArgs, batchCount: batches.length, bail, log })
41+
);
42+
const results = await runBatchedPromises(lintPromiseThunks, maxParallelism);
43+
44+
const failedBatches = results.filter((result) => !result.success);
45+
if (failedBatches.length > 0) {
46+
log.error(`Linting errors found ❌`);
47+
process.exit(1);
48+
} else {
49+
log.info('Linting successful ✅');
50+
}
51+
},
52+
{
53+
description: 'Run ESLint on all JavaScript/TypeScript files in the repository',
54+
flags: {
55+
boolean: ['bail', 'cache'],
56+
default: {
57+
bail: false,
58+
cache: true, // Enable caching by default
59+
},
60+
allowUnexpected: true,
61+
help: `
62+
--bail Stop on the first linting error
63+
--no-cache Disable ESLint caching
64+
`,
65+
},
66+
}
67+
);
68+
69+
function getLintableFileBatches() {
70+
const files = execa
71+
.sync('git', ['ls-files'], {
72+
cwd: REPO_ROOT,
73+
encoding: 'utf8',
74+
})
75+
.stdout.trim()
76+
.split('\n')
77+
.filter((file) => file.match(/\.(js|mjs|ts|tsx)$/));
78+
const batches = [];
79+
for (let i = 0; i < files.length; i += batchSize) {
80+
batches.push(files.slice(i, i + batchSize));
81+
}
82+
return { batches, files };
83+
}
84+
85+
async function lintFileBatch({
86+
batch,
87+
bail,
88+
idx,
89+
eslintArgs,
90+
batchCount,
91+
log,
92+
}: {
93+
batch: string[];
94+
bail: boolean;
95+
idx: number;
96+
eslintArgs: string[];
97+
batchCount: number;
98+
log: ToolingLog;
99+
}) {
100+
log.info(`Running batch ${idx + 1}/${batchCount} with ${batch.length} files...`);
101+
102+
const timeBefore = Date.now();
103+
const args = ['scripts/eslint'].concat(eslintArgs).concat(batch);
104+
const { stdout, stderr, exitCode } = await execa('node', args, {
105+
cwd: REPO_ROOT,
106+
env: {
107+
// Disable CI stats for individual runs, to avoid overloading ci-stats
108+
CI_STATS_DISABLED: 'true',
109+
},
110+
reject: bail, // Don't throw on non-zero exit code
111+
});
112+
113+
const time = Date.now() - timeBefore;
114+
if (exitCode !== 0) {
115+
const errorMessage = stderr?.toString() || stdout?.toString();
116+
log.error(`Batch ${idx + 1}/${batchCount} failed (${time}ms) ❌: ${errorMessage}`);
117+
return {
118+
success: false,
119+
idx,
120+
time,
121+
error: errorMessage,
122+
};
123+
} else {
124+
log.info(`Batch ${idx + 1}/${batchCount} success (${time}ms) ✅: ${stdout.toString()}`);
125+
return {
126+
success: true,
127+
idx,
128+
time,
129+
};
130+
}
131+
}
132+
133+
function runBatchedPromises<T>(
134+
promiseCreators: Array<() => Promise<T>>,
135+
maxParallel: number
136+
): Promise<T[]> {
137+
const results: T[] = [];
138+
let i = 0;
139+
140+
const next: () => Promise<any> = () => {
141+
if (i >= promiseCreators.length) {
142+
return Promise.resolve();
143+
}
144+
145+
const promiseCreator = promiseCreators[i++];
146+
return Promise.resolve(promiseCreator()).then((result) => {
147+
results.push(result);
148+
return next();
149+
});
150+
};
151+
152+
const tasks = Array.from({ length: maxParallel }, () => next());
153+
return Promise.all(tasks).then(() => results);
154+
}
155+
156+
function pretty(obj: any) {
157+
return JSON.stringify(obj, null, 2);
158+
}

src/dev/run_eslint.js

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,55 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import yargs from 'yargs';
10+
import { run } from '@kbn/dev-cli-runner';
1111

1212
import { eslintBinPath } from './eslint';
1313

14-
let quiet = true;
15-
if (process.argv.includes('--no-quiet')) {
16-
quiet = false;
17-
} else {
18-
process.argv.push('--quiet');
19-
}
20-
21-
const options = yargs(process.argv).argv;
2214
process.env.KIBANA_RESOLVER_HARD_CACHE = 'true';
2315

24-
if (!options._.length && !options.printConfig) {
25-
process.argv.push('.');
26-
}
27-
28-
if (!process.argv.includes('--no-cache')) {
29-
process.argv.push('--cache');
30-
}
31-
32-
if (!process.argv.includes('--ext')) {
33-
process.argv.push('--ext', '.js,.mjs,.ts,.tsx');
34-
}
35-
36-
// common-js is required so that logic before this executes before loading eslint
37-
require(eslintBinPath); // eslint-disable-line import/no-dynamic-require
38-
39-
if (quiet) {
40-
process.on('exit', (code) => {
41-
if (!code) {
42-
console.log('✅ no eslint errors found');
16+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
17+
console.log(
18+
"This is a wrapper around ESLint's CLI that sets some defaults - see Eslint's help for flags:"
19+
);
20+
require(eslintBinPath); // eslint-disable-line import/no-dynamic-require
21+
} else {
22+
run(
23+
({ flags }) => {
24+
flags._ = flags._ || [];
25+
26+
// verbose is only a flag for our CLI runner, not for ESLint
27+
if (process.argv.includes('--verbose')) {
28+
process.argv.splice(process.argv.indexOf('--verbose'), 1);
29+
} else {
30+
process.argv.push('--quiet');
31+
}
32+
33+
if (flags.cache) {
34+
process.argv.push('--cache');
35+
}
36+
37+
if (!flags._.ext) {
38+
process.argv.push('--ext', '.js,.mjs,.ts,.tsx');
39+
}
40+
41+
// common-js is required so that logic before this executes before loading eslint
42+
// requiring the module is still going to pass along all flags
43+
require(eslintBinPath); // eslint-disable-line import/no-dynamic-require
44+
45+
process.on('exit', (code) => {
46+
if (!code) {
47+
console.log('✅ no eslint errors found');
48+
}
49+
});
50+
},
51+
{
52+
description: 'Run ESLint on all JavaScript/TypeScript files in the repository',
53+
usage: 'node scripts/eslint.js [options] [<file>...]',
54+
flags: {
55+
allowUnexpected: true,
56+
boolean: ['cache', 'fix', 'quiet'],
57+
string: ['ext'],
58+
},
4359
}
44-
});
60+
);
4561
}

0 commit comments

Comments
 (0)