Skip to content

Commit 215c16e

Browse files
authored
chore(e2e-tests): Use tarball symlinks for E2E tests instead of verdaccio (#20386)
This PR completely removes verdaccio in favor of referencing the built tarballs directly. We rely on pnpm overrides which we inject into the test app to ensure that also transitive dependencies are correct. This is basically instant and reduces about 20s of verdaccio/registry preparation time we used to have before each e2e test app. ## How it works * We use a reference to a tarball for internal dependencies in our e2e tests. these are virtual files in /packed directory, e.g. `"@sentry/browser": "file:../../packed/sentry-browser-packed.tgz",` * `yarn test:prepare` will create/update those packed files with symlinks to the current tarballs (which have the version in their filename so we cannot just link to them directly consistently) * Additionally, we adjust the created test apps in the tmp dir by adding a `pnpm.overrides` section for all our apps pointing them to the packed folder, to ensure we get these as transitive dependencies as well Locally, you can continue to use `yarn test:run test-app-name` and it will do all of that under the hood, nothing else needed (except to generate the tarballs, as before). On CI I adjusted the places to run the necessary pieces in addition to ensure everything works as expected. ## Example generated test app It effectively creates a package.json for the test app that looks like this: ```json { "name": "node-hapi", "version": "1.0.0", "private": true, "scripts": { "build": "tsc", "start": "node src/app.js", "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install", "test:assert": "pnpm test" }, "dependencies": { "@hapi/boom": "10.0.1", "@hapi/hapi": "21.3.10", "@sentry/node": "file:../../packed/sentry-node-packed.tgz" }, "devDependencies": { "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:~/my-app/dev-packages/test-utils" }, "volta": { "extends": "~/my-app/dev-packages/e2e-tests/package.json" }, "pnpm": { "overrides": { "@sentry-internal/browser-utils": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-internal-browser-utils-packed.tgz", "@sentry-internal/eslint-config-sdk": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-internal-eslint-config-sdk-packed.tgz", "@sentry-internal/eslint-plugin-sdk": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-internal-eslint-plugin-sdk-packed.tgz", "@sentry-internal/feedback": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-internal-feedback-packed.tgz", "@sentry-internal/replay": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-internal-replay-packed.tgz", "@sentry-internal/replay-canvas": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-internal-replay-canvas-packed.tgz", "@sentry-internal/typescript": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-internal-typescript-packed.tgz", "@sentry/angular": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-angular-packed.tgz", "@sentry/astro": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-astro-packed.tgz", "@sentry/aws-serverless": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-aws-serverless-packed.tgz", "@sentry/browser": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-browser-packed.tgz", "@sentry/bun": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-bun-packed.tgz", "@sentry/cloudflare": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-cloudflare-packed.tgz", "@sentry/core": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-core-packed.tgz", "@sentry/deno": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-deno-packed.tgz", "@sentry/effect": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-effect-packed.tgz", "@sentry/elysia": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-elysia-packed.tgz", "@sentry/ember": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-ember-packed.tgz", "@sentry/gatsby": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-gatsby-packed.tgz", "@sentry/google-cloud-serverless": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-google-cloud-serverless-packed.tgz", "@sentry/hono": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-hono-packed.tgz", "@sentry/nestjs": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-nestjs-packed.tgz", "@sentry/nextjs": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-nextjs-packed.tgz", "@sentry/node": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-node-packed.tgz", "@sentry/node-core": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-node-core-packed.tgz", "@sentry/node-native": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-node-native-packed.tgz", "@sentry/nuxt": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-nuxt-packed.tgz", "@sentry/opentelemetry": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-opentelemetry-packed.tgz", "@sentry/profiling-node": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-profiling-node-packed.tgz", "@sentry/react": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-react-packed.tgz", "@sentry/react-router": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-react-router-packed.tgz", "@sentry/remix": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-remix-packed.tgz", "@sentry/solid": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-solid-packed.tgz", "@sentry/solidstart": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-solidstart-packed.tgz", "@sentry/svelte": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-svelte-packed.tgz", "@sentry/sveltekit": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-sveltekit-packed.tgz", "@sentry/tanstackstart": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-tanstackstart-packed.tgz", "@sentry/tanstackstart-react": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-tanstackstart-react-packed.tgz", "@sentry/types": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-types-packed.tgz", "@sentry/vercel-edge": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-vercel-edge-packed.tgz", "@sentry/vue": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-vue-packed.tgz", "@sentry/wasm": "file:~/my-app/dev-packages/e2e-tests/packed/sentry-wasm-packed.tgz" } } } ``` then installs this normally with the regular registry.
1 parent 617fede commit 215c16e

314 files changed

Lines changed: 677 additions & 2142 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/e2e/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ All tests completed successfully. Your SDK changes work correctly with this test
116116

117117
- **No tarballs found**: Run `yarn build && yarn build:tarball` at repository root
118118
- **Test app not found**: List available apps and ask user to clarify
119-
- **Verdaccio not running**: Tests should start Verdaccio automatically, but if issues occur, check Docker
119+
- **Packed tarballs missing**: Run `yarn build:tarball` at the repo root, then `yarn test:prepare` in `dev-packages/e2e-tests`
120120
- **Build failures**: Fix build errors before running tests
121121

122122
## Common Test Applications

.github/workflows/build.yml

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -961,18 +961,24 @@ jobs:
961961
if: steps.restore-tarball-cache.outputs.cache-hit != 'true'
962962
run: yarn build:tarball
963963

964-
- name: Validate Verdaccio
965-
run: yarn test:validate
964+
- name: Prepare e2e tests
965+
run: yarn test:prepare
966966
working-directory: dev-packages/e2e-tests
967967

968-
- name: Prepare Verdaccio
969-
run: yarn test:prepare
968+
- name: Validate e2e tests setup
969+
run: yarn test:validate
970970
working-directory: dev-packages/e2e-tests
971971

972972
- name: Copy to temp
973973
run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
974974
working-directory: dev-packages/e2e-tests
975975

976+
- name: Add pnpm overrides
977+
run:
978+
yarn ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
979+
}}/dev-packages/e2e-tests/packed
980+
working-directory: dev-packages/e2e-tests
981+
976982
- name: Build E2E app
977983
working-directory: ${{ runner.temp }}/test-application
978984
timeout-minutes: 7
@@ -1071,18 +1077,24 @@ jobs:
10711077
if: steps.restore-tarball-cache.outputs.cache-hit != 'true'
10721078
run: yarn build:tarball
10731079

1074-
- name: Validate Verdaccio
1075-
run: yarn test:validate
1080+
- name: Prepare E2E tests
1081+
run: yarn test:prepare
10761082
working-directory: dev-packages/e2e-tests
10771083

1078-
- name: Prepare Verdaccio
1079-
run: yarn test:prepare
1084+
- name: Validate test setup
1085+
run: yarn test:validate
10801086
working-directory: dev-packages/e2e-tests
10811087

10821088
- name: Copy to temp
10831089
run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
10841090
working-directory: dev-packages/e2e-tests
10851091

1092+
- name: Add pnpm overrides
1093+
run:
1094+
yarn ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
1095+
}}/dev-packages/e2e-tests/packed
1096+
working-directory: dev-packages/e2e-tests
1097+
10861098
- name: Build E2E app
10871099
working-directory: ${{ runner.temp }}/test-application
10881100
timeout-minutes: 7

.github/workflows/canary.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,24 @@ jobs:
140140
path: ${{ env.CACHED_BUILD_PATHS }}
141141
key: canary-${{ env.HEAD_COMMIT }}
142142

143-
- name: Validate Verdaccio
144-
run: yarn test:validate
143+
- name: Prepare e2e tests
144+
run: yarn test:prepare
145145
working-directory: dev-packages/e2e-tests
146146

147-
- name: Prepare Verdaccio
148-
run: yarn test:prepare
147+
- name: Validate test setup
148+
run: yarn test:validate
149149
working-directory: dev-packages/e2e-tests
150150

151151
- name: Copy to temp
152152
run: yarn ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp }}/test-application
153153
working-directory: dev-packages/e2e-tests
154154

155+
- name: Add pnpm overrides
156+
run:
157+
yarn ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
158+
}}/dev-packages/e2e-tests/packed
159+
working-directory: dev-packages/e2e-tests
160+
155161
- name: Build E2E app
156162
working-directory: ${{ runner.temp }}/test-application
157163
timeout-minutes: 7

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ local.log
3939

4040
.rpt2_cache
4141

42-
# verdaccio local registry (e2e tests)
43-
dev-packages/e2e-tests/verdaccio-config/storage/
44-
4542
lint-results.json
4643
trace.zip
4744

dev-packages/e2e-tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ tmp
44
.tmp_build_stderr
55
pnpm-lock.yaml
66
.last-run.json
7+
packed

dev-packages/e2e-tests/ciCopyToTemp.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ async function run(): Promise<void> {
1515
await copyToTemp(originalPath, tmpDirPath);
1616
}
1717

18-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
19-
run();
18+
run().catch(error => {
19+
console.error(error);
20+
process.exit(1);
21+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable no-console */
2+
3+
import { addPnpmOverrides } from './lib/pnpmOverrides';
4+
import * as path from 'path';
5+
6+
async function run(): Promise<void> {
7+
const tmpDirPath = process.argv[2];
8+
const packedDirPath = process.argv[3];
9+
10+
if (!tmpDirPath || !packedDirPath) {
11+
throw new Error('Tmp dir path and packed dir path are required');
12+
}
13+
14+
await addPnpmOverrides(path.resolve(tmpDirPath), path.resolve(packedDirPath));
15+
}
16+
17+
run().catch(error => {
18+
console.error(error);
19+
process.exit(1);
20+
});

dev-packages/e2e-tests/lib/copyToTemp.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable no-console */
22
import { readFileSync, writeFileSync } from 'fs';
33
import { cp } from 'fs/promises';
4-
import { join } from 'path';
4+
import { isAbsolute, join, resolve } from 'path';
55

66
export async function copyToTemp(originalPath: string, tmpDirPath: string): Promise<void> {
77
// copy files to tmp dir
@@ -35,7 +35,7 @@ function fixPackageJson(cwd: string): void {
3535
const extendsPath = packageJson.volta.extends;
3636
// We add a virtual dir to ensure that the relative depth is consistent
3737
// dirPath is relative to ./../test-applications/xxx
38-
const newPath = join(__dirname, 'virtual-dir/', extendsPath);
38+
const newPath = resolve(__dirname, 'virtual-dir/', extendsPath);
3939
packageJson.volta.extends = newPath;
4040
console.log(`Fixed volta.extends to ${newPath}`);
4141
} else {
@@ -45,17 +45,24 @@ function fixPackageJson(cwd: string): void {
4545
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
4646
}
4747

48-
function fixFileLinkDependencies(dependencyObj: Record<string, string>): void {
48+
// Exported for pnpmOverrides as well
49+
export function fixFileLinkDependencies(dependencyObj: Record<string, string>): void {
4950
for (const [key, value] of Object.entries(dependencyObj)) {
50-
if (value.startsWith('link:')) {
51-
const dirPath = value.replace('link:', '');
52-
53-
// We add a virtual dir to ensure that the relative depth is consistent
54-
// dirPath is relative to ./../test-applications/xxx
55-
const newPath = join(__dirname, 'virtual-dir/', dirPath);
51+
const prefix = value.startsWith('link:') ? 'link:' : value.startsWith('file:') ? 'file:' : null;
52+
if (!prefix) {
53+
continue;
54+
}
5655

57-
dependencyObj[key] = `link:${newPath}`;
58-
console.log(`Fixed ${key} dependency to ${newPath}`);
56+
const dirPath = value.slice(prefix.length);
57+
if (isAbsolute(dirPath)) {
58+
continue;
5959
}
60+
61+
// We add a virtual dir to ensure that the relative depth is consistent
62+
// dirPath is relative to ./../test-applications/xxx
63+
const absPath = resolve(__dirname, 'virtual-dir', dirPath);
64+
65+
dependencyObj[key] = `${prefix}${absPath}`;
66+
console.log(`Fixed ${key} dependency to ${absPath}`);
6067
}
6168
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as path from 'path';
2+
import * as fs from 'fs';
3+
import { sync as globSync } from 'glob';
4+
5+
const E2E_TESTS_ROOT = path.resolve(__dirname, '..');
6+
const REPOSITORY_ROOT = path.resolve(E2E_TESTS_ROOT, '../..');
7+
8+
/**
9+
* Workspace @sentry and @sentry-internal packages that have a built tarball for the E2E version.
10+
* @returns The names of the published Sentry tarball packages.
11+
*/
12+
export function getPublishedSentryTarballPackageNames(): string[] {
13+
const version = getE2eTestsPackageVersion();
14+
const names: string[] = [];
15+
16+
for (const packageJsonPath of globSync('packages/*/package.json', {
17+
cwd: REPOSITORY_ROOT,
18+
absolute: true,
19+
})) {
20+
const pkg = readJson<{ name?: string }>(packageJsonPath);
21+
const name = pkg.name;
22+
if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) {
23+
continue;
24+
}
25+
const packageDir = path.dirname(packageJsonPath);
26+
const tarball = path.join(packageDir, versionedTarballFilename(name, version));
27+
if (fs.existsSync(tarball)) {
28+
names.push(name);
29+
}
30+
}
31+
32+
return names.sort();
33+
}
34+
35+
/** Stable symlink name in `packed/` (no version segment). */
36+
export function packedSymlinkFilename(packageName: string): string {
37+
return `${npmPackBasename(packageName)}-packed.tgz`;
38+
}
39+
40+
/**
41+
* Versioned tarball filename produced by `npm pack` in a package directory.
42+
*/
43+
export function versionedTarballFilename(packageName: string, version: string): string {
44+
return `${npmPackBasename(packageName)}-${version}.tgz`;
45+
}
46+
47+
/**
48+
* npm pack tarball basename (without version and .tgz), e.g. `@sentry/core` -> `sentry-core`.
49+
*/
50+
function npmPackBasename(packageName: string): string {
51+
return packageName.replace(/^@/, '').replace(/\//g, '-');
52+
}
53+
54+
function readJson<T>(filePath: string): T {
55+
return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T;
56+
}
57+
58+
function getE2eTestsPackageVersion(): string {
59+
return readJson<{ version: string }>(path.join(E2E_TESTS_ROOT, 'package.json')).version;
60+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { readFile, writeFile } from 'fs/promises';
2+
import * as path from 'path';
3+
import { getPublishedSentryTarballPackageNames, packedSymlinkFilename } from './packedTarballUtils';
4+
import { fixFileLinkDependencies } from './copyToTemp';
5+
6+
/**
7+
* For a given temp test application directory, add pnpm.overrides to pin the internal Sentry packages to the packed tarballs.
8+
* This is used to ensure that the test application uses the correct version of the internal Sentry packages.
9+
* @param tmpDirPath - The temporary directory path of the test application.
10+
* @param packedDirPath - The path to the packed tarballs.
11+
* @param packageNames - The names of the internal Sentry packages to pin to the packed tarballs.
12+
* @returns
13+
*/
14+
export async function addPnpmOverrides(tmpDirPath: string, packedDirPath: string): Promise<void> {
15+
const packageJsonPath = path.join(tmpDirPath, 'package.json');
16+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as {
17+
pnpm?: { overrides?: Record<string, string> };
18+
};
19+
20+
const overrides: Record<string, string> = {};
21+
22+
const packageNames = getPublishedSentryTarballPackageNames();
23+
24+
for (const packageName of packageNames) {
25+
overrides[packageName] = `file:${packedDirPath}/${packedSymlinkFilename(packageName)}`;
26+
}
27+
28+
const fixedOverrides = packageJson.pnpm?.overrides ?? {};
29+
fixFileLinkDependencies(fixedOverrides);
30+
31+
packageJson.pnpm = {
32+
...packageJson.pnpm,
33+
overrides: {
34+
...overrides,
35+
...fixedOverrides,
36+
},
37+
};
38+
39+
// oxlint-disable-next-line no-console
40+
console.log(`Added ${packageNames.length} internal Sentry packages to pnpm.overrides`);
41+
42+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
43+
}

0 commit comments

Comments
 (0)