@celilo/e2e 0.19.3 → 0.20.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/README.md +30 -13
- package/bin/e2e-bake-management +171 -12
- package/bin/e2e-infra +0 -1
- package/bin/e2e-up +14 -3
- package/docker/Dockerfile.observer +12 -1
- package/docker/Dockerfile.target-machine +22 -1
- package/npm-registry-server/package.json +1 -1
- package/package.json +3 -3
- package/registry-server/package.json +1 -1
- package/scripts/pack-celilo-packages.ts +15 -0
- package/src/block-timing.test.ts +559 -0
- package/src/block-timing.ts +366 -0
- package/src/cli/build.test.ts +54 -4
- package/src/cli/build.ts +204 -88
- package/src/cli/command-registry.ts +21 -0
- package/src/cli/command-tree-parser.ts +11 -2
- package/src/cli/completion.ts +9 -0
- package/src/cli/host.ts +252 -0
- package/src/cli/index.ts +78 -51
- package/src/cli/module-discovery.ts +108 -13
- package/src/cli/scaffold.ts +18 -26
- package/src/container-manager.cleanup.test.ts +284 -0
- package/src/container-manager.runner.test.ts +351 -0
- package/src/container-manager.test.ts +84 -0
- package/src/container-manager.ts +721 -185
- package/src/docker-compose-generator.ts +135 -61
- package/src/doctor.test.ts +259 -4
- package/src/doctor.ts +276 -3
- package/src/exit-cleanup.test.ts +83 -1
- package/src/fleet-nameserver-gate.test.ts +45 -0
- package/src/host-vm.test.ts +156 -0
- package/src/host-vm.ts +230 -0
- package/src/index.ts +11 -0
- package/src/live-stack.test.ts +184 -0
- package/src/live-stack.ts +145 -0
- package/src/no-unjustified-sleep.test.ts +90 -0
- package/src/proxmox-provisioner.test.ts +18 -2
- package/src/proxmox-provisioner.ts +22 -0
- package/src/public-sim-routes.test.ts +9 -2
- package/src/repo-root.ts +33 -0
- package/src/run-args.test.ts +76 -0
- package/src/run-args.ts +89 -0
- package/src/runner.ts +213 -8
- package/src/shared-infra.ts +83 -32
- package/src/socks-proxy.ts +2 -0
- package/src/source-fingerprint.test.ts +213 -0
- package/src/source-fingerprint.ts +201 -0
- package/src/stage-simulator-inputs.ts +93 -0
- package/src/stages.ts +133 -0
- package/src/wait-for-run.ts +1 -0
package/src/cli/build.ts
CHANGED
|
@@ -19,13 +19,25 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { execSync, spawnSync } from 'node:child_process';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
closeSync,
|
|
24
|
+
existsSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
openSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
readSync,
|
|
29
|
+
readdirSync,
|
|
30
|
+
rmSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
} from 'node:fs';
|
|
23
33
|
import { basename, join, resolve } from 'node:path';
|
|
24
34
|
import { gunzipSync } from 'node:zlib';
|
|
25
35
|
import { stageAptRepo } from '../../scripts/stage-apt-repo';
|
|
26
36
|
import { stageLibsignal } from '../../scripts/stage-libsignal';
|
|
27
37
|
import { explainBuildFailure } from '../doctor';
|
|
28
38
|
import { ensureRegistryServerBundle, ensureTerraformFakeBundle } from '../registry-bundle';
|
|
39
|
+
import { findMonorepoRoot } from '../repo-root';
|
|
40
|
+
import { packNpmRegistryTarballs, stageWebsiteDist } from '../stage-simulator-inputs';
|
|
29
41
|
|
|
30
42
|
/**
|
|
31
43
|
* Ceiling on a single `docker build`. The slowest image here is a few minutes
|
|
@@ -35,27 +47,15 @@ import { ensureRegistryServerBundle, ensureTerraformFakeBundle } from '../regist
|
|
|
35
47
|
const PER_IMAGE_BUILD_TIMEOUT_MS = 15 * 60_000;
|
|
36
48
|
|
|
37
49
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* is too weak a signal: a stray empty `node_modules/modules` (left by a botched
|
|
43
|
-
* `bun add -g`) made this return the global node_modules as "the monorepo root",
|
|
44
|
-
* which skipped `stageFromPublic()` and crashed the npm-consumer path in the
|
|
45
|
-
* workspace packer (ce-dbc). Matches the marker `container-manager.ts`'s
|
|
46
|
-
* findCeliloRoot already uses. Returns null in any consumer install, so callers
|
|
47
|
-
* correctly fall through to the published-source path.
|
|
50
|
+
* Ceiling on a single `celilo module package` run. The slowest module takes
|
|
51
|
+
* a couple of minutes; 10 leaves generous headroom. Without it a wedged
|
|
52
|
+
* packaging step sat for two hours (2026-09-04, technitium: all samples in
|
|
53
|
+
* kevent64, artifact truncated at 59 percent, no error, no exit).
|
|
48
54
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const parent = resolve(dir, '..');
|
|
54
|
-
if (parent === dir) break;
|
|
55
|
-
dir = parent;
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
55
|
+
const PER_MODULE_PACKAGE_TIMEOUT_MS = 10 * 60_000;
|
|
56
|
+
|
|
57
|
+
/** Re-exported so this module's existing callers and its recurrence test keep their import path. */
|
|
58
|
+
export { findMonorepoRoot };
|
|
59
59
|
|
|
60
60
|
/** Return paths to all modules that have a manifest.yml. */
|
|
61
61
|
function discoverModuleDirs(repoRoot: string): string[] {
|
|
@@ -72,6 +72,84 @@ const red = '\x1b[31m';
|
|
|
72
72
|
const yellow = '\x1b[33m';
|
|
73
73
|
const reset = '\x1b[0m';
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Validate that a file is a complete gzip stream (full gunzip pass). This is
|
|
77
|
+
* the check a .netapp or .tgz needs: a stream truncated mid-write fails here
|
|
78
|
+
* with 'unexpected end of file' instead of surviving to the consumer, where
|
|
79
|
+
* the identical zlib error reads as a truncated DOWNLOAD and sends the
|
|
80
|
+
* debugging at the network while the artifact was born broken.
|
|
81
|
+
*
|
|
82
|
+
* Throws with an actionable message naming the file.
|
|
83
|
+
*/
|
|
84
|
+
export function assertGzipValid(filePath: string): void {
|
|
85
|
+
try {
|
|
86
|
+
gunzipSync(readFileSync(filePath));
|
|
87
|
+
return;
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${filePath} is not a valid gzip stream (${reason}). The artifact was written truncated or corrupted at rest; serving it would surface as 'zlib: unexpected end of file' on the consumer side, indistinguishable from a truncated download.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate that a file is a real .deb (ar archive). A full dpkg parse is out
|
|
98
|
+
* of scope; the magic bytes catch the truncated-at-write case, which is the
|
|
99
|
+
* one that silently ships.
|
|
100
|
+
*/
|
|
101
|
+
function assertDebValid(filePath: string): void {
|
|
102
|
+
const fd = openSync(filePath, 'r');
|
|
103
|
+
try {
|
|
104
|
+
const magic = Buffer.alloc(8);
|
|
105
|
+
const read = readSync(fd, magic, 0, 8, 0);
|
|
106
|
+
if (read < 8 || magic.toString('latin1') !== '!<arch>\n') {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`${filePath} is not a valid ar archive (missing !<arch> magic). The .deb was written truncated or corrupted at rest.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
closeSync(fd);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Verify a produced or downloaded .netapp is a complete gzip stream. Throws
|
|
118
|
+
* with an actionable message; callers fail the build rather than stage the
|
|
119
|
+
* artifact for the sim registry to serve.
|
|
120
|
+
*/
|
|
121
|
+
export function verifyNetapp(filePath: string): void {
|
|
122
|
+
if (!existsSync(filePath)) {
|
|
123
|
+
throw new Error(`${filePath} was not produced — packaging step exited 0 but left no artifact`);
|
|
124
|
+
}
|
|
125
|
+
assertGzipValid(filePath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Verify every .netapp in the staging dir before declaring the stage done.
|
|
130
|
+
* The sim registry binds this directory and serves whatever is in it, so a
|
|
131
|
+
* corrupt file here becomes a 'zlib: unexpected end of file' on some consumer
|
|
132
|
+
* that looks like a network fault. Catches stale artifacts from a previous
|
|
133
|
+
* run too — the dir is not cleaned between builds.
|
|
134
|
+
*/
|
|
135
|
+
export function verifyStagedNetapps(netappsDir: string): void {
|
|
136
|
+
if (!existsSync(netappsDir)) return;
|
|
137
|
+
const netapps = readdirSync(netappsDir).filter((f) => f.endsWith('.netapp'));
|
|
138
|
+
const failures: string[] = [];
|
|
139
|
+
for (const netapp of netapps) {
|
|
140
|
+
try {
|
|
141
|
+
verifyNetapp(join(netappsDir, netapp));
|
|
142
|
+
} catch (err) {
|
|
143
|
+
failures.push(err instanceof Error ? err.message : String(err));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (failures.length > 0) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`${failures.length} of ${netapps.length} staged .netapp file(s) failed gzip integrity check:\n ${failures.join('\n ')}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
75
153
|
interface BuildOptions {
|
|
76
154
|
pkgDir: string;
|
|
77
155
|
moduleDirs: string[];
|
|
@@ -87,19 +165,16 @@ interface BuildOptions {
|
|
|
87
165
|
}
|
|
88
166
|
|
|
89
167
|
/**
|
|
90
|
-
* Stage
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* .celilo-website-cache/ ← modules/celilo-website/site/dist/
|
|
94
|
-
* .npm-registry-cache/ ← bun pm pack of each @celilo/* workspace pkg
|
|
168
|
+
* Stage the build-time inputs that the simulator images COPY at docker-build
|
|
169
|
+
* time: the website dist and the @celilo/* tarballs (via
|
|
170
|
+
* src/stage-simulator-inputs.ts), then the apt-repo and libsignal debs.
|
|
95
171
|
*
|
|
96
|
-
* Both are .gitignored — they're build outputs, not source. Without
|
|
97
|
-
* this step,
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* regression on 2026-05-06 caught).
|
|
172
|
+
* Both cache dirs are .gitignored — they're build outputs, not source. Without
|
|
173
|
+
* this step, the simulator Dockerfiles fail at COPY (or worse, ship stale
|
|
174
|
+
* tarballs that don't match the current workspace versions, which is what the
|
|
175
|
+
* version-mismatch regression on 2026-05-06 caught).
|
|
101
176
|
*
|
|
102
|
-
* Pure no-op when the workspace doesn't have
|
|
177
|
+
* Pure no-op when the workspace doesn't have the cache targets —
|
|
103
178
|
* keeps build-infra working in npm-installed @celilo/e2e, where these
|
|
104
179
|
* staging dirs aren't relevant.
|
|
105
180
|
*/
|
|
@@ -121,8 +196,6 @@ async function stageSimulatorInputs(pkgDir: string): Promise<void> {
|
|
|
121
196
|
}
|
|
122
197
|
|
|
123
198
|
const websiteSrc = join(repoRoot, 'modules', 'celilo-website', 'site');
|
|
124
|
-
const websiteCache = join(pkgDir, '.celilo-website-cache');
|
|
125
|
-
const npmCache = join(pkgDir, '.npm-registry-cache');
|
|
126
199
|
const packScript = join(pkgDir, 'scripts', 'pack-celilo-packages.ts');
|
|
127
200
|
|
|
128
201
|
// Skip silently if neither dir exists — we're in an npm-installed
|
|
@@ -131,61 +204,16 @@ async function stageSimulatorInputs(pkgDir: string): Promise<void> {
|
|
|
131
204
|
|
|
132
205
|
console.log(`${bold}Staging simulator inputs...${reset}\n`);
|
|
133
206
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
process.stdout.write(` ${'celilo-website (build)'.padEnd(28)} `);
|
|
137
|
-
const t0 = Date.now();
|
|
138
|
-
let result = spawnSync('bun', ['install'], { cwd: websiteSrc, stdio: 'pipe' });
|
|
139
|
-
if (result.status !== 0) {
|
|
140
|
-
console.log(`${red}✗${reset}`);
|
|
141
|
-
console.error(result.stderr?.toString());
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
result = spawnSync('bun', ['run', 'build'], { cwd: websiteSrc, stdio: 'pipe' });
|
|
145
|
-
if (result.status !== 0) {
|
|
146
|
-
console.log(`${red}✗${reset}`);
|
|
147
|
-
console.error(result.stderr?.toString());
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
console.log(`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`);
|
|
151
|
-
|
|
152
|
-
process.stdout.write(` ${'celilo-website (stage)'.padEnd(28)} `);
|
|
153
|
-
rmSync(websiteCache, { recursive: true, force: true });
|
|
154
|
-
mkdirSync(websiteCache, { recursive: true });
|
|
155
|
-
cpSync(join(websiteSrc, 'dist'), websiteCache, { recursive: true });
|
|
156
|
-
console.log(`${green}✔${reset}`);
|
|
157
|
-
}
|
|
207
|
+
stageWebsiteDist(repoRoot, pkgDir);
|
|
208
|
+
packNpmRegistryTarballs(repoRoot, pkgDir);
|
|
158
209
|
|
|
159
|
-
//
|
|
160
|
-
// npm-registry-sim. Tarball versions match current workspace
|
|
161
|
-
// package.json — re-runs every build-infra to stay fresh.
|
|
162
|
-
if (existsSync(packScript)) {
|
|
163
|
-
process.stdout.write(` ${'npm-registry (pack)'.padEnd(28)} `);
|
|
164
|
-
const t0 = Date.now();
|
|
165
|
-
const result = spawnSync('bun', ['run', packScript], {
|
|
166
|
-
cwd: repoRoot,
|
|
167
|
-
stdio: 'pipe',
|
|
168
|
-
});
|
|
169
|
-
if (result.status !== 0) {
|
|
170
|
-
console.log(`${red}✗${reset}`);
|
|
171
|
-
console.error(result.stderr?.toString());
|
|
172
|
-
process.exit(1);
|
|
173
|
-
}
|
|
174
|
-
const tarballCount = existsSync(npmCache)
|
|
175
|
-
? readdirSync(npmCache).filter((f) => f.endsWith('.tgz')).length
|
|
176
|
-
: 0;
|
|
177
|
-
console.log(
|
|
178
|
-
`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s, ${tarballCount} tarball(s)${reset}`,
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// (c) Build + stage the celilo/celilo-bootstrap .debs for apt-repo-sim.
|
|
210
|
+
// Build + stage the celilo/celilo-bootstrap .debs for apt-repo-sim.
|
|
183
211
|
// Graceful no-op when packaging/ or nfpm is absent (npm-installed e2e).
|
|
184
212
|
// Dockerfile.apt-repo-sim COPYs the staged .apt-repo-cache/pool at build
|
|
185
213
|
// time, so this must run before buildDockerImages().
|
|
186
214
|
stageAptRepo(repoRoot, pkgDir);
|
|
187
215
|
|
|
188
|
-
//
|
|
216
|
+
// Build + stage the libsignal aarch64 native as a .deb, into the SAME
|
|
189
217
|
// pool stageAptRepo just populated (so it must run after it — that function
|
|
190
218
|
// recreates the pool empty). Compiled here on the host's own network rather
|
|
191
219
|
// than inside the sealed e2e network; see stage-libsignal.ts.
|
|
@@ -323,7 +351,9 @@ export async function fetchAptPool(base: string, pool: string): Promise<number>
|
|
|
323
351
|
const url = `${base}/${filename}`;
|
|
324
352
|
const res = await fetch(url);
|
|
325
353
|
if (!res.ok) throw new Error(`fetching ${url}: HTTP ${res.status}`);
|
|
326
|
-
|
|
354
|
+
const debPath = join(pool, basename(filename));
|
|
355
|
+
writeFileSync(debPath, Buffer.from(await res.arrayBuffer()));
|
|
356
|
+
assertDebValid(debPath);
|
|
327
357
|
}
|
|
328
358
|
return filenames.size;
|
|
329
359
|
}
|
|
@@ -370,7 +400,12 @@ export async function fetchPublishedTarball(
|
|
|
370
400
|
}
|
|
371
401
|
const filename =
|
|
372
402
|
tarballUrl.split('/').pop() ?? `${pkg.replace('@', '').replace('/', '-')}-${latest}.tgz`;
|
|
373
|
-
|
|
403
|
+
const tgzPath = join(destDir, filename);
|
|
404
|
+
writeFileSync(tgzPath, Buffer.from(await tgzRes.arrayBuffer()));
|
|
405
|
+
// A truncated tarball in .npm-registry-cache fails the consumer's bun add
|
|
406
|
+
// with a zlib error that reads like a network fault. Check here where the
|
|
407
|
+
// cause (a bad fetch) is still visible.
|
|
408
|
+
assertGzipValid(tgzPath);
|
|
374
409
|
}
|
|
375
410
|
|
|
376
411
|
/**
|
|
@@ -418,10 +453,14 @@ function packageNetapp(moduleDir: string, netappsDir: string): void {
|
|
|
418
453
|
|
|
419
454
|
let result: ReturnType<typeof spawnSync>;
|
|
420
455
|
if (celiloWrapper && existsSync(celiloWrapper)) {
|
|
421
|
-
result = spawnSync(celiloWrapper, ['package', absDir, '--output', out], {
|
|
456
|
+
result = spawnSync(celiloWrapper, ['package', absDir, '--output', out], {
|
|
457
|
+
stdio: 'pipe',
|
|
458
|
+
timeout: PER_MODULE_PACKAGE_TIMEOUT_MS,
|
|
459
|
+
});
|
|
422
460
|
} else if (celiloTs && existsSync(celiloTs)) {
|
|
423
461
|
result = spawnSync('bun', ['run', celiloTs, 'package', absDir, '--output', out], {
|
|
424
462
|
stdio: 'pipe',
|
|
463
|
+
timeout: PER_MODULE_PACKAGE_TIMEOUT_MS,
|
|
425
464
|
});
|
|
426
465
|
} else {
|
|
427
466
|
console.error(` ${red}skip${reset} ${name} ${dim}(celilo CLI not found)${reset}`);
|
|
@@ -429,9 +468,32 @@ function packageNetapp(moduleDir: string, netappsDir: string): void {
|
|
|
429
468
|
}
|
|
430
469
|
|
|
431
470
|
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
471
|
+
if (result.error && (result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
|
472
|
+
console.log(`${red}✗ TIMED OUT${reset} ${dim}${elapsed}s${reset}`);
|
|
473
|
+
console.error(
|
|
474
|
+
`\n${red}Packaging ${name} exceeded ${Math.round(PER_MODULE_PACKAGE_TIMEOUT_MS / 60_000)} minutes and was killed.${reset}`,
|
|
475
|
+
);
|
|
476
|
+
console.error(
|
|
477
|
+
`${dim}The stall is usually transient (2026-09-04: technitium wedged two hours, artifact truncated mid-write). Delete ${out} and re-run \`cele2e build-infra\`.${reset}`,
|
|
478
|
+
);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
432
481
|
if (result.status !== 0) {
|
|
433
482
|
console.log(`${red}✗${reset} ${dim}${result.stderr?.toString().trim()}${reset}`);
|
|
434
|
-
|
|
483
|
+
// A failed package must fail the build, not silently shrink the module
|
|
484
|
+
// set: the sim registry binds netapps/ as-is, so the omission surfaces
|
|
485
|
+
// later as a confusing deploy failure instead of a nameable one.
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Verify what we just wrote before declaring the step done. A wedge in the
|
|
490
|
+
// packaging stream leaves a truncated file that still exits 0 (celilo#1257).
|
|
491
|
+
try {
|
|
492
|
+
verifyNetapp(out);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
console.log(`${red}✗ CORRUPT${reset} ${dim}${elapsed}s${reset}`);
|
|
495
|
+
console.error(`\n${red}${err instanceof Error ? err.message : String(err)}${reset}`);
|
|
496
|
+
process.exit(1);
|
|
435
497
|
}
|
|
436
498
|
|
|
437
499
|
const size = (() => {
|
|
@@ -489,7 +551,15 @@ export async function stageNetappsFromRegistry(netappsDir: string): Promise<void
|
|
|
489
551
|
process.exit(1);
|
|
490
552
|
}
|
|
491
553
|
const bytes = new Uint8Array(await resp.arrayBuffer());
|
|
492
|
-
|
|
554
|
+
const netappPath = join(netappsDir, `${name}.netapp`);
|
|
555
|
+
writeFileSync(netappPath, bytes);
|
|
556
|
+
try {
|
|
557
|
+
verifyNetapp(netappPath);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
console.log(`${red}✗ CORRUPT${reset}`);
|
|
560
|
+
console.error(`${red} ${err instanceof Error ? err.message : String(err)}${reset}`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
493
563
|
console.log(
|
|
494
564
|
`${green}✔${reset} ${dim}${max_version} ${Math.round((Date.now() - t0) / 1000)}s ${(bytes.length / 1024).toFixed(0)}KB${reset}`,
|
|
495
565
|
);
|
|
@@ -705,6 +775,34 @@ function saveImages(pkgDir: string): void {
|
|
|
705
775
|
console.log('\nTo restore after colima restart:\n cele2e load');
|
|
706
776
|
}
|
|
707
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Collect the untagged images this build (and every build before it) left
|
|
780
|
+
* behind.
|
|
781
|
+
*
|
|
782
|
+
* `docker image prune -f` — no `-a`, no `system prune` — removes only images
|
|
783
|
+
* that carry no tag AND no container. The tagged base images every Dockerfile
|
|
784
|
+
* starts `FROM` are therefore untouched, which is the whole distinction: the
|
|
785
|
+
* warning in this repo's operating notes is about `-a` and `system prune`,
|
|
786
|
+
* which DO delete `ubuntu:22.04` and cost a full 27-image rebuild that then
|
|
787
|
+
* gets misread as a network failure (`failed to solve: … TLS handshake
|
|
788
|
+
* timeout`). The bare form is the opposite — it is what stops the pile that
|
|
789
|
+
* drives people to reach for the destructive one.
|
|
790
|
+
*
|
|
791
|
+
* Measured 2026-09-05 before this existed: 3178 dangling images, 25.8 GB, 97%
|
|
792
|
+
* of the local image store, mostly management images superseded by a bake.
|
|
793
|
+
*/
|
|
794
|
+
function pruneSupersededImages(): void {
|
|
795
|
+
process.stdout.write(`${bold}Removing superseded (untagged) images...${reset} `);
|
|
796
|
+
try {
|
|
797
|
+
const out = execSync('docker image prune -f', { encoding: 'utf-8', timeout: 300_000 });
|
|
798
|
+
const reclaimed = out.match(/Total reclaimed space:\s*(.+)/)?.[1]?.trim() ?? '0B';
|
|
799
|
+
console.log(`${green}✔${reset} ${dim}${reclaimed} reclaimed${reset}`);
|
|
800
|
+
} catch {
|
|
801
|
+
// Never fail a build over housekeeping.
|
|
802
|
+
console.log(`${yellow}⚠ skipped${reset}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
708
806
|
export async function runBuild(options: BuildOptions): Promise<void> {
|
|
709
807
|
const { pkgDir, moduleDirs, save, skipModules, published } = options;
|
|
710
808
|
const netappsDir = join(pkgDir, 'netapps');
|
|
@@ -745,6 +843,21 @@ export async function runBuild(options: BuildOptions): Promise<void> {
|
|
|
745
843
|
}
|
|
746
844
|
}
|
|
747
845
|
|
|
846
|
+
// Final gate before anything serves these files: the sim registry binds
|
|
847
|
+
// netapps/ verbatim, so every artifact in it (fresh or stale from a prior
|
|
848
|
+
// run) must be a complete gzip stream. A truncated .netapp served to a
|
|
849
|
+
// consumer reports 'zlib: unexpected end of file', byte-identical to a
|
|
850
|
+
// truncated download, which sends the debugging at the network (celilo#1257).
|
|
851
|
+
try {
|
|
852
|
+
verifyStagedNetapps(netappsDir);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
console.error(`\n${red}${err instanceof Error ? err.message : String(err)}${reset}`);
|
|
855
|
+
console.error(
|
|
856
|
+
`${dim}Delete the corrupt file(s) and re-run \`cele2e build-infra\`. If packaging reproduced the corruption, the stall was likely transient — re-run before deeper debugging.${reset}`,
|
|
857
|
+
);
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
|
|
748
861
|
console.log(`${bold}Building E2E Docker images...${reset}\n`);
|
|
749
862
|
buildDockerImages(pkgDir);
|
|
750
863
|
|
|
@@ -768,4 +881,7 @@ export async function runBuild(options: BuildOptions): Promise<void> {
|
|
|
768
881
|
if (save) {
|
|
769
882
|
saveImages(pkgDir);
|
|
770
883
|
}
|
|
884
|
+
|
|
885
|
+
// Last, so it also collects the image the bake just superseded.
|
|
886
|
+
pruneSupersededImages();
|
|
771
887
|
}
|
|
@@ -46,6 +46,11 @@ export const COMMANDS: CommandDef[] = [
|
|
|
46
46
|
{ name: '--live', description: 'Use live (non-simulated) internet' },
|
|
47
47
|
{ name: '--published', description: 'Use published .netapp packages' },
|
|
48
48
|
{ name: '--notify', description: 'Desktop notification when the run finishes' },
|
|
49
|
+
{
|
|
50
|
+
name: '--source-cli',
|
|
51
|
+
description:
|
|
52
|
+
'Run celilo from the mounted workspace instead of the baked image (roughly doubles each command start-up)',
|
|
53
|
+
},
|
|
49
54
|
{
|
|
50
55
|
name: '--shuffle',
|
|
51
56
|
description: 'Randomize test order to surface order-dependent bugs (seed is logged)',
|
|
@@ -105,6 +110,19 @@ export const COMMANDS: CommandDef[] = [
|
|
|
105
110
|
"Check everything a run needs (baked images, base images, lock, disk) — run's implicit preflight",
|
|
106
111
|
flags: [{ name: '--json', description: 'Emit the full report as JSON' }],
|
|
107
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: 'host',
|
|
115
|
+
description: 'Bring the Docker host VM up/down in the shape the rig needs',
|
|
116
|
+
subcommands: [
|
|
117
|
+
{ name: 'status', description: 'Report the VM against the policy (exit 1 if out of policy)' },
|
|
118
|
+
{ name: 'up', description: 'Start it, or say why a running one is out of policy' },
|
|
119
|
+
{ name: 'down', description: 'Stop it (refuses while the e2e run-lock is held)' },
|
|
120
|
+
{
|
|
121
|
+
name: 'reset',
|
|
122
|
+
description: 'Recreate it — the only way to change mount type. DESTROYS every image',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
108
126
|
{
|
|
109
127
|
name: 'last',
|
|
110
128
|
description: "Show the most recent run's results dir and counts",
|
|
@@ -153,8 +171,11 @@ export const RUN_FLAGS = [
|
|
|
153
171
|
'--live',
|
|
154
172
|
'--published',
|
|
155
173
|
'--notify',
|
|
174
|
+
'--source-cli',
|
|
156
175
|
];
|
|
157
176
|
|
|
177
|
+
export const HOST_VERBS = ['status', 'up', 'down', 'reset'];
|
|
178
|
+
|
|
158
179
|
export const DOWN_FLAGS = ['--keep', '--all'];
|
|
159
180
|
|
|
160
181
|
/** Read-only reporters whose only flag is `--json`. */
|
|
@@ -16,7 +16,7 @@ function runHelp(args = ''): string {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function parseCommandsFromHelp(text: string): string[] {
|
|
19
|
+
export function parseCommandsFromHelp(text: string): string[] {
|
|
20
20
|
const commands: string[] = [];
|
|
21
21
|
let inCommandsSection = false;
|
|
22
22
|
|
|
@@ -25,7 +25,16 @@ function parseCommandsFromHelp(text: string): string[] {
|
|
|
25
25
|
inCommandsSection = true;
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
// Any UNINDENTED heading ends the list. This used to name three headings
|
|
29
|
+
// literally (`Options:`, `Examples:`, `Usage:`), none of which the help
|
|
30
|
+
// actually prints — it writes `Options for \`run\`:` — so the section never
|
|
31
|
+
// closed and every 2-space-indented word after it was read as a command.
|
|
32
|
+
// Nothing showed that, because option lines start with `-` and the only
|
|
33
|
+
// other candidate block happened to sit after `Examples:`. It surfaced the
|
|
34
|
+
// day a command grew a verb list: `reset` was reported as a top-level
|
|
35
|
+
// command that the registry was missing. Keying on the help's real
|
|
36
|
+
// structure fixes the class rather than that instance.
|
|
37
|
+
if (inCommandsSection && /^\S.*:\s*$/.test(line)) {
|
|
29
38
|
inCommandsSection = false;
|
|
30
39
|
continue;
|
|
31
40
|
}
|
package/src/cli/completion.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BUILD_INFRA_FLAGS,
|
|
8
8
|
COMMANDS,
|
|
9
9
|
DOWN_FLAGS,
|
|
10
|
+
HOST_VERBS,
|
|
10
11
|
JSON_FLAG_COMMANDS,
|
|
11
12
|
RUN_FLAGS,
|
|
12
13
|
UP_PRESETS,
|
|
@@ -65,6 +66,14 @@ export function getCompletions(words: string[], current: number): string[] {
|
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
if (command === 'host' && currentIndex === 1) {
|
|
70
|
+
return filterSuggestions(HOST_VERBS, args[1] || '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (command === 'host' && args[1] === 'reset' && currentIndex === 2) {
|
|
74
|
+
return filterSuggestions(['--yes'], args[2] || '');
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
if (command === 'completion' && currentIndex === 1) {
|
|
69
78
|
return filterSuggestions(['zsh', 'bash'], args[1] || '');
|
|
70
79
|
}
|