@norskvideo/ctl-dev-kit 0.2.42 → 0.2.44
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/build/ctl-shell.nix +110 -0
- package/build/flake.nix +17 -109
- package/build/flake.sdk-app.nix +15 -22
- package/build/refresh-ctl-pin.sh +1 -1
- package/conventions/CLAUDE.core.md +1 -1
- package/conventions/build-image.yml +6 -0
- package/conventions/check-drift.ts +29 -26
- package/conventions/checks.sdk-app.yml +1 -1
- package/conventions/checks.yml +1 -1
- package/conventions/publish-docs.yml +6 -0
- package/conventions/sync-dev-kit.yml +2 -2
- package/conventions/sync-drift.ts +15 -8
- package/create-product/canon.ts +3 -0
- package/create-product/create-product.ts +1 -0
- package/local-dev/README.md +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# The shared pieces of every ctl product's dev shell. Single-sourced in
|
|
2
|
+
# @norskvideo/ctl-dev-kit (build/ctl-shell.nix), copied verbatim to
|
|
3
|
+
# nix/ctl-shell.nix in each product repo and drift-gated; norsk-ctl's own flake
|
|
4
|
+
# imports this file in place, so ctl and every product get the same browser.
|
|
5
|
+
# A product's flake.nix is its own: it imports this and adds what it alone needs.
|
|
6
|
+
#
|
|
7
|
+
# A plain function rather than a flake input: a flake only sees git-tracked
|
|
8
|
+
# files, so it cannot reach into node_modules, and a private-repo input would
|
|
9
|
+
# need GitHub auth in every shell and CI runner.
|
|
10
|
+
{ pkgs, biome }:
|
|
11
|
+
let
|
|
12
|
+
# Playwright doc-guide browser. nix-pinned headless Chromium needs a real X
|
|
13
|
+
# display on a truly headless box (page.fill silently no-ops on plain HTML
|
|
14
|
+
# inputs; some pages crash the renderer). Ubuntu's apt Xvfb is too old for
|
|
15
|
+
# Chromium 148's input dispatch — the nix xorg-server works. A product's
|
|
16
|
+
# scripts/with-display.sh wraps playwright in xvfb-run iff DISPLAY is unset.
|
|
17
|
+
browserInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [
|
|
18
|
+
pkgs.chromium
|
|
19
|
+
pkgs.xvfb-run
|
|
20
|
+
pkgs.xorg.xorgserver
|
|
21
|
+
];
|
|
22
|
+
# Headless Chromium finds no system fonts on NixOS, so text renders at zero
|
|
23
|
+
# advance width — invisible, and every toBeVisible() on text times out. Point
|
|
24
|
+
# fontconfig at a minimal bundled set so glyphs have real metrics.
|
|
25
|
+
fontsConf = pkgs.makeFontsConf {
|
|
26
|
+
fontDirectories = [ pkgs.dejavu_fonts pkgs.liberation_ttf ];
|
|
27
|
+
};
|
|
28
|
+
browserHook = pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
|
29
|
+
export BROWSER_FOR_TESTING=${pkgs.chromium}/bin/chromium
|
|
30
|
+
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
|
31
|
+
export FONTCONFIG_FILE=${fontsConf}
|
|
32
|
+
'' + pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
|
|
33
|
+
# nixpkgs has no darwin chromium, so thread the ambient Chrome
|
|
34
|
+
# through the same variable everything already honours.
|
|
35
|
+
macos_chrome="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
36
|
+
if [ -z "''${BROWSER_FOR_TESTING:-}" ] && [ -x "$macos_chrome" ]; then
|
|
37
|
+
export BROWSER_FOR_TESTING="$macos_chrome"
|
|
38
|
+
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
|
39
|
+
fi
|
|
40
|
+
unset macos_chrome
|
|
41
|
+
'';
|
|
42
|
+
|
|
43
|
+
buildTools = [
|
|
44
|
+
biome
|
|
45
|
+
pkgs.bun
|
|
46
|
+
pkgs.nodejs # @biomejs/biome's bin is a JS wrapper, not the binary — it is
|
|
47
|
+
# what reads BIOME_BINARY and execs the nix build. Something has
|
|
48
|
+
# to *parse* that wrapper first, so a modern node has to be on
|
|
49
|
+
# PATH or the host's is used: on an older distro node (Ubuntu
|
|
50
|
+
# noble ships v12) `bun run lint` dies on the wrapper's optional
|
|
51
|
+
# chaining before biome ever starts.
|
|
52
|
+
pkgs.cargo # builds the native product-signer addon (probe/native)
|
|
53
|
+
pkgs.rustc
|
|
54
|
+
pkgs.git
|
|
55
|
+
pkgs.ffmpeg # integration suites spawn a local ffmpeg (e.g. SRT egress capture)
|
|
56
|
+
pkgs.imagemagick # doc-guide manual assembly shells out to `magick` to embed captures
|
|
57
|
+
pkgs.gh # publish-docs uploads the built manual via `gh release`; runners have no system gh
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
# The released norsk-ctl daemon, pinned by version + per-platform hash in the
|
|
61
|
+
# repo's own flake.nix (the nightly bumps it there). It is NOT built from
|
|
62
|
+
# source: a product dev consumes the shipped ctl exactly as a customer does.
|
|
63
|
+
mkCtl = { version, asset, base ? "https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl" }:
|
|
64
|
+
pkgs.stdenv.mkDerivation {
|
|
65
|
+
pname = "norsk-ctl";
|
|
66
|
+
inherit version;
|
|
67
|
+
src = pkgs.fetchurl {
|
|
68
|
+
url = "${base}/${version}/norsk-ctl-${version}-${asset.plat}";
|
|
69
|
+
hash = asset.hash;
|
|
70
|
+
};
|
|
71
|
+
dontUnpack = true;
|
|
72
|
+
# bun single-file executables embed a trailing payload; stripping
|
|
73
|
+
# corrupts them, and on Linux the raw binary needs its interpreter +
|
|
74
|
+
# libstdc++ rpath patched to the nix store.
|
|
75
|
+
dontStrip = true;
|
|
76
|
+
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.autoPatchelfHook ];
|
|
77
|
+
buildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.stdenv.cc.cc.lib ];
|
|
78
|
+
installPhase = ''
|
|
79
|
+
runHook preInstall
|
|
80
|
+
install -Dm755 "$src" "$out/bin/norsk-ctl"
|
|
81
|
+
runHook postInstall
|
|
82
|
+
'';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
# Biome from its own pin so `bun run lint` works on NixOS: the node_modules
|
|
86
|
+
# @biomejs/biome binary is glibc-linked and can't run there. Each shell
|
|
87
|
+
# exports BIOME_BINARY so the npm wrapper execs this nix build instead.
|
|
88
|
+
shell = buildInputs: shellHook: pkgs.mkShell {
|
|
89
|
+
BIOME_BINARY = "${biome}/bin/biome";
|
|
90
|
+
inherit buildInputs shellHook;
|
|
91
|
+
};
|
|
92
|
+
in
|
|
93
|
+
{
|
|
94
|
+
inherit browserInputs browserHook buildTools mkCtl shell;
|
|
95
|
+
|
|
96
|
+
# The Studio-product shells:
|
|
97
|
+
# default / dev -> build tools + the pinned norsk-ctl daemon on PATH
|
|
98
|
+
# build -> build tools only; what CI (build-image / integration)
|
|
99
|
+
# uses, so image builds never depend on the ctl channel.
|
|
100
|
+
mkProductShells = { ctl, extraInputs ? [ ], extraShellHook ? "" }:
|
|
101
|
+
let
|
|
102
|
+
inputs = buildTools ++ browserInputs ++ extraInputs;
|
|
103
|
+
hook = browserHook + extraShellHook;
|
|
104
|
+
in
|
|
105
|
+
{
|
|
106
|
+
default = shell (inputs ++ [ ctl ]) hook;
|
|
107
|
+
dev = shell (inputs ++ [ ctl ]) hook;
|
|
108
|
+
build = shell inputs hook;
|
|
109
|
+
};
|
|
110
|
+
}
|
package/build/flake.nix
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
# the
|
|
1
|
+
# This product's build + dev shells. The file is the product's own: add what
|
|
2
|
+
# this product alone needs through mkProductShells' extraInputs /
|
|
3
|
+
# extraShellHook. The shared pieces (build tools, the test browser, biome, the
|
|
4
|
+
# norsk-ctl fetch) come from nix/ctl-shell.nix — a verbatim, drift-gated copy of
|
|
5
|
+
# @norskvideo/ctl-dev-kit build/ctl-shell.nix; edit that in the dev-kit, never
|
|
6
|
+
# the copy. The dev-kit's build/flake.nix is the starter a new product begins
|
|
7
|
+
# from.
|
|
6
8
|
#
|
|
7
9
|
# Three shells:
|
|
8
|
-
# nix develop -> build tools
|
|
9
|
-
# nix develop .#dev
|
|
10
|
-
# shell; the two are equivalent.
|
|
10
|
+
# nix develop -> build tools + the pinned `norsk-ctl` daemon on PATH.
|
|
11
|
+
# nix develop .#dev The everyday product dev shell; the two are equivalent.
|
|
11
12
|
# nix develop .#build -> build tools only, no ctl. What CI (build-image /
|
|
12
13
|
# integration) uses, so image builds never depend on the
|
|
13
14
|
# ctl binary channel.
|
|
14
15
|
#
|
|
15
|
-
#
|
|
16
|
-
# by version+hash (reproducible). It is NOT built from source here — a product
|
|
17
|
-
# dev consumes the shipped ctl exactly as a customer does. Bump with:
|
|
16
|
+
# The norsk-ctl pin below is bumped nightly by upgrade-latest; by hand with
|
|
18
17
|
# packages/dev-kit/build/refresh-ctl-pin.sh (writes ctlVersion + hashes)
|
|
19
18
|
{
|
|
20
19
|
description = "norsk-ctl product build + dev shell";
|
|
@@ -37,7 +36,6 @@
|
|
|
37
36
|
# --- norsk-ctl channel pin -------------------------------------------
|
|
38
37
|
# The released daemon, one build per platform. Bump these together.
|
|
39
38
|
ctlVersion = "0.1.0-2026-09-15-81bbde8";
|
|
40
|
-
ctlBase = "https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl";
|
|
41
39
|
ctlAsset = {
|
|
42
40
|
"x86_64-linux" = { plat = "linux-x64"; hash = "sha256-6Zm1iLs29o89tav+TEDfLLdq2E+blku7DSxZVAqEwF0="; };
|
|
43
41
|
"aarch64-linux" = { plat = "linux-arm64"; hash = "sha256-ziH0YFLc8j7+xuYWArCU1JzWgq3NsFvfq67FqjlxdI8="; };
|
|
@@ -45,108 +43,18 @@
|
|
|
45
43
|
"x86_64-darwin" = { plat = "darwin-x64"; hash = "sha256-GzlT0Rg9t8sAG6e7Deu+DzSbfSxcr+WpVmyOgit2Q4g="; };
|
|
46
44
|
};
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
in
|
|
53
|
-
pkgs.stdenv.mkDerivation {
|
|
54
|
-
pname = "norsk-ctl";
|
|
55
|
-
version = ctlVersion;
|
|
56
|
-
src = pkgs.fetchurl {
|
|
57
|
-
url = "${ctlBase}/${ctlVersion}/norsk-ctl-${ctlVersion}-${asset.plat}";
|
|
58
|
-
hash = asset.hash;
|
|
59
|
-
};
|
|
60
|
-
dontUnpack = true;
|
|
61
|
-
# bun single-file executables embed a trailing payload; stripping
|
|
62
|
-
# corrupts them, and on Linux the raw binary needs its interpreter +
|
|
63
|
-
# libstdc++ rpath patched to the nix store.
|
|
64
|
-
dontStrip = true;
|
|
65
|
-
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.autoPatchelfHook ];
|
|
66
|
-
buildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.stdenv.cc.cc.lib ];
|
|
67
|
-
installPhase = ''
|
|
68
|
-
runHook preInstall
|
|
69
|
-
install -Dm755 "$src" "$out/bin/norsk-ctl"
|
|
70
|
-
runHook postInstall
|
|
71
|
-
'';
|
|
72
|
-
};
|
|
46
|
+
ctlShell = system: import ./nix/ctl-shell.nix {
|
|
47
|
+
pkgs = import nixpkgs { inherit system; };
|
|
48
|
+
biome = (import nixpkgs-biome { inherit system; }).biome;
|
|
49
|
+
};
|
|
73
50
|
in {
|
|
74
51
|
packages = forAllSystems (system: {
|
|
75
|
-
norsk-ctl = mkCtl system;
|
|
52
|
+
norsk-ctl = (ctlShell system).mkCtl { version = ctlVersion; asset = ctlAsset.${system}; };
|
|
76
53
|
});
|
|
77
54
|
|
|
78
55
|
devShells = forAllSystems (system:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Biome from its own pin so `bun run lint` works on NixOS: the
|
|
82
|
-
# node_modules @biomejs/biome binary is glibc-linked and can't run
|
|
83
|
-
# here. Each shell exports BIOME_BINARY so the npm wrapper execs this
|
|
84
|
-
# nix build instead -- no patchelf, no per-command env var. Keep the
|
|
85
|
-
# pin in step with package.json's @biomejs/biome version.
|
|
86
|
-
biome = (import nixpkgs-biome { inherit system; }).biome;
|
|
87
|
-
buildTools = [
|
|
88
|
-
biome
|
|
89
|
-
pkgs.bun
|
|
90
|
-
pkgs.nodejs # @biomejs/biome's bin is a JS wrapper, not the binary —
|
|
91
|
-
# it is what reads BIOME_BINARY below and execs the nix
|
|
92
|
-
# build. Something has to *parse* that wrapper first, so a
|
|
93
|
-
# modern node has to be on PATH or the host's is used: on
|
|
94
|
-
# an older distro node (Ubuntu noble ships v12) `bun run
|
|
95
|
-
# lint` dies on the wrapper's optional chaining before
|
|
96
|
-
# biome ever starts. norsk-ctl carries nodejs for kubb and
|
|
97
|
-
# so never hit this; the split-out products had nothing
|
|
98
|
-
# putting node in the shell.
|
|
99
|
-
pkgs.cargo # builds the native product-signer addon (probe/native)
|
|
100
|
-
pkgs.rustc
|
|
101
|
-
pkgs.git
|
|
102
|
-
pkgs.ffmpeg # integration suites spawn a local ffmpeg (e.g. SRT egress capture)
|
|
103
|
-
pkgs.imagemagick # doc-guide manual assembly shells out to `magick` to embed captures
|
|
104
|
-
pkgs.gh # publish-docs uploads the built manual via `gh release`; runners have no system gh
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
# Playwright doc-guide browser. nix-pinned headless Chromium needs a
|
|
108
|
-
# real X display on a truly headless box (page.fill silently no-ops on
|
|
109
|
-
# plain HTML inputs; some pages crash the renderer). Ubuntu's apt Xvfb
|
|
110
|
-
# is too old for Chromium 148's input dispatch — the nix xorg-server
|
|
111
|
-
# works. A product's scripts/with-display.sh wraps playwright in
|
|
112
|
-
# xvfb-run iff DISPLAY is unset.
|
|
113
|
-
linuxBrowser = pkgs.lib.optionals pkgs.stdenv.isLinux [
|
|
114
|
-
pkgs.chromium
|
|
115
|
-
pkgs.xvfb-run
|
|
116
|
-
pkgs.xorg.xorgserver
|
|
117
|
-
];
|
|
118
|
-
# Headless Chromium finds no system fonts on NixOS, so text renders at
|
|
119
|
-
# zero advance width — invisible, and every toBeVisible() on text times
|
|
120
|
-
# out. Point fontconfig at a minimal bundled set so glyphs have real
|
|
121
|
-
# metrics.
|
|
122
|
-
fontsConf = pkgs.makeFontsConf {
|
|
123
|
-
fontDirectories = [ pkgs.dejavu_fonts pkgs.liberation_ttf ];
|
|
124
|
-
};
|
|
125
|
-
browserHook = pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
|
126
|
-
export BROWSER_FOR_TESTING=${pkgs.chromium}/bin/chromium
|
|
127
|
-
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
|
128
|
-
export FONTCONFIG_FILE=${fontsConf}
|
|
129
|
-
'';
|
|
130
|
-
in {
|
|
131
|
-
# Everyday dev shell — build tools + the pinned norsk-ctl daemon.
|
|
132
|
-
# `nix develop` and `nix develop .#dev` are equivalent.
|
|
133
|
-
default = pkgs.mkShell {
|
|
134
|
-
BIOME_BINARY = "${biome}/bin/biome";
|
|
135
|
-
buildInputs = buildTools ++ linuxBrowser ++ [ (mkCtl system) ];
|
|
136
|
-
shellHook = browserHook;
|
|
137
|
-
};
|
|
138
|
-
dev = pkgs.mkShell {
|
|
139
|
-
BIOME_BINARY = "${biome}/bin/biome";
|
|
140
|
-
buildInputs = buildTools ++ linuxBrowser ++ [ (mkCtl system) ];
|
|
141
|
-
shellHook = browserHook;
|
|
142
|
-
};
|
|
143
|
-
# Lean shell for CI (build-image / integration) — no ctl fetch, so
|
|
144
|
-
# image builds never depend on the ctl binary channel.
|
|
145
|
-
build = pkgs.mkShell {
|
|
146
|
-
BIOME_BINARY = "${biome}/bin/biome";
|
|
147
|
-
buildInputs = buildTools ++ linuxBrowser;
|
|
148
|
-
shellHook = browserHook;
|
|
149
|
-
};
|
|
56
|
+
(ctlShell system).mkProductShells {
|
|
57
|
+
ctl = self.packages.${system}.norsk-ctl;
|
|
150
58
|
});
|
|
151
59
|
};
|
|
152
60
|
}
|
package/build/flake.sdk-app.nix
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
# Dev shell for a ctl "sdk-app" product — a bespoke Norsk SDK application
|
|
3
3
|
# (its runtime is its own daemon, not a composed Studio workflow launched by
|
|
4
|
-
# norsk-ctl).
|
|
5
|
-
#
|
|
6
|
-
# the
|
|
7
|
-
#
|
|
8
|
-
# `"ctlProduct": { "shape": "sdk-app" }`.
|
|
4
|
+
# norsk-ctl). The file is the product's own; the dev-kit's
|
|
5
|
+
# build/flake.sdk-app.nix is the starter a new sdk-app begins from. The shared
|
|
6
|
+
# pieces (the test browser, biome) come from nix/ctl-shell.nix — a verbatim,
|
|
7
|
+
# drift-gated copy of @norskvideo/ctl-dev-kit build/ctl-shell.nix.
|
|
9
8
|
#
|
|
10
|
-
# Unlike the Studio-product flake
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# runtime).
|
|
9
|
+
# Unlike the Studio-product flake this does NOT fetch the norsk-ctl daemon —
|
|
10
|
+
# an sdk-app's dev loop is its own SDK app plus a Norsk engine, not a
|
|
11
|
+
# ctl-launched product container. Decision: bun for tooling (install / build /
|
|
12
|
+
# test), Node for the shipped runtime (the SDK's blessed runtime).
|
|
15
13
|
#
|
|
16
14
|
# nixpkgs is pinned to the same rev the Studio flake uses, so bun matches the
|
|
17
15
|
# fleet's 1.3.13.
|
|
@@ -39,25 +37,20 @@
|
|
|
39
37
|
config.allowUnfree = true;
|
|
40
38
|
};
|
|
41
39
|
biome = (import nixpkgs-biome { inherit system; }).biome;
|
|
40
|
+
ctlShell = import ./nix/ctl-shell.nix { inherit pkgs biome; };
|
|
42
41
|
ffmpegFull = pkgs.ffmpeg-full;
|
|
43
42
|
tools = [
|
|
44
|
-
biome # `bun run lint` execs this via BIOME_BINARY
|
|
43
|
+
biome # `bun run lint` execs this via BIOME_BINARY
|
|
45
44
|
pkgs.bun # tooling: install / build / test (fleet parity, dev-kit)
|
|
46
45
|
pkgs.nodejs # the shipped runtime (bin/run -> node lib/index.js); also
|
|
47
46
|
# parses @biomejs/biome's JS wrapper that reads BIOME_BINARY
|
|
48
47
|
pkgs.git
|
|
49
48
|
ffmpegFull # integration suites spawn a local ffmpeg (e.g. SRT egress capture)
|
|
50
|
-
] ++
|
|
51
|
-
shell =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
export NIXPKGS_ALLOW_UNFREE=1
|
|
56
|
-
export FFMPEG_FULL=${ffmpegFull}
|
|
57
|
-
'' + pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
|
58
|
-
export BROWSER_FOR_TESTING=${pkgs.chromium}/bin/chromium
|
|
59
|
-
'';
|
|
60
|
-
};
|
|
49
|
+
] ++ ctlShell.browserInputs;
|
|
50
|
+
shell = ctlShell.shell tools (''
|
|
51
|
+
export NIXPKGS_ALLOW_UNFREE=1
|
|
52
|
+
export FFMPEG_FULL=${ffmpegFull}
|
|
53
|
+
'' + ctlShell.browserHook);
|
|
61
54
|
in {
|
|
62
55
|
# One shell under every name the canonical workflows and the Studio
|
|
63
56
|
# flake use: `build` is what CI enters (`nix develop .#build`). An
|
package/build/refresh-ctl-pin.sh
CHANGED
|
@@ -36,4 +36,4 @@ done
|
|
|
36
36
|
|
|
37
37
|
perl -0pi -e "s{(ctlVersion = \")[^\"]*(\")}{\${1}$ver\${2}}" "$flake"
|
|
38
38
|
echo "updated $flake"
|
|
39
|
-
echo "
|
|
39
|
+
echo "this is the starter's pin; each product's own flake.nix is bumped nightly by upgrade-latest"
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
stale until this loop re-runs; don't debug a ghost.
|
|
82
82
|
- **The shared CI workflow is single-sourced too.** `.github/workflows/upgrade-latest.yml`
|
|
83
83
|
is copied verbatim from `@norskvideo/ctl-dev-kit` (`conventions/upgrade-latest.yml`),
|
|
84
|
-
same as this core block and `
|
|
84
|
+
same as this core block and `nix/ctl-shell.nix`; `check:drift` fails CI if a copy
|
|
85
85
|
diverges. Only the `product:` key is per-repo. Never hand-edit the copy — edit
|
|
86
86
|
the dev-kit source and re-sync.
|
|
87
87
|
- **A config-driven workflow needs a parity-checked invariant contract.** If this
|
|
@@ -289,6 +289,12 @@ jobs:
|
|
|
289
289
|
gate-guides:
|
|
290
290
|
needs: label
|
|
291
291
|
runs-on: x64
|
|
292
|
+
# One guide run per repo at a time, nightly and publish gate alike: both
|
|
293
|
+
# launch the same instance ids on the x64 pool's shared docker daemon, and
|
|
294
|
+
# ctl names each compose project after its instance id. Queue, never cancel.
|
|
295
|
+
concurrency:
|
|
296
|
+
group: doc-guides-${{ github.repository }}
|
|
297
|
+
cancel-in-progress: false
|
|
292
298
|
outputs:
|
|
293
299
|
guides: ${{ steps.plan.outputs.guides }}
|
|
294
300
|
reason: ${{ steps.plan.outputs.reason }}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Workstream I drift gate. Fails a product repo whose forced copies of the
|
|
2
2
|
// shared conventions have diverged from this dev-kit's canonical source. Each
|
|
3
3
|
// gated file uses the loosest mechanism that still pins what is shared:
|
|
4
|
-
// - byte-verbatim: CLAUDE.md fenced core,
|
|
4
|
+
// - byte-verbatim: CLAUDE.md fenced core, nix/ctl-shell.nix, biome.json,
|
|
5
5
|
// tsconfig.base.json, the .gitignore core block, scripts/demo (only a
|
|
6
6
|
// product with tests/demo.spec.ts is asked for it)
|
|
7
7
|
// - masked per-repo line: checks.yml + upgrade-latest.yml (`product:` key)
|
|
8
8
|
// - base + sanctioned extension block: dprint.json (repo-specific excludes)
|
|
9
|
+
// - must import the shared module: flake.nix (otherwise the product's own)
|
|
9
10
|
// - structural: root tsconfig.json (shape-dependent includes stay free),
|
|
10
11
|
// deployment/build-image.sh (per-product wrapper around a verbatim
|
|
11
12
|
// bootstrap), manifest.seed.json (per-product pins, SDK-schema shape)
|
|
@@ -103,19 +104,19 @@ function workflowProblem(
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
// The dev-only ctl pin the nightly bumps in each repo's flake: the version
|
|
106
|
-
// string and the four per-platform source hashes.
|
|
107
|
-
//
|
|
108
|
-
// a shared convention. Mask it so the gate compares flake STRUCTURE, not the pin
|
|
109
|
-
// — same idea as masking upgrade-latest's product: line.
|
|
107
|
+
// string and the four per-platform source hashes. It lives in the product's own
|
|
108
|
+
// flake.nix; sync-drift carries it across when it migrates a legacy flake.
|
|
110
109
|
export const CTL_VERSION_LINE = /ctlVersion = "[^"]*";/;
|
|
111
110
|
export const CTL_HASH_LINE = /hash = "sha256-[^"]*";/g;
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
// The shared dev-shell module and how a product's flake.nix must reach it. The
|
|
113
|
+
// flake is otherwise the product's own, so a product can add what it alone
|
|
114
|
+
// needs; building from the module is what keeps the shared pieces shared.
|
|
115
|
+
export const CTL_SHELL_PATH = "nix/ctl-shell.nix";
|
|
116
|
+
export const CTL_SHELL_IMPORT = "import ./nix/ctl-shell.nix";
|
|
117
|
+
|
|
118
|
+
export function importsCtlShell(flake: string): boolean {
|
|
119
|
+
return flake.includes(CTL_SHELL_IMPORT);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// dprint.json is canonical-plus-extension: the shared config is verbatim, but a
|
|
@@ -250,7 +251,13 @@ function coreProblem(claude: string, canonicalCore: string): string | undefined
|
|
|
250
251
|
|
|
251
252
|
export interface CanonicalBytes {
|
|
252
253
|
core: string;
|
|
254
|
+
/** build/flake.nix — the starter flake a Studio product begins from, and what
|
|
255
|
+
* sync-drift migrates a legacy verbatim flake to. Not gated: the flake is the
|
|
256
|
+
* product's own. */
|
|
253
257
|
flake: string;
|
|
258
|
+
/** build/ctl-shell.nix — the shared dev-shell module, gated verbatim at
|
|
259
|
+
* nix/ctl-shell.nix for every shape. */
|
|
260
|
+
ctlShell: string;
|
|
254
261
|
upgradeLatest: string;
|
|
255
262
|
syncDevKit: string;
|
|
256
263
|
/** conventions/sync-ctl-packages.yml — the ctl-* pin freshness PR. No per-repo
|
|
@@ -281,10 +288,10 @@ export interface CanonicalBytes {
|
|
|
281
288
|
* hand copy is how build-docs.yml came to fail in both turnkeys, which never
|
|
282
289
|
* made one. */
|
|
283
290
|
withDisplay: string;
|
|
284
|
-
/** build/flake.sdk-app.nix — the flake
|
|
291
|
+
/** build/flake.sdk-app.nix — the starter flake for a `shape: "sdk-app"`
|
|
285
292
|
* product (bun tooling + node runtime, no norsk-ctl daemon fetch). Optional so
|
|
286
|
-
* a studio-only caller
|
|
287
|
-
*
|
|
293
|
+
* a studio-only caller need not supply it; sync-drift needs it to migrate a
|
|
294
|
+
* legacy sdk-app flake. */
|
|
288
295
|
flakeSdkApp?: string;
|
|
289
296
|
/** conventions/checks.sdk-app.yml — the checks.yml variant for an sdk-app: the
|
|
290
297
|
* Studio licence step removed (it reads shared/src/version.ts +
|
|
@@ -530,21 +537,16 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
|
|
|
530
537
|
push(coreProblem(readFileSync(claudePath, "utf8"), canonical.core));
|
|
531
538
|
}
|
|
532
539
|
|
|
533
|
-
|
|
534
|
-
// the sdk-app flake does not. Both are gated the same way (ctl pins masked; the
|
|
535
|
-
// sdk-app flake carries none, so it is effectively byte-verbatim).
|
|
540
|
+
push(verbatimProblem(repoRoot, CTL_SHELL_PATH, "build/ctl-shell.nix", canonical.ctlShell));
|
|
536
541
|
const flakePath = join(repoRoot, "flake.nix");
|
|
537
|
-
|
|
538
|
-
const flakeRef = shape === "sdk-app" ? "build/flake.sdk-app.nix" : "build/flake.nix";
|
|
539
|
-
if (flakeCanon === undefined) {
|
|
540
|
-
throw new Error(`checkDrift: shape "${shape}" requires canonical.${shape === "sdk-app" ? "flakeSdkApp" : "flake"}`);
|
|
541
|
-
} else if (!existsSync(flakePath)) {
|
|
542
|
+
if (!existsSync(flakePath)) {
|
|
542
543
|
problems.push(
|
|
543
|
-
`flake.nix not found at repo root (${flakePath}).
|
|
544
|
+
`flake.nix not found at repo root (${flakePath}). Start from @norskvideo/ctl-dev-kit ${shape === "sdk-app" ? "build/flake.sdk-app.nix" : "build/flake.nix"}.`,
|
|
545
|
+
);
|
|
546
|
+
} else if (!importsCtlShell(readFileSync(flakePath, "utf8"))) {
|
|
547
|
+
problems.push(
|
|
548
|
+
`flake.nix does not build its shells from ${CTL_SHELL_PATH} (no \`${CTL_SHELL_IMPORT}\`). The shared shell pieces are single-sourced there; sync-drift migrates a legacy verbatim flake.`,
|
|
544
549
|
);
|
|
545
|
-
} else {
|
|
546
|
-
const problem = flakeProblem(readFileSync(flakePath, "utf8"), flakeCanon, flakeRef);
|
|
547
|
-
if (problem) problems.push(problem);
|
|
548
550
|
}
|
|
549
551
|
push(verbatimProblem(repoRoot, "biome.json", "conventions/biome.base.json", canonical.biome));
|
|
550
552
|
push(verbatimProblem(repoRoot, "tsconfig.base.json", "conventions/tsconfig.base.json", canonical.tsconfigBase));
|
|
@@ -810,6 +812,7 @@ if (import.meta.main) {
|
|
|
810
812
|
const canonical: CanonicalBytes = {
|
|
811
813
|
core: readFileSync(join(import.meta.dir, "CLAUDE.core.md"), "utf8"),
|
|
812
814
|
flake: readFileSync(join(import.meta.dir, "..", "build", "flake.nix"), "utf8"),
|
|
815
|
+
ctlShell: readFileSync(join(import.meta.dir, "..", "build", "ctl-shell.nix"), "utf8"),
|
|
813
816
|
upgradeLatest: readFileSync(join(import.meta.dir, "upgrade-latest.yml"), "utf8"),
|
|
814
817
|
syncDevKit: readFileSync(join(import.meta.dir, "sync-dev-kit.yml"), "utf8"),
|
|
815
818
|
syncCtlPackages: readFileSync(join(import.meta.dir, "sync-ctl-packages.yml"), "utf8"),
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# Two jobs:
|
|
9
9
|
# - drift: the RFC 0001 Workstream I shared-conventions drift-check. Fails if
|
|
10
10
|
# this repo's forced copies have diverged from the pinned dev-kit — the
|
|
11
|
-
# fenced CLAUDE.md core,
|
|
11
|
+
# fenced CLAUDE.md core, nix/ctl-shell.nix, the shared config files, the manifest
|
|
12
12
|
# seed. Copies exist so a human reads them on GitHub with no tooling; the
|
|
13
13
|
# gate is the only thing keeping them true.
|
|
14
14
|
# - quality: the repo's own lint + typecheck + unit tests + docs path lint,
|
package/conventions/checks.yml
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Two jobs:
|
|
5
5
|
# - drift: the RFC 0001 Workstream I shared-conventions drift-check. Fails if
|
|
6
6
|
# this repo's forced copies have diverged from the pinned dev-kit — the
|
|
7
|
-
# fenced CLAUDE.md core,
|
|
7
|
+
# fenced CLAUDE.md core, nix/ctl-shell.nix, the shared config files, the manifest
|
|
8
8
|
# seed. Copies exist so a human reads them on GitHub with no tooling; the
|
|
9
9
|
# gate is the only thing keeping them true.
|
|
10
10
|
# - quality: the repo's own lint + typecheck + unit tests + docs path lint,
|
|
@@ -46,6 +46,12 @@ concurrency:
|
|
|
46
46
|
jobs:
|
|
47
47
|
publish:
|
|
48
48
|
runs-on: x64
|
|
49
|
+
# One guide run per repo at a time, nightly and publish gate alike: both
|
|
50
|
+
# launch the same instance ids on the x64 pool's shared docker daemon, and
|
|
51
|
+
# ctl names each compose project after its instance id. Queue, never cancel.
|
|
52
|
+
concurrency:
|
|
53
|
+
group: doc-guides-${{ github.repository }}
|
|
54
|
+
cancel-in-progress: false
|
|
49
55
|
steps:
|
|
50
56
|
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
51
57
|
# out of the workspace for every later step in this job, and clears whatever
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Keep this product's shared conventions fresh against @norskvideo/ctl-dev-kit.
|
|
2
2
|
# The dev-kit single-sources the files every ctl product would otherwise
|
|
3
|
-
# copy-and-drift (the fenced CLAUDE.md core,
|
|
3
|
+
# copy-and-drift (the fenced CLAUDE.md core, nix/ctl-shell.nix, the biome/tsconfig/dprint
|
|
4
4
|
# bases, the shared workflows, the build bootstrap). check:drift fails CI when a
|
|
5
5
|
# copy diverges from the *installed* dev-kit — so a product goes stale two ways:
|
|
6
6
|
# its pin lags the newest published dev-kit, and once the pin bumps its copies
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
#
|
|
20
20
|
# sync-drift re-derives each copy from the canonical (never a second opinion that
|
|
21
21
|
# can disagree), preserving the per-repo bits the gate masks -- the product: keys,
|
|
22
|
-
# the
|
|
22
|
+
# the product's own flake.nix, the dprint/gitignore/CLAUDE repo content. The gate runs AFTER
|
|
23
23
|
# it, so a bad rewrite fails check:drift and a breaking convention fails the build,
|
|
24
24
|
# rather than landing silently. Structural per-repo files it never touches.
|
|
25
25
|
#
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// judgement anywhere:
|
|
9
9
|
// - verbatim (biome.json, tsconfig.base.json) -> overwrite with canonical
|
|
10
10
|
// - masked workflows (checks / upgrade / sync) -> canonical, repo's product: key kept
|
|
11
|
-
// -
|
|
11
|
+
// - nix/ctl-shell.nix -> overwrite with canonical (created if absent)
|
|
12
|
+
// - flake.nix (legacy verbatim copy only) -> the starter, repo's floating ctl pin kept
|
|
12
13
|
// - marker-block (CLAUDE core, dprint, .gitignore) -> replace the shared block, keep the rest
|
|
13
14
|
// Structural per-repo files (root tsconfig.json, deployment/build-image.sh,
|
|
14
15
|
// manifest.seed.json) have NO canonical bytes to apply — the gate checks their
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
BEGIN,
|
|
26
27
|
type CanonicalBytes,
|
|
27
28
|
CTL_HASH_LINE,
|
|
29
|
+
CTL_SHELL_PATH,
|
|
28
30
|
CTL_VERSION_LINE,
|
|
29
31
|
carriesIntegrationMarker,
|
|
30
32
|
DOCS_SITE_DIR,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
GITIGNORE_BEGIN,
|
|
33
35
|
GITIGNORE_END,
|
|
34
36
|
hasDocsSite,
|
|
37
|
+
importsCtlShell,
|
|
35
38
|
PRODUCT_LINE,
|
|
36
39
|
PRODUCT_SENTINEL,
|
|
37
40
|
type ProductShape,
|
|
@@ -149,21 +152,22 @@ function syncIntegration(repoRoot: string, canonical: string, r: SyncReport): vo
|
|
|
149
152
|
writeIfChanged(path, out, rel, r.written);
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
// flake.nix is
|
|
153
|
-
//
|
|
154
|
-
// the
|
|
155
|
-
// is replaced
|
|
156
|
-
//
|
|
157
|
-
function syncFlake(repoRoot: string,
|
|
155
|
+
// flake.nix is the product's own once it builds from nix/ctl-shell.nix, so sync
|
|
156
|
+
// leaves it alone. One that does not can only be a verbatim dev-kit copy from
|
|
157
|
+
// before the shared pieces moved into the module (the gate allowed no product
|
|
158
|
+
// edits then), so it is replaced by the starter with the repo's floating ctl pin
|
|
159
|
+
// (ctlVersion + the four per-platform hashes) carried across in order.
|
|
160
|
+
function syncFlake(repoRoot: string, starter: string, r: SyncReport): void {
|
|
158
161
|
const path = join(repoRoot, "flake.nix");
|
|
159
162
|
if (!existsSync(path)) {
|
|
160
163
|
r.skipped.push("flake.nix (absent)");
|
|
161
164
|
return;
|
|
162
165
|
}
|
|
163
166
|
const existing = readFileSync(path, "utf8");
|
|
167
|
+
if (importsCtlShell(existing)) return;
|
|
164
168
|
const version = existing.match(CTL_VERSION_LINE)?.[0];
|
|
165
169
|
const hashes = existing.match(CTL_HASH_LINE) ?? [];
|
|
166
|
-
let out = version ?
|
|
170
|
+
let out = version ? starter.replace(CTL_VERSION_LINE, version) : starter;
|
|
167
171
|
let i = 0;
|
|
168
172
|
out = out.replace(CTL_HASH_LINE, (m) => hashes[i++] ?? m);
|
|
169
173
|
writeIfChanged(path, out, "flake.nix", r.written);
|
|
@@ -336,6 +340,8 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes, shape: Pr
|
|
|
336
340
|
writeIfChanged(join(repoRoot, buildDocsRel), canonical.buildDocs, buildDocsRel, r.written);
|
|
337
341
|
}
|
|
338
342
|
|
|
343
|
+
mkdirSync(dirname(join(repoRoot, CTL_SHELL_PATH)), { recursive: true });
|
|
344
|
+
writeIfChanged(join(repoRoot, CTL_SHELL_PATH), canonical.ctlShell, CTL_SHELL_PATH, r.written);
|
|
339
345
|
syncFlake(repoRoot, flakeCanon, r);
|
|
340
346
|
syncClaude(repoRoot, canonical.core, r);
|
|
341
347
|
syncGitignore(repoRoot, canonical.gitignoreCore, r);
|
|
@@ -353,6 +359,7 @@ if (import.meta.main) {
|
|
|
353
359
|
const canonical: CanonicalBytes = {
|
|
354
360
|
core: readFileSync(join(dir, "CLAUDE.core.md"), "utf8"),
|
|
355
361
|
flake: readFileSync(join(dir, "..", "build", "flake.nix"), "utf8"),
|
|
362
|
+
ctlShell: readFileSync(join(dir, "..", "build", "ctl-shell.nix"), "utf8"),
|
|
356
363
|
upgradeLatest: readFileSync(join(dir, "upgrade-latest.yml"), "utf8"),
|
|
357
364
|
syncDevKit: readFileSync(join(dir, "sync-dev-kit.yml"), "utf8"),
|
|
358
365
|
syncCtlPackages: readFileSync(join(dir, "sync-ctl-packages.yml"), "utf8"),
|
package/create-product/canon.ts
CHANGED
|
@@ -15,6 +15,8 @@ const buildDir = join(import.meta.dir, "..", "build");
|
|
|
15
15
|
export interface Canon {
|
|
16
16
|
core: string;
|
|
17
17
|
flake: string;
|
|
18
|
+
/** build/ctl-shell.nix, emitted as nix/ctl-shell.nix — the flake imports it. */
|
|
19
|
+
ctlShell: string;
|
|
18
20
|
flakeLock: string;
|
|
19
21
|
upgradeLatest: string;
|
|
20
22
|
syncDevKit: string;
|
|
@@ -41,6 +43,7 @@ export function loadCanon(): Canon {
|
|
|
41
43
|
return {
|
|
42
44
|
core: readFileSync(join(conventionsDir, "CLAUDE.core.md"), "utf8"),
|
|
43
45
|
flake: readFileSync(join(buildDir, "flake.nix"), "utf8"),
|
|
46
|
+
ctlShell: readFileSync(join(buildDir, "ctl-shell.nix"), "utf8"),
|
|
44
47
|
flakeLock: readFileSync(join(buildDir, "flake.lock"), "utf8"),
|
|
45
48
|
upgradeLatest: readFileSync(join(conventionsDir, "upgrade-latest.yml"), "utf8"),
|
|
46
49
|
syncDevKit: readFileSync(join(conventionsDir, "sync-dev-kit.yml"), "utf8"),
|
|
@@ -203,6 +203,7 @@ function conventionFiles(ctx: ShapeContext, shape: ShapeModule): GeneratedFile[]
|
|
|
203
203
|
return [
|
|
204
204
|
{ path: "CLAUDE.md", content: `${shape.claudeHead(ctx)}\n${canon.core}\n${shape.claudeTail(ctx)}` },
|
|
205
205
|
{ path: "flake.nix", content: canon.flake },
|
|
206
|
+
{ path: "nix/ctl-shell.nix", content: canon.ctlShell },
|
|
206
207
|
{ path: "flake.lock", content: canon.flakeLock },
|
|
207
208
|
{ path: "biome.json", content: canon.biome },
|
|
208
209
|
{ path: "tsconfig.base.json", content: canon.tsconfigBase },
|
package/local-dev/README.md
CHANGED
|
@@ -9,7 +9,8 @@ test. This is the whole point of the split — you clone the product and go.
|
|
|
9
9
|
|
|
10
10
|
`norsk-ctl` (the daemon + CLI that launches product instances) is **not** an npm
|
|
11
11
|
dependency; it is the released binary, pinned by version+hash in the repo's
|
|
12
|
-
`flake.nix` (
|
|
12
|
+
`flake.nix` (the shared shell pieces come from `nix/ctl-shell.nix`, single-sourced
|
|
13
|
+
from `@norskvideo/ctl-dev-kit`). The dev shell puts
|
|
13
14
|
it on `PATH`:
|
|
14
15
|
|
|
15
16
|
```sh
|