Skip to content

Commit 77add7f

Browse files
JoshuaKGoldbergnzakasfiskerkecrilylumirlumir
authored
chore: add initial ecosystem plugin tests workflow (#19643)
* chore: add initial ecosystem plugin tests workflow * Apply suggestions from code review Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com> * Comments and structure * Switch to nano-spawn, with plugins.json * Revert unintentional docs/src/_data/further_reading_links.json changes * Tweaked docs text for runCommand * Added a script to update data * Fix: errororing on error for unknown plugin * Add weekly cron to update data * Add comments to match index.mjs * fix: .json files are formatted with two spaces * Apply suggestions from code review Co-authored-by: fisker Cheung <lionkay@gmail.com> * Added the once-a-week comment * Default to all * Mention the default of all * npm run test:ecosystem:update * rm duplicate .json * DISCORD_CONTRIBUTORS_WEBHOOK * Correct error message for all plugins * Apply suggestions from code review Co-authored-by: Percy Ma <kecrily@gmail.com> * Apply suggestions from code review Co-authored-by: 루밀LuMir <rpfos@naver.com> Co-authored-by: Percy Ma <kecrily@gmail.com> * Apply suggestions from code review Co-authored-by: 루밀LuMir <rpfos@naver.com> * Continue bumping to latest * chore: formatting * Finish migration off of chalk * Remove nano-spawn * Added typescript-eslint and docs * Apply suggestions from code review Co-authored-by: 루밀LuMir <rpfos@naver.com> * More docs, and use prettier * JSDoc correction * Import sorting, while I'm in the area * fix: proper cross-OS file path/URLs to package.json * nit: proper comment placement * 'local' ESLint, not 'built' * further 'local' * Add debug() * NI_AUTO_INSTALL * Update package.json Co-authored-by: 唯然 <weiran.zsd@outlook.com> * NI_DEFAULT_AGENT * { NI_DEFAULT_AGENT: "npm" } * Switch to hardcoded commands * feat: granular failing * fix: remove erroneous build scripts * Bump to latest commits, and add quick summaries * remove antfu/ni now * remove markdown, typescript-eslint * Apply suggestions from code review Co-authored-by: Francesco Trotta <github@fasttime.org> Co-authored-by: 루밀LuMir <rpfos@naver.com> * recursive * correction: result(s); add to ignores --------- Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com> Co-authored-by: fisker Cheung <lionkay@gmail.com> Co-authored-by: Percy Ma <kecrily@gmail.com> Co-authored-by: 루밀LuMir <rpfos@naver.com> Co-authored-by: 唯然 <weiran.zsd@outlook.com> Co-authored-by: Francesco Trotta <github@fasttime.org>
1 parent 32cc7ab commit 77add7f

12 files changed

Lines changed: 484 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Test Ecosystem Plugins
2+
3+
on:
4+
push:
5+
branches:
6+
- "ecosystem/*"
7+
schedule:
8+
# “At 00:00.” https://crontab.guru/#0_0_*_*_*
9+
- cron: "0 0 * * *"
10+
workflow_dispatch: ~
11+
12+
permissions: read-all
13+
14+
jobs:
15+
test_ecosystem:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- uses: actions/checkout@v6
20+
- uses: actions/setup-node@v6
21+
with:
22+
node-version: "lts/*"
23+
- run: npm install
24+
- id: tester
25+
run: npm run test:ecosystem -- --plugin all
26+
- if: steps.tester.outcome == 'failure' && github.head_ref == 'main'
27+
env:
28+
DISCORD_WEBHOOK: ${{ secrets.DISCORD_CONTRIBUTORS_WEBHOOK }}
29+
uses: Ilshidur/action-discord@0c4b27844ba47cb1c7bee539c8eead5284ce9fa9 # v0.3.2
30+
with:
31+
args: "Ecosystem tests failed."
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Update Test Ecosystem Plugins
2+
3+
on:
4+
schedule:
5+
# “At 00:00 on Monday.” https://crontab.guru/#0_0_*_*_1
6+
- cron: "0 0 * * 1"
7+
workflow_dispatch: ~
8+
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
13+
jobs:
14+
test_ecosystem:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v6
19+
- uses: actions/setup-node@v6
20+
with:
21+
node-version: "lts/*"
22+
- run: npm install
23+
- run: npm run test:ecosystem:update
24+
- uses: peter-evans/create-pull-request@v8
25+
with:
26+
commit-message: "chore: update ecosystem plugins"
27+
title: "chore: update ecosystem plugins"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
test.js
55
coverage/
66
build/
7+
ecosystem/
78
npm-debug.log
89
yarn-error.log
910
.pnpm-debug.log

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ docs/src/_includes
99
docs/src/use/formatters/html-formatter-example.html
1010
docs/src/use/formatters/index.md
1111
docs/src/rules/*.md
12+
ecosystem
1213
packages/js/src/configs/eslint-all.js
1314
packages/js/src/configs/eslint-recommended.js
1415
tests/fixtures

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ module.exports = defineConfig([
8383
"coverage/**",
8484
"docs/!(src|tools)/",
8585
"docs/src/!(_data)",
86+
"ecosystem/**",
8687
"jsdoc/**",
8788
"lib/types/**/*.ts",
8889
"templates/**",

knip.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"tools/generate-formatter-examples.js",
2626
],
2727
"ignoreDependencies": [
28+
// Underlying code coverage engine used in unit tests
2829
"c8",
2930
// Optional peer dependency used for loading TypeScript configuration files
3031
"jiti",

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
"test": "node Makefile.js test",
7373
"test:browser": "node Makefile.js cypress",
7474
"test:cli": "mocha",
75+
"test:ecosystem": "node tools/test-ecosystem/index.mjs",
76+
"test:ecosystem:update": "node tools/test-ecosystem/update.mjs",
7577
"test:emfile": "node tools/check-emfile-handling.js",
7678
"test:fuzz": "node Makefile.js fuzz",
7779
"test:performance": "node Makefile.js perf",

tools/test-ecosystem/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Ecosystem Tests
2+
3+
These tests run notable community plugins against the local ESLint repository.
4+
They're meant to validate that current changes to ESLint won't break downstream consumers.
5+
6+
## Running
7+
8+
To build and test all plugins:
9+
10+
```shell
11+
npm run test:ecosystem
12+
```
13+
14+
To run on just one plugin:
15+
16+
```shell
17+
npm run test:ecosystem -- --plugin <plugin-name>
18+
```
19+
20+
Plugins are stored in `plugins-data.json`.
21+
Plugin names are keys from that file.
22+
For example, to test against `@eslint/css`:
23+
24+
```shell
25+
npm run test:ecosystem -- --plugin @eslint/css
26+
```
27+
28+
### Debugging Commands
29+
30+
The [`debug`](https://www.npmjs.com/package/debug) package is used to surface the stdout of commands when `DEBUG=test:ecosystem` is enabled.
31+
32+
```shell
33+
DEBUG=test:ecosystem npm run test:ecosystem -- --plugin @eslint/css
34+
```
35+
36+
## Updating
37+
38+
`plugins-data.json` contains pinned commit hashes for each repository.
39+
Those hashes can be updated with the same script run in CI.
40+
41+
To update all plugins:
42+
43+
```shell
44+
npm run test:ecosystem:update
45+
```
46+
47+
To update just one plugin:
48+
49+
```shell
50+
npm run test:ecosystem:update -- --plugin <plugin-name>
51+
```

tools/test-ecosystem/data.mjs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @fileoverview Data utilities for ecosystem tests and updates to data.
3+
* @author Josh Goldberg
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Requirements
8+
//-----------------------------------------------------------------------------
9+
10+
import util, { styleText } from "node:util";
11+
12+
//-----------------------------------------------------------------------------
13+
// Types
14+
//-----------------------------------------------------------------------------
15+
16+
/**
17+
* Command-line scripts to run on a plugin.
18+
* @typedef {Object} PluginData
19+
* @property {string?} build Command to build files before tests, if defined.
20+
* @property {string} install Command to install dependencies.
21+
* @property {string} test Command to run tests.
22+
*/
23+
24+
/**
25+
* Settings for how to clone, set up, and test an ecosystem plugin.
26+
* @typedef {Object} PluginData
27+
* @property {PluginCommands} commands Command-line scripts to run on the plugin.
28+
* @property {string} commit Hash to check out after cloning the plugin.
29+
* @property {string} repository Repository URL to clone the plugin from.
30+
*/
31+
32+
//-----------------------------------------------------------------------------
33+
// Constants
34+
//-----------------------------------------------------------------------------
35+
36+
export const pluginDataFilePath = new URL("plugins-data.json", import.meta.url);
37+
38+
//-----------------------------------------------------------------------------
39+
// Functions
40+
//-----------------------------------------------------------------------------
41+
42+
/**
43+
* @param {"test" | "update"} action
44+
* @returns {[string, PluginData][]}
45+
*/
46+
export async function getPlugins(action) {
47+
const { values } = util.parseArgs({
48+
options: {
49+
plugin: {
50+
type: "string",
51+
},
52+
},
53+
});
54+
55+
const { plugin: pluginRequested = "all" } = values;
56+
const { default: pluginsData } = await import(pluginDataFilePath, {
57+
with: { type: "json" },
58+
});
59+
60+
if (pluginRequested !== "all" && !(pluginRequested in pluginsData)) {
61+
console.error(`The plugin "${values.plugin}" is not supported.`);
62+
console.error(
63+
`Supported plugins are: ${["", ...Object.keys(pluginsData)].join(
64+
"\n ",
65+
)}`,
66+
);
67+
console.error(
68+
`Alternately, run without --plugin to ${action} all plugins.`,
69+
);
70+
process.exit(1);
71+
}
72+
73+
const pluginsSelected =
74+
pluginRequested === "all"
75+
? Object.entries(pluginsData)
76+
: [[pluginRequested, pluginsData[pluginRequested]]];
77+
78+
console.log(
79+
`Plugins to ${action}:`,
80+
styleText("bold", pluginsSelected.map(([key]) => key).join(", ")),
81+
);
82+
83+
return { pluginsData, pluginsSelected };
84+
}

tools/test-ecosystem/index.mjs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @fileoverview A utility to test ecosystem plugin(s) against the local ESLint.
3+
* @author Josh Goldberg
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Requirements
8+
//-----------------------------------------------------------------------------
9+
10+
import debug from "debug";
11+
import spawn from "cross-spawn";
12+
import fs from "node:fs/promises";
13+
import path from "node:path";
14+
import { styleText } from "node:util";
15+
16+
import { getPlugins } from "./data.mjs";
17+
18+
const log = debug("test:ecosystem");
19+
20+
/**
21+
* @typedef {import("./data").PluginSettings} PluginSettings
22+
*/
23+
24+
//-----------------------------------------------------------------------------
25+
// Helpers
26+
//-----------------------------------------------------------------------------
27+
28+
/**
29+
* Runs ecosystem tests for a single plugin. It will:
30+
* 1. Clone the plugin repository into a sandbox directory
31+
* 2. Check out the plugin's commit to test on
32+
* 3. Install the plugin's dependencies
33+
* 4. Link the local ESLint into the plugin
34+
* 5. Build, if the plugin defines a build script
35+
* 6. Run tests
36+
* This intentionally does not try/catch: any errors will be thrown.
37+
*
38+
* @param {string} pluginKey
39+
* @param {PluginSettings} pluginSettings
40+
*/
41+
async function runTests(pluginKey, pluginSettings) {
42+
const directory = path.join(
43+
SANDBOX_DIRECTORY,
44+
pluginKey
45+
.replaceAll(/[^a-z-]/g, " ")
46+
.trim()
47+
.replaceAll(" ", "-"),
48+
);
49+
console.log(styleText("bold", `Testing ${pluginKey} in ${directory}`));
50+
51+
/**
52+
* Attempts to run a command in the plugin sandbox directory.
53+
* If it fails, any error stdout will be logged in red before a (re-)thrown error.
54+
* @param {string} command
55+
* @param {string[]} args
56+
*/
57+
const runCommand = ([command, ...args]) => {
58+
console.log(
59+
styleText("gray", `[${pluginKey}] ${[command, ...args].join(" ")}`),
60+
);
61+
62+
const result = spawn.sync(command, args, {
63+
cwd: directory,
64+
stdio: log.enabled ? "inherit" : undefined,
65+
});
66+
67+
if (result.status || result.error) {
68+
throw result.error ?? new Error(result.stderr.toString());
69+
}
70+
71+
return result;
72+
};
73+
74+
// 1. Clone the plugin repository into a sandbox directory
75+
await fs.mkdir(directory, { recursive: true });
76+
runCommand([
77+
"git",
78+
"clone",
79+
pluginSettings.repository,
80+
directory,
81+
"--depth",
82+
"1",
83+
]);
84+
85+
// 2. Check out the plugin's commit to test on
86+
runCommand(["git", "fetch", "origin", pluginSettings.commit]);
87+
runCommand(["git", "checkout", pluginSettings.commit]);
88+
89+
// 3. Install the plugin's dependencies
90+
runCommand(["pwd"]);
91+
runCommand(pluginSettings.commands.install);
92+
93+
// 4. Link the local ESLint into the plugin
94+
runCommand(["npm", "link", "eslint"]);
95+
96+
// 5. Build, if the plugin defines a build script
97+
if (pluginSettings.commands.build) {
98+
runCommand(pluginSettings.commands.build);
99+
}
100+
101+
// 6. Run the plugin's tests
102+
runCommand(pluginSettings.commands.test);
103+
}
104+
105+
//-----------------------------------------------------------------------------
106+
// Main
107+
//-----------------------------------------------------------------------------
108+
109+
const { pluginsSelected } = await getPlugins("test");
110+
111+
const SANDBOX_DIRECTORY = path.join(process.cwd(), "ecosystem");
112+
113+
console.log(`Clearing existing sandbox directory: ${SANDBOX_DIRECTORY}`);
114+
await fs.rm(SANDBOX_DIRECTORY, {
115+
force: true,
116+
maxRetries: 8,
117+
recursive: true,
118+
});
119+
await fs.mkdir(SANDBOX_DIRECTORY, { recursive: true });
120+
console.log("");
121+
122+
const errors = [];
123+
124+
// For each plugin to test, we try to runTests, recording thrown exceptions in errors
125+
for (const [pluginKey, pluginSettings] of pluginsSelected) {
126+
try {
127+
await runTests(pluginKey, pluginSettings);
128+
console.log(`Passed: ${pluginKey}\n`);
129+
} catch (error) {
130+
errors.push({ error, pluginKey });
131+
console.log(styleText("red", `Failed: ${pluginKey}\n`));
132+
}
133+
}
134+
135+
// If we had any errors, report them and exit as failed
136+
if (errors.length) {
137+
console.error(styleText("red", "Errors occurred while testing plugins:\n"));
138+
for (const { error, pluginKey } of errors) {
139+
console.error(
140+
`${styleText(["bold", "red"], pluginKey)}: ${styleText("red", `${error.stack || error}`)}\n`,
141+
);
142+
}
143+
process.exitCode = 1;
144+
} else {
145+
console.log(styleText("green", "All tests completed successfully."));
146+
}

0 commit comments

Comments
 (0)