-
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
chore: add initial ecosystem plugin tests workflow #19643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
baa84df
b53fc7a
4bb2f02
f17af7d
30404a4
d822b05
4345a1a
3e9eff8
ba6cc11
8674cba
08d32ac
073be89
ece43e6
27b1cf7
8b98d5b
d62f557
2365eaa
cb2fd6a
309d889
79b3c45
8312ff6
76daffd
1498b92
007a82e
b65db91
5454ba9
0d60d7c
5288f49
e69f023
bdf13de
aa75a32
c85df45
a03b450
afe15a4
997af03
70fef79
14d745a
ba685b1
2983232
d856e94
b185105
f60df01
5815b23
862dd99
3c712da
b941959
97e986e
0444246
4d5e41d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| with: | ||
| args: "Ecosystem tests failed." | ||
| 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" | ||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| test.js | ||
| coverage/ | ||
| build/ | ||
| ecosystem/ | ||
| npm-debug.log | ||
| yarn-error.log | ||
| .pnpm-debug.log | ||
|
|
||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
| 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> | ||
| ``` |
| 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, { | ||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 }; | ||
| } | ||
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 [@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:67It looks like the ecosystem test isn't failing when the underlying command fails. We should probably check the result of |
||
| } 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.")); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.