Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/install-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ runs:
shell: bash

- name: Check dependency cache
uses: actions/cache@v4
uses: actions/cache@v5
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover I noticed I forgot to bump, not really related but this is non-breaking for us.

id: cache_dependencies
with:
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ local.log

.rpt2_cache

# verdaccio local registry (e2e tests)
dev-packages/e2e-tests/verdaccio-config/storage/

lint-results.json
trace.zip

Expand Down
4 changes: 0 additions & 4 deletions dev-packages/e2e-tests/lib/constants.ts

This file was deleted.

33 changes: 22 additions & 11 deletions dev-packages/e2e-tests/lib/publishPackages.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
/* eslint-disable no-console */
import * as childProcess from 'child_process';
import { spawn } from 'child_process';
import { readFileSync } from 'fs';
import { globSync } from 'glob';
import * as path from 'path';

const repositoryRoot = path.resolve(__dirname, '../../..');

function npmPublish(tarballPath: string, npmrc: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn('npm', ['--userconfig', npmrc, 'publish', tarballPath], {
cwd: repositoryRoot,
stdio: 'inherit',
});

child.on('error', reject);
child.on('close', code => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Error publishing tarball ${tarballPath}`));
}
});
});
}

/**
* Publishes all built Sentry package tarballs to the local Verdaccio test registry.
* Uses async `npm publish` so an in-process Verdaccio can still handle HTTP on the event loop.
*/
export function publishPackages(): void {
export async function publishPackages(): Promise<void> {
const version = (JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8')) as { version: string })
.version;

Expand All @@ -28,14 +47,6 @@ export function publishPackages(): void {

for (const tarballPath of packageTarballPaths) {
console.log(`Publishing tarball ${tarballPath} ...`);
const result = childProcess.spawnSync('npm', ['--userconfig', npmrc, 'publish', tarballPath], {
cwd: repositoryRoot,
encoding: 'utf8',
stdio: 'inherit',
});

if (result.status !== 0) {
throw new Error(`Error publishing tarball ${tarballPath}`);
}
await npmPublish(tarballPath, npmrc);
}
}
4 changes: 3 additions & 1 deletion dev-packages/e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"test:validate-test-app-setups": "ts-node validate-test-app-setups.ts",
"test:prepare": "ts-node prepare.ts",
"test:validate": "run-s test:validate-configuration test:validate-test-app-setups",
"clean": "rimraf tmp node_modules && yarn clean:test-applications && yarn clean:pnpm",
"clean:verdaccio": "sh -c 'pkill -f verdaccio-runner.mjs 2>/dev/null || true'",
"clean": "yarn clean:verdaccio && rimraf tmp node_modules verdaccio-config/storage && yarn clean:test-applications && yarn clean:pnpm",
"ci:build-matrix": "ts-node ./lib/getTestMatrix.ts",
"ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true",
"ci:copy-to-temp": "ts-node ./ciCopyToTemp.ts",
Expand All @@ -28,6 +29,7 @@
"glob": "^13.0.6",
"rimraf": "^6.1.3",
"ts-node": "10.9.2",
"verdaccio": "6.5.0",
"yaml": "2.8.3"
},
"volta": {
Expand Down
13 changes: 5 additions & 8 deletions dev-packages/e2e-tests/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ async function run(): Promise<void> {
// Load environment variables from .env file locally
dotenv.config();

try {
registrySetup();
} catch (error) {
console.error(error);
process.exit(1);
}
await registrySetup({ daemonize: true });
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
run().catch(error => {
console.error(error);
process.exit(1);
});
158 changes: 124 additions & 34 deletions dev-packages/e2e-tests/registrySetup.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,140 @@
/* eslint-disable no-console */
import * as childProcess from 'child_process';
import { TEST_REGISTRY_CONTAINER_NAME, VERDACCIO_VERSION } from './lib/constants';
import { spawn, spawnSync, type ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
import { publishPackages } from './lib/publishPackages';

// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines
function groupCIOutput(groupTitle: string, fn: () => void): void {
const VERDACCIO_PORT = 4873;

let verdaccioChild: ChildProcess | undefined;

export interface RegistrySetupOptions {
/**
* When true, Verdaccio is spawned detached with stdio disconnected from the parent, then
* the child is unref'd after a successful setup so the parent can exit while the registry
* keeps running (e.g. `yarn test:prepare` then installs against 127.0.0.1:4873).
*/
daemonize?: boolean;
}

/** Stops any Verdaccio runner from a previous prepare/run so port 4873 is free. */
function killStrayVerdaccioRunner(): void {
spawnSync('pkill', ['-f', 'verdaccio-runner.mjs'], { stdio: 'ignore' });
}

async function groupCIOutput(groupTitle: string, fn: () => void | Promise<void>): Promise<void> {
if (process.env.CI) {
console.log(`::group::${groupTitle}`);
fn();
console.log('::endgroup::');
try {
await Promise.resolve(fn());
} finally {
console.log('::endgroup::');
}
} else {
fn();
await Promise.resolve(fn());
}
}

export function registrySetup(): void {
groupCIOutput('Test Registry Setup', () => {
// Stop test registry container (Verdaccio) if it was already running
childProcess.spawnSync('docker', ['stop', TEST_REGISTRY_CONTAINER_NAME], { stdio: 'ignore' });
console.log('Stopped previously running test registry');

// Start test registry (Verdaccio)
const startRegistryProcessResult = childProcess.spawnSync(
'docker',
[
'run',
'--detach',
'--rm',
'--name',
TEST_REGISTRY_CONTAINER_NAME,
'-p',
'4873:4873',
'-v',
`${__dirname}/verdaccio-config:/verdaccio/conf`,
`verdaccio/verdaccio:${VERDACCIO_VERSION}`,
],
{ encoding: 'utf8', stdio: 'inherit' },
);

if (startRegistryProcessResult.status !== 0) {
throw new Error('Start Registry Process failed.');
function waitUntilVerdaccioResponds(maxRetries: number = 60): Promise<void> {
const pingUrl = `http://127.0.0.1:${VERDACCIO_PORT}/-/ping`;

function tryOnce(): Promise<boolean> {
return new Promise(resolve => {
const req = http.get(pingUrl, res => {
res.resume();
resolve((res.statusCode ?? 0) > 0 && (res.statusCode ?? 500) < 500);
});
req.on('error', () => resolve(false));
req.setTimeout(2000, () => {
req.destroy();
resolve(false);
});
});
}

return (async () => {
for (let i = 0; i < maxRetries; i++) {
if (await tryOnce()) {
return;
}
await new Promise(r => setTimeout(r, 1000));
}
throw new Error('Verdaccio did not start in time.');
})();
}

function startVerdaccioChild(configPath: string, port: number, daemonize: boolean): ChildProcess {
const runnerPath = path.join(__dirname, 'verdaccio-runner.mjs');
const verbose = process.env.E2E_VERDACCIO_VERBOSE === '1';
return spawn(process.execPath, [runnerPath, configPath, String(port)], {
detached: daemonize,
stdio: daemonize && !verbose ? 'ignore' : 'inherit',
});
}

async function stopVerdaccioChild(): Promise<void> {
const child = verdaccioChild;
verdaccioChild = undefined;
if (!child || child.killed) {
return;
}
child.kill('SIGTERM');
await new Promise<void>(resolve => {
const timeoutId = setTimeout(resolve, 5000);
child.once('exit', () => {
clearTimeout(timeoutId);
resolve();
});
});
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
}

/** Drop the child handle so the parent process can exit; Verdaccio keeps running. */
function detachVerdaccioRunner(): void {
const child = verdaccioChild;
verdaccioChild = undefined;
if (child && !child.killed) {
child.unref();
}
}

export async function registrySetup(options: RegistrySetupOptions = {}): Promise<void> {
const { daemonize = false } = options;
await groupCIOutput('Test Registry Setup', async () => {
killStrayVerdaccioRunner();

const configPath = path.join(__dirname, 'verdaccio-config', 'config.yaml');
const storagePath = path.join(__dirname, 'verdaccio-config', 'storage');

// Clear previous registry storage to ensure a fresh state
fs.rmSync(storagePath, { recursive: true, force: true });

publishPackages();
// Verdaccio runs in a child process so tarball uploads are not starved by the
// same Node event loop as ts-node (in-process runServer + npm publish could hang).
console.log('Starting Verdaccio...');

verdaccioChild = startVerdaccioChild(configPath, VERDACCIO_PORT, daemonize);

try {
await waitUntilVerdaccioResponds(60);
console.log('Verdaccio is ready');

await publishPackages();
} catch (error) {
await stopVerdaccioChild();
throw error;
}
});

if (daemonize) {
detachVerdaccioRunner();
}

console.log('');
console.log('');
}

export async function registryCleanup(): Promise<void> {
await stopVerdaccioChild();
killStrayVerdaccioRunner();
}
25 changes: 15 additions & 10 deletions dev-packages/e2e-tests/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { sync as globSync } from 'glob';
import { tmpdir } from 'os';
import { join, resolve } from 'path';
import { copyToTemp } from './lib/copyToTemp';
import { registrySetup } from './registrySetup';
import { registryCleanup, registrySetup } from './registrySetup';

interface SentryTestVariant {
'build-command': string;
Expand Down Expand Up @@ -184,14 +184,16 @@ async function run(): Promise<void> {
...envVarsToInject,
};

const skipRegistry = !!process.env.SKIP_REGISTRY;

try {
if (!skipRegistry) {
await registrySetup();
}

console.log('Cleaning test-applications...');
console.log('');

if (!process.env.SKIP_REGISTRY) {
registrySetup();
}

await asyncExec('pnpm clean:test-applications', { env, cwd: __dirname });
await asyncExec('pnpm cache delete "@sentry/*"', { env, cwd: __dirname });

Expand Down Expand Up @@ -247,11 +249,14 @@ async function run(): Promise<void> {
// clean up (although this is tmp, still nice to do)
await rm(tmpDirPath, { recursive: true });
}
} catch (error) {
console.error(error);
process.exit(1);
} finally {
if (!skipRegistry) {
await registryCleanup();
}
}
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
run().catch(error => {
console.error(error);
process.exit(1);
});
6 changes: 3 additions & 3 deletions dev-packages/e2e-tests/verdaccio-config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
# https://github.com/verdaccio/verdaccio/tree/master/conf
#

# path to a directory with all packages
storage: /verdaccio/storage/data
# Repo-local storage (relative to this file). Absolute /verdaccio/... matches Docker-only templates and is not writable on typical dev machines.
storage: ./storage/data

# https://verdaccio.org/docs/configuration#authentication
auth:
htpasswd:
file: /verdaccio/storage/htpasswd
file: ./storage/htpasswd

# https://verdaccio.org/docs/configuration#uplinks
# a list of other known repositories we can talk to
Expand Down
26 changes: 26 additions & 0 deletions dev-packages/e2e-tests/verdaccio-runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable no-console */
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const { runServer } = require('verdaccio');

const configPath = process.argv[2];
const port = parseInt(process.argv[3], 10);

if (!configPath || !Number.isFinite(port)) {
console.error('verdaccio-runner: expected <configPath> <port> argv');
process.exit(1);
}

try {
// runServer resolves to the Express app; binding errors are emitted on the
// http.Server returned by app.listen(), not on the app itself.
const app = await runServer(configPath, { listenArg: String(port) });
await new Promise((resolve, reject) => {
const httpServer = app.listen(port, '127.0.0.1', () => resolve());
httpServer.once('error', reject);
});
Comment thread
cursor[bot] marked this conversation as resolved.
} catch (err) {
console.error(err);
process.exit(1);
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@
"wide-align/string-width": "4.2.3",
"cliui/wrap-ansi": "7.0.0",
"sucrase": "getsentry/sucrase#es2020-polyfills",
"**/express/path-to-regexp": "0.1.12"
"**/express/path-to-regexp": "0.1.12",
"**/@verdaccio/local-storage-legacy/lodash": "4.17.23",
"**/@verdaccio/core/minimatch": "~7.4.9"
},
"version": "0.0.0",
"name": "sentry-javascript"
Expand Down
Loading
Loading