Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ local.log

.rpt2_cache

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

lint-results.json
trace.zip

Expand Down
1 change: 0 additions & 1 deletion dev-packages/e2e-tests/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const TEST_REGISTRY_CONTAINER_NAME = 'verdaccio-e2e-test-registry';
export const DEFAULT_BUILD_TIMEOUT_SECONDS = 60 * 5;
export const DEFAULT_TEST_TIMEOUT_SECONDS = 60 * 2;
export const VERDACCIO_VERSION = '5.22.1';
Comment thread
cursor[bot] marked this conversation as resolved.
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
17 changes: 8 additions & 9 deletions dev-packages/e2e-tests/prepare.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/* eslint-disable no-console */
import * as dotenv from 'dotenv';
import { registrySetup } from './registrySetup';
import { registryRelease, registrySetup } from './registrySetup';

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 });
// Leave Verdaccio running for later CI steps (e.g. pnpm install). Detached stdio so this process can exit cleanly.
registryRelease();
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
run().catch(error => {
console.error(error);
process.exit(1);
});
160 changes: 126 additions & 34 deletions dev-packages/e2e-tests/registrySetup.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,142 @@
/* 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.
* Use for `prepare.ts` so `yarn test:prepare` can exit while the registry keeps running.
* (Inherited stdio otherwise keeps the parent process tied to the child on many systems.)
*/
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',
});
}

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

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

// 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;
}
});

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

/**
* Detach the Verdaccio child so `ts-node` can exit while the registry keeps running
* (e.g. CI: `yarn test:prepare` then `pnpm test:build` in later steps).
*/
export function registryRelease(): void {
const child = verdaccioChild;
verdaccioChild = undefined;
if (child && !child.killed) {
child.unref();
}
}

export async function registryCleanup(): Promise<void> {
await stopVerdaccioChild();
killStrayVerdaccioRunner();
const pidFile = path.join(__dirname, 'verdaccio-config', '.verdaccio.pid');
try {
fs.unlinkSync(pidFile);
} catch {
// File may not exist
}
}
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
24 changes: 24 additions & 0 deletions dev-packages/e2e-tests/verdaccio-runner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* 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 {
const server = await runServer(configPath, { listenArg: String(port) });
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', () => resolve());
});
Comment thread
cursor[bot] marked this conversation as resolved.
} catch (err) {
console.error(err);
process.exit(1);
}
Loading
Loading