From 0a4e3a330b1462b2fb578a2d9a713b0538e3e99a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 14 Oct 2026 16:57:58 +0200 Subject: [PATCH 1/3] ci(e2e): Run E2E tests without restoring the monorepo's node_modules E2E jobs restored the ~860 MB root dependency cache and the full build output only to run a few tsx scripts and to resolve the linked test-utils package. The scripts are now plain .mjs files without dependencies, and test-utils is installed from a tarball like the SDK packages. Co-Authored-By: Claude Opus 5.5 --- .github/actions/install-pnpm/action.yml | 13 +- .github/workflows/build.yml | 19 +- dev-packages/e2e-tests/ciCopyToTemp.mjs | 20 +++ dev-packages/e2e-tests/ciCopyToTemp.ts | 26 --- dev-packages/e2e-tests/ciPnpmOverrides.mjs | 12 ++ dev-packages/e2e-tests/ciPnpmOverrides.ts | 20 --- .../lib/{copyToTemp.ts => copyToTemp.mjs} | 36 ++-- .../e2e-tests/lib/packedTarballUtils.mjs | 83 +++++++++ .../e2e-tests/lib/packedTarballUtils.ts | 64 ------- .../{pnpmOverrides.ts => pnpmOverrides.mjs} | 47 +++-- .../lib/{runtimeFiles.ts => runtimeFiles.mjs} | 9 +- .../lib/syncPackedTarballSymlinks.mjs | 38 ++++ .../lib/syncPackedTarballSymlinks.ts | 65 ------- dev-packages/e2e-tests/package.json | 8 +- dev-packages/e2e-tests/prepare.mjs | 4 + dev-packages/e2e-tests/prepare.ts | 15 -- dev-packages/e2e-tests/run.ts | 8 +- dev-packages/e2e-tests/tsconfig.json | 4 +- .../validate-packed-tarball-setup.mjs | 39 +++++ .../validate-packed-tarball-setup.ts | 42 ----- ...malize-e2e-test-dump-transaction-events.js | 162 +++++++++--------- 21 files changed, 368 insertions(+), 366 deletions(-) create mode 100644 dev-packages/e2e-tests/ciCopyToTemp.mjs delete mode 100644 dev-packages/e2e-tests/ciCopyToTemp.ts create mode 100644 dev-packages/e2e-tests/ciPnpmOverrides.mjs delete mode 100644 dev-packages/e2e-tests/ciPnpmOverrides.ts rename dev-packages/e2e-tests/lib/{copyToTemp.ts => copyToTemp.mjs} (69%) create mode 100644 dev-packages/e2e-tests/lib/packedTarballUtils.mjs delete mode 100644 dev-packages/e2e-tests/lib/packedTarballUtils.ts rename dev-packages/e2e-tests/lib/{pnpmOverrides.ts => pnpmOverrides.mjs} (71%) rename dev-packages/e2e-tests/lib/{runtimeFiles.ts => runtimeFiles.mjs} (81%) create mode 100644 dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.mjs delete mode 100644 dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts create mode 100644 dev-packages/e2e-tests/prepare.mjs delete mode 100644 dev-packages/e2e-tests/prepare.ts create mode 100644 dev-packages/e2e-tests/validate-packed-tarball-setup.mjs delete mode 100644 dev-packages/e2e-tests/validate-packed-tarball-setup.ts diff --git a/.github/actions/install-pnpm/action.yml b/.github/actions/install-pnpm/action.yml index 9a7f2b45f132..79cbc8bd5062 100644 --- a/.github/actions/install-pnpm/action.yml +++ b/.github/actions/install-pnpm/action.yml @@ -1,11 +1,16 @@ name: 'Install pnpm' -description: 'Puts the pnpm version from the root devDependencies on PATH. Run it after the dependencies are installed.' +description: 'Installs the pnpm version from the root devDependencies.' runs: using: 'composite' steps: # The dev dependency is the one pnpm version for CI and local runs. An app that needs another version sets - # `packageManager` in its package.json, and pnpm switches to that version. - - name: Add pnpm to PATH + # `packageManager` in its package.json, and pnpm switches to that version. It is read from package.json rather + # than node_modules, so jobs that don't restore the monorepo's dependencies can use it. + - name: Resolve pnpm version + id: pnpm-version shell: bash - run: echo "$GITHUB_WORKSPACE/node_modules/pnpm" >> "$GITHUB_PATH" + run: echo "version=$(jq -er '.devDependencies.pnpm' "$GITHUB_WORKSPACE/package.json")" >> "$GITHUB_OUTPUT" + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: ${{ steps.pnpm-version.outputs.version }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 385be733e178..ca3a7065e0fc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1088,10 +1088,6 @@ jobs: uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} - - name: Restore caches - uses: ./.github/actions/restore-cache - with: - dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Install pnpm uses: ./.github/actions/install-pnpm @@ -1114,6 +1110,13 @@ jobs: name: build-layer-output path: ${{ env.BUILD_LAYER_PATH }} + # The Lambda functions install the built workspace packages directly (see `src/stack.ts`). + - name: Restore build artifacts + uses: actions/download-artifact@v8 + if: matrix.test-application == 'aws-serverless' + with: + name: build-output + # Installs Playwright while the app builds. # Started after the artifact downloads: starting next to them made `download-artifact` fail to read the event file. - name: Install Playwright @@ -1170,7 +1173,7 @@ jobs: - name: Pre-process E2E Test Dumps if: failure() run: | - node ./scripts/normalize-e2e-test-dump-transaction-events.js + node ./scripts/normalize-e2e-test-dump-transaction-events.js ${{ runner.temp }}/test-application/event-dumps - name: Upload E2E Test Event Dumps uses: actions/upload-artifact@v7 @@ -1239,10 +1242,6 @@ jobs: uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} - - name: Restore caches - uses: ./.github/actions/restore-cache - with: - dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Install pnpm uses: ./.github/actions/install-pnpm @@ -1301,7 +1300,7 @@ jobs: - name: Pre-process E2E Test Dumps if: failure() run: | - node ./scripts/normalize-e2e-test-dump-transaction-events.js + node ./scripts/normalize-e2e-test-dump-transaction-events.js ${{ runner.temp }}/test-application/event-dumps - name: Upload E2E Test Event Dumps uses: actions/upload-artifact@v7 diff --git a/dev-packages/e2e-tests/ciCopyToTemp.mjs b/dev-packages/e2e-tests/ciCopyToTemp.mjs new file mode 100644 index 000000000000..f257e00d12a7 --- /dev/null +++ b/dev-packages/e2e-tests/ciCopyToTemp.mjs @@ -0,0 +1,20 @@ +// @ts-check +/* eslint-disable no-console */ + +import { copyToTemp } from './lib/copyToTemp.mjs'; +import { applyRuntimeFiles } from './lib/runtimeFiles.mjs'; + +const originalPath = process.argv[2]; +const tmpDirPath = process.argv[3]; + +if (!originalPath || !tmpDirPath) { + throw new Error('Original path and tmp dir path are required'); +} + +console.log(`Copying ${originalPath} to ${tmpDirPath}...`); + +await copyToTemp(originalPath, tmpDirPath); + +if (process.env.RUNTIME) { + applyRuntimeFiles(tmpDirPath, process.env.RUNTIME); +} diff --git a/dev-packages/e2e-tests/ciCopyToTemp.ts b/dev-packages/e2e-tests/ciCopyToTemp.ts deleted file mode 100644 index 649fe01fed3f..000000000000 --- a/dev-packages/e2e-tests/ciCopyToTemp.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-console */ - -import { copyToTemp } from './lib/copyToTemp'; -import { applyRuntimeFiles } from './lib/runtimeFiles'; - -async function run(): Promise { - const originalPath = process.argv[2]; - const tmpDirPath = process.argv[3]; - - if (!originalPath || !tmpDirPath) { - throw new Error('Original path and tmp dir path are required'); - } - - console.log(`Copying ${originalPath} to ${tmpDirPath}...`); - - await copyToTemp(originalPath, tmpDirPath); - - if (process.env.RUNTIME) { - applyRuntimeFiles(tmpDirPath, process.env.RUNTIME); - } -} - -run().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/dev-packages/e2e-tests/ciPnpmOverrides.mjs b/dev-packages/e2e-tests/ciPnpmOverrides.mjs new file mode 100644 index 000000000000..a3d6d643c6fa --- /dev/null +++ b/dev-packages/e2e-tests/ciPnpmOverrides.mjs @@ -0,0 +1,12 @@ +// @ts-check +import * as path from 'node:path'; +import { addPnpmOverrides } from './lib/pnpmOverrides.mjs'; + +const tmpDirPath = process.argv[2]; +const packedDirPath = process.argv[3]; + +if (!tmpDirPath || !packedDirPath) { + throw new Error('Tmp dir path and packed dir path are required'); +} + +await addPnpmOverrides(path.resolve(tmpDirPath), path.resolve(packedDirPath)); diff --git a/dev-packages/e2e-tests/ciPnpmOverrides.ts b/dev-packages/e2e-tests/ciPnpmOverrides.ts deleted file mode 100644 index 909a59fa2e9d..000000000000 --- a/dev-packages/e2e-tests/ciPnpmOverrides.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable no-console */ - -import { addPnpmOverrides } from './lib/pnpmOverrides'; -import * as path from 'path'; - -async function run(): Promise { - const tmpDirPath = process.argv[2]; - const packedDirPath = process.argv[3]; - - if (!tmpDirPath || !packedDirPath) { - throw new Error('Tmp dir path and packed dir path are required'); - } - - await addPnpmOverrides(path.resolve(tmpDirPath), path.resolve(packedDirPath)); -} - -run().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/dev-packages/e2e-tests/lib/copyToTemp.ts b/dev-packages/e2e-tests/lib/copyToTemp.mjs similarity index 69% rename from dev-packages/e2e-tests/lib/copyToTemp.ts rename to dev-packages/e2e-tests/lib/copyToTemp.mjs index c45a5793e2fc..ae7f5e04d5fe 100644 --- a/dev-packages/e2e-tests/lib/copyToTemp.ts +++ b/dev-packages/e2e-tests/lib/copyToTemp.mjs @@ -1,22 +1,31 @@ +// @ts-check /* eslint-disable no-console */ -import { readFileSync, writeFileSync } from 'fs'; -import { cp } from 'fs/promises'; -import { isAbsolute, join, resolve } from 'path'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { cp } from 'node:fs/promises'; +import { dirname, isAbsolute, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -export async function copyToTemp(originalPath: string, tmpDirPath: string): Promise { +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** + * @param {string} originalPath + * @param {string} tmpDirPath + * @returns {Promise} + */ +export async function copyToTemp(originalPath, tmpDirPath) { // copy files to tmp dir await cp(originalPath, tmpDirPath, { recursive: true }); fixPackageJson(tmpDirPath); } -function fixPackageJson(cwd: string): void { +/** + * @param {string} cwd + */ +function fixPackageJson(cwd) { const packageJsonPath = join(cwd, 'package.json'); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { - dependencies?: Record; - devDependencies?: Record; - volta?: Record; - }; + /** @type {{ dependencies?: Record; devDependencies?: Record; volta?: Record }} */ + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); // 1. Fix file dependencies if (packageJson.dependencies) { @@ -45,8 +54,11 @@ function fixPackageJson(cwd: string): void { writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); } -// Exported for pnpmOverrides as well -export function fixFileLinkDependencies(dependencyObj: Record): void { +/** + * Exported for pnpmOverrides as well + * @param {Record} dependencyObj + */ +export function fixFileLinkDependencies(dependencyObj) { for (const [key, value] of Object.entries(dependencyObj)) { const prefix = value.startsWith('link:') ? 'link:' : value.startsWith('file:') ? 'file:' : null; if (!prefix) { diff --git a/dev-packages/e2e-tests/lib/packedTarballUtils.mjs b/dev-packages/e2e-tests/lib/packedTarballUtils.mjs new file mode 100644 index 000000000000..c46a94ff9578 --- /dev/null +++ b/dev-packages/e2e-tests/lib/packedTarballUtils.mjs @@ -0,0 +1,83 @@ +// @ts-check +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const E2E_TESTS_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const REPOSITORY_ROOT = path.resolve(E2E_TESTS_ROOT, '../..'); + +// test-utils is not published, but test apps install it from its tarball too: with a `link:`, its dependencies would +// resolve from the monorepo's root `node_modules` instead of the app's. +const TARBALL_PACKAGE_DIRS = [ + ...fs.readdirSync(path.join(REPOSITORY_ROOT, 'packages')).map(dir => path.join(REPOSITORY_ROOT, 'packages', dir)), + path.join(REPOSITORY_ROOT, 'dev-packages', 'test-utils'), +]; + +/** + * Workspace @sentry and @sentry-internal packages that have a built tarball for the E2E version. + * @returns {{ name: string; tarballPath: string }[]} The package names and their tarball paths, sorted by name. + */ +export function getPackedTarballs() { + const version = getE2eTestsPackageVersion(); + const tarballs = []; + + for (const packageDir of TARBALL_PACKAGE_DIRS) { + const packageJsonPath = path.join(packageDir, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + continue; + } + + const name = readJson(packageJsonPath).name; + if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) { + continue; + } + + const tarballPath = path.join(packageDir, versionedTarballFilename(name, version)); + if (fs.existsSync(tarballPath)) { + tarballs.push({ name, tarballPath }); + } + } + + return tarballs.sort((a, b) => a.name.localeCompare(b.name)); +} + +/** + * Stable symlink name in `packed/` (no version segment). + * @param {string} packageName + * @returns {string} + */ +export function packedSymlinkFilename(packageName) { + return `${npmPackBasename(packageName)}-packed.tgz`; +} + +/** + * Versioned tarball filename produced by `npm pack` in a package directory. + * @param {string} packageName + * @param {string} version + * @returns {string} + */ +function versionedTarballFilename(packageName, version) { + return `${npmPackBasename(packageName)}-${version}.tgz`; +} + +/** + * npm pack tarball basename (without version and .tgz), e.g. `@sentry/core` -> `sentry-core`. + * @param {string} packageName + * @returns {string} + */ +function npmPackBasename(packageName) { + return packageName.replace(/^@/, '').replace(/\//g, '-'); +} + +/** + * @param {string} filePath + * @returns {any} + */ +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +/** @returns {string} */ +export function getE2eTestsPackageVersion() { + return readJson(path.join(E2E_TESTS_ROOT, 'package.json')).version; +} diff --git a/dev-packages/e2e-tests/lib/packedTarballUtils.ts b/dev-packages/e2e-tests/lib/packedTarballUtils.ts deleted file mode 100644 index d7bbeefcb8e7..000000000000 --- a/dev-packages/e2e-tests/lib/packedTarballUtils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import { sync as globSync } from 'glob'; - -const E2E_TESTS_ROOT = path.resolve(__dirname, '..'); -const REPOSITORY_ROOT = path.resolve(E2E_TESTS_ROOT, '../..'); - -// test-utils is not published, but test apps install it from its tarball too: with a `link:`, its dependencies would -// resolve from the monorepo's root `node_modules` instead of the app's. -export const TARBALL_PACKAGE_JSON_GLOB = '{packages/*,dev-packages/test-utils}/package.json'; - -/** - * Workspace @sentry and @sentry-internal packages that have a built tarball for the E2E version. - * @returns The names of the Sentry tarball packages. - */ -export function getPublishedSentryTarballPackageNames(): string[] { - const version = getE2eTestsPackageVersion(); - const names: string[] = []; - - for (const packageJsonPath of globSync(TARBALL_PACKAGE_JSON_GLOB, { - cwd: REPOSITORY_ROOT, - absolute: true, - })) { - const pkg = readJson<{ name?: string }>(packageJsonPath); - const name = pkg.name; - if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) { - continue; - } - const packageDir = path.dirname(packageJsonPath); - const tarball = path.join(packageDir, versionedTarballFilename(name, version)); - if (fs.existsSync(tarball)) { - names.push(name); - } - } - - return names.sort(); -} - -/** Stable symlink name in `packed/` (no version segment). */ -export function packedSymlinkFilename(packageName: string): string { - return `${npmPackBasename(packageName)}-packed.tgz`; -} - -/** - * Versioned tarball filename produced by `npm pack` in a package directory. - */ -export function versionedTarballFilename(packageName: string, version: string): string { - return `${npmPackBasename(packageName)}-${version}.tgz`; -} - -/** - * npm pack tarball basename (without version and .tgz), e.g. `@sentry/core` -> `sentry-core`. - */ -function npmPackBasename(packageName: string): string { - return packageName.replace(/^@/, '').replace(/\//g, '-'); -} - -function readJson(filePath: string): T { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; -} - -function getE2eTestsPackageVersion(): string { - return readJson<{ version: string }>(path.join(E2E_TESTS_ROOT, 'package.json')).version; -} diff --git a/dev-packages/e2e-tests/lib/pnpmOverrides.ts b/dev-packages/e2e-tests/lib/pnpmOverrides.mjs similarity index 71% rename from dev-packages/e2e-tests/lib/pnpmOverrides.ts rename to dev-packages/e2e-tests/lib/pnpmOverrides.mjs index 9f5dd88a9ea8..8812886d9371 100644 --- a/dev-packages/e2e-tests/lib/pnpmOverrides.ts +++ b/dev-packages/e2e-tests/lib/pnpmOverrides.mjs @@ -1,7 +1,8 @@ -import { readFile, writeFile } from 'fs/promises'; -import * as path from 'path'; -import { getPublishedSentryTarballPackageNames, packedSymlinkFilename } from './packedTarballUtils'; -import { fixFileLinkDependencies } from './copyToTemp'; +// @ts-check +import { readFile, writeFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import { fixFileLinkDependencies } from './copyToTemp.mjs'; +import { getPackedTarballs, packedSymlinkFilename } from './packedTarballUtils.mjs'; /** * For a given temp test application directory, add pnpm overrides to pin internal Sentry dependencies to their packed tarballs. @@ -10,12 +11,14 @@ import { fixFileLinkDependencies } from './copyToTemp'; * pnpm reads overrides only from pnpm-workspace.yaml. A fixture's existing overrides take precedence. * https://pnpm.io/settings/dependency-resolution#overrides * - * @param tmpDirPath - The temporary directory path of the test application. - * @param packedDirPath - The path to the packed tarballs. + * @param {string} tmpDirPath - The temporary directory path of the test application. + * @param {string} packedDirPath - The path to the packed tarballs. + * @returns {Promise} */ -export async function addPnpmOverrides(tmpDirPath: string, packedDirPath: string): Promise { +export async function addPnpmOverrides(tmpDirPath, packedDirPath) { const packageJsonPath = path.join(tmpDirPath, 'package.json'); - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as { pnpm?: unknown; resolutions?: unknown }; + /** @type {{ pnpm?: unknown; resolutions?: unknown }} */ + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); // pnpm 11+ ignores these fields, so their settings would silently have no effect. if (packageJson.pnpm || packageJson.resolutions) { @@ -24,7 +27,7 @@ export async function addPnpmOverrides(tmpDirPath: string, packedDirPath: string ); } - const packageNames = getPublishedSentryTarballPackageNames(); + const packageNames = getPackedTarballs().map(({ name }) => name); const overrides = Object.fromEntries( packageNames.map(packageName => [packageName, `file:${packedDirPath}/${packedSymlinkFilename(packageName)}`]), ); @@ -48,7 +51,11 @@ export async function addPnpmOverrides(tmpDirPath: string, packedDirPath: string console.log(`Added ${packageNames.length} internal Sentry package overrides`); } -function parseWorkspaceOverrides(workspace: string): Record { +/** + * @param {string} workspace + * @returns {Record} + */ +function parseWorkspaceOverrides(workspace) { const overrides = /^overrides:\n((?:[ \t]+.*(?:\n|$))*)/m.exec(workspace)?.[1]; if (!overrides) { return {}; @@ -69,15 +76,27 @@ function parseWorkspaceOverrides(workspace: string): Record { ); } -function parseYamlString(quoted: string): string { - return quoted.startsWith("'") ? quoted.slice(1, -1).replace(/''/g, "'") : (JSON.parse(quoted) as string); +/** + * @param {string} quoted + * @returns {string} + */ +function parseYamlString(quoted) { + return quoted.startsWith("'") ? quoted.slice(1, -1).replace(/''/g, "'") : JSON.parse(quoted); } -function removeWorkspaceOverrides(workspace: string): string { +/** + * @param {string} workspace + * @returns {string} + */ +function removeWorkspaceOverrides(workspace) { return workspace.replace(/^overrides:\n(?:[ \t]+.*(?:\n|$))*/m, ''); } -function formatWorkspaceOverrides(overrides: Record): string { +/** + * @param {Record} overrides + * @returns {string} + */ +function formatWorkspaceOverrides(overrides) { return Object.entries(overrides) .map(([packageName, target]) => ` ${JSON.stringify(packageName)}: ${JSON.stringify(target)}`) .join('\n'); diff --git a/dev-packages/e2e-tests/lib/runtimeFiles.ts b/dev-packages/e2e-tests/lib/runtimeFiles.mjs similarity index 81% rename from dev-packages/e2e-tests/lib/runtimeFiles.ts rename to dev-packages/e2e-tests/lib/runtimeFiles.mjs index a29c0bc890d3..505d7309847b 100644 --- a/dev-packages/e2e-tests/lib/runtimeFiles.ts +++ b/dev-packages/e2e-tests/lib/runtimeFiles.mjs @@ -1,12 +1,15 @@ +// @ts-check /* eslint-disable no-console */ -import { copyFileSync, existsSync, readdirSync } from 'fs'; -import { join } from 'path'; +import { copyFileSync, existsSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; /** * Copies each file in `dir` that has `runtime` as a part of its name over the existing file without that part, for * example `app/entry.server.cloudflare.tsx` over `app/entry.server.tsx`. + * @param {string} dir + * @param {string} runtime */ -export function applyRuntimeFiles(dir: string, runtime: string): void { +export function applyRuntimeFiles(dir, runtime) { for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.name === 'node_modules' || entry.name.startsWith('.')) { continue; diff --git a/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.mjs b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.mjs new file mode 100644 index 000000000000..f74ef9488f75 --- /dev/null +++ b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.mjs @@ -0,0 +1,38 @@ +// @ts-check +/* eslint-disable no-console */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getE2eTestsPackageVersion, getPackedTarballs, packedSymlinkFilename } from './packedTarballUtils.mjs'; + +const e2eTestsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repositoryRoot = path.resolve(e2eTestsRoot, '../..'); +const packedDir = path.join(e2eTestsRoot, 'packed'); + +/** + * Ensures `packed/-packed.tgz` symlinks point at the current versioned tarballs under `packages/*`. + * Run after `yarn build:tarball` at the repo root (or from CI after restoring the tarball cache). + */ +export function syncPackedTarballSymlinks() { + fs.mkdirSync(packedDir, { recursive: true }); + + for (const entry of fs.readdirSync(packedDir, { withFileTypes: true })) { + if (!entry.name.endsWith('-packed.tgz')) { + continue; + } + fs.rmSync(path.join(packedDir, entry.name), { recursive: true, force: true }); + } + + const tarballs = getPackedTarballs(); + if (tarballs.length === 0) { + throw new Error( + `No packed tarballs found for version ${getE2eTestsPackageVersion()} under packages/*/. Run "yarn build:tarball" at the repository root.`, + ); + } + + for (const { name, tarballPath } of tarballs) { + fs.symlinkSync(tarballPath, path.join(packedDir, packedSymlinkFilename(name))); + } + + console.log(`Linked ${tarballs.length} tarball symlinks in ${path.relative(repositoryRoot, packedDir) || 'packed'}.`); +} diff --git a/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts deleted file mode 100644 index cdc42832a979..000000000000 --- a/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable no-console */ -import * as fs from 'fs'; -import * as path from 'path'; -import { sync as globSync } from 'glob'; -import { packedSymlinkFilename, TARBALL_PACKAGE_JSON_GLOB, versionedTarballFilename } from './packedTarballUtils'; - -const e2eTestsRoot = path.resolve(__dirname, '..'); -const repositoryRoot = path.resolve(e2eTestsRoot, '../..'); -const packedDir = path.join(e2eTestsRoot, 'packed'); - -function readJson(filePath: string): T { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; -} - -/** - * Ensures `packed/-packed.tgz` symlinks point at the current versioned tarballs. - * Run after `yarn build:tarball` at the repo root (or from CI after restoring the tarball cache). - */ -export function syncPackedTarballSymlinks(): void { - const { version } = readJson<{ version: string }>(path.join(e2eTestsRoot, 'package.json')); - - fs.mkdirSync(packedDir, { recursive: true }); - - for (const entry of fs.readdirSync(packedDir, { withFileTypes: true })) { - if (!entry.name.endsWith('-packed.tgz')) { - continue; - } - fs.rmSync(path.join(packedDir, entry.name), { recursive: true, force: true }); - } - - const packageJsonPaths = globSync(TARBALL_PACKAGE_JSON_GLOB, { - cwd: repositoryRoot, - absolute: true, - }); - - let linked = 0; - for (const packageJsonPath of packageJsonPaths) { - const pkg = readJson<{ name?: string }>(packageJsonPath); - const name = pkg.name; - if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) { - continue; - } - - const packageDir = path.dirname(packageJsonPath); - const expectedTarball = path.join(packageDir, versionedTarballFilename(name, version)); - - if (!fs.existsSync(expectedTarball)) { - continue; - } - - const linkName = packedSymlinkFilename(name); - const linkPath = path.join(packedDir, linkName); - - fs.symlinkSync(expectedTarball, linkPath); - linked++; - } - - if (linked === 0) { - throw new Error( - `No packed tarballs found for version ${version}. Run "yarn build:tarball" at the repository root.`, - ); - } - - console.log(`Linked ${linked} tarball symlinks in ${path.relative(repositoryRoot, packedDir) || 'packed'}.`); -} diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index a92e96a4d64c..6ac9c05d4f18 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -9,13 +9,13 @@ "lint:ts": "tsc --noEmit", "test:e2e": "run-s test:prepare test:validate test:run", "test:run": "tsx run.ts", - "test:prepare": "tsx prepare.ts", - "test:validate": "tsx validate-packed-tarball-setup.ts", + "test:prepare": "node prepare.mjs", + "test:validate": "node validate-packed-tarball-setup.mjs", "clean": "rimraf tmp node_modules packed && yarn clean:test-applications && yarn clean:pnpm", "ci:build-matrix": "node ./lib/getTestMatrix.mjs", "ci:build-matrix-optional": "node ./lib/getTestMatrix.mjs --optional=true", - "ci:copy-to-temp": "tsx ./ciCopyToTemp.ts", - "ci:pnpm-overrides": "tsx ./ciPnpmOverrides.ts", + "ci:copy-to-temp": "node ./ciCopyToTemp.mjs", + "ci:pnpm-overrides": "node ./ciPnpmOverrides.mjs", "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results,.angular,event-dumps}", "clean:pnpm": "pnpm store prune" }, diff --git a/dev-packages/e2e-tests/prepare.mjs b/dev-packages/e2e-tests/prepare.mjs new file mode 100644 index 000000000000..bcaea94b2895 --- /dev/null +++ b/dev-packages/e2e-tests/prepare.mjs @@ -0,0 +1,4 @@ +// @ts-check +import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks.mjs'; + +syncPackedTarballSymlinks(); diff --git a/dev-packages/e2e-tests/prepare.ts b/dev-packages/e2e-tests/prepare.ts deleted file mode 100644 index 049b727b1b21..000000000000 --- a/dev-packages/e2e-tests/prepare.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable no-console */ -import * as dotenv from 'dotenv'; -import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks'; - -async function run(): Promise { - // Load environment variables from .env file locally - dotenv.config(); - - syncPackedTarballSymlinks(); -} - -run().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index 3343f632a115..90c1950e0edb 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -5,10 +5,10 @@ import { mkdir, mkdtemp, readFile, rm } from 'fs/promises'; import { sync as globSync } from 'glob'; import { tmpdir } from 'os'; import { delimiter, dirname, join, resolve } from 'path'; -import { copyToTemp } from './lib/copyToTemp'; -import { applyRuntimeFiles } from './lib/runtimeFiles'; -import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks'; -import { addPnpmOverrides } from './lib/pnpmOverrides'; +import { copyToTemp } from './lib/copyToTemp.mjs'; +import { applyRuntimeFiles } from './lib/runtimeFiles.mjs'; +import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks.mjs'; +import { addPnpmOverrides } from './lib/pnpmOverrides.mjs'; interface SentryTestVariant { 'build-command': string; diff --git a/dev-packages/e2e-tests/tsconfig.json b/dev-packages/e2e-tests/tsconfig.json index 026669f596c0..f3ef44daf268 100644 --- a/dev-packages/e2e-tests/tsconfig.json +++ b/dev-packages/e2e-tests/tsconfig.json @@ -2,6 +2,8 @@ "extends": "../../tsconfig.json", "exclude": ["node_modules", "test-applications"], "compilerOptions": { - "types": ["node"] + "types": ["node"], + "allowJs": true, + "noEmit": true } } diff --git a/dev-packages/e2e-tests/validate-packed-tarball-setup.mjs b/dev-packages/e2e-tests/validate-packed-tarball-setup.mjs new file mode 100644 index 000000000000..d35627adaa2d --- /dev/null +++ b/dev-packages/e2e-tests/validate-packed-tarball-setup.mjs @@ -0,0 +1,39 @@ +// @ts-check +import * as assert from 'node:assert'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getE2eTestsPackageVersion, getPackedTarballs } from './lib/packedTarballUtils.mjs'; + +const tarballs = getPackedTarballs(); + +assert.ok( + tarballs.length > 0, + `No tarballs found for version ${getE2eTestsPackageVersion()}. Run "yarn build:tarball" at the repository root.`, +); + +const packedDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'packed'); +const symlinkPaths = fs.existsSync(packedDir) + ? fs + .readdirSync(packedDir) + .filter(name => name.endsWith('-packed.tgz')) + .map(name => path.join(packedDir, name)) + : []; + +assert.ok( + symlinkPaths.length > 0, + 'No packed tarball symlinks found. Run "yarn test:prepare" in dev-packages/e2e-tests.', +); + +assert.strictEqual( + symlinkPaths.length, + tarballs.length, + `Tarball count (${tarballs.length}) does not match packed symlink count (${symlinkPaths.length}). Re-run "yarn test:prepare".`, +); + +for (const symlinkPath of symlinkPaths) { + const st = fs.lstatSync(symlinkPath); + assert.ok(st.isSymbolicLink(), `Expected ${symlinkPath} to be a symlink.`); + const target = path.resolve(path.dirname(symlinkPath), fs.readlinkSync(symlinkPath)); + assert.ok(fs.existsSync(target), `Symlink ${symlinkPath} points to missing file: ${target}`); +} diff --git a/dev-packages/e2e-tests/validate-packed-tarball-setup.ts b/dev-packages/e2e-tests/validate-packed-tarball-setup.ts deleted file mode 100644 index 18a3545f7a1a..000000000000 --- a/dev-packages/e2e-tests/validate-packed-tarball-setup.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as assert from 'assert'; -import * as fs from 'fs'; -import { sync as globSync } from 'glob'; -import * as path from 'path'; - -const repositoryRoot = path.resolve(__dirname, '../..'); - -const e2ePkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')) as { version: string }; -const version = e2ePkg.version; - -const tarballPaths = globSync(`{packages/*,dev-packages/test-utils}/sentry-*-${version}.tgz`, { - cwd: repositoryRoot, - absolute: true, -}); - -assert.ok( - tarballPaths.length > 0, - `No tarballs found for version ${version}. Run "yarn build:tarball" at the repository root.`, -); - -const symlinkPaths = globSync('packed/*-packed.tgz', { - cwd: __dirname, - absolute: true, -}); - -assert.ok( - symlinkPaths.length > 0, - 'No packed tarball symlinks found. Run "yarn test:prepare" in dev-packages/e2e-tests.', -); - -assert.strictEqual( - symlinkPaths.length, - tarballPaths.length, - `Tarball count (${tarballPaths.length}) does not match packed symlink count (${symlinkPaths.length}). Re-run "yarn sync:packed-tarballs".`, -); - -for (const symlinkPath of symlinkPaths) { - const st = fs.lstatSync(symlinkPath); - assert.ok(st.isSymbolicLink(), `Expected ${symlinkPath} to be a symlink.`); - const target = path.resolve(path.dirname(symlinkPath), fs.readlinkSync(symlinkPath)); - assert.ok(fs.existsSync(target), `Symlink ${symlinkPath} points to missing file: ${target}`); -} diff --git a/scripts/normalize-e2e-test-dump-transaction-events.js b/scripts/normalize-e2e-test-dump-transaction-events.js index e3459d52621d..376b86d2b413 100644 --- a/scripts/normalize-e2e-test-dump-transaction-events.js +++ b/scripts/normalize-e2e-test-dump-transaction-events.js @@ -3,93 +3,91 @@ const fs = require('fs'); const path = require('path'); -const glob = require('glob'); - -glob.glob( - '../dev-packages/e2e-tests/test-applications/*/event-dumps/*.dump', - { - cwd: __dirname, - absolute: true, - }, - (err, dumpPaths) => { - if (err) { - throw err; + +const dumpDir = process.argv[2]; +if (!dumpDir) { + throw new Error('Usage: node normalize-e2e-test-dump-transaction-events.js '); +} + +const dumpPaths = fs.existsSync(dumpDir) + ? fs + .readdirSync(dumpDir) + .filter(name => name.endsWith('.dump')) + .map(name => path.join(dumpDir, name)) + : []; + +dumpPaths.forEach(dumpPath => { + const fileContents = fs.readFileSync(dumpPath, 'utf8'); + + const transactionNodes = []; + + fileContents.split('\n').forEach(serializedEnvelope => { + let envelope; + try { + envelope = JSON.parse(serializedEnvelope); + } catch { + return; + // noop } - dumpPaths.forEach(dumpPath => { - const fileContents = fs.readFileSync(dumpPath, 'utf8'); - - const transactionNodes = []; - - fileContents.split('\n').forEach(serializedEnvelope => { - let envelope; - try { - envelope = JSON.parse(serializedEnvelope); - } catch { - return; - // noop - } - - const envelopeItems = envelope[1]; - - envelopeItems.forEach(([envelopeItemHeader, transaction]) => { - if (envelopeItemHeader.type === 'transaction') { - const rootNode = { - runtime: transaction.contexts.runtime?.name, - op: transaction.contexts.trace.op, - name: transaction.transaction, - children: [], - }; - - const spanMap = new Map(); - spanMap.set(transaction.contexts.trace.span_id, rootNode); - - transaction.spans.forEach(span => { - const node = { - op: span.data['sentry.op'], - name: span.description, - parent_span_id: span.parent_span_id, - children: [], - }; - spanMap.set(span.span_id, node); - }); - - transaction.spans.forEach(span => { - const node = spanMap.get(span.span_id); - if (node?.parent_span_id) { - const parentNode = spanMap.get(node.parent_span_id); - parentNode.children.push(node); - } - }); - - transactionNodes.push(rootNode); - } + const envelopeItems = envelope[1]; + + envelopeItems.forEach(([envelopeItemHeader, transaction]) => { + if (envelopeItemHeader.type === 'transaction') { + const rootNode = { + runtime: transaction.contexts.runtime?.name, + op: transaction.contexts.trace.op, + name: transaction.transaction, + children: [], + }; + + const spanMap = new Map(); + spanMap.set(transaction.contexts.trace.span_id, rootNode); + + transaction.spans.forEach(span => { + const node = { + op: span.data['sentry.op'], + name: span.description, + parent_span_id: span.parent_span_id, + children: [], + }; + spanMap.set(span.span_id, node); }); - }); - - const output = transactionNodes - .sort((a, b) => { - const aSerialized = serializeNode(a); - const bSerialized = serializeNode(b); - if (aSerialized < bSerialized) { - return -1; - } else if (aSerialized > bSerialized) { - return 1; - } else { - return 0; + + transaction.spans.forEach(span => { + const node = spanMap.get(span.span_id); + if (node?.parent_span_id) { + const parentNode = spanMap.get(node.parent_span_id); + parentNode.children.push(node); } - }) - .map(node => buildDeterministicStringFromNode(node)) - .join('\n\n-----------------------\n\n'); - - fs.writeFileSync( - path.join(path.dirname(dumpPath), `normalized-transactions-${path.basename(dumpPath, '.dump')}.txt`), - output, - 'utf-8', - ); + }); + + transactionNodes.push(rootNode); + } }); - }, -); + }); + + const output = transactionNodes + .sort((a, b) => { + const aSerialized = serializeNode(a); + const bSerialized = serializeNode(b); + if (aSerialized < bSerialized) { + return -1; + } else if (aSerialized > bSerialized) { + return 1; + } else { + return 0; + } + }) + .map(node => buildDeterministicStringFromNode(node)) + .join('\n\n-----------------------\n\n'); + + fs.writeFileSync( + path.join(path.dirname(dumpPath), `normalized-transactions-${path.basename(dumpPath, '.dump')}.txt`), + output, + 'utf-8', + ); +}); // ------- utility fns ---------- From c3e497ec64426bee00131c4d74631763de7ffc89 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 14 Oct 2026 17:09:16 +0200 Subject: [PATCH 2/3] ref(e2e): Drop TypeScript and unused dependencies from e2e-tests With all scripts as plain .mjs files, the local runner no longer needs tsx, dotenv or glob. esbuild was unused. Type checking of this package was never run in CI, so it no longer has a tsconfig; the JSDoc types are kept for editors. Co-Authored-By: Claude Opus 5.5 --- dev-packages/e2e-tests/package.json | 10 +- dev-packages/e2e-tests/{run.ts => run.mjs} | 101 +++++++++++---------- dev-packages/e2e-tests/tsconfig.json | 9 -- yarn.lock | 10 +- 4 files changed, 62 insertions(+), 68 deletions(-) rename dev-packages/e2e-tests/{run.ts => run.mjs} (80%) delete mode 100644 dev-packages/e2e-tests/tsconfig.json diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 6ac9c05d4f18..7abbb49fd143 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -6,9 +6,8 @@ "scripts": { "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", - "lint:ts": "tsc --noEmit", "test:e2e": "run-s test:prepare test:validate test:run", - "test:run": "tsx run.ts", + "test:run": "node run.mjs", "test:prepare": "node prepare.mjs", "test:validate": "node validate-packed-tarball-setup.mjs", "clean": "rimraf tmp node_modules packed && yarn clean:test-applications && yarn clean:pnpm", @@ -20,13 +19,8 @@ "clean:pnpm": "pnpm store prune" }, "devDependencies": { - "@types/node": "^18.19.1", - "dotenv": "17.4.2", - "esbuild": "0.28.1", "eslint-plugin-regexp": "^3.1.0", - "glob": "^13.0.6", - "rimraf": "^6.1.3", - "tsx": "^4.23.0" + "rimraf": "^6.1.3" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.mjs similarity index 80% rename from dev-packages/e2e-tests/run.ts rename to dev-packages/e2e-tests/run.mjs index 90c1950e0edb..74c1c8562712 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.mjs @@ -1,43 +1,41 @@ +// @ts-check /* eslint-disable no-console */ -import { execFileSync, spawn } from 'child_process'; -import * as dotenv from 'dotenv'; -import { mkdir, mkdtemp, readFile, rm } from 'fs/promises'; -import { sync as globSync } from 'glob'; -import { tmpdir } from 'os'; -import { delimiter, dirname, join, resolve } from 'path'; +import { execFileSync, spawn } from 'node:child_process'; +import { existsSync, readdirSync } from 'node:fs'; +import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { delimiter, dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { copyToTemp } from './lib/copyToTemp.mjs'; +import { addPnpmOverrides } from './lib/pnpmOverrides.mjs'; import { applyRuntimeFiles } from './lib/runtimeFiles.mjs'; import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks.mjs'; -import { addPnpmOverrides } from './lib/pnpmOverrides.mjs'; -interface SentryTestVariant { - 'build-command': string; - 'assert-command'?: string; - label?: string; - skip?: boolean; - /** Runtime of the variant (`bun`, `deno` or `cloudflare`). Sets `RUNTIME` and selects the runtime-specific files. */ - runtime?: string; -} +const __dirname = dirname(fileURLToPath(import.meta.url)); -interface PackageJson { - sentryTest?: { - variants?: SentryTestVariant[]; - optionalVariants?: SentryTestVariant[]; - }; -} +/** + * `runtime` is the runtime of the variant (`bun`, `deno` or `cloudflare`). It sets `RUNTIME` and selects the + * runtime-specific files. + * @typedef {{ 'build-command': string; 'assert-command'?: string; label?: string; skip?: boolean; runtime?: string }} SentryTestVariant + */ + +/** @typedef {{ sentryTest?: { variants?: SentryTestVariant[]; optionalVariants?: SentryTestVariant[] } }} PackageJson */ const DEFAULT_DSN = 'https://username@domain/123'; const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; -function asyncExec( - command: string | string[], - options: { env: Record; cwd: string }, -): Promise { +/** + * @param {string | string[]} command + * @param {{ env: Record; cwd: string }} options + * @returns {Promise} + */ +function asyncExec(command, options) { return new Promise((resolve, reject) => { // If command is an array, use spawn with separate command and args (safer) // If command is a string, maintain backward compatibility with shell: true - let process: ReturnType; + /** @type {ReturnType} */ + let process; if (typeof command === 'string') { process = spawn(command, { ...options, shell: true }); } else { @@ -76,27 +74,28 @@ function asyncExec( }); } -function findMatchingVariant(variants: SentryTestVariant[], variantLabel: string): SentryTestVariant | undefined { +/** + * @param {SentryTestVariant[]} variants + * @param {string} variantLabel + * @returns {SentryTestVariant | undefined} + */ +function findMatchingVariant(variants, variantLabel) { const variantLabelLower = variantLabel.toLowerCase(); return variants.find(variant => variant.label?.toLowerCase().includes(variantLabelLower)); } -async function getVariantBuildCommand( - packageJsonPath: string, - variantLabel: string, - testAppPath: string, -): Promise<{ - buildCommand: string; - assertCommand: string; - testLabel: string; - matchedVariantLabel?: string; - skip?: boolean; - runtime?: string; -}> { +/** + * @param {string} packageJsonPath + * @param {string} variantLabel + * @param {string} testAppPath + * @returns {Promise<{ buildCommand: string; assertCommand: string; testLabel: string; matchedVariantLabel?: string; skip?: boolean; runtime?: string }>} + */ +async function getVariantBuildCommand(packageJsonPath, variantLabel, testAppPath) { try { const packageJsonContent = await readFile(packageJsonPath, 'utf-8'); - const packageJson: PackageJson = JSON.parse(packageJsonContent); + /** @type {PackageJson} */ + const packageJson = JSON.parse(packageJsonContent); const allVariants = [ ...(packageJson.sentryTest?.variants || []), @@ -132,12 +131,14 @@ async function getVariantBuildCommand( * The directory of the node version that the app pins. CI uses the same pin (see `node-version-file` in * .github/workflows/build.yml). Only node comes from Volta: Volta cannot run pnpm 12 * (https://github.com/volta-cli/volta/issues/2114), so pnpm comes from the root devDependencies. + * @param {string} cwd + * @returns {string | undefined} */ -function getPinnedNodeDir(cwd: string): string | undefined { +function getPinnedNodeDir(cwd) { try { return dirname(execFileSync('volta', ['run', 'node', '-p', 'process.execPath'], { cwd, encoding: 'utf8' }).trim()); } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + if (/** @type {NodeJS.ErrnoException} */ (error).code !== 'ENOENT') { throw error; } console.warn(`Volta is not installed, so ${cwd} runs on node ${process.version} instead of the version it pins.`); @@ -145,9 +146,12 @@ function getPinnedNodeDir(cwd: string): string | undefined { } } -async function run(): Promise { +async function run() { // Load environment variables from .env file locally - dotenv.config(); + const envFile = join(__dirname, '.env'); + if (existsSync(envFile)) { + process.loadEnvFile(envFile); + } // Allow to run a single app only via `yarn test:run ` const appName = process.argv[2] || ''; @@ -155,7 +159,8 @@ async function run(): Promise { const allTestFlags = process.argv.slice(3); // Check for --variant flag - let variantLabel: string | undefined; + /** @type {string | undefined} */ + let variantLabel; let skipNextFlag = false; const testFlags = allTestFlags.filter((flag, index) => { @@ -225,7 +230,11 @@ async function run(): Promise { await asyncExec('yarn clean:test-applications', { env, cwd: __dirname }); await asyncExec('pnpm cache delete "@sentry/*"', { env, cwd: __dirname }); - const testAppPaths = appName ? [appName.trim()] : globSync('*', { cwd: `${__dirname}/test-applications/` }); + const testAppPaths = appName + ? [appName.trim()] + : readdirSync(join(__dirname, 'test-applications'), { withFileTypes: true }) + .filter(entry => entry.isDirectory() && !entry.name.startsWith('.')) + .map(entry => entry.name); console.log(`Runnings tests for: ${testAppPaths.join(', ')}`); console.log(''); diff --git a/dev-packages/e2e-tests/tsconfig.json b/dev-packages/e2e-tests/tsconfig.json deleted file mode 100644 index f3ef44daf268..000000000000 --- a/dev-packages/e2e-tests/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "exclude": ["node_modules", "test-applications"], - "compilerOptions": { - "types": ["node"], - "allowJs": true, - "noEmit": true - } -} diff --git a/yarn.lock b/yarn.lock index cbd7ab1e2803..051639144bbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14207,16 +14207,16 @@ dotenv@16.4.7: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== -dotenv@17.4.2, dotenv@^17.3.1, dotenv@^17.4.2: - version "17.4.2" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" - integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== - dotenv@^16.4.5, dotenv@^16.6.1: version "16.6.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== +dotenv@^17.3.1, dotenv@^17.4.2: + version "17.4.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" + integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== + dset@^3.1.3, dset@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248" From 5225fe3030f85328b970b75a4e8e79ddbcf6fc78 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 16 Oct 2026 11:15:37 +0200 Subject: [PATCH 3/3] ci(e2e): Keep install-pnpm unchanged and install pnpm directly in E2E jobs Only the E2E jobs drop the root node_modules, so only they need another way to get pnpm. Canary, Lighthouse and local runs keep getting it through yarn.lock. Co-Authored-By: Claude Opus 5.5 --- .github/actions/install-pnpm/action.yml | 13 ++++--------- .github/workflows/build.yml | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/actions/install-pnpm/action.yml b/.github/actions/install-pnpm/action.yml index 79cbc8bd5062..9a7f2b45f132 100644 --- a/.github/actions/install-pnpm/action.yml +++ b/.github/actions/install-pnpm/action.yml @@ -1,16 +1,11 @@ name: 'Install pnpm' -description: 'Installs the pnpm version from the root devDependencies.' +description: 'Puts the pnpm version from the root devDependencies on PATH. Run it after the dependencies are installed.' runs: using: 'composite' steps: # The dev dependency is the one pnpm version for CI and local runs. An app that needs another version sets - # `packageManager` in its package.json, and pnpm switches to that version. It is read from package.json rather - # than node_modules, so jobs that don't restore the monorepo's dependencies can use it. - - name: Resolve pnpm version - id: pnpm-version + # `packageManager` in its package.json, and pnpm switches to that version. + - name: Add pnpm to PATH shell: bash - run: echo "version=$(jq -er '.devDependencies.pnpm' "$GITHUB_WORKSPACE/package.json")" >> "$GITHUB_OUTPUT" - - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - with: - version: ${{ steps.pnpm-version.outputs.version }} + run: echo "$GITHUB_WORKSPACE/node_modules/pnpm" >> "$GITHUB_PATH" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca3a7065e0fc..849817bb7a09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1088,8 +1088,14 @@ jobs: uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} + # Not `install-pnpm`: it takes pnpm from the root node_modules, which this job doesn't restore. + - name: Resolve pnpm version + id: pnpm-version + run: echo "version=$(jq -er '.devDependencies.pnpm' package.json)" >> "$GITHUB_OUTPUT" - name: Install pnpm - uses: ./.github/actions/install-pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: ${{ steps.pnpm-version.outputs.version }} - name: Restore tarball artifacts uses: actions/download-artifact@v8 @@ -1242,8 +1248,14 @@ jobs: uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} + # Not `install-pnpm`: it takes pnpm from the root node_modules, which this job doesn't restore. + - name: Resolve pnpm version + id: pnpm-version + run: echo "version=$(jq -er '.devDependencies.pnpm' package.json)" >> "$GITHUB_OUTPUT" - name: Install pnpm - uses: ./.github/actions/install-pnpm + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + with: + version: ${{ steps.pnpm-version.outputs.version }} - name: Restore tarball artifacts uses: actions/download-artifact@v8