@norskvideo/ctl-dev-kit 0.2.44 → 0.2.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.
|
@@ -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
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
#
|
|
136
|
-
|
|
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,14 @@ jobs:
|
|
|
148
156
|
run: |
|
|
149
157
|
nix develop .#build --command bash -c '
|
|
150
158
|
set -euo pipefail
|
|
151
|
-
|
|
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 everywhere: bun install keeps a
|
|
162
|
+
# locked version that still satisfies its range, so a nested
|
|
163
|
+
# workspace could otherwise stay on an old dev-kit under a new pin.
|
|
164
|
+
for lock in $(git ls-files "*bun.lock"); do
|
|
165
|
+
(cd "$(dirname "$lock")" && bun install && bun update @norskvideo/ctl-dev-kit)
|
|
166
|
+
done
|
|
152
167
|
'
|
|
153
168
|
|
|
154
169
|
- name: Re-sync the copies from canonical and run the product gate
|
package/docs/path-existence.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// walk and nothing is ignored.
|
|
14
14
|
import { readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
15
15
|
import { basename, dirname, join, posix } from "node:path";
|
|
16
|
+
import { BEGIN_MARKER, END_MARKER } from "./splice-generated.ts";
|
|
16
17
|
|
|
17
18
|
export interface PathFinding {
|
|
18
19
|
file: string;
|
|
@@ -39,6 +40,9 @@ export const PATH_ALLOW = "<!-- path-allow -->";
|
|
|
39
40
|
// conventions, not claims about this repo's tree.
|
|
40
41
|
const CORE_BEGIN = "<!-- BEGIN ctl-shared-conventions v1 -->";
|
|
41
42
|
const CORE_END = "<!-- END ctl-shared-conventions v1 -->";
|
|
43
|
+
// A spliced generated block (splice-generated.ts) is a tool's output, gated
|
|
44
|
+
// against that tool; its paths name the tool's world (an export bundle's
|
|
45
|
+
// `template/workflow.yml`), not this repo's tree.
|
|
42
46
|
|
|
43
47
|
// The five prefixes the review names (its `@norskvideo/` widened to every
|
|
44
48
|
// scoped npm name), plus the classes they imply: any absolute path (container
|
|
@@ -207,6 +211,7 @@ export function checkPathExistence(
|
|
|
207
211
|
const lines = readFileSync(join(repoRoot, file), "utf8").split("\n");
|
|
208
212
|
let inFence = false;
|
|
209
213
|
let inCore = false;
|
|
214
|
+
let inGenerated = false;
|
|
210
215
|
lines.forEach((line, index) => {
|
|
211
216
|
if (/^\s*(```|~~~)/.test(line)) {
|
|
212
217
|
inFence = !inFence;
|
|
@@ -214,7 +219,9 @@ export function checkPathExistence(
|
|
|
214
219
|
}
|
|
215
220
|
if (line.includes(CORE_BEGIN)) inCore = true;
|
|
216
221
|
if (line.includes(CORE_END)) inCore = false;
|
|
217
|
-
if (
|
|
222
|
+
if (line.startsWith(BEGIN_MARKER)) inGenerated = true;
|
|
223
|
+
if (line.trim() === END_MARKER) inGenerated = false;
|
|
224
|
+
if (inFence || inCore || inGenerated || line.includes(PATH_ALLOW)) return;
|
|
218
225
|
for (const token of extractPathTokens(line)) {
|
|
219
226
|
if (isSkipped(token, skipPrefixes)) continue;
|
|
220
227
|
if (!token.includes("/")) {
|