@celilo/e2e 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cele2e +14 -0
- package/bin/e2e-cache-images +57 -0
- package/bin/e2e-shell +36 -13
- package/bin/e2e-status +8 -2
- package/bin/e2e-up +5 -5
- package/completions/install-zsh-completion.sh +78 -0
- package/config/dns/iamtheinternet.org.zone +1 -1
- package/config/routing/docker-image-preload.service +14 -0
- package/config/routing/docker-image-preload.sh +24 -0
- package/config/routing/management-routes.sh +5 -2
- package/docker/Dockerfile.management +2 -0
- package/docker/Dockerfile.target-machine +20 -0
- package/docker/Dockerfile.target-machine-docker +11 -19
- package/docker/Dockerfile.target-technitium +97 -0
- package/netapps/authentik.netapp +0 -0
- package/netapps/caddy.netapp +0 -0
- package/netapps/greenwave.netapp +0 -0
- package/netapps/homebridge.netapp +0 -0
- package/netapps/iptables.netapp +0 -0
- package/netapps/knot-unbound-internal.netapp +0 -0
- package/netapps/namecheap.netapp +0 -0
- package/netapps/technitium.netapp +0 -0
- package/netapps/test-ansible-output.netapp +0 -0
- package/package.json +7 -1
- package/simulators/greenwave/state.ts +7 -12
- package/src/cli/build.ts +204 -0
- package/src/cli/command-registry.ts +99 -0
- package/src/cli/command-tree-parser.ts +52 -0
- package/src/cli/completion.ts +171 -0
- package/src/cli/index.ts +203 -0
- package/src/cli/scaffold.ts +109 -0
- package/src/container-manager.ts +16 -6
- package/src/docker-compose-generator.ts +12 -10
- package/src/network-builder.ts +21 -0
- package/src/parse-line.ts +136 -0
- package/src/runner.ts +81 -88
- package/src/types.ts +4 -0
- package/bin/cache-docker-images +0 -46
package/src/runner.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { basename, join, resolve } from 'node:path';
|
|
23
|
+
import { parseLine } from './parse-line';
|
|
23
24
|
|
|
24
25
|
// ─── Config ──────────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -33,6 +34,9 @@ const persistentFile = join(testDir, '.e2e-persistent.json');
|
|
|
33
34
|
const rawArgs = process.argv.slice(2);
|
|
34
35
|
const flagKeep = rawArgs.includes('--keep');
|
|
35
36
|
const flagReuse = rawArgs.includes('--reuse');
|
|
37
|
+
const flagLive = rawArgs.includes('--live');
|
|
38
|
+
const flagPublished = rawArgs.includes('--published');
|
|
39
|
+
const flagVerbose = rawArgs.includes('--verbose');
|
|
36
40
|
const patterns = rawArgs.filter((a) => !a.startsWith('--'));
|
|
37
41
|
|
|
38
42
|
// ─── Colors ──────────────────────────────────────────────────────────
|
|
@@ -48,7 +52,7 @@ const reset = '\x1b[0m';
|
|
|
48
52
|
const CLEAR_LINE = '\x1b[2K\r';
|
|
49
53
|
const MOVE_UP = '\x1b[A';
|
|
50
54
|
|
|
51
|
-
const interactive = process.stdout.isTTY && !process.argv.includes('--no-interactive');
|
|
55
|
+
const interactive = process.stdout.isTTY && !process.argv.includes('--no-interactive') && !flagVerbose;
|
|
52
56
|
|
|
53
57
|
// ─── Timing ──────────────────────────────────────────────────────────
|
|
54
58
|
|
|
@@ -199,112 +203,66 @@ class ProgressDisplay {
|
|
|
199
203
|
this.pending = null;
|
|
200
204
|
this.subEventCount = 0;
|
|
201
205
|
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ─── Line Parsing ────────────────────────────────────────────────────
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
display.startStep(startMatch[1], startMatch[2]);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Protocol: [progress:done] optional message
|
|
215
|
-
const doneMatch = line.match(/\[progress:done\](.*)/);
|
|
216
|
-
if (doneMatch) {
|
|
217
|
-
const msg = doneMatch[1].trim();
|
|
218
|
-
display.doneStep(msg || undefined);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Protocol: [progress:fail] message
|
|
223
|
-
const failMatch = line.match(/\[progress:fail\] (.*)/);
|
|
224
|
-
if (failMatch) {
|
|
225
|
-
display.failStep(failMatch[1]);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Legacy: [progress] message (treat as instant event)
|
|
230
|
-
const legacyMatch = line.match(/\[progress\] (.*)/);
|
|
231
|
-
if (legacyMatch) {
|
|
232
|
-
display.instantEvent(legacyMatch[1]);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Celilo CLI signals — shown as sub-events under the current step.
|
|
237
|
-
// These must NOT finalize the test's progress step.
|
|
238
|
-
const deployMatch = line.match(/Module '([^']+)' deployed successfully/);
|
|
239
|
-
if (deployMatch) {
|
|
240
|
-
display.subEvent(`${green}✔${reset} ${deployMatch[1]} deployed`);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const importMatch = line.match(/Successfully imported module: (\S+)/);
|
|
245
|
-
if (importMatch) {
|
|
246
|
-
display.subEvent(`${green}✔${reset} ${importMatch[1]} imported`);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (line.includes('Deploying software')) {
|
|
251
|
-
display.subEvent('running Ansible');
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
if (line.includes('Infrastructure selected')) {
|
|
255
|
-
display.subEvent('infrastructure selected');
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (line.includes('Terraform skipped')) {
|
|
259
|
-
display.subEvent('terraform skipped (existing machine)');
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (line.includes('terraform init') || line.includes('Terraform init')) {
|
|
263
|
-
display.subEvent('running terraform init');
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (line.includes('terraform apply') || line.includes('Terraform apply')) {
|
|
267
|
-
display.subEvent('running terraform apply');
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const retryMatch = line.match(/RETRYING.*?(\d+) retries left/);
|
|
272
|
-
if (retryMatch) {
|
|
273
|
-
display.subEvent(`health check (${retryMatch[1]} retries left)`);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Error lines
|
|
278
|
-
if (line.includes('Error:') && !line.includes('bun test') && !line.includes('ipv4: Address already')) {
|
|
279
|
-
const err = line.replace(/.*Error: /, '').slice(0, 70);
|
|
280
|
-
display.subEvent(`${red}✗${reset} ${err}`);
|
|
207
|
+
/** Like abandon() but marks the step as failed so the user can see where it broke */
|
|
208
|
+
abandonFailed() {
|
|
209
|
+
if (!this.pending) return;
|
|
210
|
+
this.finalizeFailed(this.pending.doingMsg);
|
|
281
211
|
}
|
|
282
212
|
}
|
|
283
213
|
|
|
284
214
|
// ─── Error Extraction ────────────────────────────────────────────────
|
|
285
215
|
|
|
216
|
+
// Strips ANSI escape sequences so pattern matching works on bun's coloured output
|
|
217
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
218
|
+
function stripAnsi(s: string): string {
|
|
219
|
+
return s.replace(ANSI_RE, '');
|
|
220
|
+
}
|
|
221
|
+
|
|
286
222
|
function extractFailureMessage(lines: string[], exitCode: number): string {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
223
|
+
const plain = lines.map(stripAnsi);
|
|
224
|
+
|
|
225
|
+
// bun's own test failure: "✗ test name [Xms]" — extract the failing test name
|
|
226
|
+
const bunFailIdx = plain.findIndex((l) => /^\s*✗\s+\S/.test(l) && !l.includes('failed ('));
|
|
227
|
+
if (bunFailIdx >= 0) {
|
|
228
|
+
const testName = plain[bunFailIdx].replace(/^\s*✗\s+/, '').replace(/\s*\[\d+ms\].*$/, '').trim();
|
|
229
|
+
// Grab the error detail on the next few lines (bun indents it under the test)
|
|
230
|
+
const detail = plain
|
|
231
|
+
.slice(bunFailIdx + 1, bunFailIdx + 6)
|
|
232
|
+
.map((l) => l.trim())
|
|
233
|
+
.filter((l) => l.length > 0 && !l.startsWith('at ') && !l.startsWith('│') && l !== '└─')
|
|
234
|
+
.slice(0, 2)
|
|
235
|
+
.join(' — ');
|
|
236
|
+
return detail ? `${testName}: ${detail}` : testName;
|
|
237
|
+
}
|
|
290
238
|
|
|
239
|
+
// "error: ..." lines (may be indented in bun output)
|
|
240
|
+
const errorIdx = plain.findIndex(
|
|
241
|
+
(l) => l.trimStart().startsWith('error:') && !l.includes('bun test'),
|
|
242
|
+
);
|
|
291
243
|
if (errorIdx >= 0) {
|
|
292
|
-
const context =
|
|
244
|
+
const context = plain
|
|
293
245
|
.slice(errorIdx, errorIdx + 5)
|
|
294
246
|
.map((l) => l.trim())
|
|
295
247
|
.filter((l) => l.length > 0 && !l.startsWith('at '));
|
|
296
248
|
return context.join('\n ');
|
|
297
249
|
}
|
|
298
250
|
|
|
299
|
-
const celiloError =
|
|
251
|
+
const celiloError = plain.find(
|
|
300
252
|
(l) => l.includes('CeliloCommandError') || (l.includes('celilo') && l.includes('failed')),
|
|
301
253
|
);
|
|
302
254
|
if (celiloError) return celiloError.slice(0, 120);
|
|
303
255
|
|
|
304
|
-
const timeout =
|
|
256
|
+
const timeout = plain.find((l) => l.includes('Timeout') || l.includes('timed out'));
|
|
305
257
|
if (timeout) return timeout.slice(0, 120);
|
|
306
258
|
|
|
307
|
-
|
|
259
|
+
// Last resort: print the last few non-empty, non-progress lines from the output
|
|
260
|
+
const tail = plain
|
|
261
|
+
.filter((l) => l.length > 0 && !l.startsWith('[progress') && !l.startsWith('[e2e:') && !l.startsWith('[ansible:'))
|
|
262
|
+
.slice(-3)
|
|
263
|
+
.map((l) => l.trim())
|
|
264
|
+
.join(' | ');
|
|
265
|
+
return tail || `exit code ${exitCode}`;
|
|
308
266
|
}
|
|
309
267
|
|
|
310
268
|
// ─── Test Execution ──────────────────────────────────────────────────
|
|
@@ -314,6 +272,8 @@ interface TestResult {
|
|
|
314
272
|
status: 'pass' | 'fail' | 'suspicious';
|
|
315
273
|
duration: number;
|
|
316
274
|
error?: string;
|
|
275
|
+
/** Last ~30 lines of raw test output, shown on failure so the user doesn't have to dig in logs */
|
|
276
|
+
rawTail?: string;
|
|
317
277
|
hadDebugPause?: boolean;
|
|
318
278
|
projectName?: string;
|
|
319
279
|
}
|
|
@@ -333,9 +293,19 @@ async function runTest(
|
|
|
333
293
|
let capturedProjectName: string | undefined;
|
|
334
294
|
|
|
335
295
|
return new Promise<TestResult>((resolveTest) => {
|
|
336
|
-
// Env vars let the test detect --keep / --reuse modes
|
|
296
|
+
// Env vars let the test detect --keep / --reuse / --live / --published modes
|
|
337
297
|
const testEnv: NodeJS.ProcessEnv = { ...process.env };
|
|
338
298
|
if (flagKeep) testEnv.CELILO_E2E_KEEP = '1';
|
|
299
|
+
if (flagLive) {
|
|
300
|
+
// Resolve infra root relative to the test directory (sibling repo convention)
|
|
301
|
+
const inferredRoot = resolve(testDir, '../../infra');
|
|
302
|
+
testEnv.CELILO_E2E_INFRA_ROOT = existsSync(join(inferredRoot, 'apps', 'celilo'))
|
|
303
|
+
? inferredRoot
|
|
304
|
+
: resolve(PKG_DIR, '..', '..'); // fallback: @celilo/e2e is inside the infra monorepo
|
|
305
|
+
}
|
|
306
|
+
if (flagPublished) {
|
|
307
|
+
testEnv.CELILO_E2E_INFRA_ROOT = ''; // empty = force published mode
|
|
308
|
+
}
|
|
339
309
|
if (flagReuse) {
|
|
340
310
|
// Read the persistent project and pass it to the test
|
|
341
311
|
try {
|
|
@@ -438,7 +408,7 @@ async function runTest(
|
|
|
438
408
|
if (code === 0) {
|
|
439
409
|
display.flush();
|
|
440
410
|
} else {
|
|
441
|
-
display.
|
|
411
|
+
display.abandonFailed();
|
|
442
412
|
}
|
|
443
413
|
|
|
444
414
|
writeFileSync(logFile, stdout + '\n---STDERR---\n' + stderr);
|
|
@@ -460,6 +430,20 @@ async function runTest(
|
|
|
460
430
|
resolveTest({ name, status: 'pass', duration, hadDebugPause, projectName: capturedProjectName });
|
|
461
431
|
}
|
|
462
432
|
} else {
|
|
433
|
+
// Collect a raw tail: strip ANSI, drop internal protocol lines, keep last 30
|
|
434
|
+
const rawTail = (stdout + stderr)
|
|
435
|
+
.split('\n')
|
|
436
|
+
.map(stripAnsi)
|
|
437
|
+
.map((l) => l.trimEnd())
|
|
438
|
+
.filter((l) =>
|
|
439
|
+
l.length > 0 &&
|
|
440
|
+
!l.startsWith('[progress') &&
|
|
441
|
+
!l.startsWith('[e2e:') &&
|
|
442
|
+
!l.startsWith('[ansible:') &&
|
|
443
|
+
!l.startsWith('[debug:'),
|
|
444
|
+
)
|
|
445
|
+
.slice(-30)
|
|
446
|
+
.join('\n');
|
|
463
447
|
resolveTest({
|
|
464
448
|
name,
|
|
465
449
|
status: 'fail',
|
|
@@ -467,6 +451,7 @@ async function runTest(
|
|
|
467
451
|
hadDebugPause,
|
|
468
452
|
projectName: capturedProjectName,
|
|
469
453
|
error: extractFailureMessage(lines, code ?? 1),
|
|
454
|
+
rawTail,
|
|
470
455
|
});
|
|
471
456
|
}
|
|
472
457
|
});
|
|
@@ -615,6 +600,14 @@ async function main() {
|
|
|
615
600
|
if (result.error) {
|
|
616
601
|
console.log(` ${red} └─ ${result.error}${reset}`);
|
|
617
602
|
}
|
|
603
|
+
if (result.rawTail) {
|
|
604
|
+
console.log();
|
|
605
|
+
console.log(`${dim} ── test output ──────────────────────────────────────${reset}`);
|
|
606
|
+
for (const line of result.rawTail.split('\n')) {
|
|
607
|
+
console.log(` ${dim}${line}${reset}`);
|
|
608
|
+
}
|
|
609
|
+
console.log(`${dim} ─────────────────────────────────────────────────────${reset}`);
|
|
610
|
+
}
|
|
618
611
|
failed++;
|
|
619
612
|
}
|
|
620
613
|
console.log();
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,10 @@ export interface MachineSpec {
|
|
|
4
4
|
name: string;
|
|
5
5
|
ip: string;
|
|
6
6
|
zone: Zone;
|
|
7
|
+
/** Use the Docker-capable image (normally only app zone; set for internal machines that need Docker) */
|
|
8
|
+
docker?: boolean;
|
|
9
|
+
/** Override the Dockerfile used for this machine (relative to the e2e package) */
|
|
10
|
+
dockerfile?: string;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export type TopologyPreset = 'default' | 'direct-internet';
|
package/bin/cache-docker-images
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Pre-pull and save Docker images needed by e2e test modules.
|
|
4
|
-
# Saves them as tarballs that get mounted into app-zone containers
|
|
5
|
-
# for fast loading at boot time.
|
|
6
|
-
#
|
|
7
|
-
# Run this once (or after updating image versions) to populate the cache.
|
|
8
|
-
# Images are stored in e2e/docker-image-cache/
|
|
9
|
-
|
|
10
|
-
set -e
|
|
11
|
-
|
|
12
|
-
CACHE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/docker-image-cache"
|
|
13
|
-
mkdir -p "$CACHE_DIR"
|
|
14
|
-
|
|
15
|
-
IMAGES=(
|
|
16
|
-
"docker.io/library/postgres:15-alpine"
|
|
17
|
-
"docker.io/library/redis:alpine"
|
|
18
|
-
"ghcr.io/goauthentik/server:2024.2.2"
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
echo "Caching Docker images for e2e tests..."
|
|
22
|
-
echo "Cache directory: $CACHE_DIR"
|
|
23
|
-
echo ""
|
|
24
|
-
|
|
25
|
-
for image in "${IMAGES[@]}"; do
|
|
26
|
-
# Create a safe filename from the image name
|
|
27
|
-
filename=$(echo "$image" | sed 's|[/:]|_|g').tar
|
|
28
|
-
|
|
29
|
-
if [[ -f "$CACHE_DIR/$filename" ]]; then
|
|
30
|
-
echo "✓ $image (already cached)"
|
|
31
|
-
continue
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
echo "▸ Pulling $image..."
|
|
35
|
-
docker pull "$image"
|
|
36
|
-
|
|
37
|
-
echo " Saving to $filename..."
|
|
38
|
-
docker save "$image" > "$CACHE_DIR/$filename"
|
|
39
|
-
|
|
40
|
-
size=$(du -h "$CACHE_DIR/$filename" | cut -f1)
|
|
41
|
-
echo "✓ $image ($size)"
|
|
42
|
-
done
|
|
43
|
-
|
|
44
|
-
echo ""
|
|
45
|
-
echo "Cache complete. $(ls "$CACHE_DIR"/*.tar 2>/dev/null | wc -l | tr -d ' ') images cached."
|
|
46
|
-
echo "Total size: $(du -sh "$CACHE_DIR" | cut -f1)"
|