@norskvideo/ctl-dev-kit 0.2.45 → 0.2.47

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.
@@ -18,11 +18,13 @@
18
18
  // dev-kit forces a re-sync. Canonical bytes ship alongside this script
19
19
  // (conventions/* + build/*), resolved package-relative so they work both
20
20
  // workspace-symlinked and installed from the published tarball.
21
+
21
22
  import { execFileSync } from "node:child_process";
22
23
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
23
24
  import { join } from "node:path";
24
25
  import { parseManifestSeed } from "@norskvideo/ctl-sdk/manifest-seed";
25
26
  import { checkTheme } from "../docs-theme/sync.ts";
27
+ import { ctlPinProblems } from "./ctl-pin-agreement.ts";
26
28
 
27
29
  export interface DriftReport {
28
30
  ok: boolean;
@@ -803,6 +805,7 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
803
805
 
804
806
  for (const p of demoProblems(repoRoot, canonical.demoShim)) push(p);
805
807
  for (const p of productLogsProblems(repoRoot)) push(p);
808
+ for (const p of ctlPinProblems(repoRoot)) push(p);
806
809
 
807
810
  return { ok: problems.length === 0, problems };
808
811
  }
@@ -0,0 +1,68 @@
1
+ // Every package.json in a product repo pins each @norskvideo/ctl-* package at
2
+ // the SAME range.
3
+ //
4
+ // A repo is a workspace of packages, and a nested one (tests/, shared/) can pin
5
+ // a ctl package of its own. When the two ranges differ, the nested package runs
6
+ // a different copy from the one the root was moved to, and nothing says so:
7
+ // playout's tests/ pinned the dev-kit ^0.1.57 while the root moved to 0.2, its
8
+ // guides ran a guide proxy that built the wrong Studio prefix, and first-light
9
+ // failed every night from 2026-09-07. sync-dev-kit.yml and
10
+ // sync-ctl-packages.yml now move every pin; this keeps a hand edit (or a new
11
+ // nested package) from splitting them again.
12
+ import { lstatSync, readdirSync, readFileSync } from "node:fs";
13
+ import { join, relative } from "node:path";
14
+
15
+ const SCOPE = "@norskvideo/ctl-";
16
+ /** Directories that hold installed or generated packages, not this repo's own. */
17
+ const SKIP = new Set(["node_modules", ".git", "dist", "build", "test-temp", "generated", ".nix"]);
18
+
19
+ function manifests(root: string, dir = root, out: string[] = []): string[] {
20
+ for (const name of readdirSync(dir)) {
21
+ if (SKIP.has(name)) continue;
22
+ const path = join(dir, name);
23
+ // lstat: never follow a symlink. A repo's own packages are real
24
+ // directories, and a link (a nix `result`, a workspace link) can lead
25
+ // anywhere, including out of the repo entirely.
26
+ let st: ReturnType<typeof lstatSync>;
27
+ try {
28
+ st = lstatSync(path);
29
+ } catch {
30
+ continue;
31
+ }
32
+ if (st.isDirectory()) manifests(root, path, out);
33
+ else if (name === "package.json") out.push(path);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ export function ctlPinProblems(repoRoot: string): string[] {
39
+ const problems: string[] = [];
40
+ /** package -> range -> the files pinning it at that range */
41
+ const pins = new Map<string, Map<string, string[]>>();
42
+ for (const path of manifests(repoRoot).sort()) {
43
+ const rel = relative(repoRoot, path);
44
+ let parsed: Record<string, Record<string, string> | undefined>;
45
+ try {
46
+ parsed = JSON.parse(readFileSync(path, "utf8"));
47
+ } catch (e) {
48
+ problems.push(`${rel} is not parseable JSON (${e instanceof Error ? e.message : String(e)}).`);
49
+ continue;
50
+ }
51
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
52
+ for (const [name, range] of Object.entries(parsed[field] ?? {})) {
53
+ if (!name.startsWith(SCOPE) || typeof range !== "string" || range.startsWith("workspace:")) continue;
54
+ const byRange = pins.get(name) ?? new Map<string, string[]>();
55
+ byRange.set(range, [...(byRange.get(range) ?? []), rel]);
56
+ pins.set(name, byRange);
57
+ }
58
+ }
59
+ }
60
+ for (const [name, byRange] of pins) {
61
+ if (byRange.size < 2) continue;
62
+ const where = [...byRange].flatMap(([range, files]) => files.map((f) => `${f} ${range}`)).join(", ");
63
+ problems.push(
64
+ `${name} is pinned at ${byRange.size} different ranges (${where}). A nested package then runs a different copy from the root's; pin it at one range everywhere (sync-dev-kit / sync-ctl-packages move every pin).`,
65
+ );
66
+ }
67
+ return problems;
68
+ }
@@ -117,24 +117,32 @@ jobs:
117
117
  new="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/$enc" | jq -r '."dist-tags".latest')"
118
118
  [ -n "$new" ] && [ "$new" != "null" ] || { echo "could not resolve $pkg latest"; exit 1; }
119
119
 
120
- # The current pin, whatever its range operator. A repo with no dev-kit
121
- # dep has nothing to sync.
122
- old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" package.json | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
123
- if [ -z "$old" ]; then
124
- echo "$pkg is not pinned in package.json -- nothing to sync."
125
- echo "changed=0" >> "$GITHUB_OUTPUT"
126
- exit 0
127
- fi
128
- old_bare="${old#^}"; old_bare="${old_bare#~}"
129
- echo "dev-kit: $old -> ^$new"
130
- if [ "$old_bare" = "$new" ]; then
131
- echo "already on the newest dev-kit ($new)."
120
+ # Every tracked package.json, not just the root. A nested workspace can
121
+ # pin the dev-kit too, and a root-only rewrite left it behind: playout's
122
+ # tests/ sat on 0.1.x after the root moved to 0.2, ran a guide proxy
123
+ # that built the wrong Studio prefix, and first-light failed every
124
+ # night from 2026-09-07. Same walk as sync-ctl-packages.yml.
125
+ mapfile -t manifests < <(git ls-files '*package.json' ':!:**/node_modules/**')
126
+ moved=""
127
+ for m in "${manifests[@]}"; do
128
+ old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$m" | head -1 | sed -E 's/.*"([^"]*)"$/\1/' || true)"
129
+ # Absent here, or a workspace protocol spec that names no version.
130
+ case "$old" in "" | workspace:*) continue ;; esac
131
+ old_bare="${old#^}"; old_bare="${old_bare#~}"
132
+ [ "$old_bare" != "$new" ] || continue
133
+ # Surgical single-line rewrite so the file keeps its formatting
134
+ # (a jq reserialize would reflow it and trip the formatter).
135
+ sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" "$m"
136
+ echo "dev-kit: $m $old -> ^$new"
137
+ moved="$moved $old_bare"
138
+ done
139
+ if [ -z "$moved" ]; then
140
+ echo "every dev-kit pin is already on the newest dev-kit ($new)."
132
141
  echo "changed=0" >> "$GITHUB_OUTPUT"
133
142
  exit 0
134
143
  fi
135
- # Surgical single-line rewrite so package.json keeps its formatting
136
- # (a jq reserialize would reflow the whole file and trip the formatter).
137
- sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" package.json
144
+ # `old` names the lowest pin moved, for the commit message and PR title.
145
+ old_bare="$(printf '%s\n' $moved | sort -V | head -1)"
138
146
  echo "changed=1" >> "$GITHUB_OUTPUT"
139
147
  echo "old=$old_bare" >> "$GITHUB_OUTPUT"
140
148
  echo "new=$new" >> "$GITHUB_OUTPUT"
@@ -148,7 +156,19 @@ jobs:
148
156
  run: |
149
157
  nix develop .#build --command bash -c '
150
158
  set -euo pipefail
151
- bun install
159
+ # Per install root, as sync-ctl-packages.yml does: a manifest outside
160
+ # the root workspace owns its own lockfile. Then move the locked
161
+ # dev-kit to the newest in range: bun install keeps a locked version
162
+ # that still satisfies its range, so a nested workspace could
163
+ # otherwise stay on an old dev-kit under a new pin. Only where the
164
+ # lockfile already holds the dev-kit: bun update ADDS a package it
165
+ # does not find, and put the dev-kit into funke proxy/ui that way.
166
+ for lock in $(git ls-files "*bun.lock"); do
167
+ (cd "$(dirname "$lock")" && bun install)
168
+ if grep -q "\"@norskvideo/ctl-dev-kit@" "$lock"; then
169
+ (cd "$(dirname "$lock")" && bun update @norskvideo/ctl-dev-kit)
170
+ fi
171
+ done
152
172
  '
153
173
 
154
174
  - name: Re-sync the copies from canonical and run the product gate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",