ci: Use node-js script for running eslint
This is unnecessary hard in shell when compared to a proper programming language. It becomes even easier with a node-js script, as that gives us access to the underlying ESLint module rather than just the CLI interface. Besides that, node-js has the added benefit that we don't need to add more dependencies to the CI image. https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1497
This commit is contained in:
parent
9ef59b3b4f
commit
8c13e3855e
@ -9,6 +9,7 @@ stages:
|
|||||||
variables:
|
variables:
|
||||||
BUNDLE: "extensions-git.flatpak"
|
BUNDLE: "extensions-git.flatpak"
|
||||||
JS_LOG: "js-report.txt"
|
JS_LOG: "js-report.txt"
|
||||||
|
LINT_LOG: "eslint-report.txt"
|
||||||
|
|
||||||
.only_default: &only_default
|
.only_default: &only_default
|
||||||
only:
|
only:
|
||||||
@ -42,11 +43,12 @@ eslint:
|
|||||||
image: registry.gitlab.gnome.org/gnome/gnome-shell/extension-ci:v2
|
image: registry.gitlab.gnome.org/gnome/gnome-shell/extension-ci:v2
|
||||||
stage: review
|
stage: review
|
||||||
script:
|
script:
|
||||||
- ./.gitlab-ci/run-eslint.sh
|
- export NODE_PATH=$(npm root -g)
|
||||||
|
- ./.gitlab-ci/run-eslint --output-file ${LINT_LOG}
|
||||||
<<: *only_default
|
<<: *only_default
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- reports
|
- ${LINT_LOG}
|
||||||
when: always
|
when: always
|
||||||
|
|
||||||
potfile_check:
|
potfile_check:
|
||||||
|
128
.gitlab-ci/run-eslint
Executable file
128
.gitlab-ci/run-eslint
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { ESLint } = require('eslint');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
function createConfig(config) {
|
||||||
|
const options = {
|
||||||
|
cache: true,
|
||||||
|
cacheLocation: `.eslintcache-${config}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config === 'legacy')
|
||||||
|
options.overrideConfigFile='lint/eslintrc-legacy.yml';
|
||||||
|
|
||||||
|
return new ESLint(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function git(...args) {
|
||||||
|
const git = spawn('git', args, { stdio: ['ignore', null, 'ignore'] });
|
||||||
|
git.stdout.setEncoding('utf8');
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let out = '';
|
||||||
|
git.stdout.on('data', chunk => out += chunk);
|
||||||
|
git.stdout.on('end', () => resolve(out.trim()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCommon(report1, report2, ignoreColumn=false) {
|
||||||
|
return report1.map(result => {
|
||||||
|
const { filePath, messages } = result;
|
||||||
|
const match =
|
||||||
|
report2.find(r => r.filePath === filePath) || { messages: [] };
|
||||||
|
|
||||||
|
const filteredMessages = messages.filter(
|
||||||
|
msg => match.messages.some(
|
||||||
|
m => m.line === msg.line && (ignoreColumn || m.column === msg.column)));
|
||||||
|
|
||||||
|
const [errorCount, warningCount] = filteredMessages.reduce(
|
||||||
|
([e, w], msg) => {
|
||||||
|
return [
|
||||||
|
e + Number(msg.severity === 2),
|
||||||
|
w + Number(msg.severity === 1)];
|
||||||
|
}, [0, 0]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
messages: filteredMessages,
|
||||||
|
errorCount,
|
||||||
|
warningCount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMergeRequestChanges(remote, branch) {
|
||||||
|
await git('fetch', remote, branch);
|
||||||
|
const branchPoint = await git('merge-base', 'HEAD', 'FETCH_HEAD');
|
||||||
|
const diff = await git('diff', '-U0', `${branchPoint}...HEAD`);
|
||||||
|
|
||||||
|
const report = [];
|
||||||
|
let messages = null;
|
||||||
|
for (const line of diff.split('\n')) {
|
||||||
|
if (line.startsWith('+++ b/')) {
|
||||||
|
const filePath = path.resolve(line.substring(6));
|
||||||
|
messages = filePath.endsWith('.js') ? [] : null;
|
||||||
|
if (messages)
|
||||||
|
report.push({ filePath, messages });
|
||||||
|
} else if (messages && line.startsWith('@@ ')) {
|
||||||
|
[, , changes] = line.split(' ');
|
||||||
|
[start, count] = `${changes},1`.split(',').map(i => parseInt(i));
|
||||||
|
for (let i = start; i < start + count; i++)
|
||||||
|
messages.push({ line: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOption(...names) {
|
||||||
|
const optIndex =
|
||||||
|
process.argv.findIndex(arg => names.includes(arg)) + 1;
|
||||||
|
|
||||||
|
if (optIndex === 0)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
return process.argv[optIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function main() {
|
||||||
|
const outputOption = getOption('--output-file', '-o');
|
||||||
|
const outputPath = outputOption ? path.resolve(outputOption) : null;
|
||||||
|
|
||||||
|
const sourceDir = path.dirname(process.argv[1]);
|
||||||
|
process.chdir(path.resolve(sourceDir, '..'));
|
||||||
|
|
||||||
|
const remote = getOption('--remote') || 'origin';
|
||||||
|
const branch = getOption('--branch', '-b');
|
||||||
|
|
||||||
|
const sources = ['js', 'subprojects/extensions-app/js'];
|
||||||
|
const regular = createConfig('regular');
|
||||||
|
|
||||||
|
const ops = [];
|
||||||
|
ops.push(regular.lintFiles(sources));
|
||||||
|
if (branch)
|
||||||
|
ops.push(getMergeRequestChanges(remote, branch));
|
||||||
|
else
|
||||||
|
ops.push(createConfig('legacy').lintFiles(sources));
|
||||||
|
|
||||||
|
const results = await Promise.all(ops);
|
||||||
|
const commonResults = createCommon(...results, branch !== undefined);
|
||||||
|
|
||||||
|
const formatter = await regular.loadFormatter();
|
||||||
|
const resultText = formatter.format(commonResults);
|
||||||
|
|
||||||
|
if (outputPath) {
|
||||||
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
fs.writeFileSync(outputPath, resultText);
|
||||||
|
} else {
|
||||||
|
console.log(resultText);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exitCode = commonResults.some(r => r.errorCount > 0) ? 1 : 0;
|
||||||
|
})().catch((error) => {
|
||||||
|
process.exitCode = 1;
|
||||||
|
console.error(error);
|
||||||
|
});
|
@ -1,116 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
OUTPUT_REGULAR=reports/lint-regular-report.txt
|
|
||||||
OUTPUT_LEGACY=reports/lint-legacy-report.txt
|
|
||||||
OUTPUT_FINAL=reports/lint-common-report.txt
|
|
||||||
|
|
||||||
OUTPUT_MR=reports/lint-mr-report.txt
|
|
||||||
|
|
||||||
LINE_CHANGES=changed-lines.txt
|
|
||||||
|
|
||||||
is_empty() {
|
|
||||||
(! grep -q . $1)
|
|
||||||
}
|
|
||||||
|
|
||||||
run_eslint() {
|
|
||||||
ARGS_LEGACY='--config lint/eslintrc-legacy.yml'
|
|
||||||
|
|
||||||
local extra_args=ARGS_$1
|
|
||||||
local output_var=OUTPUT_$1
|
|
||||||
local output=${!output_var}
|
|
||||||
local cache=.eslintcache-${1,,}
|
|
||||||
|
|
||||||
# ensure output exists even if eslint doesn't report any errors
|
|
||||||
mkdir -p $(dirname $output)
|
|
||||||
touch $output
|
|
||||||
|
|
||||||
eslint -f unix --cache --cache-location $cache ${!extra_args} -o $output \
|
|
||||||
js subprojects/extensions-app/js
|
|
||||||
}
|
|
||||||
|
|
||||||
list_commit_range_additions() {
|
|
||||||
# Turn raw context-less git-diff into a list of
|
|
||||||
# filename:lineno pairs of new (+) lines
|
|
||||||
git diff -U0 "$@" -- js |
|
|
||||||
awk '
|
|
||||||
BEGIN { file=""; }
|
|
||||||
/^+++ b/ { file=substr($0,7); }
|
|
||||||
/^@@ / {
|
|
||||||
len = split($3,a,",")
|
|
||||||
start=a[1]
|
|
||||||
count=(len > 1) ? a[2] : 1
|
|
||||||
|
|
||||||
for (line=start; line<start+count; line++)
|
|
||||||
printf "%s/%s:%d:\n",ENVIRON["PWD"],file,line;
|
|
||||||
}'
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_matched_lines() {
|
|
||||||
local source=$1
|
|
||||||
local matches=$2
|
|
||||||
local target=$3
|
|
||||||
|
|
||||||
echo -n > $target
|
|
||||||
for l in $(<$matches); do
|
|
||||||
grep $l $source >> $target
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
create_common() {
|
|
||||||
# comm requires sorted input;
|
|
||||||
# we also strip the error message to make the following a "common" error:
|
|
||||||
# regular:
|
|
||||||
# file.js:42:23 Indentation of 55, expected 42
|
|
||||||
# legacy:
|
|
||||||
# file.js:42:23 Indentation of 55, extected 24
|
|
||||||
prepare() {
|
|
||||||
sed 's: .*::' $1 | sort
|
|
||||||
}
|
|
||||||
|
|
||||||
comm -12 <(prepare $OUTPUT_REGULAR) <(prepare $OUTPUT_LEGACY) >$OUTPUT_FINAL.tmp
|
|
||||||
|
|
||||||
# Now add back the stripped error messages
|
|
||||||
copy_matched_lines $OUTPUT_REGULAR $OUTPUT_FINAL.tmp $OUTPUT_FINAL
|
|
||||||
rm $OUTPUT_FINAL.tmp
|
|
||||||
}
|
|
||||||
|
|
||||||
# Disable MR handling for now. We aren't ready to enforce
|
|
||||||
# non-legacy style just yet ...
|
|
||||||
unset CI_MERGE_REQUEST_TARGET_BRANCH_NAME
|
|
||||||
|
|
||||||
REMOTE=${1:-$CI_MERGE_REQUEST_PROJECT_URL.git}
|
|
||||||
BRANCH_NAME=${2:-$CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
|
|
||||||
|
|
||||||
if [ "$BRANCH_NAME" ]; then
|
|
||||||
git fetch $REMOTE $BRANCH_NAME
|
|
||||||
branch_point=$(git merge-base HEAD FETCH_HEAD)
|
|
||||||
commit_range=$branch_point...HEAD
|
|
||||||
|
|
||||||
list_commit_range_additions $commit_range > $LINE_CHANGES
|
|
||||||
|
|
||||||
# Don't bother with running lint when no JS changed
|
|
||||||
if is_empty $LINE_CHANGES; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo Generating lint report using regular configuration
|
|
||||||
run_eslint REGULAR
|
|
||||||
echo Generating lint report using legacy configuration
|
|
||||||
run_eslint LEGACY
|
|
||||||
echo Done.
|
|
||||||
create_common
|
|
||||||
|
|
||||||
if ! is_empty $OUTPUT_FINAL; then
|
|
||||||
cat $OUTPUT_FINAL
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Just show the report and succeed when not testing a MR
|
|
||||||
if [ -z "$BRANCH_NAME" ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
copy_matched_lines $OUTPUT_REGULAR $LINE_CHANGES $OUTPUT_MR
|
|
||||||
cat $OUTPUT_MR
|
|
||||||
is_empty $OUTPUT_MR
|
|
Loading…
Reference in New Issue
Block a user