|
1 | 1 | /* 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'; |
4 | 6 | import { publishPackages } from './lib/publishPackages'; |
5 | 7 |
|
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> { |
8 | 27 | if (process.env.CI) { |
9 | 28 | console.log(`::group::${groupTitle}`); |
10 | | - fn(); |
11 | | - console.log('::endgroup::'); |
| 29 | + try { |
| 30 | + await Promise.resolve(fn()); |
| 31 | + } finally { |
| 32 | + console.log('::endgroup::'); |
| 33 | + } |
12 | 34 | } else { |
13 | | - fn(); |
| 35 | + await Promise.resolve(fn()); |
14 | 36 | } |
15 | 37 | } |
16 | 38 |
|
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)); |
43 | 62 | } |
| 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 }); |
44 | 108 |
|
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 | + } |
46 | 124 | }); |
47 | 125 |
|
| 126 | + if (daemonize) { |
| 127 | + detachVerdaccioRunner(); |
| 128 | + } |
| 129 | + |
48 | 130 | console.log(''); |
49 | 131 | console.log(''); |
50 | 132 | } |
| 133 | + |
| 134 | +export async function registryCleanup(): Promise<void> { |
| 135 | + await stopVerdaccioChild(); |
| 136 | + killStrayVerdaccioRunner(); |
| 137 | +} |
0 commit comments