@norskvideo/ctl-dev-kit 0.2.64 → 0.2.65

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.
@@ -5,8 +5,10 @@
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), publish-docs-site.yml
7
7
  // (only a repo with a docs site may carry it, and never a turnkey)
8
- // - masked per-repo line: checks.yml, upgrade-latest.yml, publish-docs.yml
9
- // and the other dispatching workflows (`product:` key)
8
+ // - masked per-repo line: checks.yml, bump.yml, publish-docs.yml and the
9
+ // other dispatching workflows (`product:` key)
10
+ // - retired: upgrade-latest.yml, sync-dev-kit.yml, sync-ctl-packages.yml,
11
+ // which bump.yml replaced; carrying one is drift
10
12
  // - base + sanctioned extension block: dprint.json (repo-specific excludes)
11
13
  // - must import the shared module: flake.nix (otherwise the product's own)
12
14
  // - structural: root tsconfig.json (shape-dependent includes stay free),
@@ -262,14 +264,13 @@ export interface CanonicalBytes {
262
264
  /** build/ctl-shell.nix — the shared dev-shell module, gated verbatim at
263
265
  * nix/ctl-shell.nix for every shape. */
264
266
  ctlShell: string;
265
- upgradeLatest: string;
266
- syncDevKit: string;
267
- /** conventions/sync-ctl-packages.yml — the ctl-* pin freshness PR. No per-repo
268
- * line at all, so it is verbatim and sync-drift creates it where absent. */
269
- syncCtlPackages: string;
270
- /** conventions/bot-prs.yml -- merges the pin bots' green PRs and reports red
271
- * ones. Verbatim (it reads its dashboard key at run time), so sync-drift
272
- * creates it where absent, like sync-ctl-packages. */
267
+ /** conventions/bump.yml -- the one nightly bump (dev-kit, ctl-* and moq-*,
268
+ * then the engine/Studio/norsk-ctl pins) and its one PR. Masked like
269
+ * checks.yml; required, and sync-drift creates it where absent. */
270
+ bump: string;
271
+ /** conventions/bot-prs.yml -- merges the bump's green PR and reports a red
272
+ * one. Verbatim (it reads its dashboard key at run time), so sync-drift
273
+ * creates it where absent. */
273
274
  botPrs: string;
274
275
  publishDocs: string;
275
276
  /** conventions/build-docs.yml — the embedded docs variant (bundle -> image, no upload). */
@@ -305,10 +306,10 @@ export interface CanonicalBytes {
305
306
  * Studio licence step removed (it reads shared/src/version.ts +
306
307
  * product-template.ts). Optional for the same reason as flakeSdkApp. */
307
308
  checksSdkApp?: string;
308
- /** conventions/upgrade-latest.sdk-app.yml — the upgrade-latest.yml variant for
309
- * an sdk-app: no Studio pair or manifest.seed.json, the norsk family bumped off
310
- * the SDK nightly. Optional for the same reason as flakeSdkApp. */
311
- upgradeLatestSdkApp?: string;
309
+ /** conventions/bump.sdk-app.yml — the bump.yml variant for an sdk-app: its
310
+ * pins step has no Studio pair or manifest.seed.json, and bumps the norsk
311
+ * family off the SDK nightly. Optional for the same reason as flakeSdkApp. */
312
+ bumpSdkApp?: string;
312
313
  }
313
314
 
314
315
  // 05-demo s6: a product that has a demo (tests/demo.spec.ts) exposes it the
@@ -514,6 +515,16 @@ export function resolveRepoName(repoRoot: string, env: NodeJS.ProcessEnv = proce
514
515
  const PUBLIC_DOCS_REPO = /^norsk-ctl-product-/;
515
516
  const TURNKEY_REPO = /^norsk-ctl-turnkey-/;
516
517
 
518
+ /** The one nightly bump (ci-fragility proposal B2). */
519
+ export const BUMP_WORKFLOW = ".github/workflows/bump.yml";
520
+
521
+ /** The three bump bots bump.yml replaced, removed from a product by sync-drift. */
522
+ export const RETIRED_BUMP_WORKFLOWS = [
523
+ ".github/workflows/upgrade-latest.yml",
524
+ ".github/workflows/sync-dev-kit.yml",
525
+ ".github/workflows/sync-ctl-packages.yml",
526
+ ] as const;
527
+
517
528
  /** The docs site's publish workflow, where a repo carries one. */
518
529
  export const DOCS_SITE_WORKFLOW = ".github/workflows/publish-docs-site.yml";
519
530
 
@@ -657,7 +668,7 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
657
668
  // checks.yml is REQUIRED — it is the CI entry point that runs this very gate
658
669
  // (plus the shared quality jobs), so a repo without it never re-converges.
659
670
  // Only the `product:` dispatch key is per-repo; it is masked like
660
- // upgrade-latest.yml's.
671
+ // bump.yml's.
661
672
  const checksPath = join(repoRoot, ".github", "workflows", "checks.yml");
662
673
  const checksRel = ".github/workflows/checks.yml";
663
674
  const checksCanon = shape === "sdk-app" ? canonical.checksSdkApp : canonical.checks;
@@ -702,52 +713,35 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
702
713
  }
703
714
  }
704
715
 
705
- // upgrade-latest.yml is OPTIONAL — a product may ship without a nightly bump
706
- // (e.g. until it has an integration tier). But when present it is the shared,
707
- // single-sourced workflow and must not diverge. Shape-selected like checks.yml:
708
- // an sdk-app has no Studio pair to bump, so it carries its own variant.
709
- const workflowPath = join(repoRoot, ".github", "workflows", "upgrade-latest.yml");
710
- if (existsSync(workflowPath)) {
711
- const upgradeCanon = shape === "sdk-app" ? canonical.upgradeLatestSdkApp : canonical.upgradeLatest;
712
- if (upgradeCanon === undefined) {
713
- throw new Error(`checkDrift: shape "${shape}" requires canonical.upgradeLatestSdkApp`);
714
- }
715
- push(
716
- workflowProblem(
717
- ".github/workflows/upgrade-latest.yml",
718
- shape === "sdk-app" ? "conventions/upgrade-latest.sdk-app.yml" : "conventions/upgrade-latest.yml",
719
- readFileSync(workflowPath, "utf8"),
720
- upgradeCanon,
721
- ),
722
- );
723
- }
724
-
725
- // sync-dev-kit.yml is OPTIONAL for the same reason as upgrade-latest.yml — a
726
- // product may not carry it yet — but when present it is the shared,
727
- // single-sourced workflow that keeps this repo's dev-kit pin fresh, and must
728
- // not diverge. Only its `product:` dispatch key is per-repo.
729
- const syncPath = join(repoRoot, ".github", "workflows", "sync-dev-kit.yml");
730
- if (existsSync(syncPath)) {
731
- push(
732
- workflowProblem(
733
- ".github/workflows/sync-dev-kit.yml",
734
- "conventions/sync-dev-kit.yml",
735
- readFileSync(syncPath, "utf8"),
736
- canonical.syncDevKit,
737
- ),
716
+ // bump.yml is REQUIRED: the one nightly bump (ci-fragility proposal B2), and
717
+ // sync-drift creates it. Shape-selected like checks.yml: an sdk-app has no
718
+ // Studio pair to bump, so it carries its own variant. Only its `product:`
719
+ // dispatch keys are per-repo.
720
+ const bumpPath = join(repoRoot, BUMP_WORKFLOW);
721
+ const bumpCanon = shape === "sdk-app" ? canonical.bumpSdkApp : canonical.bump;
722
+ const bumpRef = shape === "sdk-app" ? "conventions/bump.sdk-app.yml" : "conventions/bump.yml";
723
+ if (bumpCanon === undefined) {
724
+ throw new Error(`checkDrift: shape "${shape}" requires canonical.bumpSdkApp`);
725
+ } else if (!existsSync(bumpPath)) {
726
+ problems.push(
727
+ `${BUMP_WORKFLOW} not found (${bumpPath}). It is this repo's one nightly bump: \`bun run sync:drift\` creates it from @norskvideo/ctl-dev-kit ${bumpRef}, keeping this repo's dashboard key.`,
738
728
  );
729
+ } else {
730
+ push(workflowProblem(BUMP_WORKFLOW, bumpRef, readFileSync(bumpPath, "utf8"), bumpCanon));
739
731
  }
740
732
 
741
- // sync-ctl-packages.yml is OPTIONAL until a repo has been synced once — it is
742
- // NEW, and sync-drift creates it. Unlike its neighbours it carries no per-repo
743
- // line (it dispatches to no dashboard), so the whole file is canonical.
744
- const syncCtlPath = join(repoRoot, ".github", "workflows", "sync-ctl-packages.yml");
745
- if (existsSync(syncCtlPath) && readFileSync(syncCtlPath, "utf8") !== canonical.syncCtlPackages) {
746
- push(`.github/workflows/sync-ctl-packages.yml differs from conventions/sync-ctl-packages.yml. ${RESYNC}`);
733
+ // The three bots bump.yml replaced. One left beside it would run a second
734
+ // bump that conflicts with the first and is tested alone -- what B2 ended.
735
+ for (const rel of RETIRED_BUMP_WORKFLOWS) {
736
+ if (existsSync(join(repoRoot, rel))) {
737
+ problems.push(
738
+ `${rel} is retired: ${BUMP_WORKFLOW} does its job now, in the one nightly bump. Delete it (\`bun run sync:drift\` removes it once ${BUMP_WORKFLOW} exists).`,
739
+ );
740
+ }
747
741
  }
748
742
 
749
- // bot-prs.yml: the same shape -- optional until a repo's first sync creates
750
- // it, verbatim once present.
743
+ // bot-prs.yml: optional until a repo's first sync creates it, verbatim once
744
+ // present.
751
745
  const botPrsPath = join(repoRoot, ".github", "workflows", "bot-prs.yml");
752
746
  if (existsSync(botPrsPath) && readFileSync(botPrsPath, "utf8") !== canonical.botPrs) {
753
747
  push(`.github/workflows/bot-prs.yml differs from conventions/bot-prs.yml. ${RESYNC}`);
@@ -879,9 +873,7 @@ if (import.meta.main) {
879
873
  core: readFileSync(join(import.meta.dir, "CLAUDE.core.md"), "utf8"),
880
874
  flake: readFileSync(join(import.meta.dir, "..", "build", "flake.nix"), "utf8"),
881
875
  ctlShell: readFileSync(join(import.meta.dir, "..", "build", "ctl-shell.nix"), "utf8"),
882
- upgradeLatest: readFileSync(join(import.meta.dir, "upgrade-latest.yml"), "utf8"),
883
- syncDevKit: readFileSync(join(import.meta.dir, "sync-dev-kit.yml"), "utf8"),
884
- syncCtlPackages: readFileSync(join(import.meta.dir, "sync-ctl-packages.yml"), "utf8"),
876
+ bump: readFileSync(join(import.meta.dir, "bump.yml"), "utf8"),
885
877
  botPrs: readFileSync(join(import.meta.dir, "bot-prs.yml"), "utf8"),
886
878
  publishDocs: readFileSync(join(import.meta.dir, "publish-docs.yml"), "utf8"),
887
879
  buildDocs: readFileSync(join(import.meta.dir, "build-docs.yml"), "utf8"),
@@ -899,7 +891,7 @@ if (import.meta.main) {
899
891
  demoShim: readFileSync(join(import.meta.dir, "demo.sh"), "utf8"),
900
892
  flakeSdkApp: readFileSync(join(import.meta.dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
901
893
  checksSdkApp: readFileSync(join(import.meta.dir, "checks.sdk-app.yml"), "utf8"),
902
- upgradeLatestSdkApp: readFileSync(join(import.meta.dir, "upgrade-latest.sdk-app.yml"), "utf8"),
894
+ bumpSdkApp: readFileSync(join(import.meta.dir, "bump.sdk-app.yml"), "utf8"),
903
895
  };
904
896
  const report = checkDrift(repoRoot, canonical, {
905
897
  repoName: resolveRepoName(repoRoot),
@@ -6,9 +6,8 @@
6
6
  // a different copy from the one the root was moved to, and nothing says so:
7
7
  // playout's tests/ pinned the dev-kit ^0.1.57 while the root moved to 0.2, its
8
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.
9
+ // failed every night from 2026-09-07. bump.yml now moves every pin; this keeps
10
+ // a hand edit (or a new nested package) from splitting them again.
12
11
  import { lstatSync, readdirSync, readFileSync } from "node:fs";
13
12
  import { join, relative } from "node:path";
14
13
 
@@ -61,7 +60,7 @@ export function ctlPinProblems(repoRoot: string): string[] {
61
60
  if (byRange.size < 2) continue;
62
61
  const where = [...byRange].flatMap(([range, files]) => files.map((f) => `${f} ${range}`)).join(", ");
63
62
  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).`,
63
+ `${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 (the nightly bump.yml moves every pin).`,
65
64
  );
66
65
  }
67
66
  return problems;
@@ -132,7 +132,7 @@ reaches every product's manual at once.
132
132
  shared workflows — the clause is conditional on `website/astro.config.mjs`
133
133
  existing, so a repo without a site is not asked for one.
134
134
  - `bun run sync:drift` rewrites the copies from the installed dev-kit. The
135
- nightly `sync-dev-kit.yml` runs it after bumping the pin, so a theme change
135
+ nightly `bump.yml` runs it after bumping the pin, so a theme change
136
136
  rolls out hands-free and only a change that genuinely breaks the site goes red.
137
137
 
138
138
  **Keeping the copies at exactly those paths is load-bearing**, not tidiness:
@@ -246,11 +246,11 @@ that branch (`--branch` is the ref that ran), never over production.
246
246
  Two steps, once a dev-kit carrying the workflow is published:
247
247
 
248
248
  ```
249
- bun update @norskvideo/ctl-dev-kit # bump the pin (or let sync-dev-kit.yml do it)
249
+ bun update @norskvideo/ctl-dev-kit # bump the pin (or let bump.yml do it)
250
250
  bun run sync:drift # writes .github/workflows/publish-docs-site.yml
251
251
  ```
252
252
 
253
- Commit the result; `bun run check:drift` passes. The nightly `sync-dev-kit.yml`
253
+ Commit the result; `bun run check:drift` passes. The nightly `bump.yml`
254
254
  does both hands-free, so a product with a site acquires the workflow on its
255
255
  next sync without anyone touching the repo. Then create the Pages project, set
256
256
  the two secrets and the variable, and the next push to `main` publishes.
@@ -265,7 +265,7 @@ converge: move the repo's bespoke steps into the hooks above, replace the
265
265
  workflow with the canonical (product and runner filled in), and prove it with
266
266
  `gh workflow run integration.yml --ref <branch>` before merging — the tier
267
267
  only fires on `main` and on dispatch, so a PR's checks never run it. Once the
268
- marker is in, `sync-dev-kit` keeps the copy current and `check:drift` names any
268
+ marker is in, the nightly `bump.yml` keeps the copy current and `check:drift` names any
269
269
  hand edit.
270
270
 
271
271
  **Why the pipe is shaped the way it is.** The suite runs inside a shell
@@ -39,8 +39,9 @@
39
39
  # topology step below says so in NORSK_TEST_HOST/NORSK_TEST_NET); the GPU box
40
40
  # is '["ARM64","nvidia"]'.
41
41
  #
42
- # On a pull request the suite runs for a BOT branch only (upgrade-latest/*,
43
- # sync-dev-kit/*, chore/sync-ctl-packages): a bump reaches main only once this
42
+ # On a pull request the suite runs for a BOT branch only (bump/nightly, and the
43
+ # retired bots' upgrade-latest/*, sync-dev-kit/* and chore/sync-ctl-packages,
44
+ # which the PR that brings bump.yml rides on): a bump reaches main only once this
44
45
  # tier has passed on it, and bot-prs merges on that (ci-workflows
45
46
  # docs/dashboard/ci-fragility.md, proposal B). A human PR does not pay for the
46
47
  # slow tier; it runs on main after the merge, as before. A PR's result is the
@@ -91,7 +92,7 @@ concurrency:
91
92
 
92
93
  jobs:
93
94
  integration:
94
- if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && (startsWith(github.head_ref, 'upgrade-latest/') || startsWith(github.head_ref, 'sync-dev-kit/') || github.head_ref == 'chore/sync-ctl-packages')) }}
95
+ if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && (github.head_ref == 'bump/nightly' || startsWith(github.head_ref, 'upgrade-latest/') || startsWith(github.head_ref, 'sync-dev-kit/') || github.head_ref == 'chore/sync-ctl-packages')) }}
95
96
  strategy:
96
97
  matrix:
97
98
  include:
@@ -313,7 +314,7 @@ jobs:
313
314
  # deep in the suite. No `|| true` on --version: a bad binary must fail here.
314
315
  #
315
316
  # WHICH binary: the version this commit pins, flake.nix ctlVersion -- the
316
- # daemon the dev shell launches, moved by upgrade-latest's bump PR. It used
317
+ # daemon the dev shell launches, moved by bump.yml's nightly PR. It used
317
318
  # to be S3's moving `latest`, read at run time, so a result could change
318
319
  # with no product commit and a re-run was never a real repeat (funke went
319
320
  # red and green again on 2026-10-09 without a commit either time;
@@ -336,7 +337,7 @@ jobs:
336
337
  echo "norsk-ctl $CTL_CHANNEL channel -> $ver ($arch), in place of the repo pin"
337
338
  else
338
339
  ver="$( { sed -nE 's/^[[:space:]]*ctlVersion = "([^"]+)";.*/\1/p' flake.nix 2>/dev/null || true; } | head -1)"
339
- [ -n "$ver" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl this commit pins (upgrade-latest moves it); add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
340
+ [ -n "$ver" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl this commit pins (the nightly bump.yml moves it); add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
340
341
  echo "norsk-ctl pinned by flake.nix ctlVersion -> $ver ($arch)"
341
342
  fi
342
343
  curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$S3/$ver/norsk-ctl-$ver-$arch" -o "$RUNNER_TEMP/norsk-ctl"
@@ -205,14 +205,14 @@ jobs:
205
205
  done <<<"$left"
206
206
 
207
207
  # The norsk-ctl this commit pins (flake.nix ctlVersion, moved by
208
- # upgrade-latest's bump PR), never S3's moving `latest`: a guide run is then
208
+ # bump.yml's nightly PR), never S3's moving `latest`: a guide run is then
209
209
  # a real repeat, and a red belongs to a commit (ci-fragility proposal A).
210
210
  - name: Download the pinned norsk-ctl binary (flake.nix ctlVersion)
211
211
  run: |
212
212
  set -euo pipefail
213
213
  S3="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
214
214
  ver="$( { sed -nE 's/^[[:space:]]*ctlVersion = "([^"]+)";.*/\1/p' flake.nix 2>/dev/null || true; } | head -1)"
215
- [ -n "$ver" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl this commit pins (upgrade-latest moves it); add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
215
+ [ -n "$ver" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl this commit pins (the nightly bump.yml moves it); add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
216
216
  echo "norsk-ctl pinned by flake.nix ctlVersion -> $ver"
217
217
  curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$S3/$ver/norsk-ctl-$ver-linux-x64" -o "$RUNNER_TEMP/norsk-ctl"
218
218
  chmod +x "$RUNNER_TEMP/norsk-ctl"
@@ -223,7 +223,7 @@ jobs:
223
223
  # deep in the suite. No `|| true` on --version: a bad binary must fail here.
224
224
  #
225
225
  # WHICH binary: the version this commit pins, flake.nix ctlVersion -- the
226
- # daemon the dev shell launches, moved by upgrade-latest's bump PR. It used
226
+ # daemon the dev shell launches, moved by bump.yml's nightly PR. It used
227
227
  # to be S3's moving `latest`, read at run time, so a result could change
228
228
  # with no product commit and a re-run was never a real repeat (funke went
229
229
  # red and green again on 2026-10-09 without a commit either time;
@@ -246,7 +246,7 @@ jobs:
246
246
  echo "norsk-ctl $CTL_CHANNEL channel -> $ver ($arch), in place of the repo pin"
247
247
  else
248
248
  ver="$( { sed -nE 's/^[[:space:]]*ctlVersion = "([^"]+)";.*/\1/p' flake.nix 2>/dev/null || true; } | head -1)"
249
- [ -n "$ver" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl this commit pins (upgrade-latest moves it); add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
249
+ [ -n "$ver" ] || { echo "::error::flake.nix pins no ctlVersion. CI runs the norsk-ctl this commit pins (the nightly bump.yml moves it); add ctlVersion = \"<version>\"; to flake.nix."; exit 1; }
250
250
  echo "norsk-ctl pinned by flake.nix ctlVersion -> $ver ($arch)"
251
251
  fi
252
252
  curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$S3/$ver/norsk-ctl-$ver-$arch" -o "$RUNNER_TEMP/norsk-ctl"
@@ -7,8 +7,12 @@
7
7
  // It only touches copies whose convergent form is mechanical — there is no
8
8
  // judgement anywhere:
9
9
  // - verbatim (biome.json, tsconfig.base.json) -> overwrite with canonical
10
- // - masked workflows (checks / upgrade / sync / -> canonical, repo's product: key kept
10
+ // - masked workflows (checks / bump / -> canonical, repo's product: key kept
11
11
  // publish-docs)
12
+ // - bump.yml -> CREATED where absent, under the key the
13
+ // retired bump bots carried; then those
14
+ // three (upgrade-latest / sync-dev-kit /
15
+ // sync-ctl-packages) are DELETED
12
16
  // - nix/ctl-shell.nix -> overwrite with canonical (created if absent)
13
17
  // - flake.nix (legacy verbatim copy only) -> the starter, repo's floating ctl pin kept
14
18
  // - marker-block (CLAUDE core, dprint, .gitignore) -> replace the shared block, keep the rest
@@ -22,11 +26,12 @@
22
26
  // cannot mechanically converge (e.g. a workflow with no product key to preserve)
23
27
  // is left for check-drift to flag, and a convention that genuinely breaks the
24
28
  // product surfaces as a red lint/typecheck/test — not as a silent bad copy.
25
- import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
29
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
26
30
  import { dirname, join } from "node:path";
27
31
  import { checkTheme, syncTheme } from "../docs-theme/sync.ts";
28
32
  import {
29
33
  BEGIN,
34
+ BUMP_WORKFLOW,
30
35
  type CanonicalBytes,
31
36
  CTL_HASH_LINE,
32
37
  CTL_SHELL_PATH,
@@ -43,6 +48,7 @@ import {
43
48
  PRODUCT_SENTINEL,
44
49
  type ProductShape,
45
50
  PUBLISH_IMAGE_LINE,
51
+ RETIRED_BUMP_WORKFLOWS,
46
52
  RUNNER_LINE,
47
53
  resolveRepoName,
48
54
  resolveShape,
@@ -69,7 +75,7 @@ function writeIfChanged(path: string, content: string, rel: string, written: str
69
75
  }
70
76
  }
71
77
 
72
- // checks.yml / upgrade-latest.yml / sync-dev-kit.yml: canonical byte-for-byte
78
+ // checks.yml / publish-docs.yml: canonical byte-for-byte
73
79
  // except the one `product:` dispatch key, which the gate masks. Re-emit the
74
80
  // canonical with the repo's own key filled back in. A workflow with no key to
75
81
  // preserve (absent, or still the sentinel) is left for the gate to flag.
@@ -87,6 +93,48 @@ function syncWorkflow(repoRoot: string, rel: string, canonical: string, r: SyncR
87
93
  writeIfChanged(path, canonical.replace(PRODUCT_LINE, `$1${key}`), rel, r.written);
88
94
  }
89
95
 
96
+ const productKey = (path: string): string | undefined => {
97
+ if (!existsSync(path)) return undefined;
98
+ const key = readFileSync(path, "utf8").match(/^\s*product:\s*(\S+)/m)?.[1];
99
+ return key && key !== PRODUCT_SENTINEL ? key : undefined;
100
+ };
101
+
102
+ // bump.yml, the one nightly bump (ci-fragility proposal B2), and the end of
103
+ // the three bots it replaced. This is how it reaches a product: the old
104
+ // sync-dev-kit.yml installs a dev-kit that has it and runs this writer, which
105
+ // creates bump.yml under the dashboard key the old bots reported under and
106
+ // deletes them -- sync-dev-kit.yml included, in the very PR it is opening. The
107
+ // old bot-prs on main still triages that PR (its branch is sync-dev-kit/*),
108
+ // and the new integration and build-image it brings still run on that branch,
109
+ // so the transition lands like any other bump. Once landed, bump.yml owns the
110
+ // night and the old three are gone.
111
+ //
112
+ // The key comes from bump.yml itself once it has one; before that from
113
+ // upgrade-latest.yml (the nightly column's reporter), sync-dev-kit.yml, and
114
+ // last checks.yml (required, always filled). With no key at all nothing is
115
+ // created and nothing removed: deleting the old bots before their replacement
116
+ // exists would leave the product with no nightly bump, silently. The gate then
117
+ // names the missing bump.yml.
118
+ function syncBump(repoRoot: string, canonical: string, r: SyncReport): void {
119
+ const wf = (rel: string) => join(repoRoot, rel);
120
+ const key =
121
+ productKey(wf(BUMP_WORKFLOW)) ??
122
+ productKey(wf(".github/workflows/upgrade-latest.yml")) ??
123
+ productKey(wf(".github/workflows/sync-dev-kit.yml")) ??
124
+ productKey(wf(".github/workflows/checks.yml"));
125
+ if (key === undefined) {
126
+ r.skipped.push(`${BUMP_WORKFLOW} (no product: key to give it; the retired bump workflows are left in place)`);
127
+ return;
128
+ }
129
+ mkdirSync(dirname(wf(BUMP_WORKFLOW)), { recursive: true });
130
+ writeIfChanged(wf(BUMP_WORKFLOW), canonical.replace(PRODUCT_LINE, `$1${key}`), BUMP_WORKFLOW, r.written);
131
+ for (const rel of RETIRED_BUMP_WORKFLOWS) {
132
+ if (!existsSync(wf(rel))) continue;
133
+ rmSync(wf(rel));
134
+ r.written.push(rel);
135
+ }
136
+ }
137
+
90
138
  // publish-docs.yml: optional, and masked like checks.yml (only re-synced where
91
139
  // the repo already carries it). Every copy predating the mask was written
92
140
  // verbatim with the sentinel, so a copy with no key of its own takes the one
@@ -307,7 +355,7 @@ function syncWithDisplay(repoRoot: string, canonical: string, r: SyncReport): vo
307
355
  // carry one. It is CREATED only where the gate would accept it: a repo with a
308
356
  // site (there is something to publish), named, and not a turnkey (a turnkey has
309
357
  // no public site, ADR-0012). That is how a product that grows a site acquires
310
- // its publish: the next sync-dev-kit run installs it, green until the Pages
358
+ // its publish: the next nightly bump installs it, green until the Pages
311
359
  // project exists. A name this writer cannot determine creates nothing, since
312
360
  // the gate could only refuse the result.
313
361
  const TURNKEY_REPO = /^norsk-ctl-turnkey-/;
@@ -338,7 +386,7 @@ function syncPublishDocsSite(repoRoot: string, canonical: string, repoName: stri
338
386
  // Only for a repo that HAS a site: sync never creates one (a site is a nested
339
387
  // standalone package with its own install, not a file this writer can conjure),
340
388
  // it only keeps an existing one's copies in step. That is what makes a theme
341
- // change roll out through sync-dev-kit.yml like any other convention.
389
+ // change roll out through bump.yml like any other convention.
342
390
  function syncDocsTheme(repoRoot: string, r: SyncReport): void {
343
391
  if (!hasDocsSite(repoRoot)) {
344
392
  r.skipped.push(`${DOCS_SITE_DIR}/ docs theme (no docs site)`);
@@ -372,21 +420,12 @@ export function syncDrift(
372
420
  writeIfChanged(join(repoRoot, "tsconfig.base.json"), canonical.tsconfigBase, "tsconfig.base.json", r.written);
373
421
 
374
422
  syncWorkflow(repoRoot, ".github/workflows/checks.yml", checksCanon, r);
375
- // upgrade-latest too: an sdk-app has no Studio pair, so it has its own variant
376
- const upgradeCanon = shape === "sdk-app" ? canonical.upgradeLatestSdkApp : canonical.upgradeLatest;
377
- if (upgradeCanon === undefined) throw new Error(`syncDrift: shape "${shape}" requires canonical.upgradeLatestSdkApp`);
378
- syncWorkflow(repoRoot, ".github/workflows/upgrade-latest.yml", upgradeCanon, r);
379
- syncWorkflow(repoRoot, ".github/workflows/sync-dev-kit.yml", canonical.syncDevKit, r);
380
- // No per-repo line to preserve, so it is written whole -- and CREATED where
381
- // absent, unlike syncWorkflow's copies. That is how a repo acquires the
382
- // workflow at all: its next sync run installs it.
383
- writeIfChanged(
384
- join(repoRoot, ".github/workflows/sync-ctl-packages.yml"),
385
- canonical.syncCtlPackages,
386
- ".github/workflows/sync-ctl-packages.yml",
387
- r.written,
388
- );
389
- // bot-prs.yml likewise: verbatim, and created where absent.
423
+ // bump.yml: an sdk-app has no Studio pair, so it has its own variant.
424
+ const bumpCanon = shape === "sdk-app" ? canonical.bumpSdkApp : canonical.bump;
425
+ if (bumpCanon === undefined) throw new Error(`syncDrift: shape "${shape}" requires canonical.bumpSdkApp`);
426
+ syncBump(repoRoot, bumpCanon, r);
427
+ // bot-prs.yml: no per-repo line to preserve, so it is written whole -- and
428
+ // CREATED where absent. That is how a repo acquires it: its next sync installs it.
390
429
  writeIfChanged(
391
430
  join(repoRoot, ".github/workflows/bot-prs.yml"),
392
431
  canonical.botPrs,
@@ -428,9 +467,7 @@ if (import.meta.main) {
428
467
  core: readFileSync(join(dir, "CLAUDE.core.md"), "utf8"),
429
468
  flake: readFileSync(join(dir, "..", "build", "flake.nix"), "utf8"),
430
469
  ctlShell: readFileSync(join(dir, "..", "build", "ctl-shell.nix"), "utf8"),
431
- upgradeLatest: readFileSync(join(dir, "upgrade-latest.yml"), "utf8"),
432
- syncDevKit: readFileSync(join(dir, "sync-dev-kit.yml"), "utf8"),
433
- syncCtlPackages: readFileSync(join(dir, "sync-ctl-packages.yml"), "utf8"),
470
+ bump: readFileSync(join(dir, "bump.yml"), "utf8"),
434
471
  botPrs: readFileSync(join(dir, "bot-prs.yml"), "utf8"),
435
472
  publishDocs: readFileSync(join(dir, "publish-docs.yml"), "utf8"),
436
473
  buildDocs: readFileSync(join(dir, "build-docs.yml"), "utf8"),
@@ -448,7 +485,7 @@ if (import.meta.main) {
448
485
  demoShim: readFileSync(join(dir, "demo.sh"), "utf8"),
449
486
  flakeSdkApp: readFileSync(join(dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
450
487
  checksSdkApp: readFileSync(join(dir, "checks.sdk-app.yml"), "utf8"),
451
- upgradeLatestSdkApp: readFileSync(join(dir, "upgrade-latest.sdk-app.yml"), "utf8"),
488
+ bumpSdkApp: readFileSync(join(dir, "bump.sdk-app.yml"), "utf8"),
452
489
  };
453
490
  const report = syncDrift(repoRoot, canonical, resolveShape(repoRoot), { repoName: resolveRepoName(repoRoot) });
454
491
  if (report.written.length === 0) {
@@ -18,9 +18,7 @@ export interface Canon {
18
18
  /** build/ctl-shell.nix, emitted as nix/ctl-shell.nix — the flake imports it. */
19
19
  ctlShell: string;
20
20
  flakeLock: string;
21
- upgradeLatest: string;
22
- syncDevKit: string;
23
- syncCtlPackages: string;
21
+ bump: string;
24
22
  botPrs: string;
25
23
  checks: string;
26
24
  biome: string;
@@ -45,9 +43,7 @@ export function loadCanon(): Canon {
45
43
  flake: readFileSync(join(buildDir, "flake.nix"), "utf8"),
46
44
  ctlShell: readFileSync(join(buildDir, "ctl-shell.nix"), "utf8"),
47
45
  flakeLock: readFileSync(join(buildDir, "flake.lock"), "utf8"),
48
- upgradeLatest: readFileSync(join(conventionsDir, "upgrade-latest.yml"), "utf8"),
49
- syncDevKit: readFileSync(join(conventionsDir, "sync-dev-kit.yml"), "utf8"),
50
- syncCtlPackages: readFileSync(join(conventionsDir, "sync-ctl-packages.yml"), "utf8"),
46
+ bump: readFileSync(join(conventionsDir, "bump.yml"), "utf8"),
51
47
  botPrs: readFileSync(join(conventionsDir, "bot-prs.yml"), "utf8"),
52
48
  checks: readFileSync(join(conventionsDir, "checks.yml"), "utf8"),
53
49
  buildDocs: readFileSync(join(conventionsDir, "build-docs.yml"), "utf8"),
@@ -220,9 +220,7 @@ function conventionFiles(ctx: ShapeContext, shape: ShapeModule): GeneratedFile[]
220
220
  },
221
221
  { path: ".gitignore", content: `${canon.gitignoreCore}\n${shape.gitignoreTail(ctx)}` },
222
222
  { path: ".github/workflows/checks.yml", content: fillProduct(canon.checks) },
223
- { path: ".github/workflows/upgrade-latest.yml", content: fillProduct(canon.upgradeLatest) },
224
- { path: ".github/workflows/sync-dev-kit.yml", content: fillProduct(canon.syncDevKit) },
225
- { path: ".github/workflows/sync-ctl-packages.yml", content: canon.syncCtlPackages },
223
+ { path: ".github/workflows/bump.yml", content: fillProduct(canon.bump) },
226
224
  { path: ".github/workflows/bot-prs.yml", content: canon.botPrs },
227
225
  { path: ".github/workflows/build-image.yml", content: fillProduct(canon.buildImage) },
228
226
  { path: ".github/actions/ci-status-dispatch/action.yml", content: loadAsset("ci-status-dispatch.yml") },
@@ -228,7 +228,7 @@ machine-writable source of truth — RFC 0001 Workstream C) via
228
228
  \`shared/src/version.ts\`, which validates the seed through the SDK's
229
229
  \`parseManifestSeed\` at module load. \`version.test.ts\` fails if the seed and
230
230
  the \`norsk-sdk\`/\`norsk-studio*\` npm pins in \`shared/package.json\` drift
231
- apart — the shared \`upgrade-latest.yml\` bumps them all in lockstep nightly.
231
+ apart — the shared \`bump.yml\` bumps them all in lockstep nightly.
232
232
 
233
233
  ## The stored-template trap
234
234
 
@@ -537,7 +537,7 @@ describe("npm pins stay coupled to the seed", () => {
537
537
  // the studio npm packages and the studio Docker image from different commits,
538
538
  // so the image rides the newest published Docker Hub tag on the same
539
539
  // major.minor.patch line as the npm nightly (exactly how the shared
540
- // upgrade-latest.yml advances the pair).
540
+ // bump.yml advances the pair).
541
541
  it("studio image stays on the npm pin's major.minor.patch line", () => {
542
542
  expect(STUDIO_IMAGE_TAG.split("-")[0]).toBe(studioNpmPin.split("-")[0]);
543
543
  });
@@ -85,7 +85,7 @@ 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 like any
88
+ half, so `bump.yml` rolls a theme change out to every product like any
89
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.64",
3
+ "version": "0.2.65",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",