Skip to content
Open
Show file tree
Hide file tree
Changes from 24 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@v4
- uses: actions/setup-node@v4
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@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
- run: npm install
- run: npm run test:ecosystem:update -- --plugin all
- 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 @@ -3,6 +3,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 nano-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
4 changes: 4 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:fuzz": "node Makefile.js fuzz",
"test:performance": "node Makefile.js perf",
"test:emfile": "node tools/check-emfile-handling.js",
Expand Down Expand Up @@ -141,6 +143,7 @@
"optionator": "^0.9.3"
},
"devDependencies": {
"@antfu/ni": "^24.3.0",
"@arethetypeswrong/cli": "^0.18.0",
"@babel/core": "^7.4.3",
"@babel/preset-env": "^7.4.3",
Expand Down Expand Up @@ -188,6 +191,7 @@
"metascraper-logo-favicon": "^5.25.7",
"metascraper-title": "^5.25.7",
"mocha": "^11.7.1",
"nano-spawn": "^1.0.1",
"node-polyfill-webpack-plugin": "^1.0.3",
"npm-license": "^0.3.3",
"pirates": "^4.0.5",
Expand Down
82 changes: 82 additions & 0 deletions tools/test-ecosystem/data.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @fileoverview Data utilities for ecosystem tests and updates to data.
* @author Josh Goldberg
*/

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

import chalk from "chalk";
import util from "node:util";
import path from "node:path";

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

/**
* Settings for how to clone, set up, and test an ecosystem plugin.
* @typedef {Object} PluginData
* @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 = path.join(
import.meta.dirname,
"plugins-data.json",
);

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

/**
*
* @param {"test" | "update"} action
* @returns {[string, PluginData][]}
*/
export async function getPlugins(action) {
const { values } = util.parseArgs({
options: {
plugin: {
type: "string",
help: `The name of the plugin to ${action}, or 'all' for all plugins (default: 'all')`,
},
},
});

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}:`,
chalk.bold(pluginsSelected.map(([key]) => key).join(", ")),
);

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

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

import chalk from "chalk";
import spawn from "nano-spawn";
import fs from "node:fs/promises";
import path from "node:path";
import { getPlugins } from "./data.mjs";

/**
* @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 built 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(chalk.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 = async (command, ...args) => {
console.log(chalk.gray(`[${pluginKey}]`, [command, ...args].join(" ")));
try {
return await spawn(command, args, {
cwd: directory,
});
} catch (error) {
console.error(
chalk.red(`[${pluginKey}]`),
"stdout" in error ? error.stdout : error,
);
throw error;
}
};

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

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

// 3. Install the plugin's dependencies
await runCommand("pwd");
await runCommand("ni");

// 4. Link the built ESLint into the plugin
await runCommand("npm", "link", "eslint");

const packageJsonPath = path.resolve(
process.cwd(),
path.join(directory, "package.json"),
);
const packageJson = await import(packageJsonPath, {
with: { type: "json" },
});

// 5. Build, if the plugin defines a build script
if (packageJson.default.scripts.build) {
await runCommand("nr", "build");
}

// 6. Run test
await runCommand("nr", "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(chalk.red("Errors occurred while testing plugins:"));
for (const { error, pluginKey } of errors) {
console.error(`${chalk.bold.red(pluginKey)}: ${chalk.red(error)}`);
}
process.exitCode = 1;
} else {
console.log(chalk.green("All tests completed successfully."));
}
26 changes: 26 additions & 0 deletions tools/test-ecosystem/plugins-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"@eslint/css": {
"commit": "26b902c4f42cccb33d7f8119a3376773e0ad91bd",
"repository": "https://github.com/eslint/css"
},
"@eslint/json": {
"commit": "2d9722a9a3bab75301906d574266281305ec91df",
"repository": "https://github.com/eslint/json"
},
"@eslint/markdown": {
"commit": "614ef3e0bf539655170f202467119fbbbf8963f6",
"repository": "https://github.com/eslint/markdown"
},
"@eslint-community/eslint-plugin-eslint-comments": {
"commit": "d6869dfbb794437c00e0e27e6cc0dbdfd3c56fac",
"repository": "https://github.com/eslint-community/eslint-plugin-eslint-comments"
},
"eslint-plugin-unicorn": {
"commit": "f4eb2e731358d68b031d7fd42bf725a23f9d1e4d",
"repository": "https://github.com/sindresorhus/eslint-plugin-unicorn"
},
"eslint-plugin-vue": {
"commit": "fe0ce5a0590ee8b7aec57712f56f0d1d557035cb",
"repository": "https://github.com/vuejs/eslint-plugin-vue"
}
}
Loading
Loading