@norskvideo/ctl-test-harness 0.1.58 → 0.1.60
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/container-logs.d.ts +11 -0
- package/container-logs.js +35 -1
- package/demo/harness.js +11 -1
- package/demo/run.d.ts +4 -0
- package/demo/run.js +21 -0
- package/package.json +5 -1
- package/reap.d.ts +37 -0
- package/reap.js +129 -0
- package/smoke.js +16 -2
package/container-logs.d.ts
CHANGED
|
@@ -28,6 +28,17 @@ export declare function formatContainerLogs(instanceId: string, entries: Contain
|
|
|
28
28
|
export declare function collectInstanceContainerLogs(instanceId: string, tail?: number, spawn?: SpawnLike): ContainerLog[];
|
|
29
29
|
/** The ready-to-print diagnostic block (empty-marker when nothing is found). */
|
|
30
30
|
export declare function dumpInstanceContainerLogs(instanceId: string, tail?: number, spawn?: SpawnLike): string;
|
|
31
|
+
/** Write every container of one instance to `<dest>/<container>.log`, for a CI
|
|
32
|
+
* artifact. The companion to {@link preserveEngineLogs}, which keeps the media
|
|
33
|
+
* container's bind-mounted engine tree: this keeps what ONLY Docker has, and
|
|
34
|
+
* the studio container's stdout is where "Refusing to run workflow" is printed
|
|
35
|
+
* — the line the gate failures point at and that teardown then destroys.
|
|
36
|
+
*
|
|
37
|
+
* Call BEFORE cleanup. Best-effort throughout and never throws: a diagnostic
|
|
38
|
+
* that fails the run it was meant to explain is worse than no diagnostic.
|
|
39
|
+
* Returns the number of logs written, and creates nothing when there are none.
|
|
40
|
+
*/
|
|
41
|
+
export declare function preserveInstanceContainerLogs(instanceId: string, dest: string, tail?: number, spawn?: SpawnLike): number;
|
|
31
42
|
export interface LogHit {
|
|
32
43
|
container: string;
|
|
33
44
|
line: string;
|
package/container-logs.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Pure argv/parse/format helpers; only the two exported probes shell out,
|
|
9
9
|
// through an injectable spawn.
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
-
import { cpSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { join, relative } from "node:path";
|
|
13
13
|
const defaultSpawn = (cmd, args, opts) => {
|
|
14
14
|
const r = spawnSync(cmd, args, opts);
|
|
@@ -94,6 +94,40 @@ export function collectInstanceContainerLogs(instanceId, tail = DEFAULT_TAIL, sp
|
|
|
94
94
|
export function dumpInstanceContainerLogs(instanceId, tail = DEFAULT_TAIL, spawn = defaultSpawn) {
|
|
95
95
|
return formatContainerLogs(instanceId, collectInstanceContainerLogs(instanceId, tail, spawn));
|
|
96
96
|
}
|
|
97
|
+
/** Write every container of one instance to `<dest>/<container>.log`, for a CI
|
|
98
|
+
* artifact. The companion to {@link preserveEngineLogs}, which keeps the media
|
|
99
|
+
* container's bind-mounted engine tree: this keeps what ONLY Docker has, and
|
|
100
|
+
* the studio container's stdout is where "Refusing to run workflow" is printed
|
|
101
|
+
* — the line the gate failures point at and that teardown then destroys.
|
|
102
|
+
*
|
|
103
|
+
* Call BEFORE cleanup. Best-effort throughout and never throws: a diagnostic
|
|
104
|
+
* that fails the run it was meant to explain is worse than no diagnostic.
|
|
105
|
+
* Returns the number of logs written, and creates nothing when there are none.
|
|
106
|
+
*/
|
|
107
|
+
export function preserveInstanceContainerLogs(instanceId, dest, tail = DEFAULT_TAIL, spawn = defaultSpawn) {
|
|
108
|
+
const entries = collectInstanceContainerLogs(instanceId, tail, spawn);
|
|
109
|
+
if (entries.length === 0)
|
|
110
|
+
return 0;
|
|
111
|
+
let written = 0;
|
|
112
|
+
try {
|
|
113
|
+
mkdirSync(dest, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
for (const e of entries) {
|
|
119
|
+
try {
|
|
120
|
+
// The state goes in the file, not just the name: an `exited` container
|
|
121
|
+
// with an empty log says something a missing file does not.
|
|
122
|
+
writeFileSync(join(dest, `${e.name}.log`), `# ${e.name} (${e.state})\n${e.log}`);
|
|
123
|
+
written++;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Keep going: one unwritable file must not cost the others.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return written;
|
|
130
|
+
}
|
|
97
131
|
/** Copy every engine `logs` tree under an instance-runtime root to `dest`,
|
|
98
132
|
* preserving the relative structure. Why this and not `docker logs`:
|
|
99
133
|
* norsk-ctl bind-mounts each media container's `/var/log/norsk` to
|
package/demo/harness.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { netReachMode, STUDIO_INTERNAL_PORT, studioBaseFrom } from "../container-net.js";
|
|
17
17
|
import { ctlSupportsPublishDebugPorts } from "../launch.js";
|
|
18
18
|
import { pollUntil } from "../poll.js";
|
|
19
|
+
import { launchWithPortConflictRetry, reapInstance } from "../reap.js";
|
|
19
20
|
import { DemoGateFailure } from "./gates.js";
|
|
20
21
|
import { DemoSession, defaultDemoDeps, demoSlug, expandHome, resolveIngestPort, } from "./run.js";
|
|
21
22
|
const STUDIO_HOST_PORT_PARAM = "STUDIO_HOST_PORT";
|
|
@@ -165,7 +166,16 @@ export class ProductHarness {
|
|
|
165
166
|
// the reap step whether or not this process lives to teardown.
|
|
166
167
|
if (this.launchedManifest)
|
|
167
168
|
this.deps.writeFile(this.launchedManifest, `${this.launchedIds.join("\n")}\n`);
|
|
168
|
-
|
|
169
|
+
// A run cancelled before teardown leaves this id's compose stack up, still
|
|
170
|
+
// holding its host ports, and nothing that asks a live daemon can see it
|
|
171
|
+
// (the leaker's daemon is gone). Reap by label first, then retry once if the
|
|
172
|
+
// bind still loses the advisory-check race — see reap.ts.
|
|
173
|
+
reapInstance(instanceId);
|
|
174
|
+
await launchWithPortConflictRetry({
|
|
175
|
+
instanceId,
|
|
176
|
+
attempt: () => s.cliOk(launchArgv),
|
|
177
|
+
log: deps.log,
|
|
178
|
+
});
|
|
169
179
|
await s.waitRunning(instanceId);
|
|
170
180
|
const rows = await s.ingestPorts(instanceId);
|
|
171
181
|
const templateParams = { declared, overrides: specParams };
|
package/demo/run.d.ts
CHANGED
|
@@ -315,6 +315,10 @@ export declare class DemoSession {
|
|
|
315
315
|
/** Tear down in reverse: sources, instance, then — private only — the
|
|
316
316
|
* daemon (with its proxy), the dev backend, the store. Reuse leaves the
|
|
317
317
|
* developer's daemon, store and registration as they are. */
|
|
318
|
+
/** Keep each instance's container logs under NORSK_CI_LOG_DIR for the
|
|
319
|
+
* workflow's artifact upload. No env var (local dev) means no-op; every
|
|
320
|
+
* failure inside is swallowed by the helper. */
|
|
321
|
+
private preserveContainerLogs;
|
|
318
322
|
teardown(opts: {
|
|
319
323
|
instances: string[];
|
|
320
324
|
handles: SourceHandle[];
|
package/demo/run.js
CHANGED
|
@@ -29,6 +29,7 @@ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSy
|
|
|
29
29
|
import { homedir } from "node:os";
|
|
30
30
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
31
31
|
import { parseManifestSeed, repoOf } from "@norskvideo/ctl-sdk/manifest-seed";
|
|
32
|
+
import { preserveInstanceContainerLogs } from "../container-logs.js";
|
|
32
33
|
import { DOCKER_NETWORK_NAME, ensureRunnerOnNetwork, netReachMode } from "../container-net.js";
|
|
33
34
|
import { cleanupDaemon, requireLicenseFile, runCli, startDaemon, } from "../daemon.js";
|
|
34
35
|
import { hashSlot } from "../harness-config.js";
|
|
@@ -653,7 +654,27 @@ export class DemoSession {
|
|
|
653
654
|
/** Tear down in reverse: sources, instance, then — private only — the
|
|
654
655
|
* daemon (with its proxy), the dev backend, the store. Reuse leaves the
|
|
655
656
|
* developer's daemon, store and registration as they are. */
|
|
657
|
+
/** Keep each instance's container logs under NORSK_CI_LOG_DIR for the
|
|
658
|
+
* workflow's artifact upload. No env var (local dev) means no-op; every
|
|
659
|
+
* failure inside is swallowed by the helper. */
|
|
660
|
+
preserveContainerLogs(instances) {
|
|
661
|
+
const ciLogDir = process.env.NORSK_CI_LOG_DIR;
|
|
662
|
+
if (!ciLogDir)
|
|
663
|
+
return;
|
|
664
|
+
for (const id of instances) {
|
|
665
|
+
const n = preserveInstanceContainerLogs(id, join(ciLogDir, "containers", id));
|
|
666
|
+
if (n > 0)
|
|
667
|
+
this.deps.log(`preserved ${n} container log(s) for ${id} -> ${join(ciLogDir, "containers", id)}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
656
670
|
async teardown(opts) {
|
|
671
|
+
// FIRST, while the containers are still standing. Everything below removes
|
|
672
|
+
// them, and `docker logs` is the only copy of the studio container's stdout
|
|
673
|
+
// — the "Refusing to run workflow" line that the workflow-never-started
|
|
674
|
+
// gates tell you to go and read. Unconditional rather than on-failure,
|
|
675
|
+
// because teardown is not told the verdict, and a tail per container is
|
|
676
|
+
// cheap next to a re-run.
|
|
677
|
+
this.preserveContainerLogs(opts.instances);
|
|
657
678
|
if (opts.handles.length)
|
|
658
679
|
await this.deps.stopSources(opts.handles).catch(() => { });
|
|
659
680
|
if (this.policy === "reuse") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@norskvideo/ctl-test-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.60",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./launch.d.ts",
|
|
28
28
|
"default": "./launch.js"
|
|
29
29
|
},
|
|
30
|
+
"./reap": {
|
|
31
|
+
"types": "./reap.d.ts",
|
|
32
|
+
"default": "./reap.js"
|
|
33
|
+
},
|
|
30
34
|
"./source-pump": {
|
|
31
35
|
"types": "./source-pump.d.ts",
|
|
32
36
|
"default": "./source-pump.js"
|
package/reap.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
/** True when `text` is a host-port binding conflict — the signal to reap and
|
|
3
|
+
* retry. Deliberately narrow: an unhealthy dependency is also a launch
|
|
4
|
+
* failure, and retrying that one just burns the clock. */
|
|
5
|
+
export declare function isPortConflict(text: string): boolean;
|
|
6
|
+
/** The conflicting host port, or null when the message carries no extractable
|
|
7
|
+
* one (still a conflict per {@link isPortConflict}, just not attributable to a
|
|
8
|
+
* port, so only the instance-scoped reap can run). */
|
|
9
|
+
export declare function extractConflictPort(text: string): number | null;
|
|
10
|
+
/** `docker ps` argv listing container ids for ONE instance id — the exact scope
|
|
11
|
+
* that spares co-running products. */
|
|
12
|
+
export declare function instancePsArgs(instanceId: string): string[];
|
|
13
|
+
/** `docker ps` argv listing norsk-ctl container ids publishing a given host
|
|
14
|
+
* port. Role-filtered, so a squatter outside norsk-ctl is never touched. */
|
|
15
|
+
export declare function portPsArgs(port: number): string[];
|
|
16
|
+
type Spawn = typeof spawnSync;
|
|
17
|
+
/** Reap every leftover container for one instance id (media, studio and source
|
|
18
|
+
* all carry the `norsk-ctl.instance` label). */
|
|
19
|
+
export declare function reapInstance(instanceId: string, spawn?: Spawn): string[];
|
|
20
|
+
/** Reap whatever norsk-ctl container holds a specific host port — the fallback
|
|
21
|
+
* when the holder belongs to a different instance, so the instance-id reap
|
|
22
|
+
* alone would not free it. */
|
|
23
|
+
export declare function reapPort(port: number, spawn?: Spawn): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Run a launch, and on a host-port conflict reap the holder and retry ONCE.
|
|
26
|
+
*
|
|
27
|
+
* Once, not a loop: one retry clears a leaked holder or a lost race, and a
|
|
28
|
+
* second conflict means something is genuinely sitting on the port — where the
|
|
29
|
+
* launch's own message, naming the port, is more use than another attempt.
|
|
30
|
+
*/
|
|
31
|
+
export declare function launchWithPortConflictRetry<T>(opts: {
|
|
32
|
+
instanceId: string;
|
|
33
|
+
attempt: () => Promise<T>;
|
|
34
|
+
spawn?: Spawn;
|
|
35
|
+
log?: (line: string) => void;
|
|
36
|
+
}): Promise<T>;
|
|
37
|
+
export {};
|
package/reap.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Orphan-container reaper for launches that lose a host port.
|
|
2
|
+
//
|
|
3
|
+
// ctl's port-availability check at validation is ADVISORY — the probe holds no
|
|
4
|
+
// reservation, and docker binds host ports at container START — so a port free
|
|
5
|
+
// when the launch was validated can be taken by the time compose binds it
|
|
6
|
+
// (`launch-service.ts`, HOST_PORT_CONFLICT). Its own message says to launch
|
|
7
|
+
// again. On a shared self-hosted runner that race is routine rather than
|
|
8
|
+
// exotic, in two flavours:
|
|
9
|
+
//
|
|
10
|
+
// * a CANCELLED run: `cancel-in-progress` SIGKILLs `bun test` before the
|
|
11
|
+
// harness can tear down, so a `compose up -d` stack outlives its (now dead)
|
|
12
|
+
// daemon and keeps holding the port. Nothing that asks a LIVE daemon can
|
|
13
|
+
// see it, because the leaker's daemon is gone — which is why this reaps off
|
|
14
|
+
// Docker by label rather than through the API.
|
|
15
|
+
// * co-running products: six product repos' CI can fire within seconds of
|
|
16
|
+
// each other on one Docker daemon, and a fixed host port (the MoQ preview's
|
|
17
|
+
// `preferred: 4443`) is picked by each of them independently.
|
|
18
|
+
//
|
|
19
|
+
// Scoping is the whole safety argument. Every norsk-ctl container carries
|
|
20
|
+
// `norsk-ctl.role`; per-instance ones also carry `norsk-ctl.instance=<id>`. We
|
|
21
|
+
// reap by the exact instance id, and by port only among containers that carry
|
|
22
|
+
// the role label — so a co-running product is untouched and a squatter outside
|
|
23
|
+
// norsk-ctl is never removed, only reported by the launch that then fails.
|
|
24
|
+
//
|
|
25
|
+
// The regex and argv builders are pure so tests/reap.test.ts covers them with
|
|
26
|
+
// zero Docker; only `reapInstance`/`reapPort` shell out.
|
|
27
|
+
//
|
|
28
|
+
// Lifted from norsk-ctl-product-playout's tests/integration/support/reap.ts,
|
|
29
|
+
// which is where this was worked out. It lived in one product while probe,
|
|
30
|
+
// commentary, reuters and funke had no equivalent and lost tests to the same
|
|
31
|
+
// race, so it belongs here instead.
|
|
32
|
+
import { spawnSync } from "node:child_process";
|
|
33
|
+
// A launch failure that means "the host port I need is already bound". Covers
|
|
34
|
+
// the daemon's own wrapper and the raw docker/kernel strings it quotes, since
|
|
35
|
+
// either can surface depending on where the bind loses the race.
|
|
36
|
+
const CONFLICT_PATTERNS = [
|
|
37
|
+
/was taken between validation and container start/i,
|
|
38
|
+
/port is already allocated/i,
|
|
39
|
+
/address already in use/i,
|
|
40
|
+
];
|
|
41
|
+
// Same failure, with the offending port captured. Ordered most-specific first:
|
|
42
|
+
// the daemon wrapper names the port up front, the raw docker/kernel lines carry
|
|
43
|
+
// it in the bind address.
|
|
44
|
+
const PORT_PATTERNS = [
|
|
45
|
+
/host port (\d{2,5})\b/i,
|
|
46
|
+
/bind for [\d.:[\]]*?:(\d{2,5})\b[^:]*:\s*port is already allocated/i,
|
|
47
|
+
/:(\d{2,5}):\s*bind:\s*address already in use/i,
|
|
48
|
+
/port (\d{2,5}) .*already/i,
|
|
49
|
+
];
|
|
50
|
+
/** True when `text` is a host-port binding conflict — the signal to reap and
|
|
51
|
+
* retry. Deliberately narrow: an unhealthy dependency is also a launch
|
|
52
|
+
* failure, and retrying that one just burns the clock. */
|
|
53
|
+
export function isPortConflict(text) {
|
|
54
|
+
return CONFLICT_PATTERNS.some((re) => re.test(text));
|
|
55
|
+
}
|
|
56
|
+
/** The conflicting host port, or null when the message carries no extractable
|
|
57
|
+
* one (still a conflict per {@link isPortConflict}, just not attributable to a
|
|
58
|
+
* port, so only the instance-scoped reap can run). */
|
|
59
|
+
export function extractConflictPort(text) {
|
|
60
|
+
for (const re of PORT_PATTERNS) {
|
|
61
|
+
const m = text.match(re);
|
|
62
|
+
if (m?.[1])
|
|
63
|
+
return Number(m[1]);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/** `docker ps` argv listing container ids for ONE instance id — the exact scope
|
|
68
|
+
* that spares co-running products. */
|
|
69
|
+
export function instancePsArgs(instanceId) {
|
|
70
|
+
return ["ps", "-aq", "--filter", `label=norsk-ctl.instance=${instanceId}`];
|
|
71
|
+
}
|
|
72
|
+
/** `docker ps` argv listing norsk-ctl container ids publishing a given host
|
|
73
|
+
* port. Role-filtered, so a squatter outside norsk-ctl is never touched. */
|
|
74
|
+
export function portPsArgs(port) {
|
|
75
|
+
return ["ps", "-aq", "--filter", "label=norsk-ctl.role", "--filter", `publish=${port}`];
|
|
76
|
+
}
|
|
77
|
+
/** Force-remove the containers `docker ps <psArgs>` lists. Best-effort and
|
|
78
|
+
* synchronous (it must finish before the relaunch); a Docker that is absent or
|
|
79
|
+
* errors leaves the launch to fail with its own clearer message. Returns the
|
|
80
|
+
* ids it removed, for logging. */
|
|
81
|
+
function reap(psArgs, spawn) {
|
|
82
|
+
try {
|
|
83
|
+
const ps = spawn("docker", psArgs, { encoding: "utf8", timeout: 15_000 });
|
|
84
|
+
const ids = (ps.stdout ?? "")
|
|
85
|
+
.split("\n")
|
|
86
|
+
.map((s) => s.trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
if (ids.length > 0)
|
|
89
|
+
spawn("docker", ["rm", "-f", ...ids], { encoding: "utf8", timeout: 30_000 });
|
|
90
|
+
return ids;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Reap every leftover container for one instance id (media, studio and source
|
|
97
|
+
* all carry the `norsk-ctl.instance` label). */
|
|
98
|
+
export function reapInstance(instanceId, spawn = spawnSync) {
|
|
99
|
+
return reap(instancePsArgs(instanceId), spawn);
|
|
100
|
+
}
|
|
101
|
+
/** Reap whatever norsk-ctl container holds a specific host port — the fallback
|
|
102
|
+
* when the holder belongs to a different instance, so the instance-id reap
|
|
103
|
+
* alone would not free it. */
|
|
104
|
+
export function reapPort(port, spawn = spawnSync) {
|
|
105
|
+
return reap(portPsArgs(port), spawn);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Run a launch, and on a host-port conflict reap the holder and retry ONCE.
|
|
109
|
+
*
|
|
110
|
+
* Once, not a loop: one retry clears a leaked holder or a lost race, and a
|
|
111
|
+
* second conflict means something is genuinely sitting on the port — where the
|
|
112
|
+
* launch's own message, naming the port, is more use than another attempt.
|
|
113
|
+
*/
|
|
114
|
+
export async function launchWithPortConflictRetry(opts) {
|
|
115
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
116
|
+
try {
|
|
117
|
+
return await opts.attempt();
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
const text = e instanceof Error ? e.message : String(e);
|
|
121
|
+
if (!isPortConflict(text))
|
|
122
|
+
throw e;
|
|
123
|
+
const port = extractConflictPort(text);
|
|
124
|
+
const reaped = [...reapInstance(opts.instanceId, spawn), ...(port !== null ? reapPort(port, spawn) : [])];
|
|
125
|
+
opts.log?.(`host port conflict on ${port !== null ? `:${port}` : "an unnamed port"} launching ${opts.instanceId}; ` +
|
|
126
|
+
`reaped ${reaped.length} container(s) and retrying once`);
|
|
127
|
+
return await opts.attempt();
|
|
128
|
+
}
|
|
129
|
+
}
|
package/smoke.js
CHANGED
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
// rule); a product wanting typed commands bridges with toArgv in its own spec.
|
|
20
20
|
import { existsSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
-
import { dumpInstanceContainerLogs, preserveEngineLogs } from "./container-logs.js";
|
|
22
|
+
import { dumpInstanceContainerLogs, preserveEngineLogs, preserveInstanceContainerLogs } from "./container-logs.js";
|
|
23
23
|
import { ensureRunnerOnNetwork, netReachMode, STUDIO_INTERNAL_PORT, studioBaseFrom, } from "./container-net.js";
|
|
24
24
|
import { cleanupDaemon, requireLicenseFile, runCli, startDaemon, } from "./daemon.js";
|
|
25
25
|
import { hashSlot } from "./harness-config.js";
|
|
26
26
|
import { ctlSupportsNoPublish, ctlSupportsPublishDebugPorts, runnerContainerUser } from "./launch.js";
|
|
27
27
|
import { pollUntil } from "./poll.js";
|
|
28
|
+
import { launchWithPortConflictRetry, reapInstance } from "./reap.js";
|
|
28
29
|
import { nukePathAsRoot, reportForeignOwners } from "./root-nuke.js";
|
|
29
30
|
import { readProxySecret, startSrtSources } from "./source-pump.js";
|
|
30
31
|
import { applyTestHost, fetchComponentState, fetchComponents, fetchStreamMappings, isSrtListenerState, } from "./studio-state.js";
|
|
@@ -339,7 +340,15 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
|
339
340
|
launchArgv.push("--publish-debug-ports");
|
|
340
341
|
}
|
|
341
342
|
launched = true;
|
|
342
|
-
|
|
343
|
+
// Same advisory-check race as the demo tier, same remedy: reap this id's
|
|
344
|
+
// orphans from a run that was killed before teardown, then retry once if the
|
|
345
|
+
// bind loses anyway (reap.ts).
|
|
346
|
+
reapInstance(instanceId);
|
|
347
|
+
await launchWithPortConflictRetry({
|
|
348
|
+
instanceId,
|
|
349
|
+
attempt: () => cliOk(launchArgv),
|
|
350
|
+
log: deps.log,
|
|
351
|
+
});
|
|
343
352
|
await pollUntil(async () => {
|
|
344
353
|
const r = await cli(["instance", "list"], { output: "json" });
|
|
345
354
|
if (r.exitCode !== 0)
|
|
@@ -425,6 +434,11 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
|
|
|
425
434
|
if (ciLogDir) {
|
|
426
435
|
const n = preserveEngineLogs(storeDir, join(ciLogDir, slug));
|
|
427
436
|
deps.log(`${slug}: preserved ${n} engine log tree(s) -> ${join(ciLogDir, slug)}`);
|
|
437
|
+
// And the half the bind-mounted tree does not hold: the STUDIO container's
|
|
438
|
+
// stdout, where "Refusing to run workflow" is printed. Still standing at
|
|
439
|
+
// this point; everything below removes it.
|
|
440
|
+
const c = preserveInstanceContainerLogs(instanceId, join(ciLogDir, slug, "containers", instanceId));
|
|
441
|
+
deps.log(`${slug}: preserved ${c} container log(s) -> ${join(ciLogDir, slug, "containers", instanceId)}`);
|
|
428
442
|
}
|
|
429
443
|
if (handles.length)
|
|
430
444
|
await deps.stopSources(handles).catch(() => { });
|