@norskvideo/ctl-test-harness 0.1.59 → 0.1.61

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.
@@ -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/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.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -85,9 +85,9 @@
85
85
  "devDependencies": {
86
86
  "@types/bun": "latest",
87
87
  "@types/node": "^22.10.0",
88
- "@norskvideo/norsk-studio": "1.30.0-2026-09-24-fe25440e",
89
- "@norskvideo/norsk-studio-built-ins": "1.30.0-2026-09-24-fe25440e",
90
- "@norskvideo/norsk-sdk": "1.3.1-2026-09-25-2aec49ef"
88
+ "@norskvideo/norsk-studio": "1.30.0-2026-09-28-b92e8139",
89
+ "@norskvideo/norsk-studio-built-ins": "1.30.0-2026-09-28-b92e8139",
90
+ "@norskvideo/norsk-sdk": "1.3.1-2026-09-30-acca6a9d"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@norskvideo/norsk-studio": {
package/smoke.js CHANGED
@@ -19,7 +19,7 @@
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";
@@ -434,6 +434,11 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
434
434
  if (ciLogDir) {
435
435
  const n = preserveEngineLogs(storeDir, join(ciLogDir, slug));
436
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)}`);
437
442
  }
438
443
  if (handles.length)
439
444
  await deps.stopSources(handles).catch(() => { });