Skip to content

Commit 213b16a

Browse files
committed
Merge remote-tracking branch 'origin/fn/node-verdaccio' into fn/playwright-docker-images
2 parents 5e8c190 + 40904f0 commit 213b16a

File tree

12 files changed

+1202
-246
lines changed

12 files changed

+1202
-246
lines changed

.github/actions/install-dependencies/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ runs:
1515
shell: bash
1616

1717
- name: Check dependency cache
18-
uses: actions/cache@v4
18+
uses: actions/cache@v5
1919
id: cache_dependencies
2020
with:
2121
path: ${{ env.CACHED_DEPENDENCY_PATHS }}

.gitignore

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

4040
.rpt2_cache
4141

42+
# verdaccio local registry (e2e tests)
43+
dev-packages/e2e-tests/verdaccio-config/storage/
44+
4245
lint-results.json
4346
trace.zip
4447

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

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
11
/* eslint-disable no-console */
2-
import * as childProcess from 'child_process';
2+
import { spawn } from 'child_process';
33
import { readFileSync } from 'fs';
44
import { globSync } from 'glob';
55
import * as path from 'path';
66

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

9+
function npmPublish(tarballPath: string, npmrc: string): Promise<void> {
10+
return new Promise((resolve, reject) => {
11+
const child = spawn('npm', ['--userconfig', npmrc, 'publish', tarballPath], {
12+
cwd: repositoryRoot,
13+
stdio: 'inherit',
14+
});
15+
16+
child.on('error', reject);
17+
child.on('close', code => {
18+
if (code === 0) {
19+
resolve();
20+
} else {
21+
reject(new Error(`Error publishing tarball ${tarballPath}`));
22+
}
23+
});
24+
});
25+
}
26+
927
/**
1028
* Publishes all built Sentry package tarballs to the local Verdaccio test registry.
29+
* Uses async `npm publish` so an in-process Verdaccio can still handle HTTP on the event loop.
1130
*/
12-
export function publishPackages(): void {
31+
export async function publishPackages(): Promise<void> {
1332
const version = (JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8')) as { version: string })
1433
.version;
1534

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

2948
for (const tarballPath of packageTarballPaths) {
3049
console.log(`Publishing tarball ${tarballPath} ...`);
31-
const result = childProcess.spawnSync('npm', ['--userconfig', npmrc, 'publish', tarballPath], {
32-
cwd: repositoryRoot,
33-
encoding: 'utf8',
34-
stdio: 'inherit',
35-
});
36-
37-
if (result.status !== 0) {
38-
throw new Error(`Error publishing tarball ${tarballPath}`);
39-
}
50+
await npmPublish(tarballPath, npmrc);
4051
}
4152
}

dev-packages/e2e-tests/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"test:validate-test-app-setups": "ts-node validate-test-app-setups.ts",
1414
"test:prepare": "ts-node prepare.ts",
1515
"test:validate": "run-s test:validate-configuration test:validate-test-app-setups",
16-
"clean": "rimraf tmp node_modules && yarn clean:test-applications && yarn clean:pnpm",
16+
"clean:verdaccio": "sh -c 'pkill -f verdaccio-runner.mjs 2>/dev/null || true'",
17+
"clean": "yarn clean:verdaccio && rimraf tmp node_modules verdaccio-config/storage && yarn clean:test-applications && yarn clean:pnpm",
1718
"ci:build-matrix": "ts-node ./lib/getTestMatrix.ts",
1819
"ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true",
1920
"ci:copy-to-temp": "ts-node ./ciCopyToTemp.ts",
@@ -28,6 +29,7 @@
2829
"glob": "^13.0.6",
2930
"rimraf": "^6.1.3",
3031
"ts-node": "10.9.2",
32+
"verdaccio": "6.5.0",
3133
"yaml": "2.8.3"
3234
},
3335
"volta": {

dev-packages/e2e-tests/prepare.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ async function run(): Promise<void> {
66
// Load environment variables from .env file locally
77
dotenv.config();
88

9-
try {
10-
registrySetup();
11-
} catch (error) {
12-
console.error(error);
13-
process.exit(1);
14-
}
9+
await registrySetup({ daemonize: true });
1510
}
1611

17-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
18-
run();
12+
run().catch(error => {
13+
console.error(error);
14+
process.exit(1);
15+
});
Lines changed: 121 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,137 @@
11
/* eslint-disable no-console */
2-
import * as childProcess from 'child_process';
3-
import { TEST_REGISTRY_CONTAINER_NAME, VERDACCIO_VERSION } from './lib/constants';
2+
import { spawn, spawnSync, type ChildProcess } from 'child_process';
3+
import * as fs from 'fs';
4+
import * as http from 'http';
5+
import * as path from 'path';
46
import { publishPackages } from './lib/publishPackages';
57

6-
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines
7-
function groupCIOutput(groupTitle: string, fn: () => void): void {
8+
const VERDACCIO_PORT = 4873;
9+
10+
let verdaccioChild: ChildProcess | undefined;
11+
12+
export interface RegistrySetupOptions {
13+
/**
14+
* When true, Verdaccio is spawned detached with stdio disconnected from the parent, then
15+
* the child is unref'd after a successful setup so the parent can exit while the registry
16+
* keeps running (e.g. `yarn test:prepare` then installs against 127.0.0.1:4873).
17+
*/
18+
daemonize?: boolean;
19+
}
20+
21+
/** Stops any Verdaccio runner from a previous prepare/run so port 4873 is free. */
22+
function killStrayVerdaccioRunner(): void {
23+
spawnSync('pkill', ['-f', 'verdaccio-runner.mjs'], { stdio: 'ignore' });
24+
}
25+
26+
async function groupCIOutput(groupTitle: string, fn: () => void | Promise<void>): Promise<void> {
827
if (process.env.CI) {
928
console.log(`::group::${groupTitle}`);
10-
fn();
11-
console.log('::endgroup::');
29+
try {
30+
await Promise.resolve(fn());
31+
} finally {
32+
console.log('::endgroup::');
33+
}
1234
} else {
13-
fn();
35+
await Promise.resolve(fn());
1436
}
1537
}
1638

17-
export function registrySetup(): void {
18-
groupCIOutput('Test Registry Setup', () => {
19-
// Stop test registry container (Verdaccio) if it was already running
20-
childProcess.spawnSync('docker', ['stop', TEST_REGISTRY_CONTAINER_NAME], { stdio: 'ignore' });
21-
console.log('Stopped previously running test registry');
22-
23-
// Start test registry (Verdaccio)
24-
const startRegistryProcessResult = childProcess.spawnSync(
25-
'docker',
26-
[
27-
'run',
28-
'--detach',
29-
'--rm',
30-
'--name',
31-
TEST_REGISTRY_CONTAINER_NAME,
32-
'-p',
33-
'4873:4873',
34-
'-v',
35-
`${__dirname}/verdaccio-config:/verdaccio/conf`,
36-
`verdaccio/verdaccio:${VERDACCIO_VERSION}`,
37-
],
38-
{ encoding: 'utf8', stdio: 'inherit' },
39-
);
40-
41-
if (startRegistryProcessResult.status !== 0) {
42-
throw new Error('Start Registry Process failed.');
39+
function waitUntilVerdaccioResponds(maxRetries: number = 60): Promise<void> {
40+
const pingUrl = `http://127.0.0.1:${VERDACCIO_PORT}/-/ping`;
41+
42+
function tryOnce(): Promise<boolean> {
43+
return new Promise(resolve => {
44+
const req = http.get(pingUrl, res => {
45+
res.resume();
46+
resolve((res.statusCode ?? 0) > 0 && (res.statusCode ?? 500) < 500);
47+
});
48+
req.on('error', () => resolve(false));
49+
req.setTimeout(2000, () => {
50+
req.destroy();
51+
resolve(false);
52+
});
53+
});
54+
}
55+
56+
return (async () => {
57+
for (let i = 0; i < maxRetries; i++) {
58+
if (await tryOnce()) {
59+
return;
60+
}
61+
await new Promise(r => setTimeout(r, 1000));
4362
}
63+
throw new Error('Verdaccio did not start in time.');
64+
})();
65+
}
66+
67+
function startVerdaccioChild(configPath: string, port: number, daemonize: boolean): ChildProcess {
68+
const runnerPath = path.join(__dirname, 'verdaccio-runner.mjs');
69+
const verbose = process.env.E2E_VERDACCIO_VERBOSE === '1';
70+
return spawn(process.execPath, [runnerPath, configPath, String(port)], {
71+
detached: daemonize,
72+
stdio: daemonize && !verbose ? 'ignore' : 'inherit',
73+
});
74+
}
75+
76+
async function stopVerdaccioChild(): Promise<void> {
77+
const child = verdaccioChild;
78+
verdaccioChild = undefined;
79+
if (!child || child.killed) {
80+
return;
81+
}
82+
child.kill('SIGTERM');
83+
await new Promise<void>(resolve => {
84+
child.once('exit', () => resolve());
85+
setTimeout(resolve, 5000);
86+
});
87+
}
88+
89+
/** Drop the child handle so the parent process can exit; Verdaccio keeps running. */
90+
function detachVerdaccioRunner(): void {
91+
const child = verdaccioChild;
92+
verdaccioChild = undefined;
93+
if (child && !child.killed) {
94+
child.unref();
95+
}
96+
}
97+
98+
export async function registrySetup(options: RegistrySetupOptions = {}): Promise<void> {
99+
const { daemonize = false } = options;
100+
await groupCIOutput('Test Registry Setup', async () => {
101+
killStrayVerdaccioRunner();
102+
103+
const configPath = path.join(__dirname, 'verdaccio-config', 'config.yaml');
104+
const storagePath = path.join(__dirname, 'verdaccio-config', 'storage');
105+
106+
// Clear previous registry storage to ensure a fresh state
107+
fs.rmSync(storagePath, { recursive: true, force: true });
44108

45-
publishPackages();
109+
// Verdaccio runs in a child process so tarball uploads are not starved by the
110+
// same Node event loop as ts-node (in-process runServer + npm publish could hang).
111+
console.log('Starting Verdaccio...');
112+
113+
verdaccioChild = startVerdaccioChild(configPath, VERDACCIO_PORT, daemonize);
114+
115+
try {
116+
await waitUntilVerdaccioResponds(60);
117+
console.log('Verdaccio is ready');
118+
119+
await publishPackages();
120+
} catch (error) {
121+
await stopVerdaccioChild();
122+
throw error;
123+
}
46124
});
47125

126+
if (daemonize) {
127+
detachVerdaccioRunner();
128+
}
129+
48130
console.log('');
49131
console.log('');
50132
}
133+
134+
export async function registryCleanup(): Promise<void> {
135+
await stopVerdaccioChild();
136+
killStrayVerdaccioRunner();
137+
}

dev-packages/e2e-tests/run.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { sync as globSync } from 'glob';
66
import { tmpdir } from 'os';
77
import { join, resolve } from 'path';
88
import { copyToTemp } from './lib/copyToTemp';
9-
import { registrySetup } from './registrySetup';
9+
import { registryCleanup, registrySetup } from './registrySetup';
1010

1111
interface SentryTestVariant {
1212
'build-command': string;
@@ -184,14 +184,16 @@ async function run(): Promise<void> {
184184
...envVarsToInject,
185185
};
186186

187+
const skipRegistry = !!process.env.SKIP_REGISTRY;
188+
187189
try {
190+
if (!skipRegistry) {
191+
await registrySetup();
192+
}
193+
188194
console.log('Cleaning test-applications...');
189195
console.log('');
190196

191-
if (!process.env.SKIP_REGISTRY) {
192-
registrySetup();
193-
}
194-
195197
await asyncExec('pnpm clean:test-applications', { env, cwd: __dirname });
196198
await asyncExec('pnpm cache delete "@sentry/*"', { env, cwd: __dirname });
197199

@@ -247,11 +249,14 @@ async function run(): Promise<void> {
247249
// clean up (although this is tmp, still nice to do)
248250
await rm(tmpDirPath, { recursive: true });
249251
}
250-
} catch (error) {
251-
console.error(error);
252-
process.exit(1);
252+
} finally {
253+
if (!skipRegistry) {
254+
await registryCleanup();
255+
}
253256
}
254257
}
255258

256-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
257-
run();
259+
run().catch(error => {
260+
console.error(error);
261+
process.exit(1);
262+
});

dev-packages/e2e-tests/verdaccio-config/config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
# https://github.com/verdaccio/verdaccio/tree/master/conf
99
#
1010

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

1414
# https://verdaccio.org/docs/configuration#authentication
1515
auth:
1616
htpasswd:
17-
file: /verdaccio/storage/htpasswd
17+
file: ./storage/htpasswd
1818

1919
# https://verdaccio.org/docs/configuration#uplinks
2020
# a list of other known repositories we can talk to
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* eslint-disable no-console */
2+
import { createRequire } from 'node:module';
3+
4+
const require = createRequire(import.meta.url);
5+
const { runServer } = require('verdaccio');
6+
7+
const configPath = process.argv[2];
8+
const port = parseInt(process.argv[3], 10);
9+
10+
if (!configPath || !Number.isFinite(port)) {
11+
console.error('verdaccio-runner: expected <configPath> <port> argv');
12+
process.exit(1);
13+
}
14+
15+
try {
16+
const server = await runServer(configPath, { listenArg: String(port) });
17+
await new Promise((resolve, reject) => {
18+
server.once('error', reject);
19+
server.listen(port, '127.0.0.1', () => resolve());
20+
});
21+
} catch (err) {
22+
console.error(err);
23+
process.exit(1);
24+
}

0 commit comments

Comments
 (0)