@norskvideo/ctl-dev-kit 0.2.63 → 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 +16 -7
- package/conventions/bot-prs.yml +91 -61
- package/conventions/build-docs.yml +27 -2
- package/conventions/build-image.yml +290 -22
- package/conventions/bump.sdk-app.yml +808 -0
- package/conventions/bump.yml +916 -0
- package/conventions/check-drift.ts +52 -60
- package/conventions/checks.sdk-app.yml +20 -1
- package/conventions/checks.yml +20 -1
- 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 +31 -5
- package/conventions/publish-docs.yml +107 -12
- package/conventions/quarantine.ts +209 -0
- package/conventions/smoke.yml +27 -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/doc-guide/engine-verdict.js +3 -3
- 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),
|
|
@@ -127,6 +127,9 @@ jobs:
|
|
|
127
127
|
|
|
128
128
|
quality:
|
|
129
129
|
runs-on: x64
|
|
130
|
+
outputs:
|
|
131
|
+
quarantined: ${{ steps.quarantine.outputs.quarantined }}
|
|
132
|
+
quarantine_failed: ${{ steps.quarantine.outputs.quarantine_failed }}
|
|
130
133
|
steps:
|
|
131
134
|
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
132
135
|
# out of the workspace for every later step in this job, and clears whatever
|
|
@@ -200,7 +203,11 @@ jobs:
|
|
|
200
203
|
bun install --frozen-lockfile
|
|
201
204
|
bun run lint
|
|
202
205
|
bun run typecheck
|
|
203
|
-
|
|
206
|
+
# Known-flaky tests (.github/quarantine.json) leave the gating run
|
|
207
|
+
# here and run on their own in the next step (conventions/quarantine.ts).
|
|
208
|
+
q=node_modules/@norskvideo/ctl-dev-kit/conventions/quarantine.ts
|
|
209
|
+
bun "$q" check
|
|
210
|
+
BUN_OPTIONS="$(bun "$q" bun-options)" bun run test:unit
|
|
204
211
|
bun run docs:check
|
|
205
212
|
# The tree must resolve ONE zod 4 (conventions/zod-singleton.ts).
|
|
206
213
|
# studio guards this in-repo but never shipped the guard; products
|
|
@@ -208,6 +215,16 @@ jobs:
|
|
|
208
215
|
# in here and breaks a schema handed across the instance boundary.
|
|
209
216
|
bun node_modules/@norskvideo/ctl-dev-kit/conventions/zod-singleton.ts
|
|
210
217
|
'
|
|
218
|
+
# The quarantined tests, each run on its own and reported: a summary
|
|
219
|
+
# table, a warning per failure, and counts for the checks cell's detail.
|
|
220
|
+
# It never fails the job -- that is what quarantine means -- and it runs
|
|
221
|
+
# even when the gate is red, so the flake's record has no holes. Nothing
|
|
222
|
+
# listed, nothing run.
|
|
223
|
+
- name: Quarantined tests (run and reported, not gating)
|
|
224
|
+
id: quarantine
|
|
225
|
+
if: ${{ !cancelled() }}
|
|
226
|
+
continue-on-error: true
|
|
227
|
+
run: nix develop .#build --command bun node_modules/@norskvideo/ctl-dev-kit/conventions/quarantine.ts run
|
|
211
228
|
# licenseTemplate.json records the entry a licence is minted from: this
|
|
212
229
|
# product at the image CI builds. One naming another image mints a licence
|
|
213
230
|
# that entitles nothing, first seen as a refused launch at the customer.
|
|
@@ -335,6 +352,8 @@ jobs:
|
|
|
335
352
|
product: __PRODUCT__
|
|
336
353
|
pipeline: checks
|
|
337
354
|
status: ${{ steps.meta.outputs.state }}
|
|
355
|
+
# Quarantined tests leave the gate, never the board: say how many.
|
|
356
|
+
detail: "${{ needs.quality.outputs.quarantined > 0 && format('quarantined: {0} test(s), {1} not passing this run (not gating; .github/quarantine.json)', needs.quality.outputs.quarantined, needs.quality.outputs.quarantine_failed) || '' }}"
|
|
338
357
|
- name: Reap the containers this job launched
|
|
339
358
|
# Whatever the tests did. A teardown cut short by a test timeout leaves
|
|
340
359
|
# its instance running on the shared Docker daemon (see JOB_REAP_STEP in
|
package/conventions/checks.yml
CHANGED
|
@@ -130,6 +130,9 @@ jobs:
|
|
|
130
130
|
|
|
131
131
|
quality:
|
|
132
132
|
runs-on: x64
|
|
133
|
+
outputs:
|
|
134
|
+
quarantined: ${{ steps.quarantine.outputs.quarantined }}
|
|
135
|
+
quarantine_failed: ${{ steps.quarantine.outputs.quarantine_failed }}
|
|
133
136
|
steps:
|
|
134
137
|
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
135
138
|
# out of the workspace for every later step in this job, and clears whatever
|
|
@@ -203,7 +206,11 @@ jobs:
|
|
|
203
206
|
bun install --frozen-lockfile
|
|
204
207
|
bun run lint
|
|
205
208
|
bun run typecheck
|
|
206
|
-
|
|
209
|
+
# Known-flaky tests (.github/quarantine.json) leave the gating run
|
|
210
|
+
# here and run on their own in the next step (conventions/quarantine.ts).
|
|
211
|
+
q=node_modules/@norskvideo/ctl-dev-kit/conventions/quarantine.ts
|
|
212
|
+
bun "$q" check
|
|
213
|
+
BUN_OPTIONS="$(bun "$q" bun-options)" bun run test:unit
|
|
207
214
|
bun run docs:check
|
|
208
215
|
# The tree must resolve ONE zod 4 (conventions/zod-singleton.ts).
|
|
209
216
|
# studio guards this in-repo but never shipped the guard; products
|
|
@@ -211,6 +218,16 @@ jobs:
|
|
|
211
218
|
# in here and breaks a schema handed across the instance boundary.
|
|
212
219
|
bun node_modules/@norskvideo/ctl-dev-kit/conventions/zod-singleton.ts
|
|
213
220
|
'
|
|
221
|
+
# The quarantined tests, each run on its own and reported: a summary
|
|
222
|
+
# table, a warning per failure, and counts for the checks cell's detail.
|
|
223
|
+
# It never fails the job -- that is what quarantine means -- and it runs
|
|
224
|
+
# even when the gate is red, so the flake's record has no holes. Nothing
|
|
225
|
+
# listed, nothing run.
|
|
226
|
+
- name: Quarantined tests (run and reported, not gating)
|
|
227
|
+
id: quarantine
|
|
228
|
+
if: ${{ !cancelled() }}
|
|
229
|
+
continue-on-error: true
|
|
230
|
+
run: nix develop .#build --command bun node_modules/@norskvideo/ctl-dev-kit/conventions/quarantine.ts run
|
|
214
231
|
|
|
215
232
|
# The two halves of one licence guarantee, run together because they only
|
|
216
233
|
# work as a pair: check-template asserts this repo RECORDS the entitlement
|
|
@@ -356,6 +373,8 @@ jobs:
|
|
|
356
373
|
product: __PRODUCT__
|
|
357
374
|
pipeline: checks
|
|
358
375
|
status: ${{ steps.meta.outputs.state }}
|
|
376
|
+
# Quarantined tests leave the gate, never the board: say how many.
|
|
377
|
+
detail: "${{ needs.quality.outputs.quarantined > 0 && format('quarantined: {0} test(s), {1} not passing this run (not gating; .github/quarantine.json)', needs.quality.outputs.quarantined, needs.quality.outputs.quarantine_failed) || '' }}"
|
|
359
378
|
- name: Reap the containers this job launched
|
|
360
379
|
# Whatever the tests did. A teardown cut short by a test timeout leaves
|
|
361
380
|
# its instance running on the shared Docker daemon (see JOB_REAP_STEP in
|
|
@@ -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:
|
|
@@ -283,12 +284,37 @@ jobs:
|
|
|
283
284
|
NORSK_LICENSE_V2: ${{ secrets.NORSK_LICENSE_V2 }}
|
|
284
285
|
run: printf '%s' "$NORSK_LICENSE_V2" > "$RUNNER_TEMP/norsk-license.json"
|
|
285
286
|
|
|
287
|
+
# Every norsk-ctl daemon starts the node MoQ relay (norsk-moq-relay) unless
|
|
288
|
+
# told not to, and nothing removes it: it is a node singleton, and the reap
|
|
289
|
+
# step only takes containers that mount this job's test-temp. So the first
|
|
290
|
+
# test daemon on a shared runner left one, and every later one adopted it
|
|
291
|
+
# (probe's smoke ran in relay mode on 2026-10-10; relay's smoke lost 4443
|
|
292
|
+
# to it). This job's daemons run without the node relay and discovery, so
|
|
293
|
+
# they neither start nor adopt one, and whatever is already on the host is
|
|
294
|
+
# named here. It reports and never fails: the leftover may be someone's.
|
|
295
|
+
- name: Isolate this job from node singletons other jobs left on this host
|
|
296
|
+
run: |
|
|
297
|
+
set -uo pipefail
|
|
298
|
+
echo "NORSK_CTL_MOQ_RELAY_ENABLE=0" >> "$GITHUB_ENV"
|
|
299
|
+
echo "NORSK_CTL_DISCOVERY_ENABLE=0" >> "$GITHUB_ENV"
|
|
300
|
+
left="$(docker ps -a --filter label=norsk-ctl.role --format '{{.Names}} {{.Label "norsk-ctl.role"}} {{.Status}} (created {{.CreatedAt}})' 2>/dev/null || true)"
|
|
301
|
+
if [ -z "$left" ]; then echo "no norsk-ctl containers on this host"; exit 0; fi
|
|
302
|
+
echo "norsk-ctl containers already on this host (other jobs' running instances are expected):"
|
|
303
|
+
echo "$left"
|
|
304
|
+
while IFS= read -r line; do
|
|
305
|
+
role="$(awk '{print $2}' <<<"$line")"
|
|
306
|
+
case "$role" in
|
|
307
|
+
moq-relay|discovery)
|
|
308
|
+
echo "::warning title=node singleton left on this host::$line -- left by an earlier job (a daemon's auto-start, or a run killed before its reap). This job's daemons run without it and will not adopt it; remove it with docker rm -f if nothing on this host needs it." ;;
|
|
309
|
+
esac
|
|
310
|
+
done <<<"$left"
|
|
311
|
+
|
|
286
312
|
# Pick the binary for THIS runner's architecture: the GPU box is aarch64,
|
|
287
313
|
# the x64 pool is x86_64, and the wrong one dies with "Exec format error"
|
|
288
314
|
# deep in the suite. No `|| true` on --version: a bad binary must fail here.
|
|
289
315
|
#
|
|
290
316
|
# WHICH binary: the version this commit pins, flake.nix ctlVersion -- the
|
|
291
|
-
# daemon the dev shell launches, moved by
|
|
317
|
+
# daemon the dev shell launches, moved by bump.yml's nightly PR. It used
|
|
292
318
|
# to be S3's moving `latest`, read at run time, so a result could change
|
|
293
319
|
# with no product commit and a re-run was never a real repeat (funke went
|
|
294
320
|
# red and green again on 2026-10-09 without a commit either time;
|
|
@@ -311,7 +337,7 @@ jobs:
|
|
|
311
337
|
echo "norsk-ctl $CTL_CHANNEL channel -> $ver ($arch), in place of the repo pin"
|
|
312
338
|
else
|
|
313
339
|
ver="$( { sed -nE 's/^[[:space:]]*ctlVersion = "([^"]+)";.*/\1/p' flake.nix 2>/dev/null || true; } | head -1)"
|
|
314
|
-
[ -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; }
|
|
315
341
|
echo "norsk-ctl pinned by flake.nix ctlVersion -> $ver ($arch)"
|
|
316
342
|
fi
|
|
317
343
|
curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$S3/$ver/norsk-ctl-$ver-$arch" -o "$RUNNER_TEMP/norsk-ctl"
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
#
|
|
23
23
|
# Nightly-only by design: a guide's engine tier stands up real instances (slow,
|
|
24
24
|
# and subject to teardown flakes), so it does not ride every push. The
|
|
25
|
+
# deterministic tiers decide this workflow's result; the engine tier reports
|
|
26
|
+
# without failing it (norsk-ctl task 57), and a failed engine tier keeps
|
|
27
|
+
# yesterday's manual rather than publishing one with gaps. The
|
|
25
28
|
# licensed-runner setup mirrors integration.yml — ctl comes from the released
|
|
26
29
|
# binary via NORSK_CTL_BINARY, Docker is used directly on the x64 pool.
|
|
27
30
|
name: publish-docs
|
|
@@ -46,6 +49,8 @@ concurrency:
|
|
|
46
49
|
jobs:
|
|
47
50
|
publish:
|
|
48
51
|
runs-on: x64
|
|
52
|
+
outputs:
|
|
53
|
+
engine: ${{ steps.engine.outcome }}
|
|
49
54
|
# One guide run per repo at a time, nightly and publish gate alike: both
|
|
50
55
|
# launch the same instance ids on the x64 pool's shared docker daemon, and
|
|
51
56
|
# ctl names each compose project after its instance id. Queue, never cancel.
|
|
@@ -174,15 +179,40 @@ jobs:
|
|
|
174
179
|
NORSK_LICENSE_V2: ${{ secrets.NORSK_LICENSE_V2 }}
|
|
175
180
|
run: printf '%s' "$NORSK_LICENSE_V2" > "$RUNNER_TEMP/norsk-license.json"
|
|
176
181
|
|
|
182
|
+
# Every norsk-ctl daemon starts the node MoQ relay (norsk-moq-relay) unless
|
|
183
|
+
# told not to, and nothing removes it: it is a node singleton, and the reap
|
|
184
|
+
# step only takes containers that mount this job's test-temp. So the first
|
|
185
|
+
# test daemon on a shared runner left one, and every later one adopted it
|
|
186
|
+
# (probe's smoke ran in relay mode on 2026-10-10; relay's smoke lost 4443
|
|
187
|
+
# to it). This job's daemons run without the node relay and discovery, so
|
|
188
|
+
# they neither start nor adopt one, and whatever is already on the host is
|
|
189
|
+
# named here. It reports and never fails: the leftover may be someone's.
|
|
190
|
+
- name: Isolate this job from node singletons other jobs left on this host
|
|
191
|
+
run: |
|
|
192
|
+
set -uo pipefail
|
|
193
|
+
echo "NORSK_CTL_MOQ_RELAY_ENABLE=0" >> "$GITHUB_ENV"
|
|
194
|
+
echo "NORSK_CTL_DISCOVERY_ENABLE=0" >> "$GITHUB_ENV"
|
|
195
|
+
left="$(docker ps -a --filter label=norsk-ctl.role --format '{{.Names}} {{.Label "norsk-ctl.role"}} {{.Status}} (created {{.CreatedAt}})' 2>/dev/null || true)"
|
|
196
|
+
if [ -z "$left" ]; then echo "no norsk-ctl containers on this host"; exit 0; fi
|
|
197
|
+
echo "norsk-ctl containers already on this host (other jobs' running instances are expected):"
|
|
198
|
+
echo "$left"
|
|
199
|
+
while IFS= read -r line; do
|
|
200
|
+
role="$(awk '{print $2}' <<<"$line")"
|
|
201
|
+
case "$role" in
|
|
202
|
+
moq-relay|discovery)
|
|
203
|
+
echo "::warning title=node singleton left on this host::$line -- left by an earlier job (a daemon's auto-start, or a run killed before its reap). This job's daemons run without it and will not adopt it; remove it with docker rm -f if nothing on this host needs it." ;;
|
|
204
|
+
esac
|
|
205
|
+
done <<<"$left"
|
|
206
|
+
|
|
177
207
|
# The norsk-ctl this commit pins (flake.nix ctlVersion, moved by
|
|
178
|
-
#
|
|
208
|
+
# bump.yml's nightly PR), never S3's moving `latest`: a guide run is then
|
|
179
209
|
# a real repeat, and a red belongs to a commit (ci-fragility proposal A).
|
|
180
210
|
- name: Download the pinned norsk-ctl binary (flake.nix ctlVersion)
|
|
181
211
|
run: |
|
|
182
212
|
set -euo pipefail
|
|
183
213
|
S3="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
|
|
184
214
|
ver="$( { sed -nE 's/^[[:space:]]*ctlVersion = "([^"]+)";.*/\1/p' flake.nix 2>/dev/null || true; } | head -1)"
|
|
185
|
-
[ -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; }
|
|
186
216
|
echo "norsk-ctl pinned by flake.nix ctlVersion -> $ver"
|
|
187
217
|
curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$S3/$ver/norsk-ctl-$ver-linux-x64" -o "$RUNNER_TEMP/norsk-ctl"
|
|
188
218
|
chmod +x "$RUNNER_TEMP/norsk-ctl"
|
|
@@ -204,13 +234,15 @@ jobs:
|
|
|
204
234
|
./scripts/doc-guide/prepull-extra.sh
|
|
205
235
|
fi
|
|
206
236
|
|
|
207
|
-
#
|
|
208
|
-
# the
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
|
|
237
|
+
# The deterministic tiers (fixture and frontend: no daemon, no instances),
|
|
238
|
+
# then the manual assembled from what they captured. These gate. The engine
|
|
239
|
+
# tier runs on its own below: it stands up real instances, is known-flaky
|
|
240
|
+
# on container DNS (norsk-ctl task 57), and reports without gating until
|
|
241
|
+
# that is fixed (ci-workflows docs/dashboard/ci-fragility.md, proposal E).
|
|
242
|
+
# regen-manual.sh owns the slug list and honours SKIP_ENGINE; it rebuilds
|
|
243
|
+
# every workspace the manual depends on first, and with-display.sh gives
|
|
244
|
+
# headless chromium an X display.
|
|
245
|
+
- name: Regenerate the illustrated manual (deterministic tiers)
|
|
214
246
|
env:
|
|
215
247
|
NORSK_CTL_BINARY: ${{ runner.temp }}/norsk-ctl
|
|
216
248
|
NORSK_LICENSE_FILE: ${{ runner.temp }}/norsk-license.json
|
|
@@ -223,6 +255,7 @@ jobs:
|
|
|
223
255
|
# them binding no host ports — so concurrent doc-guide instances can't
|
|
224
256
|
# collide on the product's deterministic host-port bands.
|
|
225
257
|
NORSK_TEST_NET: direct
|
|
258
|
+
SKIP_ENGINE: "1"
|
|
226
259
|
run: |
|
|
227
260
|
nix develop .#build --command bash -c '
|
|
228
261
|
set -euo pipefail
|
|
@@ -237,11 +270,71 @@ jobs:
|
|
|
237
270
|
bash scripts/with-display.sh scripts/doc-guide/regen-manual.sh
|
|
238
271
|
'
|
|
239
272
|
|
|
273
|
+
# The engine tier: real instances, driven in a browser. Reported, not
|
|
274
|
+
# gating (see the deterministic step). A non-zero exit is forgiven only
|
|
275
|
+
# when the tier's own results say a teardown hook timed out after every
|
|
276
|
+
# guide's test passed (doc-guide/engine-verdict.ts), as regen-manual.sh
|
|
277
|
+
# forgives it. A product with no docs:guides:engine script has no engine
|
|
278
|
+
# tier. Runs on the workspaces the step before it installed and built.
|
|
279
|
+
- name: Run the engine-tier guides (reported, not gating)
|
|
280
|
+
id: engine
|
|
281
|
+
continue-on-error: true
|
|
282
|
+
env:
|
|
283
|
+
NORSK_CTL_BINARY: ${{ runner.temp }}/norsk-ctl
|
|
284
|
+
NORSK_LICENSE_FILE: ${{ runner.temp }}/norsk-license.json
|
|
285
|
+
# This runner launches Studio as a HOST sibling (DooD), so the harness
|
|
286
|
+
# reaches host-published ports via the host-gateway alias, not
|
|
287
|
+
# localhost — same as the integration suite.
|
|
288
|
+
NORSK_TEST_HOST: host.docker.internal
|
|
289
|
+
# Reach launched instances over norsk-net by service DNS (as the
|
|
290
|
+
# integration tier already does) AND, on a ctl that supports it, launch
|
|
291
|
+
# them binding no host ports — so concurrent doc-guide instances can't
|
|
292
|
+
# collide on the product's deterministic host-port bands.
|
|
293
|
+
NORSK_TEST_NET: direct
|
|
294
|
+
run: |
|
|
295
|
+
nix develop .#build --command bash -c '
|
|
296
|
+
set -euo pipefail
|
|
297
|
+
if ! jq -e ".scripts[\"docs:guides:engine\"]" package.json >/dev/null; then
|
|
298
|
+
echo "no docs:guides:engine script: this product has no engine tier"
|
|
299
|
+
exit 0
|
|
300
|
+
fi
|
|
301
|
+
export DOCS_GENERATE=1
|
|
302
|
+
log="$(mktemp)"
|
|
303
|
+
if bash scripts/with-display.sh bun run docs:guides:engine >"$log" 2>&1; then
|
|
304
|
+
cat "$log"
|
|
305
|
+
else
|
|
306
|
+
cat "$log"
|
|
307
|
+
verdict="$(bun -e "console.log(Bun.resolveSync(\"@norskvideo/ctl-dev-kit/doc-guide/engine-verdict\", process.cwd()))")"
|
|
308
|
+
bun "$verdict" "$log"
|
|
309
|
+
fi
|
|
310
|
+
'
|
|
311
|
+
|
|
312
|
+
# Only after a passing engine tier: the deterministic step assembled a
|
|
313
|
+
# manual with the engine pages as gaps.
|
|
314
|
+
- name: Assemble the manual with the engine captures
|
|
315
|
+
if: ${{ steps.engine.outcome == 'success' }}
|
|
316
|
+
run: nix develop .#build --command bun run docs:manual
|
|
317
|
+
|
|
318
|
+
# Said three ways, so a failure is never silent: a warning on the run, a
|
|
319
|
+
# line in its summary, and the cell's detail (the notify job).
|
|
320
|
+
- name: Report the engine tier (not gating)
|
|
321
|
+
if: ${{ !cancelled() }}
|
|
322
|
+
env:
|
|
323
|
+
OUTCOME: ${{ steps.engine.outcome }}
|
|
324
|
+
run: |
|
|
325
|
+
case "$OUTCOME" in
|
|
326
|
+
success) echo "engine-tier guides passed (or this product has none)" ;;
|
|
327
|
+
failure)
|
|
328
|
+
echo "::warning title=engine-tier guides failed (not gating)::the engine tier does not gate until norsk-ctl task 57 is fixed. The manual was not republished: yesterday's stays. See the engine step's log."
|
|
329
|
+
printf '### Engine-tier guides: failed (not gating)\n\nThe deterministic guide tiers gate; the engine tier only reports until its container networking is fixed (norsk-ctl task 57). The manual was not republished: yesterday's stays. See the step log.\n' >> "$GITHUB_STEP_SUMMARY" ;;
|
|
330
|
+
*) echo "engine tier: ${OUTCOME:-not run}" ;;
|
|
331
|
+
esac
|
|
332
|
+
|
|
240
333
|
# Publish the assembled manual as the moving docs-latest release asset.
|
|
241
334
|
# gh is on PATH inside the nix shell; `upload --clobber` replaces the
|
|
242
335
|
# same-named asset in place, so the release stays latest-only.
|
|
243
336
|
- name: Publish manual.html to the moving docs-latest release
|
|
244
|
-
if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }}
|
|
337
|
+
if: ${{ steps.engine.outcome == 'success' && (github.event_name != 'workflow_dispatch' || inputs.publish) }}
|
|
245
338
|
env:
|
|
246
339
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
247
340
|
run: |
|
|
@@ -290,7 +383,7 @@ jobs:
|
|
|
290
383
|
# exports map, so this works whether ctl-dev-kit is the published package or
|
|
291
384
|
# a workspace link (scripts/set-ctl-source.ts local mode).
|
|
292
385
|
- name: Publish the versioned docs bundle and move the pointer
|
|
293
|
-
if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }}
|
|
386
|
+
if: ${{ steps.engine.outcome == 'success' && (github.event_name != 'workflow_dispatch' || inputs.publish) }}
|
|
294
387
|
env:
|
|
295
388
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
296
389
|
# How many bundles to keep on the release, the current one included.
|
|
@@ -325,7 +418,7 @@ jobs:
|
|
|
325
418
|
# key through ci-workflows' registry, so a new product needs no new mapping
|
|
326
419
|
# here. Not wrapped in nix -- plain curl, and the quoting stays readable.
|
|
327
420
|
- name: Tell norsk-ctl a new bundle is published
|
|
328
|
-
if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }}
|
|
421
|
+
if: ${{ steps.engine.outcome == 'success' && (github.event_name != 'workflow_dispatch' || inputs.publish) }}
|
|
329
422
|
env:
|
|
330
423
|
DISPATCH_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
331
424
|
run: |
|
|
@@ -437,6 +530,8 @@ jobs:
|
|
|
437
530
|
product: __PRODUCT__
|
|
438
531
|
pipeline: docs
|
|
439
532
|
status: ${{ steps.meta.outputs.state }}
|
|
533
|
+
# The engine tier reports, never gates; a failure keeps yesterday's manual.
|
|
534
|
+
detail: "${{ needs.publish.outputs.engine == 'failure' && 'engine-tier guides failed (not gating; norsk-ctl task 57): the manual was not republished' || '' }}"
|
|
440
535
|
- name: Reap the containers this job launched
|
|
441
536
|
# Whatever the tests did. A teardown cut short by a test timeout leaves
|
|
442
537
|
# its instance running on the shared Docker daemon (see JOB_REAP_STEP in
|