@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.
- package/build/flake.nix +1 -1
- package/build/flake.sdk-app.nix +2 -2
- package/build/refresh-ctl-pin.sh +1 -1
- package/conventions/CLAUDE.core.md +10 -7
- package/conventions/bot-prs.yml +91 -61
- package/conventions/build-docs.yml +2 -2
- package/conventions/build-image.yml +8 -7
- package/conventions/bump.sdk-app.yml +808 -0
- package/conventions/bump.yml +916 -0
- package/conventions/check-drift.ts +52 -60
- package/conventions/ctl-pin-agreement.ts +3 -4
- package/conventions/docs-site.md +3 -3
- package/conventions/integration-testing.md +1 -1
- package/conventions/integration.yml +6 -5
- package/conventions/publish-docs.yml +2 -2
- package/conventions/smoke.yml +2 -2
- package/conventions/sync-drift.ts +61 -24
- package/create-product/canon.ts +2 -6
- package/create-product/create-product.ts +1 -3
- package/create-product/turnkey.ts +2 -2
- package/docs-theme/README.md +1 -1
- package/package.json +1 -1
- package/conventions/sync-ctl-packages.yml +0 -309
- package/conventions/sync-dev-kit.yml +0 -404
- package/conventions/upgrade-latest.sdk-app.yml +0 -456
- package/conventions/upgrade-latest.yml +0 -567
|
@@ -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,
|
|
9
|
-
//
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
*
|
|
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/
|
|
309
|
-
*
|
|
310
|
-
* the SDK nightly. Optional for the same reason as flakeSdkApp. */
|
|
311
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
push(
|
|
716
|
-
|
|
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
|
-
//
|
|
742
|
-
//
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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:
|
|
750
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
10
|
-
//
|
|
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 (
|
|
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;
|
package/conventions/docs-site.md
CHANGED
|
@@ -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 `
|
|
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
|
|
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 `
|
|
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, `
|
|
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 (
|
|
43
|
-
# sync-dev-kit
|
|
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
|
|
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 (
|
|
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
|
-
#
|
|
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 (
|
|
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"
|
package/conventions/smoke.yml
CHANGED
|
@@ -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
|
|
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 (
|
|
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 /
|
|
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 /
|
|
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
|
|
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
|
|
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
|
-
//
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/create-product/canon.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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/
|
|
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 \`
|
|
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
|
-
//
|
|
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
|
});
|
package/docs-theme/README.md
CHANGED
|
@@ -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 `
|
|
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
|
|