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 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'));