@norskvideo/ctl-dev-kit 0.1.16 → 0.1.18
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/flake.nix
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
description = "norsk-ctl product build + dev shell";
|
|
21
21
|
|
|
22
22
|
inputs = {
|
|
23
|
-
nixpkgs.url = "github:NixOS/nixpkgs/d233902339c02a9c334e7e593de68855ad26c4cb"; # nixpkgs-unstable 2026-05-15, bun 1.3.13
|
|
23
|
+
nixpkgs.url = "github:NixOS/nixpkgs/d233902339c02a9c334e7e593de68855ad26c4cb"; # nixpkgs-unstable 2026-05-15, bun 1.3.13, chromium 148.0.7778.167
|
|
24
24
|
# Biome must match the version pinned in package.json (@biomejs/biome 2.5.5)
|
|
25
25
|
# exactly: 2.4.x can't parse this repo's biome.json (`rules.preset` is a
|
|
26
26
|
# 2.5 key) and any other 2.5.x emits a version-mismatch info that breaks the
|
|
@@ -92,22 +92,49 @@
|
|
|
92
92
|
pkgs.git
|
|
93
93
|
pkgs.ffmpeg # integration suites spawn a local ffmpeg (e.g. SRT egress capture)
|
|
94
94
|
];
|
|
95
|
+
|
|
96
|
+
# Playwright doc-guide browser. nix-pinned headless Chromium needs a
|
|
97
|
+
# real X display on a truly headless box (page.fill silently no-ops on
|
|
98
|
+
# plain HTML inputs; some pages crash the renderer). Ubuntu's apt Xvfb
|
|
99
|
+
# is too old for Chromium 148's input dispatch — the nix xorg-server
|
|
100
|
+
# works. A product's scripts/with-display.sh wraps playwright in
|
|
101
|
+
# xvfb-run iff DISPLAY is unset.
|
|
102
|
+
linuxBrowser = pkgs.lib.optionals pkgs.stdenv.isLinux [
|
|
103
|
+
pkgs.chromium
|
|
104
|
+
pkgs.xvfb-run
|
|
105
|
+
pkgs.xorg.xorgserver
|
|
106
|
+
];
|
|
107
|
+
# Headless Chromium finds no system fonts on NixOS, so text renders at
|
|
108
|
+
# zero advance width — invisible, and every toBeVisible() on text times
|
|
109
|
+
# out. Point fontconfig at a minimal bundled set so glyphs have real
|
|
110
|
+
# metrics.
|
|
111
|
+
fontsConf = pkgs.makeFontsConf {
|
|
112
|
+
fontDirectories = [ pkgs.dejavu_fonts pkgs.liberation_ttf ];
|
|
113
|
+
};
|
|
114
|
+
browserHook = pkgs.lib.optionalString pkgs.stdenv.isLinux ''
|
|
115
|
+
export BROWSER_FOR_TESTING=${pkgs.chromium}/bin/chromium
|
|
116
|
+
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
|
117
|
+
export FONTCONFIG_FILE=${fontsConf}
|
|
118
|
+
'';
|
|
95
119
|
in {
|
|
96
120
|
# Everyday dev shell — build tools + the pinned norsk-ctl daemon.
|
|
97
121
|
# `nix develop` and `nix develop .#dev` are equivalent.
|
|
98
122
|
default = pkgs.mkShell {
|
|
99
123
|
BIOME_BINARY = "${biome}/bin/biome";
|
|
100
|
-
buildInputs = buildTools ++ [ (mkCtl system) ];
|
|
124
|
+
buildInputs = buildTools ++ linuxBrowser ++ [ (mkCtl system) ];
|
|
125
|
+
shellHook = browserHook;
|
|
101
126
|
};
|
|
102
127
|
dev = pkgs.mkShell {
|
|
103
128
|
BIOME_BINARY = "${biome}/bin/biome";
|
|
104
|
-
buildInputs = buildTools ++ [ (mkCtl system) ];
|
|
129
|
+
buildInputs = buildTools ++ linuxBrowser ++ [ (mkCtl system) ];
|
|
130
|
+
shellHook = browserHook;
|
|
105
131
|
};
|
|
106
132
|
# Lean shell for CI (build-image / integration) — no ctl fetch, so
|
|
107
133
|
# image builds never depend on the ctl binary channel.
|
|
108
134
|
build = pkgs.mkShell {
|
|
109
135
|
BIOME_BINARY = "${biome}/bin/biome";
|
|
110
|
-
buildInputs = buildTools;
|
|
136
|
+
buildInputs = buildTools ++ linuxBrowser;
|
|
137
|
+
shellHook = browserHook;
|
|
111
138
|
};
|
|
112
139
|
});
|
|
113
140
|
};
|
|
@@ -211,6 +211,7 @@ export interface CanonicalBytes {
|
|
|
211
211
|
core: string;
|
|
212
212
|
flake: string;
|
|
213
213
|
upgradeLatest: string;
|
|
214
|
+
syncDevKit: string;
|
|
214
215
|
checks: string;
|
|
215
216
|
biome: string;
|
|
216
217
|
tsconfigBase: string;
|
|
@@ -336,6 +337,22 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes): DriftRe
|
|
|
336
337
|
);
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
// sync-dev-kit.yml is OPTIONAL for the same reason as upgrade-latest.yml — a
|
|
341
|
+
// product may not carry it yet — but when present it is the shared,
|
|
342
|
+
// single-sourced workflow that keeps this repo's dev-kit pin fresh, and must
|
|
343
|
+
// not diverge. Only its `product:` dispatch key is per-repo.
|
|
344
|
+
const syncPath = join(repoRoot, ".github", "workflows", "sync-dev-kit.yml");
|
|
345
|
+
if (existsSync(syncPath)) {
|
|
346
|
+
push(
|
|
347
|
+
workflowProblem(
|
|
348
|
+
".github/workflows/sync-dev-kit.yml",
|
|
349
|
+
"conventions/sync-dev-kit.yml",
|
|
350
|
+
readFileSync(syncPath, "utf8"),
|
|
351
|
+
canonical.syncDevKit,
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
339
356
|
return { ok: problems.length === 0, problems };
|
|
340
357
|
}
|
|
341
358
|
|
|
@@ -345,6 +362,7 @@ if (import.meta.main) {
|
|
|
345
362
|
core: readFileSync(join(import.meta.dir, "CLAUDE.core.md"), "utf8"),
|
|
346
363
|
flake: readFileSync(join(import.meta.dir, "..", "build", "flake.nix"), "utf8"),
|
|
347
364
|
upgradeLatest: readFileSync(join(import.meta.dir, "upgrade-latest.yml"), "utf8"),
|
|
365
|
+
syncDevKit: readFileSync(join(import.meta.dir, "sync-dev-kit.yml"), "utf8"),
|
|
348
366
|
checks: readFileSync(join(import.meta.dir, "checks.yml"), "utf8"),
|
|
349
367
|
biome: readFileSync(join(import.meta.dir, "biome.base.json"), "utf8"),
|
|
350
368
|
tsconfigBase: readFileSync(join(import.meta.dir, "tsconfig.base.json"), "utf8"),
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Keep this product's shared conventions fresh against @norskvideo/ctl-dev-kit.
|
|
2
|
+
# The dev-kit single-sources the files every ctl product would otherwise
|
|
3
|
+
# copy-and-drift (the fenced CLAUDE.md core, flake.nix, the biome/tsconfig/dprint
|
|
4
|
+
# bases, the shared workflows, the build bootstrap). check:drift fails CI when a
|
|
5
|
+
# copy diverges from the *installed* dev-kit — so a product goes stale two ways:
|
|
6
|
+
# its pin lags the newest published dev-kit, and once the pin bumps its copies
|
|
7
|
+
# need re-syncing. This workflow closes the first gap and surfaces the second.
|
|
8
|
+
#
|
|
9
|
+
# It bumps the @norskvideo/ctl-dev-kit pin to the newest published version,
|
|
10
|
+
# reinstalls, and runs this product's own gate (check:drift + lint + typecheck +
|
|
11
|
+
# unit tests) against the new bytes:
|
|
12
|
+
#
|
|
13
|
+
# - gate green -> a clean freshness bump; commit straight to main, like
|
|
14
|
+
# upgrade-latest.yml. The common case: a dev-kit release that doesn't touch
|
|
15
|
+
# this product's copies (or only verbatim ones already reconciled).
|
|
16
|
+
# - gate red -> the new dev-kit needs a human: a drifted copy to re-sync, or
|
|
17
|
+
# a convention change that breaks this product. Push a branch and open a PR;
|
|
18
|
+
# check:drift's own output names each drifted file and the first diffing line.
|
|
19
|
+
# Re-sync on the branch until green, then merge.
|
|
20
|
+
#
|
|
21
|
+
# The re-sync is deliberately NOT automated. check:drift already emits the exact
|
|
22
|
+
# fix, the masked / marker-block copies (the flake ctl pin, the CLAUDE core, the
|
|
23
|
+
# dprint and gitignore blocks) need judgement, and a bot that blindly rewrote
|
|
24
|
+
# them could commit a subtly-wrong splice. Detection plus a ready branch is the
|
|
25
|
+
# leverage; the fix stays human.
|
|
26
|
+
#
|
|
27
|
+
# Single-sourced in @norskvideo/ctl-dev-kit (conventions/sync-dev-kit.yml) and
|
|
28
|
+
# copied verbatim into each product repo; the check:drift gate fails CI if a copy
|
|
29
|
+
# diverges. Only the `product:` value below is per-repo (set it to this repo's
|
|
30
|
+
# dashboard key); everything else must match the dev-kit source. Never hand-edit
|
|
31
|
+
# the copy -- edit the dev-kit source and re-sync.
|
|
32
|
+
name: sync-dev-kit
|
|
33
|
+
|
|
34
|
+
on:
|
|
35
|
+
schedule:
|
|
36
|
+
- cron: "37 4 * * *"
|
|
37
|
+
workflow_dispatch:
|
|
38
|
+
|
|
39
|
+
permissions:
|
|
40
|
+
contents: write
|
|
41
|
+
pull-requests: write
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
sync:
|
|
45
|
+
runs-on: x64
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v5
|
|
48
|
+
|
|
49
|
+
- name: Resolve the newest published dev-kit and bump the pin
|
|
50
|
+
id: sync
|
|
51
|
+
run: |
|
|
52
|
+
set -euo pipefail
|
|
53
|
+
reg="https://registry.npmjs.org"
|
|
54
|
+
pkg="@norskvideo/ctl-dev-kit"
|
|
55
|
+
enc="@norskvideo%2Fctl-dev-kit"
|
|
56
|
+
|
|
57
|
+
# The runner has no npm; read the dist-tag off the registry with curl+jq
|
|
58
|
+
# (same as upgrade-latest.yml resolves the Norsk nightly).
|
|
59
|
+
new="$(curl -fsSL "$reg/$enc" | jq -r '."dist-tags".latest')"
|
|
60
|
+
[ -n "$new" ] && [ "$new" != "null" ] || { echo "could not resolve $pkg latest"; exit 1; }
|
|
61
|
+
|
|
62
|
+
# The current pin, whatever its range operator. A repo with no dev-kit
|
|
63
|
+
# dep has nothing to sync.
|
|
64
|
+
old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" package.json | head -1 | sed -E 's/.*"([^"]*)"$/\1/')"
|
|
65
|
+
if [ -z "$old" ]; then
|
|
66
|
+
echo "$pkg is not pinned in package.json -- nothing to sync."
|
|
67
|
+
echo "changed=0" >> "$GITHUB_OUTPUT"
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
old_bare="${old#^}"; old_bare="${old_bare#~}"
|
|
71
|
+
echo "dev-kit: $old -> ^$new"
|
|
72
|
+
if [ "$old_bare" = "$new" ]; then
|
|
73
|
+
echo "already on the newest dev-kit ($new)."
|
|
74
|
+
echo "changed=0" >> "$GITHUB_OUTPUT"
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
# Surgical single-line rewrite so package.json keeps its formatting
|
|
78
|
+
# (a jq reserialize would reflow the whole file and trip the formatter).
|
|
79
|
+
sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" package.json
|
|
80
|
+
echo "changed=1" >> "$GITHUB_OUTPUT"
|
|
81
|
+
echo "old=$old_bare" >> "$GITHUB_OUTPUT"
|
|
82
|
+
echo "new=$new" >> "$GITHUB_OUTPUT"
|
|
83
|
+
|
|
84
|
+
- name: Reinstall and run the product gate against the new dev-kit
|
|
85
|
+
id: gate
|
|
86
|
+
if: steps.sync.outputs.changed == '1'
|
|
87
|
+
continue-on-error: true
|
|
88
|
+
run: |
|
|
89
|
+
nix develop .#build --command bash -c '
|
|
90
|
+
set -euo pipefail
|
|
91
|
+
bun install
|
|
92
|
+
bun run check:drift
|
|
93
|
+
bun run lint
|
|
94
|
+
bun run typecheck
|
|
95
|
+
bun run test:unit
|
|
96
|
+
'
|
|
97
|
+
|
|
98
|
+
- name: Land the freshness bump on main (gate green)
|
|
99
|
+
if: steps.sync.outputs.changed == '1' && steps.gate.outcome == 'success'
|
|
100
|
+
run: |
|
|
101
|
+
set -euo pipefail
|
|
102
|
+
git config user.name "github-actions[bot]"
|
|
103
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
104
|
+
git add -A
|
|
105
|
+
git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}
|
|
106
|
+
|
|
107
|
+
@norskvideo/ctl-dev-kit pin bumped to the newest published version;
|
|
108
|
+
check:drift + lint + typecheck + unit tests green against the new bytes.
|
|
109
|
+
Lockfile regenerated."
|
|
110
|
+
# A GITHUB_TOKEN push does not retrigger checks (Actions' recursion
|
|
111
|
+
# guard) -- fine, the gate above already ran them. Rebase-and-retry so a
|
|
112
|
+
# commit landing mid-run does not red the sync; a real conflict aborts.
|
|
113
|
+
for attempt in 1 2 3 4 5; do
|
|
114
|
+
if git push origin HEAD:main; then exit 0; fi
|
|
115
|
+
echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
|
|
116
|
+
git fetch origin main
|
|
117
|
+
git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
|
|
118
|
+
done
|
|
119
|
+
echo "push still failing after 5 rebase attempts"; exit 1
|
|
120
|
+
|
|
121
|
+
- name: Open a reconciliation PR (gate red)
|
|
122
|
+
if: steps.sync.outputs.changed == '1' && steps.gate.outcome == 'failure'
|
|
123
|
+
env:
|
|
124
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
125
|
+
run: |
|
|
126
|
+
set -euo pipefail
|
|
127
|
+
branch="sync-dev-kit/${{ steps.sync.outputs.new }}"
|
|
128
|
+
git config user.name "github-actions[bot]"
|
|
129
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
130
|
+
git checkout -b "$branch"
|
|
131
|
+
git add -A
|
|
132
|
+
git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }} (needs reconciliation)"
|
|
133
|
+
git push -f origin "$branch"
|
|
134
|
+
title="Sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}"
|
|
135
|
+
body="$(printf '%s\n' \
|
|
136
|
+
"Automated dev-kit sync: the @norskvideo/ctl-dev-kit pin moved to the newest" \
|
|
137
|
+
"published version, but this product's gate is **red** against the new bytes," \
|
|
138
|
+
"so the sync needs a human." \
|
|
139
|
+
"" \
|
|
140
|
+
"Run the gate locally to see what to fix:" \
|
|
141
|
+
"" \
|
|
142
|
+
" bun install" \
|
|
143
|
+
" bun run check:drift # names each drifted copy and the first diffing line" \
|
|
144
|
+
" bun run lint && bun run typecheck && bun run test:unit" \
|
|
145
|
+
"" \
|
|
146
|
+
"Re-sync the named copies from node_modules/@norskvideo/ctl-dev-kit (never" \
|
|
147
|
+
"hand-edit against memory), or resolve the convention change this pin" \
|
|
148
|
+
"introduced, pushing to this branch until green -- then merge.")"
|
|
149
|
+
if existing="$(gh pr list --head "$branch" --state open --json number -q '.[0].number')" && [ -n "$existing" ]; then
|
|
150
|
+
gh pr edit "$existing" --title "$title" --body "$body"
|
|
151
|
+
else
|
|
152
|
+
gh pr create --base main --head "$branch" --title "$title" --body "$body"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
- name: Report "already fresh"
|
|
156
|
+
if: steps.sync.outputs.changed != '1'
|
|
157
|
+
run: echo "dev-kit pin already current -- nothing to sync."
|
|
158
|
+
|
|
159
|
+
# Report the sync result to the aggregated product CI dashboard
|
|
160
|
+
# (id3as/ci-workflows). always() so a failed sync shows a sad pony. A gate-red
|
|
161
|
+
# run that opened a PR is still a successful sync -- the red lives on the PR's
|
|
162
|
+
# own checks run, not here.
|
|
163
|
+
notify:
|
|
164
|
+
needs: sync
|
|
165
|
+
if: always()
|
|
166
|
+
runs-on: x64
|
|
167
|
+
steps:
|
|
168
|
+
- uses: actions/checkout@v5
|
|
169
|
+
- id: meta
|
|
170
|
+
run: |
|
|
171
|
+
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
|
|
172
|
+
echo "state=failure" >> "$GITHUB_OUTPUT"
|
|
173
|
+
else
|
|
174
|
+
echo "state=success" >> "$GITHUB_OUTPUT"
|
|
175
|
+
fi
|
|
176
|
+
- uses: ./.github/actions/ci-status-dispatch
|
|
177
|
+
with:
|
|
178
|
+
token: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
179
|
+
product: __PRODUCT__
|
|
180
|
+
pipeline: dev-kit
|
|
181
|
+
status: ${{ steps.meta.outputs.state }}
|
package/create-product/canon.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Canon {
|
|
|
17
17
|
flake: string;
|
|
18
18
|
flakeLock: string;
|
|
19
19
|
upgradeLatest: string;
|
|
20
|
+
syncDevKit: string;
|
|
20
21
|
checks: string;
|
|
21
22
|
biome: string;
|
|
22
23
|
tsconfigBase: string;
|
|
@@ -32,6 +33,7 @@ export function loadCanon(): Canon {
|
|
|
32
33
|
flake: readFileSync(join(buildDir, "flake.nix"), "utf8"),
|
|
33
34
|
flakeLock: readFileSync(join(buildDir, "flake.lock"), "utf8"),
|
|
34
35
|
upgradeLatest: readFileSync(join(conventionsDir, "upgrade-latest.yml"), "utf8"),
|
|
36
|
+
syncDevKit: readFileSync(join(conventionsDir, "sync-dev-kit.yml"), "utf8"),
|
|
35
37
|
checks: readFileSync(join(conventionsDir, "checks.yml"), "utf8"),
|
|
36
38
|
biome: readFileSync(join(conventionsDir, "biome.base.json"), "utf8"),
|
|
37
39
|
tsconfigBase: readFileSync(join(conventionsDir, "tsconfig.base.json"), "utf8"),
|
|
@@ -110,6 +110,7 @@ function conventionFiles(ctx: ShapeContext, shape: ShapeModule): GeneratedFile[]
|
|
|
110
110
|
},
|
|
111
111
|
{ path: ".github/workflows/checks.yml", content: fillProduct(canon.checks) },
|
|
112
112
|
{ path: ".github/workflows/upgrade-latest.yml", content: fillProduct(canon.upgradeLatest) },
|
|
113
|
+
{ path: ".github/workflows/sync-dev-kit.yml", content: fillProduct(canon.syncDevKit) },
|
|
113
114
|
{ path: ".github/actions/ci-status-dispatch/action.yml", content: loadAsset("ci-status-dispatch.yml") },
|
|
114
115
|
{ path: "manifest.seed.json", content: seedJson(ctx) },
|
|
115
116
|
{ path: "deployment/build-image.sh", content: buildImageWrapper(ctx), executable: true },
|