@norskvideo/ctl-dev-kit 0.2.64 → 0.2.66

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.
@@ -1,404 +0,0 @@
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, nix/ctl-shell.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, re-syncs this product's forced copies from the new canonical
11
- # (sync-drift), runs the gate (check:drift + lint + typecheck + unit tests), and
12
- # ALWAYS opens (or updates) a PR on sync-dev-kit/<version> -- never a push to
13
- # main. bot-prs merges it once checks, smoke, integration and the guides gate
14
- # are green on its head. The gate's verdict heads the PR body: red means the new
15
- # dev-kit genuinely does not fit this product (a convention that fails
16
- # lint/typecheck/tests, or a copy sync-drift could not converge), and adopting
17
- # it is real engineering on the branch, not a copy-paste.
18
- #
19
- # Why a PR even when the gate is green: a new dev-kit changes the copied
20
- # workflows themselves, and those are exactly what the gate does not run. On
21
- # 2026-10-09 a newer copied build-image.yml, never run against oracle-reasoning's
22
- # set-up, broke it on main after a green gate landed it (ci-workflows
23
- # docs/dashboard/ci-fragility.md, proposal B). On the PR, build-image's build
24
- # and guides run against the new copy before main has it.
25
- #
26
- # sync-drift re-derives each copy from the canonical (never a second opinion that
27
- # can disagree), preserving the per-repo bits the gate masks -- the product: keys,
28
- # the product's own flake.nix, the dprint/gitignore/CLAUDE repo content. The gate runs AFTER
29
- # it, so a bad rewrite fails check:drift and a breaking convention fails the build,
30
- # rather than landing silently. Structural per-repo files it never touches.
31
- #
32
- # Single-sourced in @norskvideo/ctl-dev-kit (conventions/sync-dev-kit.yml) and
33
- # copied verbatim into each product repo; the check:drift gate fails CI if a copy
34
- # diverges. Only the `product:` value below is per-repo (set it to this repo's
35
- # dashboard key); everything else must match the dev-kit source. Never hand-edit
36
- # the copy -- edit the dev-kit source and re-sync.
37
- name: sync-dev-kit
38
-
39
- on:
40
- schedule:
41
- - cron: "37 4 * * *"
42
- workflow_dispatch:
43
-
44
- permissions:
45
- contents: write
46
- pull-requests: write
47
-
48
- jobs:
49
- sync:
50
- runs-on: x64
51
- steps:
52
- # Runs BEFORE checkout, and does two things: points the harness's temp base
53
- # out of the workspace for every later step in this job, and clears whatever
54
- # the old repo-local default left behind on this runner. The body carries the
55
- # why. It fails the job only when legacy litter survives -- which would take
56
- # the checkout down a step later anyway, but report nothing useful.
57
- - name: Route test-temp out of the workspace (pre-checkout)
58
- run: |
59
- set -uo pipefail
60
- # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
61
- # HOST daemon has not seen before is created by dockerd, as ROOT, on the
62
- # host -- even when the launch passes --container-user (reproduced
63
- # 2026-09-04). The harness's default base is repo-local for an OrbStack
64
- # inotify constraint that binds macOS dev and nothing here, so every run
65
- # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
66
- # cannot remove. That took commentary's checks AND integration red on
67
- # 2026-09-03. Use the sanctioned override, per-run and outside the
68
- # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
69
- base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
70
- mkdir -p "$base"
71
- echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
72
- # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
73
- # EACCESes. They cannot wedge a checkout, but they do fill the disk.
74
- for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
75
- docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
76
- done
77
- # LEGACY: a runner that ran the old default still carries a workspace
78
- # test-temp, and a checkout with clean:true dies on it. Reap the holders
79
- # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
80
- # RESTARTING leaked container re-creates the path as root seconds after
81
- # any clean. Match on mount SOURCE, not a label: the holders are untracked
82
- # by construction, because a killed run labels nothing.
83
- tt="$GITHUB_WORKSPACE/test-temp"
84
- [ -d "$tt" ] || exit 0
85
- stuck=""
86
- for c in $(docker ps -aq 2>/dev/null || true); do
87
- if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
88
- stuck="$stuck $c"
89
- fi
90
- done
91
- if [ -n "$stuck" ]; then
92
- echo "reaping containers holding mounts under $tt:$stuck"
93
- docker rm -f $stuck || true
94
- fi
95
- # The DIRECTORY, not just its contents: an empty but root-owned base
96
- # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
97
- # container unlink the leaf.
98
- docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
99
- sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
100
- # Fail HERE if it survived. The checkout fails on it either way, but
101
- # reports only an EACCES rmdir with no clue what was holding the path.
102
- if [ -e "$tt" ]; then
103
- echo "::error::$tt survived the pre-checkout clean"
104
- ls -lan "$tt" || true
105
- exit 1
106
- fi
107
-
108
- - uses: actions/checkout@v5
109
- with:
110
- token: ${{ secrets.CI_DISPATCH_TOKEN }}
111
- clean: true
112
-
113
- - name: Resolve the newest published dev-kit and bump the pin
114
- id: sync
115
- run: |
116
- set -euo pipefail
117
- reg="https://registry.npmjs.org"
118
- pkg="@norskvideo/ctl-dev-kit"
119
- enc="@norskvideo%2Fctl-dev-kit"
120
-
121
- # The runner has no npm; read the dist-tag off the registry with curl+jq
122
- # (same as upgrade-latest.yml resolves the Norsk nightly).
123
- new="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/$enc" | jq -r '."dist-tags".latest')"
124
- [ -n "$new" ] && [ "$new" != "null" ] || { echo "could not resolve $pkg latest"; exit 1; }
125
-
126
- # Every tracked package.json, not just the root. A nested workspace can
127
- # pin the dev-kit too, and a root-only rewrite left it behind: playout's
128
- # tests/ sat on 0.1.x after the root moved to 0.2, ran a guide proxy
129
- # that built the wrong Studio prefix, and first-light failed every
130
- # night from 2026-09-07. Same walk as sync-ctl-packages.yml.
131
- mapfile -t manifests < <(git ls-files '*package.json' ':!:**/node_modules/**')
132
- moved=""
133
- for m in "${manifests[@]}"; do
134
- old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$m" | head -1 | sed -E 's/.*"([^"]*)"$/\1/' || true)"
135
- # Absent here, or a workspace protocol spec that names no version.
136
- case "$old" in "" | workspace:*) continue ;; esac
137
- old_bare="${old#^}"; old_bare="${old_bare#~}"
138
- [ "$old_bare" != "$new" ] || continue
139
- # Surgical single-line rewrite so the file keeps its formatting
140
- # (a jq reserialize would reflow it and trip the formatter).
141
- sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" "$m"
142
- echo "dev-kit: $m $old -> ^$new"
143
- moved="$moved $old_bare"
144
- done
145
- if [ -z "$moved" ]; then
146
- echo "every dev-kit pin is already on the newest dev-kit ($new)."
147
- echo "changed=0" >> "$GITHUB_OUTPUT"
148
- exit 0
149
- fi
150
- # `old` names the lowest pin moved, for the commit message and PR title.
151
- old_bare="$(printf '%s\n' $moved | sort -V | head -1)"
152
- echo "changed=1" >> "$GITHUB_OUTPUT"
153
- echo "old=$old_bare" >> "$GITHUB_OUTPUT"
154
- echo "new=$new" >> "$GITHUB_OUTPUT"
155
-
156
- - name: Reinstall against the new pin
157
- if: steps.sync.outputs.changed == '1'
158
- # NOT continue-on-error, unlike the gate below. An install that dies
159
- # part-way (the runner OOM-killed sync-ctl-packages' one on 2026-10-03)
160
- # would otherwise read as a red gate and open a reconciliation PR around
161
- # a stale lockfile. Fail here instead, before anything is committed.
162
- run: |
163
- nix develop .#build --command bash -c '
164
- set -euo pipefail
165
- # Per install root, as sync-ctl-packages.yml does: a manifest outside
166
- # the root workspace owns its own lockfile. Then move the locked
167
- # dev-kit to the newest in range: bun install keeps a locked version
168
- # that still satisfies its range, so a nested workspace could
169
- # otherwise stay on an old dev-kit under a new pin. Only where the
170
- # lockfile already holds the dev-kit: bun update ADDS a package it
171
- # does not find, and put the dev-kit into funke proxy/ui that way.
172
- for lock in $(git ls-files "*bun.lock"); do
173
- (cd "$(dirname "$lock")" && bun install)
174
- if grep -q "\"@norskvideo/ctl-dev-kit@" "$lock"; then
175
- (cd "$(dirname "$lock")" && bun update @norskvideo/ctl-dev-kit)
176
- fi
177
- done
178
- '
179
-
180
- - name: Re-sync the copies from canonical and run the product gate
181
- id: gate
182
- if: steps.sync.outputs.changed == '1'
183
- continue-on-error: true
184
- run: |
185
- nix develop .#build --command bash -c '
186
- set -euo pipefail
187
- bun run node_modules/@norskvideo/ctl-dev-kit/conventions/sync-drift.ts
188
- bun run check:drift
189
- bun run lint
190
- bun run typecheck
191
- bun run test:unit
192
- '
193
-
194
- # The bump's only way to main is this PR. bot-prs merges it once every tier
195
- # that runs on a bot PR -- checks, smoke, integration and build-image's
196
- # guides gate, wherever this repo has them -- is green on its head
197
- # (ci-workflows docs/dashboard/ci-fragility.md, proposal B). Landing on a
198
- # green gate kept lint, typecheck and unit failures off main, but the tiers
199
- # that break on an upstream change ran only after landing, so a bad
200
- # upstream release still turned main red in every repo that took it. The
201
- # gate above still runs: its verdict heads the PR body, and a red one
202
- # shows on the PR's own checks too. Never a draft: bot-prs reads red from
203
- # the tiers, and a draft could never become ready again by PATCH.
204
- - name: Open (or update) the bump PR
205
- if: steps.sync.outputs.changed == '1'
206
- env:
207
- API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
208
- GATE: ${{ steps.gate.outcome }}
209
- run: |
210
- set -euo pipefail
211
- branch="sync-dev-kit/${{ steps.sync.outputs.new }}"
212
- prefix="sync-dev-kit/"
213
- title="Sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}"
214
- repo="${{ github.repository }}"; owner="${repo%%/*}"
215
- api="https://api.github.com/repos/$repo/pulls"
216
- # An older PR of this bot's family is obsolete once this one is open:
217
- # left open they piled up (nine were closed by hand on 2026-10-03).
218
- # Best-effort: a failure here must not fail the bump.
219
- close_superseded() {
220
- local keep="$1" n
221
- auth=(-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
222
- for n in $(curl -sS "${auth[@]}" "$api?state=open&per_page=100" \
223
- | jq -r --arg repo "$repo" --arg prefix "$prefix" --argjson keep "$keep" \
224
- '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith($prefix)) and .number != $keep) | .number'); do
225
- curl -sS -o /dev/null "${auth[@]}" -X POST "https://api.github.com/repos/$repo/issues/$n/comments" \
226
- -d "$(jq -nc --arg b "Superseded by #$keep, a later run of this bot." '{body:$b}')"
227
- curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/$n" -d '{"state":"closed"}'
228
- echo "closed superseded PR #$n"
229
- done
230
- }
231
- git config user.name "github-actions[bot]"
232
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
233
- git checkout -B "$branch"
234
- git add -A
235
- git commit -m "chore: sync dev-kit ${{ steps.sync.outputs.old }} -> ${{ steps.sync.outputs.new }}
236
-
237
- @norskvideo/ctl-dev-kit pin bumped to the newest published version and the
238
- forced copies re-synced from the new canonical (sync-drift). Lockfile
239
- regenerated."
240
- # One branch per bump, force-pushed from today's main -- but only when
241
- # its tree changed. A red bump PR left standing is rebuilt by every run
242
- # of this bot, and pushing the same tree again would re-run every slow
243
- # tier on it for nothing.
244
- if git fetch -q origin "$branch" 2>/dev/null && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse 'FETCH_HEAD^{tree}')" ]; then
245
- echo "$branch already holds this bump; not re-pushing it"
246
- else
247
- git push -f origin "$branch"
248
- fi
249
- if [ "$GATE" = "success" ]; then
250
- verdict="Gate green: check:drift, lint, typecheck and unit tests pass against the new dev-kit."
251
- else
252
- verdict="**Gate red**: the new dev-kit does not fit this product yet. Run the gate to see which part: \`bun install\`, then \`bun run check:drift\` (a copy sync-drift could not converge) and \`bun run lint && bun run typecheck && bun run test:unit\` (a convention that breaks real code). Fix it on this branch until green."
253
- fi
254
- body="$(printf '%s\n' \
255
- "Automated dev-kit sync: the @norskvideo/ctl-dev-kit pin moved to the newest published version and sync-drift re-synced the forced copies from the new canonical." \
256
- "" \
257
- "$verdict" \
258
- "" \
259
- "bot-prs merges this once checks, smoke, integration and the guides gate" \
260
- "(build-image) have all passed on its head, wherever this repo runs them." \
261
- "Until then main does not move; a red tier keeps it open and reports it on" \
262
- "the product dashboard.")"
263
- # gh is not on the self-hosted runner PATH (it lives only inside
264
- # `nix develop`, and this step runs outside it), so create/update the PR
265
- # straight against the REST API with the same PAT that pushed the branch.
266
- # The PAT (not GITHUB_TOKEN) is what lets the PR's own workflows run,
267
- # which is the whole point: the bump is judged before it lands.
268
- curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
269
- existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
270
- if [ -n "$existing" ]; then
271
- curl "${curl_api[@]}" -X PATCH "$api/$existing" \
272
- --data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" > /dev/null
273
- number="$existing"
274
- echo "updated PR #$number"
275
- else
276
- number="$(curl "${curl_api[@]}" -X POST "$api" \
277
- --data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" \
278
- '{title:$t, body:$b, head:$h, base:"main"}')" | jq -r .number)"
279
- echo "opened PR #$number"
280
- fi
281
- close_superseded "$number" || true
282
-
283
- - name: Report "already fresh"
284
- if: steps.sync.outputs.changed != '1'
285
- run: echo "dev-kit pin already current -- nothing to sync."
286
- - name: Reap the containers this job launched
287
- # Whatever the tests did. A teardown cut short by a test timeout leaves
288
- # its instance running on the shared Docker daemon (see JOB_REAP_STEP in
289
- # conventions/workflow-shell.ts); everything this job launched mounts its
290
- # own test-temp, so that is what goes.
291
- if: always()
292
- run: |
293
- base="${NORSK_CTL_TEST_TMP:-}"
294
- tt="$GITHUB_WORKSPACE/test-temp"
295
- s=""
296
- for c in $(docker ps -aq); do
297
- src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
298
- if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
299
- done
300
- if [ -n "$s" ]; then
301
- echo "::warning::removing containers this job left running:$s"
302
- docker rm -f $s
303
- fi
304
-
305
- # Report the sync result to the aggregated product CI dashboard
306
- # (id3as/ci-workflows). !cancelled() so a failed sync shows a sad pony but a
307
- # cancelled/superseded run does not false-red the board. A gate-red run that
308
- # opened a PR is still a successful sync -- the red lives on the PR's own checks
309
- # run, not here.
310
- notify:
311
- needs: sync
312
- if: ${{ !cancelled() }}
313
- runs-on: x64
314
- steps:
315
- # Runs BEFORE checkout, and does two things: points the harness's temp base
316
- # out of the workspace for every later step in this job, and clears whatever
317
- # the old repo-local default left behind on this runner. The body carries the
318
- # why. It fails the job only when legacy litter survives -- which would take
319
- # the checkout down a step later anyway, but report nothing useful.
320
- - name: Route test-temp out of the workspace (pre-checkout)
321
- run: |
322
- set -uo pipefail
323
- # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
324
- # HOST daemon has not seen before is created by dockerd, as ROOT, on the
325
- # host -- even when the launch passes --container-user (reproduced
326
- # 2026-09-04). The harness's default base is repo-local for an OrbStack
327
- # inotify constraint that binds macOS dev and nothing here, so every run
328
- # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
329
- # cannot remove. That took commentary's checks AND integration red on
330
- # 2026-09-03. Use the sanctioned override, per-run and outside the
331
- # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
332
- base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
333
- mkdir -p "$base"
334
- echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
335
- # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
336
- # EACCESes. They cannot wedge a checkout, but they do fill the disk.
337
- for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
338
- docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
339
- done
340
- # LEGACY: a runner that ran the old default still carries a workspace
341
- # test-temp, and a checkout with clean:true dies on it. Reap the holders
342
- # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
343
- # RESTARTING leaked container re-creates the path as root seconds after
344
- # any clean. Match on mount SOURCE, not a label: the holders are untracked
345
- # by construction, because a killed run labels nothing.
346
- tt="$GITHUB_WORKSPACE/test-temp"
347
- [ -d "$tt" ] || exit 0
348
- stuck=""
349
- for c in $(docker ps -aq 2>/dev/null || true); do
350
- if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
351
- stuck="$stuck $c"
352
- fi
353
- done
354
- if [ -n "$stuck" ]; then
355
- echo "reaping containers holding mounts under $tt:$stuck"
356
- docker rm -f $stuck || true
357
- fi
358
- # The DIRECTORY, not just its contents: an empty but root-owned base
359
- # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
360
- # container unlink the leaf.
361
- docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
362
- sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
363
- # Fail HERE if it survived. The checkout fails on it either way, but
364
- # reports only an EACCES rmdir with no clue what was holding the path.
365
- if [ -e "$tt" ]; then
366
- echo "::error::$tt survived the pre-checkout clean"
367
- ls -lan "$tt" || true
368
- exit 1
369
- fi
370
-
371
- - uses: actions/checkout@v5
372
- with:
373
- clean: true
374
- - id: meta
375
- run: |
376
- if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
377
- echo "state=failure" >> "$GITHUB_OUTPUT"
378
- else
379
- echo "state=success" >> "$GITHUB_OUTPUT"
380
- fi
381
- - uses: ./.github/actions/ci-status-dispatch
382
- with:
383
- token: ${{ secrets.CI_DISPATCH_TOKEN }}
384
- product: __PRODUCT__
385
- pipeline: dev-kit
386
- status: ${{ steps.meta.outputs.state }}
387
- - name: Reap the containers this job launched
388
- # Whatever the tests did. A teardown cut short by a test timeout leaves
389
- # its instance running on the shared Docker daemon (see JOB_REAP_STEP in
390
- # conventions/workflow-shell.ts); everything this job launched mounts its
391
- # own test-temp, so that is what goes.
392
- if: always()
393
- run: |
394
- base="${NORSK_CTL_TEST_TMP:-}"
395
- tt="$GITHUB_WORKSPACE/test-temp"
396
- s=""
397
- for c in $(docker ps -aq); do
398
- src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
399
- if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
400
- done
401
- if [ -n "$s" ]; then
402
- echo "::warning::removing containers this job left running:$s"
403
- docker rm -f $s
404
- fi