Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
baa84df
chore: add initial ecosystem plugin tests workflow
JoshuaKGoldberg Apr 21, 2025
b53fc7a
Merge branch 'main' into ecosystem-tests
JoshuaKGoldberg Apr 22, 2025
4bb2f02
Apply suggestions from code review
JoshuaKGoldberg May 12, 2025
f17af7d
Comments and structure
JoshuaKGoldberg May 12, 2025
30404a4
Switch to nano-spawn, with plugins.json
JoshuaKGoldberg May 19, 2025
d822b05
Merge branch 'main'
JoshuaKGoldberg May 19, 2025
4345a1a
Revert unintentional docs/src/_data/further_reading_links.json changes
JoshuaKGoldberg May 19, 2025
3e9eff8
Tweaked docs text for runCommand
JoshuaKGoldberg May 19, 2025
ba6cc11
Added a script to update data
JoshuaKGoldberg Jun 18, 2025
8674cba
Fix: errororing on error for unknown plugin
JoshuaKGoldberg Jun 18, 2025
08d32ac
Add weekly cron to update data
JoshuaKGoldberg Jun 18, 2025
073be89
Add comments to match index.mjs
JoshuaKGoldberg Jun 18, 2025
ece43e6
fix: .json files are formatted with two spaces
JoshuaKGoldberg Jun 21, 2025
27b1cf7
Apply suggestions from code review
JoshuaKGoldberg Jun 30, 2025
8b98d5b
Added the once-a-week comment
JoshuaKGoldberg Jul 15, 2025
d62f557
Merge branch 'main'
JoshuaKGoldberg Jul 15, 2025
2365eaa
Default to all
JoshuaKGoldberg Jul 15, 2025
cb2fd6a
Mention the default of all
JoshuaKGoldberg Jul 15, 2025
309d889
Merge branch 'main' into ecosystem-tests
JoshuaKGoldberg Jul 24, 2025
79b3c45
npm run test:ecosystem:update
JoshuaKGoldberg Jul 24, 2025
8312ff6
rm duplicate .json
JoshuaKGoldberg Jul 30, 2025
76daffd
Merge branch 'main' into ecosystem-tests
JoshuaKGoldberg Sep 25, 2025
1498b92
DISCORD_CONTRIBUTORS_WEBHOOK
JoshuaKGoldberg Sep 25, 2025
007a82e
Correct error message for all plugins
JoshuaKGoldberg Sep 25, 2025
b65db91
Merge branch 'main'
JoshuaKGoldberg Nov 10, 2025
5454ba9
Apply suggestions from code review
JoshuaKGoldberg Nov 10, 2025
0d60d7c
Apply suggestions from code review
JoshuaKGoldberg Nov 10, 2025
5288f49
Apply suggestions from code review
JoshuaKGoldberg Nov 10, 2025
e69f023
Continue bumping to latest
JoshuaKGoldberg Nov 10, 2025
bdf13de
chore: formatting
JoshuaKGoldberg Nov 10, 2025
aa75a32
Finish migration off of chalk
JoshuaKGoldberg Nov 10, 2025
c85df45
Remove nano-spawn
JoshuaKGoldberg Nov 11, 2025
a03b450
Added typescript-eslint and docs
JoshuaKGoldberg Nov 11, 2025
afe15a4
Merge branch 'main' into ecosystem-tests
JoshuaKGoldberg Nov 11, 2025
997af03
Apply suggestions from code review
JoshuaKGoldberg Nov 12, 2025
70fef79
More docs, and use prettier
JoshuaKGoldberg Nov 12, 2025
14d745a
JSDoc correction
JoshuaKGoldberg Nov 12, 2025
ba685b1
Import sorting, while I'm in the area
JoshuaKGoldberg Nov 12, 2025
2983232
fix: proper cross-OS file path/URLs to package.json
JoshuaKGoldberg Dec 20, 2025
d856e94
nit: proper comment placement
JoshuaKGoldberg Dec 20, 2025
b185105
'local' ESLint, not 'built'
JoshuaKGoldberg Dec 23, 2025
f60df01
further 'local'
JoshuaKGoldberg Dec 28, 2025
5815b23
Add debug()
JoshuaKGoldberg Dec 28, 2025
862dd99
NI_AUTO_INSTALL
JoshuaKGoldberg Jan 2, 2026
3c712da
Update package.json
JoshuaKGoldberg Jan 2, 2026
b941959
NI_DEFAULT_AGENT
JoshuaKGoldberg Jan 4, 2026
97e986e
{ NI_DEFAULT_AGENT: "npm" }
JoshuaKGoldberg Jan 4, 2026
0444246
Switch to hardcoded commands
JoshuaKGoldberg Jan 6, 2026
4d5e41d
Merge branch 'main' into ecosystem-tests
JoshuaKGoldberg Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ecosystem-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Test Ecosystem Plugins

on:
push:
branches:
- "ecosystem/*"
schedule:
# “At 00:00.” https://crontab.guru/#0_0_*_*_*
- cron: "0 0 * * *"
workflow_dispatch: ~

permissions: read-all

jobs:
test_ecosystem:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: "lts/*"
- run: npm install
- id: tester
run: npm run test:ecosystem -- --plugin all
- if: steps.tester.outcome == 'failure' && github.head_ref == 'main'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_CONTRIBUTORS_WEBHOOK }}
uses: Ilshidur/action-discord@0c4b27844ba47cb1c7bee539c8eead5284ce9fa9 # v0.3.2
with:
args: "Ecosystem tests failed."
25 changes: 25 additions & 0 deletions .github/workflows/ecosystem-updates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Update Test Ecosystem Plugins

on:
schedule:
# “At 00:00 on Monday.” https://crontab.guru/#0_0_*_*_1
- cron: "0 0 * * 1"
workflow_dispatch: ~

permissions: read-all

jobs:
test_ecosystem:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: "lts/*"
- run: npm install
- run: npm run test:ecosystem:update
- uses: peter-evans/[email protected]
with:
commit-message: "chore: update ecosystem plugins"
title: "chore: update ecosystem plugins"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
test.js
coverage/
build/
ecosystem/
npm-debug.log
yarn-error.log
.pnpm-debug.log
Expand Down
3 changes: 3 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"tools/generate-formatter-examples.js",
],
"ignoreDependencies": [
// Used in ecosystem.yml via cross-spawn shells
"@antfu/ni",
// Underlying code coverage engine used in unit tests
"c8",
// Optional peer dependency used for loading TypeScript configuration files
"jiti",
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
"test": "node Makefile.js test",
"test:browser": "node Makefile.js cypress",
"test:cli": "mocha",
"test:ecosystem": "node tools/test-ecosystem/index.mjs",
"test:ecosystem:update": "node tools/test-ecosystem/update.mjs",
"test:emfile": "node tools/check-emfile-handling.js",
"test:fuzz": "node Makefile.js fuzz",
"test:performance": "node Makefile.js perf",
Expand Down Expand Up @@ -138,6 +140,7 @@
"optionator": "^0.9.3"
},
"devDependencies": {
"@antfu/ni": "^28.0.0",
"@arethetypeswrong/cli": "^0.18.0",
"@babel/core": "^7.4.3",
"@babel/preset-env": "^7.4.3",
Expand Down
51 changes: 51 additions & 0 deletions tools/test-ecosystem/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Ecosystem Tests

These tests run notable community plugins against the local ESLint repository.
They're meant to validate that current changes to ESLint won't break downstream consumers.

## Running

To build and test all plugins:

```shell
npm run test:ecosystem
```

To run on just one plugin:

```shell
npm run test:ecosystem -- --plugin <plugin-name>
```

Plugins are stored in `plugins-data.json`.
Plugin names are keys from that file.
For example, to test against `@eslint/css`:

```shell
npm run test:ecosystem -- --plugin @eslint/css
```

### Debugging Commands

The [`debug`](https://www.npmjs.com/package/debug) package is used to surface the stdout of commands when `DEBUG=test:ecosystem` is enabled.

```shell
DEBUG=test:ecosystem npm run test:ecosystem -- --plugin @eslint/css
```

## Updating

`plugins-data.json` contains pinned commit hashes for each repository.
Those hashes can be updated with the same script run in CI.

To update all plugins:

```shell
npm run test:ecosystem:update
```

To update just one plugin:

```shell
npm run test:ecosystem:update -- --plugin <plugin-name>
```
84 changes: 84 additions & 0 deletions tools/test-ecosystem/data.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @fileoverview Data utilities for ecosystem tests and updates to data.
* @author Josh Goldberg
*/

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

import util, { styleText } from "node:util";

//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------

/**
* Command-line scripts to run on a plugin.
* @typedef {Object} PluginData
* @property {string?} build Command to build files before tests, if defined.
* @property {string} install Command to install dependencies.
* @property {string} test Command to run tests.
*/

/**
* Settings for how to clone, set up, and test an ecosystem plugin.
* @typedef {Object} PluginData
* @property {PluginCommands} commands Command-line scripts to run on the plugin.
* @property {string} commit Hash to check out after cloning the plugin.
* @property {string} repository Repository URL to clone the plugin from.
*/

//-----------------------------------------------------------------------------
// Constants
//-----------------------------------------------------------------------------

export const pluginDataFilePath = new URL("plugins-data.json", import.meta.url);

//-----------------------------------------------------------------------------
// Functions
//-----------------------------------------------------------------------------

/**
* @param {"test" | "update"} action
* @returns {[string, PluginData][]}
*/
export async function getPlugins(action) {
const { values } = util.parseArgs({
options: {
plugin: {
type: "string",
},
},
});

const { plugin: pluginRequested = "all" } = values;
const { default: pluginsData } = await import(pluginDataFilePath, {
with: { type: "json" },
});

if (pluginRequested !== "all" && !(pluginRequested in pluginsData)) {
console.error(`The plugin "${values.plugin}" is not supported.`);
console.error(
`Supported plugins are: ${["", ...Object.keys(pluginsData)].join(
"\n ",
)}`,
);
console.error(
`Alternately, run without --plugin to ${action} all plugins.`,
);
process.exit(1);
}

const pluginsSelected =
pluginRequested === "all"
? Object.entries(pluginsData)
: [[pluginRequested, pluginsData[pluginRequested]]];

console.log(
`Plugins to ${action}:`,
styleText("bold", pluginsSelected.map(([key]) => key).join(", ")),
);

return { pluginsData, pluginsSelected };
}
147 changes: 147 additions & 0 deletions tools/test-ecosystem/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* @fileoverview A utility to test ecosystem plugin(s) against the local ESLint.
* @author Josh Goldberg
*/

//-----------------------------------------------------------------------------
// Requirements
//-----------------------------------------------------------------------------

import debug from "debug";
import spawn from "cross-spawn";
import fs from "node:fs/promises";
import path from "node:path";
import { styleText } from "node:util";

import { getPlugins } from "./data.mjs";

const log = debug("test:ecosystem");

/**
* @typedef {import("./data").PluginSettings} PluginSettings
*/

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

/**
* Runs ecosystem tests for a single plugin. It will:
* 1. Clone the plugin repository into a sandbox directory
* 2. Check out the plugin's commit to test on
* 3. Install the plugin's dependencies
* 4. Link the local ESLint into the plugin
* 5. Build, if the plugin defines a build script
* 6. Run tests
* This intentionally does not try/catch: any errors will be thrown.
*
* @param {string} pluginKey
* @param {PluginSettings} pluginSettings
*/
async function runTests(pluginKey, pluginSettings) {
const directory = path.join(
SANDBOX_DIRECTORY,
pluginKey
.replaceAll(/[^a-z-]/g, " ")
.trim()
.replaceAll(" ", "-"),
);
console.log(styleText("bold", `Testing ${pluginKey} in ${directory}`));

/**
* Attempts to run a command in the plugin sandbox directory.
* If it fails, any error stdout will be logged in red before a re-throw.
* @param {string} command
* @param {string[]} args
*/
const runCommand = ([command, ...args]) => {
console.log(
styleText("gray", `[${pluginKey}] ${[command, ...args].join(" ")}`),
);
try {
return spawn.sync(command, args, {
cwd: directory,
stdio: log.enabled ? "inherit" : undefined,
});
Comment on lines +62 to +65
Copy link
Member

@fasttime fasttime Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next issue I'm seeing is that the ecosystem test passes even when an individual package test fails. For example npm install in @eslint/markdown is currently failing due to a build error:

[@eslint/markdown] npm install

> @eslint/[email protected] prepare
> npm run build


> @eslint/[email protected] build
> npm run build:rules && tsc && npm run build:update-rules-docs


> @eslint/[email protected] build:rules
> node tools/build-rules.js

Recommended rules generated successfully.
Rules import file generated successfully.
src/index.js:67:2 - error TS2742: The inferred type of 'configs' cannot be named without a reference to '../node_modules/eslint/node_modules/@eslint/core/dist/cjs/types.cjs'. This is likely not portable. A type annotation is necessary.

 67  configs: {
     ~~~~~~~~~~
 68   "recommended-legacy": {
    ~~~~~~~~~~~~~~~~~~~~~~~~~
...
129   ],
    ~~~~
130  },
    ~~


Found 1 error in src/index.js:67

It looks like the ecosystem test isn't failing when the underlying command fails. We should probably check the result of spawn.sync() to determine if the command has failed. The status property should contain the exit code of the command, if the command was run.

} catch (error) {
console.error(
styleText("red", `[${pluginKey}]`),
error.stdout || error,
);
throw error;
}
};

// 1. Clone the plugin repository into a sandbox directory
await fs.mkdir(directory, { force: true });
runCommand([
"git",
"clone",
pluginSettings.repository,
directory,
"--depth",
"1",
]);

// 2. Check out the plugin's commit to test on
runCommand(["git", "fetch", "origin", pluginSettings.commit]);
runCommand(["git", "checkout", pluginSettings.commit]);

// 3. Install the plugin's dependencies
runCommand(["pwd"]);
runCommand(pluginSettings.commands.install);

// 4. Link the local ESLint into the plugin
runCommand(["npm", "link", "eslint"]);

// 5. Build, if the plugin defines a build script
if (pluginSettings.commands.build) {
runCommand(pluginSettings.commands.build);
}

// 6. Run the plugin's tests
runCommand(pluginSettings.commands.test);
}

//-----------------------------------------------------------------------------
// Main
//-----------------------------------------------------------------------------

const { pluginsSelected } = await getPlugins("test");

const SANDBOX_DIRECTORY = path.join(process.cwd(), "ecosystem");

console.log(`Clearing existing sandbox directory: ${SANDBOX_DIRECTORY}`);
await fs.rm(SANDBOX_DIRECTORY, {
force: true,
maxRetries: 8,
recursive: true,
});
await fs.mkdir(SANDBOX_DIRECTORY, { recursive: true });
console.log("");

const errors = [];

// For each plugin to test, we try to runTests, recording thrown exceptions in errors
for (const [pluginKey, pluginSettings] of pluginsSelected) {
try {
await runTests(pluginKey, pluginSettings);
} catch (error) {
errors.push({ error, pluginKey });
}

console.log("");
}

// If we had any errors, report them and exit as failed
if (errors.length) {
console.error(styleText("red", "Errors occurred while testing plugins:"));
for (const { error, pluginKey } of errors) {
console.error(
`${styleText(["bold", "red"], pluginKey)}: ${styleText("red", `${error.stack || error}`)}`,
);
}
process.exitCode = 1;
} else {
console.log(styleText("green", "All tests completed successfully."));
}
Loading
Loading