Skip to content

Commit aacc8f2

Browse files
chore(manifest): harden e2e workflow
1 parent 8a80605 commit aacc8f2

File tree

3 files changed

+258
-2
lines changed

3 files changed

+258
-2
lines changed

.github/workflows/e2e-manifest.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ jobs:
4646

4747
- name: E2E Test for Manifest Demo Development
4848
if: steps.check-ci.outcome == 'success'
49-
run: pnpm run app:manifest:dev & echo "done" && npx wait-on tcp:3009 && npx wait-on tcp:3012 && npx wait-on http://127.0.0.1:4001/ && npx nx run-many --target=e2e --projects=manifest-webpack-host --parallel=2 && npx kill-port 3013 3009 3010 3011 3012 4001
49+
run: pnpm run manifest:e2e:dev
5050

5151
- name: E2E Test for Manifest Demo Production
5252
if: steps.check-ci.outcome == 'success'
53-
run: pnpm run app:manifest:prod & echo "done" && npx wait-on tcp:3009 && npx wait-on tcp:3012 && npx wait-on http://127.0.0.1:4001/ && npx nx run-many --target=e2e --projects=manifest-webpack-host --parallel=1 && npx kill-port 3013 3009 3010 3011 3012 4001
53+
run: pnpm run manifest:e2e:prod

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
"app:runtime:dev": "nx run-many --target=serve -p 3005-runtime-host,3006-runtime-remote,3007-runtime-remote",
4646
"app:manifest:dev": "NX_TUI=false nx run-many --target=serve --configuration=development --parallel=100 -p modernjs,manifest-webpack-host,3009-webpack-provider,3010-rspack-provider,3011-rspack-manifest-provider,3012-rspack-js-entry-provider",
4747
"app:manifest:prod": "NX_TUI=false nx run-many --target=serve --configuration=production --parallel=100 -p modernjs,manifest-webpack-host,3009-webpack-provider,3010-rspack-provider,3011-rspack-manifest-provider,3012-rspack-js-entry-provider",
48+
"manifest:e2e:dev": "node ./tools/scripts/run-manifest-e2e.mjs --mode=dev",
49+
"manifest:e2e:prod": "node ./tools/scripts/run-manifest-e2e.mjs --mode=prod",
50+
"manifest:e2e:ci": "node ./tools/scripts/run-manifest-e2e.mjs --mode=all",
4851
"app:ts:dev": "nx run-many --target=serve -p react_ts_host,react_ts_nested_remote,react_ts_remote",
4952
"app:component-data-fetch:dev": "NX_TUI=false nx run-many --target=serve --parallel=3 --configuration=development -p modernjs-ssr-data-fetch-provider,modernjs-ssr-data-fetch-provider-csr,modernjs-ssr-data-fetch-host",
5053
"app:modern:dev": "NX_TUI=false nx run-many --target=serve --parallel=10 --configuration=development -p modernjs-ssr-dynamic-nested-remote,modernjs-ssr-dynamic-remote,modernjs-ssr-dynamic-remote-new-version,modernjs-ssr-host,modernjs-ssr-nested-remote,modernjs-ssr-remote,modernjs-ssr-remote-new-version",

tools/scripts/run-manifest-e2e.mjs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/env node
2+
import { spawn } from 'node:child_process';
3+
4+
const MANIFEST_WAIT_TARGETS = [
5+
'tcp:3009',
6+
'tcp:3012',
7+
'http://127.0.0.1:4001/',
8+
];
9+
10+
const KILL_PORT_ARGS = [
11+
'npx',
12+
'kill-port',
13+
'3013',
14+
'3009',
15+
'3010',
16+
'3011',
17+
'3012',
18+
'4001',
19+
];
20+
21+
const SCENARIOS = {
22+
dev: {
23+
label: 'manifest development',
24+
serveCmd: ['pnpm', 'run', 'app:manifest:dev'],
25+
e2eCmd: [
26+
'npx',
27+
'nx',
28+
'run-many',
29+
'--target=e2e',
30+
'--projects=manifest-webpack-host',
31+
'--parallel=2',
32+
],
33+
waitTargets: MANIFEST_WAIT_TARGETS,
34+
},
35+
prod: {
36+
label: 'manifest production',
37+
serveCmd: ['pnpm', 'run', 'app:manifest:prod'],
38+
e2eCmd: [
39+
'npx',
40+
'nx',
41+
'run-many',
42+
'--target=e2e',
43+
'--projects=manifest-webpack-host',
44+
'--parallel=1',
45+
],
46+
waitTargets: MANIFEST_WAIT_TARGETS,
47+
},
48+
};
49+
50+
const VALID_MODES = new Set(['dev', 'prod', 'all']);
51+
52+
async function main() {
53+
const modeArg = process.argv.find((arg) => arg.startsWith('--mode='));
54+
const mode = modeArg ? modeArg.split('=')[1] : 'all';
55+
56+
if (!VALID_MODES.has(mode)) {
57+
console.error(
58+
`Unknown mode "${mode}". Expected one of ${Array.from(VALID_MODES).join(', ')}`,
59+
);
60+
process.exitCode = 1;
61+
return;
62+
}
63+
64+
const targets = mode === 'all' ? ['dev', 'prod'] : [mode];
65+
66+
for (const target of targets) {
67+
await runScenario(target);
68+
}
69+
}
70+
71+
async function runScenario(name) {
72+
const scenario = SCENARIOS[name];
73+
if (!scenario) {
74+
throw new Error(`Unknown scenario: ${name}`);
75+
}
76+
77+
console.log(`\n[manifest-e2e] Starting ${scenario.label}`);
78+
79+
const serve = spawn(scenario.serveCmd[0], scenario.serveCmd.slice(1), {
80+
stdio: 'inherit',
81+
});
82+
83+
let serveExitInfo;
84+
let shutdownRequested = false;
85+
86+
const serveExitPromise = new Promise((resolve, reject) => {
87+
serve.on('exit', (code, signal) => {
88+
serveExitInfo = { code, signal };
89+
resolve(serveExitInfo);
90+
});
91+
serve.on('error', reject);
92+
});
93+
94+
const guard = (commandDescription, factory) => {
95+
const controller = new AbortController();
96+
const { signal } = controller;
97+
const { child, promise } = factory(signal);
98+
99+
const watchingPromise = serveExitPromise.then((info) => {
100+
if (!shutdownRequested) {
101+
if (child.exitCode === null && child.signalCode === null) {
102+
controller.abort();
103+
}
104+
throw new Error(
105+
`Serve process exited while ${commandDescription}: ${formatExit(info)}`,
106+
);
107+
}
108+
return info;
109+
});
110+
111+
return Promise.race([promise, watchingPromise]).finally(() => {
112+
if (child.exitCode === null && child.signalCode === null) {
113+
controller.abort();
114+
}
115+
});
116+
};
117+
118+
const runCommand = (cmd, args, signal) => {
119+
const child = spawn(cmd, args, {
120+
stdio: 'inherit',
121+
signal,
122+
});
123+
124+
const promise = new Promise((resolve, reject) => {
125+
child.on('exit', (code, childSignal) => {
126+
if (code === 0) {
127+
resolve({ code, signal: childSignal });
128+
} else {
129+
reject(
130+
new Error(
131+
`${cmd} ${args.join(' ')} exited with ${formatExit({ code, signal: childSignal })}`,
132+
),
133+
);
134+
}
135+
});
136+
child.on('error', reject);
137+
});
138+
139+
return { child, promise };
140+
};
141+
142+
try {
143+
await guard('waiting for manifest services', (signal) =>
144+
runCommand('npx', ['wait-on', ...scenario.waitTargets], signal),
145+
);
146+
147+
await guard('running manifest e2e tests', (signal) =>
148+
runCommand(scenario.e2eCmd[0], scenario.e2eCmd.slice(1), signal),
149+
);
150+
} finally {
151+
shutdownRequested = true;
152+
153+
if (serve.exitCode === null && serve.signalCode === null) {
154+
serve.kill('SIGINT');
155+
}
156+
157+
let serveExitError = null;
158+
try {
159+
await serveExitPromise;
160+
} catch (error) {
161+
console.error('[manifest-e2e] Serve command emitted error:', error);
162+
serveExitError = error;
163+
}
164+
165+
await runKillPort();
166+
167+
if (serveExitError) {
168+
throw serveExitError;
169+
}
170+
}
171+
172+
if (!isExpectedServeExit(serveExitInfo)) {
173+
throw new Error(
174+
`Serve command for ${scenario.label} exited unexpectedly with ${formatExit(serveExitInfo)}`,
175+
);
176+
}
177+
178+
console.log(`[manifest-e2e] Finished ${scenario.label}`);
179+
}
180+
181+
async function runKillPort() {
182+
const { promise } = spawnWithPromise(
183+
KILL_PORT_ARGS[0],
184+
KILL_PORT_ARGS.slice(1),
185+
);
186+
try {
187+
await promise;
188+
} catch (error) {
189+
console.warn('[manifest-e2e] kill-port command failed:', error.message);
190+
}
191+
}
192+
193+
function spawnWithPromise(cmd, args, options = {}) {
194+
const child = spawn(cmd, args, {
195+
stdio: 'inherit',
196+
...options,
197+
});
198+
199+
const promise = new Promise((resolve, reject) => {
200+
child.on('exit', (code, signal) => {
201+
if (code === 0) {
202+
resolve({ code, signal });
203+
} else {
204+
reject(
205+
new Error(
206+
`${cmd} ${args.join(' ')} exited with ${formatExit({ code, signal })}`,
207+
),
208+
);
209+
}
210+
});
211+
child.on('error', reject);
212+
});
213+
214+
return { child, promise };
215+
}
216+
217+
function isExpectedServeExit(info) {
218+
if (!info) {
219+
return false;
220+
}
221+
222+
const { code, signal } = info;
223+
224+
if (code === 0) {
225+
return true;
226+
}
227+
228+
if (code === 130 || code === 143) {
229+
return true;
230+
}
231+
232+
if (code == null && (signal === 'SIGINT' || signal === 'SIGTERM')) {
233+
return true;
234+
}
235+
236+
return false;
237+
}
238+
239+
function formatExit({ code, signal }) {
240+
const parts = [];
241+
if (code !== null && code !== undefined) {
242+
parts.push(`code ${code}`);
243+
}
244+
if (signal) {
245+
parts.push(`signal ${signal}`);
246+
}
247+
return parts.length > 0 ? parts.join(', ') : 'unknown status';
248+
}
249+
250+
main().catch((error) => {
251+
console.error('[manifest-e2e] Error:', error);
252+
process.exitCode = 1;
253+
});

0 commit comments

Comments
 (0)