@norskvideo/ctl-test-harness 0.1.44 → 0.1.46

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.
@@ -32,6 +32,16 @@ export interface LogHit {
32
32
  container: string;
33
33
  line: string;
34
34
  }
35
+ /** Copy every engine `logs` tree under an instance-runtime root to `dest`,
36
+ * preserving the relative structure. Why this and not `docker logs`:
37
+ * norsk-ctl bind-mounts each media container's `/var/log/norsk` to
38
+ * `<runnerDir>/logs/media` on the host (override-generator), so the FULL
39
+ * structured engine log (`debug.json`, every level — not the notice+ subset
40
+ * `docker logs` sees, and immune to a decode-error stdout flood drowning the
41
+ * tail) lives on disk there until teardown nukes the store. Call this BEFORE
42
+ * cleanup to keep those logs for a CI artifact. Best-effort: never throws;
43
+ * returns the number of `logs` trees copied. */
44
+ export declare function preserveEngineLogs(runtimeRoot: string, dest: string): number;
35
45
  /** A string forbids a substring; a RegExp is tested per line. */
36
46
  export type LogPattern = string | RegExp;
37
47
  /** Every line, in any container of the instance, that matches a forbidden
package/container-logs.js CHANGED
@@ -8,6 +8,8 @@
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";
12
+ import { join, relative } from "node:path";
11
13
  const defaultSpawn = (cmd, args, opts) => {
12
14
  const r = spawnSync(cmd, args, opts);
13
15
  return {
@@ -92,6 +94,68 @@ export function collectInstanceContainerLogs(instanceId, tail = DEFAULT_TAIL, sp
92
94
  export function dumpInstanceContainerLogs(instanceId, tail = DEFAULT_TAIL, spawn = defaultSpawn) {
93
95
  return formatContainerLogs(instanceId, collectInstanceContainerLogs(instanceId, tail, spawn));
94
96
  }
97
+ /** Copy every engine `logs` tree under an instance-runtime root to `dest`,
98
+ * preserving the relative structure. Why this and not `docker logs`:
99
+ * norsk-ctl bind-mounts each media container's `/var/log/norsk` to
100
+ * `<runnerDir>/logs/media` on the host (override-generator), so the FULL
101
+ * structured engine log (`debug.json`, every level — not the notice+ subset
102
+ * `docker logs` sees, and immune to a decode-error stdout flood drowning the
103
+ * tail) lives on disk there until teardown nukes the store. Call this BEFORE
104
+ * cleanup to keep those logs for a CI artifact. Best-effort: never throws;
105
+ * returns the number of `logs` trees copied. */
106
+ export function preserveEngineLogs(runtimeRoot, dest) {
107
+ if (!existsSync(runtimeRoot))
108
+ return 0;
109
+ let copied = 0;
110
+ const walk = (dir) => {
111
+ let names;
112
+ try {
113
+ names = readdirSync(dir);
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ for (const name of names) {
119
+ const p = join(dir, name);
120
+ try {
121
+ if (!statSync(p).isDirectory())
122
+ continue;
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ if (name === "logs") {
128
+ // Copy the tree, but CHILD BY CHILD: a root-owned sibling (e.g. studio
129
+ // logs when studio runs as root) must not abort the copy of the media
130
+ // logs we came for. Don't descend further past a logs/ dir.
131
+ let children;
132
+ try {
133
+ children = readdirSync(p);
134
+ }
135
+ catch {
136
+ continue;
137
+ }
138
+ let any = false;
139
+ for (const child of children) {
140
+ try {
141
+ cpSync(join(p, child), join(dest, relative(runtimeRoot, p), child), { recursive: true });
142
+ any = true;
143
+ }
144
+ catch {
145
+ /* best-effort per child */
146
+ }
147
+ }
148
+ if (any)
149
+ copied += 1;
150
+ }
151
+ else {
152
+ walk(p);
153
+ }
154
+ }
155
+ };
156
+ walk(runtimeRoot);
157
+ return copied;
158
+ }
95
159
  const matches = (pattern, line) => typeof pattern === "string" ? line.includes(pattern) : pattern.test(line);
96
160
  /** Every line, in any container of the instance, that matches a forbidden
97
161
  * pattern. `clean` when there are none — including when docker cannot be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-test-harness",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
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, readFileSync, writeFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
- import { dumpInstanceContainerLogs } from "./container-logs.js";
22
+ import { dumpInstanceContainerLogs, preserveEngineLogs } 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";
@@ -391,6 +391,20 @@ export async function runProductSmoke(slug, spec, deps = defaultSmokeDeps) {
391
391
  catch (e) {
392
392
  journeyError = e;
393
393
  }
394
+ // Preserve the engine logs BEFORE anything tears the store down. norsk-ctl
395
+ // bind-mounts each media container's /var/log/norsk to <runnerDir>/logs/media,
396
+ // and runnerDir is <storeDir>/product-instances/<id> -- a SIBLING of the
397
+ // workdir (:/data, under <storeDir>/norsk-runtime), NOT under it. So walk the
398
+ // whole storeDir, not just norsk-runtime, or the logs/ tree is never found
399
+ // (this is why it preserved 0). `product remove`, `instance delete --purge`,
400
+ // cleanup and the root nuke below all destroy it, and we pretty much always
401
+ // want the Norsk logs from a smoke run; when a CI log dir is set, copy every
402
+ // logs/ tree out first so the workflow can upload it.
403
+ const ciLogDir = process.env.NORSK_CI_LOG_DIR;
404
+ if (ciLogDir) {
405
+ const n = preserveEngineLogs(storeDir, join(ciLogDir, slug));
406
+ deps.log(`${slug}: preserved ${n} engine log tree(s) -> ${join(ciLogDir, slug)}`);
407
+ }
394
408
  if (handles.length)
395
409
  await deps.stopSources(handles).catch(() => { });
396
410
  // The daemon reaps its own control-plane containers on shutdown (daemon.ts,