From f200fdb5f3ff0ebf2ec603aa817cd286e96704c5 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Wed, 4 Nov 2026 22:47:09 +0800 Subject: [PATCH 1/2] feat: run rstack-editor E2E in the rstest and rslint suites The editor's E2E fixtures install published packages, so the suites inject the local build through RSTACK_E2E_TOOLCHAIN_OVERRIDES: a tarball of @rstest/core or of @rslint/core plus its host native package. Both rslint packages come from getRslintPackages so the platform tuple is never hardcoded. The VS Code download is cached outside the checkout, which the suite cleans with git clean -fdxq. --- tests/rslint/rstack-editor.ts | 18 +-------- tests/rstest/rstack-editor.ts | 16 +------- tests/shared/rstack-editor.ts | 69 +++++++++++++++++++++++++++++++++++ types.d.ts | 4 +- utils.ts | 26 ++++++++----- 5 files changed, 90 insertions(+), 43 deletions(-) create mode 100644 tests/shared/rstack-editor.ts diff --git a/tests/rslint/rstack-editor.ts b/tests/rslint/rstack-editor.ts index ed79d524..9b1ed5c0 100644 --- a/tests/rslint/rstack-editor.ts +++ b/tests/rslint/rstack-editor.ts @@ -1,17 +1 @@ -import type { RunOptions } from '../../types'; -import { runInRepo } from '../../utils'; - -// `test:unit` runs the editor's unit tests, which load `@rslint/core` in the -// lint worker, and `lint` runs `rs lint --type-check`, which type-checks the -// editor against `@rslint/core` (the worker imports its `config-loader` and -// `eslint-plugin` modules) and uses that core as the linter engine. -// E2E tests are intentionally excluded because they need VS Code and a -// display server. -export async function test(options: RunOptions) { - await runInRepo({ - ...options, - repo: 'rstackjs/rstack-editor', - branch: process.env.RSTACK_EDITOR ?? 'main', - test: ['test:unit', 'lint'], - }); -} +export { test } from '../shared/rstack-editor'; diff --git a/tests/rstest/rstack-editor.ts b/tests/rstest/rstack-editor.ts index a0f0082c..9b1ed5c0 100644 --- a/tests/rstest/rstack-editor.ts +++ b/tests/rstest/rstack-editor.ts @@ -1,15 +1 @@ -import type { RunOptions } from '../../types'; -import { runInRepo } from '../../utils'; - -// `test:unit` runs the editor's unit tests on Rstest, and `lint` runs -// `rs lint --type-check`, which type-checks the code against `@rstest/core`. -// E2E tests are intentionally excluded because they need VS Code and a -// display server. -export async function test(options: RunOptions) { - await runInRepo({ - ...options, - repo: 'rstackjs/rstack-editor', - branch: 'main', - test: ['test:unit', 'lint'], - }); -} +export { test } from '../shared/rstack-editor'; diff --git a/tests/shared/rstack-editor.ts b/tests/shared/rstack-editor.ts new file mode 100644 index 00000000..bb52bb53 --- /dev/null +++ b/tests/shared/rstack-editor.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert'; +import path from 'node:path'; +import type { RunOptions } from '../../types'; +import { + execa, + getMonorepoPackages, + getRslintPackages, + runInRepo, +} from '../../utils'; + +const REPO = 'rstackjs/rstack-editor'; + +// `test:unit` runs the editor's unit tests on the stack in question, `lint` +// runs `rs lint --type-check`, and `build` produces `packages/vscode/dist`, +// which the E2E extension host loads. +// +// The E2E fixtures install published packages, so the unreleased stack build +// is injected through `RSTACK_E2E_TOOLCHAIN_OVERRIDES` (tarballs packed from +// the stack checkout), never through eco-ci's consumer overrides. Rslint needs +// both `@rslint/core` and its host `@rslint/native-`: core resolves its +// binary through `require.resolve('@rslint/native-/bin')`, so +// overriding core alone would leave the published binary in place. Rstest +// needs only `@rstest/core`, found by package name through the cached +// monorepo scan, so neither suite hardcodes the upstream layout. +async function toolchainOverrides(options: RunOptions): Promise { + if (options.stack === 'rslint') { + return getRslintPackages(options.stackPath) + .map(({ name, directory }) => `${name}=${directory}`) + .join(','); + } + const core = (await getMonorepoPackages('rstest')).find( + ({ name }) => name === '@rstest/core', + ); + assert(core, `@rstest/core not found in ${options.stackPath}`); + return `@rstest/core=${core.directory}`; +} + +// The VS Code download is cached beside the checkout because runInRepo runs +// `git clean -fdxq` in it and would otherwise wipe `.vscode-test/`. Only the +// rslint suite has ever taken its branch from `RSTACK_EDITOR`. +export async function test(options: RunOptions) { + assert( + options.stack === 'rstest' || options.stack === 'rslint', + `rstack-editor E2E does not support the ${options.stack} stack`, + ); + const isRstest = options.stack === 'rstest'; + await runInRepo({ + ...options, + repo: REPO, + branch: isRstest ? 'main' : (process.env.RSTACK_EDITOR ?? 'main'), + build: 'build', + test: [ + 'test:unit', + 'lint', + async ({ dir }) => { + await execa(`node e2e/run.mjs ${isRstest ? 'rstest' : 'smoke lint'}`, { + cwd: path.join(dir, 'packages/vscode'), + env: { + RSTACK_E2E_TOOLCHAIN_OVERRIDES: await toolchainOverrides(options), + VSCODE_TEST_CACHE_PATH: path.join( + options.workspace, + '.vscode-test', + ), + }, + }); + }, + ], + }); +} diff --git a/types.d.ts b/types.d.ts index c7ae7a05..f32b0eac 100644 --- a/types.d.ts +++ b/types.d.ts @@ -59,7 +59,9 @@ export interface RunOptions { suiteCommit?: string; } -export type Task = string | (() => Promise); +// Function tasks receive the checkout `runInRepo` resolved, so a task can work +// inside the consumer without re-deriving the directory naming rule. +export type Task = string | ((context: { dir: string }) => Promise); export interface CommandOptions { stack?: Stack; diff --git a/utils.ts b/utils.ts index b162a3c5..c798a812 100644 --- a/utils.ts +++ b/utils.ts @@ -168,11 +168,16 @@ export const execa = async (cmd: string, options?: ExecaOptions) => { console.log(`${cwd} $> ${cmd}`); } + // `options.env` would replace the environment `setupEnvironment` built + // (`CI`, `ECOSYSTEM_CI`, `NODE_OPTIONS`, …) rather than merge into it — + // execa's `extendEnv` only re-adds `process.env`. Fold caller additions into + // the snapshot; every other option keeps its precedence over the defaults. + const { env: extraEnv, ...rest } = options ?? {}; const proc = execaCommand(cmd, { - env, + env: { ...env, ...extraEnv }, cwd, stdio: 'pipe', - ...options, + ...rest, }); proc.stdin?.pipe(process.stdin); proc.stdout?.pipe(process.stdout); @@ -323,6 +328,7 @@ export async function setupRepo(options: RepoOptions) { function toCommand( task: Task | Task[] | void, agent: Agent, + dir: string, ): ((scripts: any) => Promise) | void { return async (scripts: any) => { const tasks = Array.isArray(task) ? task : [task]; @@ -345,7 +351,7 @@ function toCommand( await $`${task}`; } } else if (typeof task === 'function') { - await task(); + await task({ dir }); } else { throw new Error( `invalid task, expected string or function but got ${typeof task}: ${task}`, @@ -402,7 +408,7 @@ export async function discoverMonorepoPackages( }); } -async function getMonorepoPackages(stack: MonorepoStack) { +export async function getMonorepoPackages(stack: MonorepoStack) { const cached = monorepoPackagesCache[stack]; if (cached) { return cached; @@ -679,12 +685,12 @@ export async function runInRepo(options: RunOptions & RepoOptions) { ); } const agent = options.agent; - const beforeInstallCommand = toCommand(beforeInstall, agent); - const afterInstallCommand = toCommand(afterInstall, agent); - const beforeBuildCommand = toCommand(beforeBuild, agent); - const beforeTestCommand = toCommand(beforeTest, agent); - const buildCommand = toCommand(build, agent); - const testCommand = toCommand(test, agent); + const beforeInstallCommand = toCommand(beforeInstall, agent, dir); + const afterInstallCommand = toCommand(afterInstall, agent, dir); + const beforeBuildCommand = toCommand(beforeBuild, agent, dir); + const beforeTestCommand = toCommand(beforeTest, agent, dir); + const buildCommand = toCommand(build, agent, dir); + const testCommand = toCommand(test, agent, dir); const pkgFile = path.join(dir, 'package.json'); const pkg = JSON.parse(await fs.promises.readFile(pkgFile, 'utf-8')); From a8f6da9f760bda4e37dedd430c3fd57959ac4a1e Mon Sep 17 00:00:00 2001 From: fi3ework Date: Wed, 4 Nov 2026 22:47:12 +0800 Subject: [PATCH 2/2] ci: cap rstest suite jobs and raise the rslint binding timeout The rstest execute jobs had no timeout (GitHub's default is 360 min); the rstack-editor job with E2E measured ~2 min. Prepare Rslint Binding measured 36-48 min in the last runs against a 60 min limit. --- .github/workflows/rslint-ecosystem-ci-from-commit.yml | 2 +- .github/workflows/rslint-ecosystem-ci-from-pr.yml | 2 +- .github/workflows/rslint-ecosystem-ci-selected.yml | 2 +- .github/workflows/rstest-ecosystem-ci-from-commit.yml | 2 ++ .github/workflows/rstest-ecosystem-ci-from-pr.yml | 2 ++ .github/workflows/rstest-ecosystem-ci-selected.yml | 2 ++ 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rslint-ecosystem-ci-from-commit.yml b/.github/workflows/rslint-ecosystem-ci-from-commit.yml index 0673ab8c..0c4d587b 100644 --- a/.github/workflows/rslint-ecosystem-ci-from-commit.yml +++ b/.github/workflows/rslint-ecosystem-ci-from-commit.yml @@ -68,7 +68,7 @@ jobs: prepare-binding: name: Prepare Rslint Binding runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 90 outputs: commit-sha: ${{ steps.prepare.outputs.commit-sha }} steps: diff --git a/.github/workflows/rslint-ecosystem-ci-from-pr.yml b/.github/workflows/rslint-ecosystem-ci-from-pr.yml index 9aabdae5..adc083b7 100644 --- a/.github/workflows/rslint-ecosystem-ci-from-pr.yml +++ b/.github/workflows/rslint-ecosystem-ci-from-pr.yml @@ -73,7 +73,7 @@ jobs: prepare-binding: name: Prepare Rslint Binding runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 90 outputs: commit-sha: ${{ steps.prepare.outputs.commit-sha }} steps: diff --git a/.github/workflows/rslint-ecosystem-ci-selected.yml b/.github/workflows/rslint-ecosystem-ci-selected.yml index dc436126..9fba27bf 100644 --- a/.github/workflows/rslint-ecosystem-ci-selected.yml +++ b/.github/workflows/rslint-ecosystem-ci-selected.yml @@ -69,7 +69,7 @@ jobs: prepare-binding: name: Prepare Rslint Binding runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 90 outputs: commit-sha: ${{ steps.prepare.outputs.commit-sha }} steps: diff --git a/.github/workflows/rstest-ecosystem-ci-from-commit.yml b/.github/workflows/rstest-ecosystem-ci-from-commit.yml index 9fd96b04..7c95edb2 100644 --- a/.github/workflows/rstest-ecosystem-ci-from-commit.yml +++ b/.github/workflows/rstest-ecosystem-ci-from-commit.yml @@ -73,6 +73,7 @@ jobs: execute-selected-suite: runs-on: ubuntu-latest if: "inputs.suite != '-'" + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: ./.github/actions/build-rstest @@ -115,6 +116,7 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} name: execute-all (${{ matrix.suite }}) + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: ./.github/actions/build-rstest diff --git a/.github/workflows/rstest-ecosystem-ci-from-pr.yml b/.github/workflows/rstest-ecosystem-ci-from-pr.yml index 858f5985..f64b319d 100644 --- a/.github/workflows/rstest-ecosystem-ci-from-pr.yml +++ b/.github/workflows/rstest-ecosystem-ci-from-pr.yml @@ -72,6 +72,7 @@ jobs: execute-selected-suite: runs-on: ubuntu-latest if: "inputs.suite != '-'" + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: ./.github/actions/build-rstest @@ -112,6 +113,7 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} name: execute-all (${{ matrix.suite }}) + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: ./.github/actions/build-rstest diff --git a/.github/workflows/rstest-ecosystem-ci-selected.yml b/.github/workflows/rstest-ecosystem-ci-selected.yml index 5f4cecf8..2570fc34 100644 --- a/.github/workflows/rstest-ecosystem-ci-selected.yml +++ b/.github/workflows/rstest-ecosystem-ci-selected.yml @@ -79,6 +79,7 @@ jobs: execute-selected-suite: runs-on: ubuntu-latest if: "inputs.suite != '-'" + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: ./.github/actions/build-rstest @@ -121,6 +122,7 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} name: execute-all (${{ matrix.suite }}) + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - uses: ./.github/actions/build-rstest