@norskvideo/ctl-dev-kit 0.2.60 → 0.2.62

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.
@@ -9,22 +9,29 @@
9
9
  # tracked package.json actually depends on them) move in lockstep via a global
10
10
  # string-replace of old->new across every tracked file — which also updates the
11
11
  # version-pin guard test, no drift. A repo with no studio npm deps needs no
12
- # package.json `overrides` block at all. Also rebumps the dev flake's pinned
13
- # norsk-ctl daemon to the `latest` channel (dev-only; CI downloads ctl
14
- # directly, so a stale pin never affects CI or customers).
12
+ # package.json `overrides` block at all. Also moves flake.nix's norsk-ctl pin
13
+ # (ctlVersion + the per-platform hashes) to the `latest` channel. That pin is
14
+ # not dev-only: every CI tier runs the norsk-ctl it names, so a product's result
15
+ # belongs to a commit (ctl-pinned-in-repo.test.ts).
15
16
  #
16
17
  # Nightly cron + on demand. The bump runs the product's own gate (lint,
17
- # typecheck, unit tests) BEFORE it lands, exactly as sync-dev-kit does: green
18
- # lands on main, red opens a reconciliation PR and main stays green.
18
+ # typecheck, unit tests), then ALWAYS opens (or updates) a PR on
19
+ # upgrade-latest/nightly, as sync-ctl-packages does; it never pushes to main.
20
+ # bot-prs merges the PR once checks, smoke, integration and the guides gate are
21
+ # green on its head, so a bad Norsk nightly or norsk-ctl shows up as a red bot
22
+ # PR and main stays green.
19
23
  #
20
- # It used to commit straight to main and rely on the push-triggered run for its
21
- # verdict. The push is made with CI_DISPATCH_TOKEN (a PAT), so that run really
22
- # does happen -- a GITHUB_TOKEN push would fire nothing (Actions' recursion
23
- # guard) -- but it makes a RED `main` the alarm, and nobody watches a product
24
- # repo's own `main`: playout sat red ~12h, then funke, then probe, inside three
25
- # days (fleet review 04c, class 8). Gating first turns "a bad Norsk nightly reds
26
- # six repos overnight" into "a bad Norsk nightly opens a PR that says so".
27
- # Pinned for the class by conventions/gate-before-main.test.ts.
24
+ # History, because each step was a fix. It first committed straight to main and
25
+ # let the push-triggered run be the verdict -- a RED `main` as the alarm, and
26
+ # nobody watches a product repo's own `main`: playout sat red ~12h, then funke,
27
+ # then probe, inside three days (fleet review 04c, class 8). Then it gated
28
+ # first and landed on green. That kept lint/typecheck/unit failures off main,
29
+ # but smoke, integration and the guides ran only after landing, so a bad
30
+ # upstream release still reddened main in every repo that took it
31
+ # (ci-workflows docs/dashboard/ci-fragility.md, problem 2 and proposal B).
32
+ # Pinned for the class by conventions/gate-before-main.test.ts. The branch is
33
+ # pushed with CI_DISPATCH_TOKEN (a PAT): a GITHUB_TOKEN push would fire none of
34
+ # the PR's workflows (Actions' recursion guard).
28
35
  #
29
36
  # Single-sourced in @norskvideo/ctl-dev-kit (conventions/upgrade-latest.yml) and
30
37
  # copied verbatim into each product repo; the check:drift gate fails CI if a copy
@@ -116,6 +123,18 @@ jobs:
116
123
  reg="https://registry.npmjs.org"
117
124
  changed=0
118
125
 
126
+ # NORSK-CTL: the version flake.nix's ctlVersion names is the daemon every
127
+ # CI tier runs (ctl-pinned-in-repo.test.ts) as well as the one the dev
128
+ # shell launches, so it moves here like the engine does and the bump
129
+ # PR's tiers are what test it (ci-workflows docs/dashboard/
130
+ # ci-fragility.md, proposal A). Resolved up front, with everything
131
+ # else, so a refusal rewrites nothing.
132
+ ctl_base="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
133
+ old_ctl="$( { grep -oE 'ctlVersion = "[^"]+"' flake.nix 2>/dev/null || true; } | head -1 | sed -E 's/.*"(.*)"/\1/')"
134
+ [ -n "$old_ctl" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl it names, so add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
135
+ new_ctl="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$ctl_base/latest")"
136
+ [ -n "$new_ctl" ] || { echo "::error::could not resolve norsk-ctl's latest channel"; exit 1; }
137
+
119
138
  # Accumulate a COMPACT summary: only the components that actually moved
120
139
  # get an entry, so a no-op nightly (old == new) posts nothing. Was: the
121
140
  # full media + studio-lib + studio-image triple every night for every
@@ -264,18 +283,16 @@ jobs:
264
283
  parts+=("studio-lib \`$new_studio_lib\`")
265
284
  fi
266
285
 
267
- # --- ctl daemon pin (flake.nix) -----------------------------------
268
- # The pinned norsk-ctl in the dev flake is dev-only (CI downloads the
269
- # released binary directly), but keep it fresh too. Bump ctlVersion +
270
- # the four per-platform hashes to the `latest` channel. Mirrors
286
+ # --- norsk-ctl pin (flake.nix) ------------------------------------
287
+ # ctlVersion, plus the per-platform hash of each platform the flake
288
+ # fetches the daemon for. A Studio product's flake fetches all four; an
289
+ # sdk-app's fetches none and carries the version alone. Mirrors
271
290
  # packages/dev-kit/build/refresh-ctl-pin.sh in the ctl monorepo.
272
- ctl_base="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
273
- old_ctl="$(grep -oE 'ctlVersion = "[^"]+"' flake.nix | sed -E 's/.*"(.*)"/\1/')"
274
- new_ctl="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$ctl_base/latest")"
275
- if [ -n "$new_ctl" ] && [ "$old_ctl" != "$new_ctl" ]; then
291
+ if [ "$old_ctl" != "$new_ctl" ]; then
276
292
  declare -A ctlplat=([x86_64-linux]=linux-x64 [aarch64-linux]=linux-arm64 [aarch64-darwin]=darwin-arm64 [x86_64-darwin]=darwin-x64)
277
293
  for sys in "${!ctlplat[@]}"; do
278
294
  p="${ctlplat[$sys]}"
295
+ grep -qF "plat = \"$p\";" flake.nix || continue
279
296
  hex="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$ctl_base/$new_ctl/norsk-ctl-$new_ctl-$p.sha256" | awk '{print $1}')"
280
297
  [ -n "$hex" ] || { echo "no sha256 sidecar for $p"; exit 1; }
281
298
  sri="$(nix hash convert --hash-algo sha256 --to sri "$hex")"
@@ -327,31 +344,51 @@ jobs:
327
344
  bun run test:unit
328
345
  '
329
346
 
330
- - name: Land the bump on main (gate green)
331
- if: steps.bump.outputs.changed == '1' && steps.gate.outcome == 'success'
347
+ # The bump's only way to main is this PR. bot-prs merges it once every tier
348
+ # that runs on a bot PR -- checks, smoke, integration and build-image's
349
+ # guides gate, wherever this repo has them -- is green on its head
350
+ # (ci-workflows docs/dashboard/ci-fragility.md, proposal B). Landing on a
351
+ # green gate kept lint, typecheck and unit failures off main, but the tiers
352
+ # that break on an upstream change ran only after landing, so a bad
353
+ # upstream release still turned main red in every repo that took it. The
354
+ # gate above still runs: its verdict heads the PR body, and a red one
355
+ # shows on the PR's own checks too. Never a draft: bot-prs reads red from
356
+ # the tiers, and a draft could never become ready again by PATCH.
357
+ - name: Open (or update) the bump PR
358
+ if: steps.bump.outputs.changed == '1'
332
359
  env:
333
360
  API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
334
- # Through env, never pasted into the script (see the PR step below).
361
+ GATE: ${{ steps.gate.outcome }}
362
+ # Through env, never pasted into the script: the summary wraps each
363
+ # version in backticks, and inside a double-quoted shell string those
364
+ # ran as commands ("1.3.1-...: command not found"), dropping every
365
+ # version from the commit message and PR body.
335
366
  SUMMARY: ${{ steps.bump.outputs.summary }}
336
367
  run: |
337
368
  set -euo pipefail
338
- # This bump is about to land, which makes any upgrade-latest/* PR an earlier
339
- # red run opened obsolete. Left open they piled up -- nine were closed
340
- # by hand on 2026-10-03 -- so close them once the push has landed.
341
- # Best-effort: a failure here must not fail the landing.
369
+ branch="upgrade-latest/nightly"
370
+ prefix="upgrade-latest/"
371
+ title="Bump Norsk nightly + ctl pins to latest channel"
372
+ repo="${{ github.repository }}"; owner="${repo%%/*}"
373
+ api="https://api.github.com/repos/$repo/pulls"
374
+ # An older PR of this bot's family is obsolete once this one is open:
375
+ # left open they piled up (nine were closed by hand on 2026-10-03).
376
+ # Best-effort: a failure here must not fail the bump.
342
377
  close_superseded() {
343
- api="https://api.github.com/repos/$GITHUB_REPOSITORY"
378
+ local keep="$1" n
344
379
  auth=(-H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
345
- for n in $(curl -sS "${auth[@]}" "$api/pulls?state=open&per_page=100" \
346
- | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith("upgrade-latest/"))) | .number'); do
347
- curl -sS -o /dev/null "${auth[@]}" -X POST "$api/issues/$n/comments" \
348
- -d '{"body":"Superseded: a later upgrade-latest run landed its bump on main."}'
349
- curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/pulls/$n" -d '{"state":"closed"}'
380
+ for n in $(curl -sS "${auth[@]}" "$api?state=open&per_page=100" \
381
+ | jq -r --arg repo "$repo" --arg prefix "$prefix" --argjson keep "$keep" \
382
+ '.[] | select(.head.repo.full_name == $repo and (.head.ref | startswith($prefix)) and .number != $keep) | .number'); do
383
+ curl -sS -o /dev/null "${auth[@]}" -X POST "https://api.github.com/repos/$repo/issues/$n/comments" \
384
+ -d "$(jq -nc --arg b "Superseded by #$keep, a later run of this bot." '{body:$b}')"
385
+ curl -sS -o /dev/null "${auth[@]}" -X PATCH "$api/$n" -d '{"state":"closed"}'
350
386
  echo "closed superseded PR #$n"
351
387
  done
352
388
  }
353
389
  git config user.name "github-actions[bot]"
354
390
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
391
+ git checkout -B "$branch"
355
392
  git add -A
356
393
  git commit -m "chore: bump Norsk nightly + ctl pins to latest channel
357
394
 
@@ -359,81 +396,52 @@ jobs:
359
396
 
360
397
  Coupled Norsk pins (npm overrides + manifest.seed.json image tags +
361
398
  version-pin guard test) moved together via a global version-string
362
- replace; lockfile regenerated. Dev flake norsk-ctl pin (version + 4
363
- platform hashes) rebumped to the latest channel."
364
- # main can advance under a long bump (lockfile regen + registry
365
- # lookups run for minutes); a bare push then loses a non-fast-forward
366
- # race and reds the nightly. Rebase-and-retry so an unrelated commit
367
- # landing mid-run does not fail the bump; a genuine content conflict
368
- # still aborts loudly rather than force-pushing over other work.
369
- for attempt in 1 2 3 4 5; do
370
- if git push origin HEAD:main; then close_superseded || true; exit 0; fi
371
- echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
372
- # --unshallow, not a plain fetch: these runners keep their .git
373
- # between jobs, and a commit an earlier job fetched at depth 1 stays
374
- # recorded as parentless. A plain fetch does not re-fetch an object it
375
- # already holds, so main's tip can look like an unrelated root and the
376
- # rebase replays our base commit over it -- add/add conflicts on every
377
- # file it touches (playout + studio, 2026-10-03). The fallback covers
378
- # a repo that is already complete, where --unshallow is refused.
379
- git fetch --unshallow origin main 2>/dev/null || git fetch origin main
380
- git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
381
- done
382
- echo "push still failing after 5 rebase attempts"; exit 1
383
-
384
- - name: Open a reconciliation PR (gate red)
385
- if: steps.bump.outputs.changed == '1' && steps.gate.outcome == 'failure'
386
- env:
387
- API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
388
- # Through env, never pasted into the script: the summary wraps each
389
- # version in backticks, and inside a double-quoted shell string those
390
- # ran as commands ("1.3.1-...: command not found"), dropping every
391
- # version from the commit message and PR body.
392
- SUMMARY: ${{ steps.bump.outputs.summary }}
393
- run: |
394
- set -euo pipefail
395
- branch="upgrade-latest/nightly"
396
- git config user.name "github-actions[bot]"
397
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
398
- git checkout -b "$branch"
399
- git add -A
400
- git commit -m "chore: bump Norsk nightly + ctl pins to latest channel (needs reconciliation)
401
-
402
- ${SUMMARY}"
403
- git push -f origin "$branch"
404
- title="Bump Norsk nightly + ctl pins to latest channel"
399
+ replace; lockfile regenerated. flake.nix norsk-ctl pin (version + the
400
+ per-platform hashes), which CI runs, moved to the latest channel."
401
+ # One branch per bump, force-pushed from today's main -- but only when
402
+ # its tree changed. A red bump PR left standing is rebuilt by every run
403
+ # of this bot, and pushing the same tree again would re-run every slow
404
+ # tier on it for nothing.
405
+ if git fetch -q origin "$branch" 2>/dev/null && [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse 'FETCH_HEAD^{tree}')" ]; then
406
+ echo "$branch already holds this bump; not re-pushing it"
407
+ else
408
+ git push -f origin "$branch"
409
+ fi
410
+ if [ "$GATE" = "success" ]; then
411
+ verdict="Gate green: lint, typecheck and unit tests pass on the bumped pins."
412
+ else
413
+ verdict="**Gate red**: lint, typecheck or unit tests fail on the bumped pins. That usually means the new Norsk nightly is bad, or genuinely changed something the product depends on. Fix it on this branch, or close this PR to skip the bump -- the next nightly run reopens it against whatever the channel holds then."
414
+ fi
405
415
  body="$(printf '%s\n' \
406
- "Automated nightly pin bump: the Norsk nightly channel moved and the coupled" \
407
- "pins came with it -- but the product gate is **red**, so this bump needs a" \
408
- "human before it reaches main." \
416
+ "Automated nightly pin bump: the Norsk nightly channel and norsk-ctl latest moved, and the coupled pins came with them." \
409
417
  "" \
410
418
  " ${SUMMARY}" \
411
419
  "" \
412
- "A red gate here usually means the new Norsk nightly is bad, or genuinely" \
413
- "changed something the product depends on. Run it to see which:" \
414
- "" \
415
- " bun run lint && bun run typecheck && bun run test:unit" \
420
+ "$verdict" \
416
421
  "" \
417
- "Fix it on this branch and merge, or close this PR to skip the bump -- the" \
418
- "next nightly run reopens it against whatever the channel holds then.")"
422
+ "bot-prs merges this once checks, smoke, integration and the guides gate" \
423
+ "(build-image) have all passed on its head, wherever this repo runs them." \
424
+ "Until then main does not move; a red tier keeps it open and reports it on" \
425
+ "the product dashboard.")"
419
426
  # gh is not on the self-hosted runner PATH (it lives only inside
420
427
  # `nix develop`, and this step runs outside it), so create/update the PR
421
428
  # straight against the REST API with the same PAT that pushed the branch.
422
- # The PAT (not GITHUB_TOKEN) is what lets the PR's own checks run, which
423
- # is the whole point: the bump is judged before it lands, not after.
424
- repo="${{ github.repository }}"; owner="${repo%%/*}"
425
- api="https://api.github.com/repos/$repo/pulls"
429
+ # The PAT (not GITHUB_TOKEN) is what lets the PR's own workflows run,
430
+ # which is the whole point: the bump is judged before it lands.
426
431
  curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
427
432
  existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
428
433
  if [ -n "$existing" ]; then
429
434
  curl "${curl_api[@]}" -X PATCH "$api/$existing" \
430
435
  --data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" > /dev/null
431
- echo "updated PR #$existing"
436
+ number="$existing"
437
+ echo "updated PR #$number"
432
438
  else
433
- curl "${curl_api[@]}" -X POST "$api" \
439
+ number="$(curl "${curl_api[@]}" -X POST "$api" \
434
440
  --data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" \
435
- '{title:$t, body:$b, head:$h, base:"main"}')" | jq -r '"opened PR #\(.number)"'
441
+ '{title:$t, body:$b, head:$h, base:"main"}')" | jq -r .number)"
442
+ echo "opened PR #$number"
436
443
  fi
444
+ close_superseded "$number" || true
437
445
 
438
446
  - name: Report "already fresh"
439
447
  if: steps.bump.outputs.changed != '1'
@@ -85,8 +85,8 @@ a class attribute on every page of the site.
85
85
  site: `hasDocsSite()` tests for `website/astro.config.mjs`, and
86
86
  `docsThemeProblems()` routes this directory's `checkTheme` into the same report as
87
87
  `biome.json` and the shared workflows. `conventions/sync-drift.ts` is the writer
88
- half, so `sync-dev-kit.yml` rolls a theme change out to every product hands-free
89
- like any other convention. The site directory is `website/` across the fleet
88
+ half, so `sync-dev-kit.yml` rolls a theme change out to every product like any
89
+ other convention, through a bot PR that `bot-prs.yml` merges once its tiers pass. The site directory is `website/` across the fleet
90
90
  (`DOCS_SITE_DIR`).
91
91
 
92
92
  A site inside THIS monorepo still runs `sync.ts check` from its own test suite,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.60",
3
+ "version": "0.2.62",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",