@norskvideo/ctl-dev-kit 0.1.44 → 0.1.45

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.
@@ -60,6 +60,11 @@ export const PRODUCT_SENTINEL = "__PRODUCT__";
60
60
  // unfilled-sentinel check.
61
61
  export const PUBLISH_IMAGE_LINE = /^(\s*PUBLISH_IMAGE:\s*).*$/gm;
62
62
 
63
+ // smoke.yml carries a second per-repo line: the matrix `runner:` label array
64
+ // the journey runs on ('["x64"]' for the pool, '["ARM64","nvidia"]' for the GPU
65
+ // box). Masked like product:; any filled value is valid, so no sentinel check.
66
+ export const RUNNER_LINE = /^(\s*-?\s*runner:\s*).*$/gm;
67
+
63
68
  function workflowProblem(
64
69
  relPath: string,
65
70
  canonicalRef: string,
@@ -74,7 +79,7 @@ function workflowProblem(
74
79
  if (mask(actual) !== mask(canonical)) {
75
80
  return `${relPath} has drifted from @norskvideo/ctl-dev-kit ${canonicalRef} (${firstDiffLine(mask(actual), mask(canonical))}). ${RESYNC}`;
76
81
  }
77
- if (actual.match(/^\s*product:\s*(\S+)/m)?.[1] === PRODUCT_SENTINEL) {
82
+ if (new RegExp(`^\\s*product:\\s*${PRODUCT_SENTINEL}\\s*$`, "m").test(actual)) {
78
83
  return `${relPath} still has the ${PRODUCT_SENTINEL} placeholder — set \`product:\` to this repo's dashboard key.`;
79
84
  }
80
85
  return undefined;
@@ -239,6 +244,7 @@ export interface CanonicalBytes {
239
244
  buildImageBootstrap: string;
240
245
  gitignoreCore: string;
241
246
  buildImage: string;
247
+ smoke: string;
242
248
  }
243
249
 
244
250
  // A file that must be a verbatim, byte-for-byte copy of its dev-kit canonical.
@@ -397,6 +403,23 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes): DriftRe
397
403
  );
398
404
  }
399
405
 
406
+ // smoke.yml is OPTIONAL until every product carries a tests/smoke.spec.ts
407
+ // (04b s7.2 sequences the required-file rule last). When present it is the
408
+ // shared, single-sourced workflow; only its `product:` dispatch key and the
409
+ // matrix `runner:` label array are per-repo.
410
+ const smokePath = join(repoRoot, ".github", "workflows", "smoke.yml");
411
+ if (existsSync(smokePath)) {
412
+ push(
413
+ workflowProblem(
414
+ ".github/workflows/smoke.yml",
415
+ "conventions/smoke.yml",
416
+ readFileSync(smokePath, "utf8"),
417
+ canonical.smoke,
418
+ [PRODUCT_LINE, RUNNER_LINE],
419
+ ),
420
+ );
421
+ }
422
+
400
423
  // publish-docs.yml is OPTIONAL — a product carries it once it has an
401
424
  // illustrated manual to publish nightly. Unlike the workflows above it has NO
402
425
  // per-repo line: it is deliberately product-agnostic (per-repo bits live in
@@ -430,6 +453,7 @@ if (import.meta.main) {
430
453
  buildImageBootstrap: readFileSync(join(import.meta.dir, "..", "build", "build-image.bootstrap.sh"), "utf8"),
431
454
  gitignoreCore: readFileSync(join(import.meta.dir, "gitignore.core"), "utf8"),
432
455
  buildImage: readFileSync(join(import.meta.dir, "build-image.yml"), "utf8"),
456
+ smoke: readFileSync(join(import.meta.dir, "smoke.yml"), "utf8"),
433
457
  };
434
458
  const report = checkDrift(repoRoot, canonical);
435
459
  if (report.ok) {
@@ -0,0 +1,202 @@
1
+ # The smoke tier, single-sourced in @norskvideo/ctl-dev-kit (conventions/smoke.yml)
2
+ # and drift-checked — the two per-repo lines are the ci-status-dispatch
3
+ # `product:` key and the `runner:` label array.
4
+ #
5
+ # One journey per product (04b s7.2): launch from the stored template, accept
6
+ # the source the manifest advertises, produce the observable it advertises —
7
+ # tests/smoke.spec.ts over @norskvideo/ctl-test-harness/smoke. The image under
8
+ # test is built HERE (`bun run build:image`), on the same runner that holds the
9
+ # licence, so the tier runs on pull requests and needs no cross-workflow state;
10
+ # the bytes build-image pushed are covered by ctl's manifest-driven job from the
11
+ # published tag instead. The daemon is the RELEASED norsk-ctl binary, as a
12
+ # customer would run it.
13
+ #
14
+ # Runner selection is a one-entry matrix carrying a JSON label array, because a
15
+ # string like '[ARM64, nvidia]' in runs-on is ONE label, not two: the x64 pool
16
+ # is '["x64"]', the GPU box '["ARM64","nvidia"]'.
17
+ name: smoke
18
+
19
+ on:
20
+ pull_request:
21
+ push:
22
+ branches: [main]
23
+ repository_dispatch:
24
+ types: [ctl-candidate, pins-bumped]
25
+ workflow_dispatch:
26
+ inputs:
27
+ ctl_channel:
28
+ description: "norsk-ctl S3 channel to test against (release-gate dispatches 'rc' to validate a candidate before promoting latest)."
29
+ type: string
30
+ default: latest
31
+
32
+ permissions:
33
+ contents: read
34
+
35
+ # Key the group on event_name so a push never cancels an in-flight ctl-candidate
36
+ # validation: pushes cancel pushes, candidate validations cancel only newer
37
+ # candidates. A cancelled validation would score as a red at release-collect.
38
+ concurrency:
39
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
40
+ cancel-in-progress: true
41
+
42
+ jobs:
43
+ smoke:
44
+ strategy:
45
+ matrix:
46
+ include:
47
+ - runner: '["x64"]'
48
+ runs-on: ${{ fromJSON(matrix.runner) }}
49
+ steps:
50
+ # BEFORE checkout: launched instances leave root-owned bind-mount target
51
+ # dirs under test-temp/ that the non-root runner user cannot remove, so
52
+ # actions/checkout's own cleanup would fail the job before anything runs.
53
+ # Nuke them from a throwaway root container first. Best-effort.
54
+ - name: Clear stale root-owned test-temp (pre-checkout)
55
+ run: |
56
+ set -uo pipefail
57
+ tt="$GITHUB_WORKSPACE/test-temp"
58
+ [ -d "$tt" ] || exit 0
59
+ docker run --rm --user 0:0 -v "$tt":/t alpine sh \
60
+ -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
61
+
62
+ - uses: actions/checkout@v5
63
+ with:
64
+ clean: false
65
+
66
+ # A containerised runner (docker-outside-of-docker) reaches host-published
67
+ # ports via the host-gateway alias and can join norsk-net for direct reach;
68
+ # a bare-metal host runner (the GPU box) must stay on localhost + publish.
69
+ # Detect rather than hardcode, so one byte-identical file serves both.
70
+ - name: Probe runner docker topology and set NORSK_TEST_HOST + NORSK_NET_REACH
71
+ run: |
72
+ set -uo pipefail
73
+ echo "hostname=$(hostname)"
74
+ if docker inspect "$(hostname)" >/dev/null 2>&1; then
75
+ echo "TOPO: hostname resolves to a container -> runner is containerized (DooD)"
76
+ echo "NORSK_TEST_HOST=host.docker.internal" >> "$GITHUB_ENV"
77
+ echo "NORSK_NET_REACH=direct" >> "$GITHUB_ENV"
78
+ else
79
+ echo "TOPO: hostname is not a container -> runner is host-based"
80
+ echo "NORSK_TEST_HOST=localhost" >> "$GITHUB_ENV"
81
+ echo "NORSK_NET_REACH=publish" >> "$GITHUB_ENV"
82
+ fi
83
+
84
+ - name: Write the Norsk license (from the org secret)
85
+ env:
86
+ NORSK_LICENSE_V2: ${{ secrets.NORSK_LICENSE_V2 }}
87
+ run: printf '%s' "$NORSK_LICENSE_V2" > "$RUNNER_TEMP/norsk-license.json"
88
+
89
+ # Pick the binary for THIS runner's architecture: the GPU box is aarch64,
90
+ # the x64 pool is x86_64, and the wrong one dies with "Exec format error"
91
+ # deep in the suite. No `|| true` on --version: a bad binary must fail here.
92
+ - name: Download the released norsk-ctl binary
93
+ run: |
94
+ set -euo pipefail
95
+ channel="${{ github.event.client_payload.channel || inputs.ctl_channel || 'latest' }}"
96
+ case "$(uname -m)" in
97
+ x86_64) arch=linux-x64 ;;
98
+ aarch64 | arm64) arch=linux-arm64 ;;
99
+ *) echo "unsupported runner arch: $(uname -m)" >&2; exit 1 ;;
100
+ esac
101
+ S3="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
102
+ ver="$(curl -fsSL "$S3/$channel")"
103
+ echo "norsk-ctl $channel channel -> $ver ($arch)"
104
+ curl -fsSL "$S3/$ver/norsk-ctl-$ver-$arch" -o "$RUNNER_TEMP/norsk-ctl"
105
+ chmod +x "$RUNNER_TEMP/norsk-ctl"
106
+ "$RUNNER_TEMP/norsk-ctl" --version
107
+
108
+ # Instance launch docker-composes the media + studio images; on a cold
109
+ # runner the first pull overruns the per-test timeout. Pre-pull them (tags
110
+ # from manifest.seed.json), plus whatever the repo lists in the optional
111
+ # scripts/smoke/prepull-extra.sh hook (a driver image, a sidecar).
112
+ - name: Pre-pull the media + studio images
113
+ run: |
114
+ set -euo pipefail
115
+ for img in "$(jq -r '.latest.media' manifest.seed.json)" "$(jq -r '.latest.studio' manifest.seed.json)"; do
116
+ echo "pre-pulling $img"
117
+ docker pull "$img"
118
+ done
119
+ if [ -x scripts/smoke/prepull-extra.sh ]; then
120
+ echo "running scripts/smoke/prepull-extra.sh"
121
+ ./scripts/smoke/prepull-extra.sh
122
+ fi
123
+
124
+ # The image is built UNDER THE LICENSED REPO: a V2 licence entitles a
125
+ # product by image repo (norskvideo/norsk-<x>-product), so `product add`
126
+ # refuses a throwaway name. That repo is the PUBLISH_IMAGE line
127
+ # build-image.yml already carries per repo; a build-only product (empty)
128
+ # falls back to its build script default, which its own licence must name.
129
+ - name: Build the image and run the smoke journey against it
130
+ env:
131
+ NORSK_CTL_BINARY: ${{ runner.temp }}/norsk-ctl
132
+ NORSK_LICENSE_FILE: ${{ runner.temp }}/norsk-license.json
133
+ run: |
134
+ nix develop .#build --command bash -c '
135
+ set -euo pipefail
136
+ # clean:false keeps node_modules between runs, and a frozen install
137
+ # does not reliably relink a hoisted package whose version moved: a
138
+ # stale test-harness once survived a bump and a new export was "not
139
+ # found" at import. Drop the harness copy so the pinned version runs.
140
+ rm -rf node_modules/@norskvideo/ctl-test-harness
141
+ bun install --frozen-lockfile
142
+ pub="$(sed -nE "s/^[[:space:]]*PUBLISH_IMAGE:[[:space:]]*\"?([^\" ]*)\"?[[:space:]]*$/\\1/p" .github/workflows/build-image.yml | head -1)"
143
+ def="$(sed -nE "s/.*IMAGE_TAG:-([^}]+)\\}.*/\\1/p" deployment/build-image.sh | head -1)"
144
+ export IMAGE_TAG="${pub:+$pub:smoke}"
145
+ export IMAGE_TAG="${IMAGE_TAG:-$def}"
146
+ echo "smoke image: $IMAGE_TAG"
147
+ bun run build:image
148
+ SMOKE_IMAGE_TAG="$IMAGE_TAG" bun run test:smoke
149
+ '
150
+
151
+ # Report this pipeline's result to the aggregated product CI dashboard
152
+ # (id3as/ci-workflows). !cancelled() so a real pass/fail reports, but a
153
+ # cancelled/superseded run does NOT — a cancellation dispatched as failure
154
+ # would false-red the board. Candidate validations report to ctl instead.
155
+ notify:
156
+ needs: smoke
157
+ if: ${{ !cancelled() && github.event_name != 'repository_dispatch' }}
158
+ runs-on: x64
159
+ steps:
160
+ - uses: actions/checkout@v5
161
+ with:
162
+ clean: false
163
+ - id: meta
164
+ run: |
165
+ if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
166
+ echo "state=failure" >> "$GITHUB_OUTPUT"
167
+ else
168
+ echo "state=success" >> "$GITHUB_OUTPUT"
169
+ fi
170
+ - uses: ./.github/actions/ci-status-dispatch
171
+ with:
172
+ token: ${{ secrets.CI_DISPATCH_TOKEN }}
173
+ product: __PRODUCT__
174
+ pipeline: smoke
175
+ status: ${{ steps.meta.outputs.state }}
176
+
177
+ # Report the rc-candidate validation result to the norsk-ctl release gate. Only
178
+ # on a ctl-candidate dispatch; release-collect tallies these and promotes
179
+ # latest once every gating product is green on the candidate.
180
+ report-candidate:
181
+ needs: smoke
182
+ if: ${{ always() && github.event_name == 'repository_dispatch' && github.event.action == 'ctl-candidate' }}
183
+ runs-on: x64
184
+ steps:
185
+ - name: Fire ctl-candidate-result at norsk-ctl
186
+ env:
187
+ GH_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
188
+ # Lowercase on purpose: the drift gate masks every `product:` line, so
189
+ # one per-repo key serves both the notify and the report jobs.
190
+ product: __PRODUCT__
191
+ VERSION: ${{ github.event.client_payload.version }}
192
+ RESULT: ${{ needs.smoke.result }}
193
+ run: |
194
+ set -euo pipefail
195
+ state=failure; [ "$RESULT" = "success" ] && state=success
196
+ body="$(jq -nc --arg p "$product" --arg v "$VERSION" --arg r "$state" \
197
+ '{event_type:"ctl-candidate-result",client_payload:{product:$p,version:$v,result:$r}}')"
198
+ code="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
199
+ -H "Authorization: Bearer $GH_TOKEN" \
200
+ -H "Accept: application/vnd.github+json" \
201
+ https://api.github.com/repos/id3as/norsk-ctl/dispatches -d "$body")"
202
+ [ "$code" = "204" ] || { echo "::error::report to norsk-ctl failed (HTTP $code)"; exit 1; }
@@ -31,6 +31,7 @@ import {
31
31
  PRODUCT_LINE,
32
32
  PRODUCT_SENTINEL,
33
33
  PUBLISH_IMAGE_LINE,
34
+ RUNNER_LINE,
34
35
  splitAtMarkers,
35
36
  } from "./check-drift.ts";
36
37
 
@@ -91,6 +92,30 @@ function syncBuildImage(repoRoot: string, canonical: string, r: SyncReport): voi
91
92
  writeIfChanged(path, out, rel, r.written);
92
93
  }
93
94
 
95
+ // smoke.yml: optional (a repo carries it once it has a smoke spec), canonical
96
+ // byte-for-byte except the notify/report `product:` key and the matrix
97
+ // `runner:` label array. Re-emit the canonical with both restored; the runner
98
+ // line goes back through a function replacer so a `$` in it is never read as a
99
+ // backreference.
100
+ function syncSmoke(repoRoot: string, canonical: string, r: SyncReport): void {
101
+ const rel = ".github/workflows/smoke.yml";
102
+ const path = join(repoRoot, rel);
103
+ if (!existsSync(path)) {
104
+ r.skipped.push(`${rel} (absent)`);
105
+ return;
106
+ }
107
+ const existing = readFileSync(path, "utf8");
108
+ const key = existing.match(/^\s*product:\s*(\S+)/m)?.[1];
109
+ if (!key || key === PRODUCT_SENTINEL) {
110
+ r.skipped.push(`${rel} (no product: key to preserve)`);
111
+ return;
112
+ }
113
+ const runnerLine = existing.match(/^\s*-?\s*runner:.*$/m)?.[0];
114
+ let out = canonical.replace(PRODUCT_LINE, `$1${key}`);
115
+ if (runnerLine !== undefined) out = out.replace(RUNNER_LINE, () => runnerLine);
116
+ writeIfChanged(path, out, rel, r.written);
117
+ }
118
+
94
119
  // flake.nix is verbatim except the dev-only ctl pin (ctlVersion + the four
95
120
  // per-platform hashes), which floats per repo. Re-emit canonical STRUCTURE with
96
121
  // the repo's pin restored in order — so a structural edit (e.g. a stray comment)
@@ -180,6 +205,7 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes): SyncRepo
180
205
  syncWorkflow(repoRoot, ".github/workflows/upgrade-latest.yml", canonical.upgradeLatest, r);
181
206
  syncWorkflow(repoRoot, ".github/workflows/sync-dev-kit.yml", canonical.syncDevKit, r);
182
207
  syncBuildImage(repoRoot, canonical.buildImage, r);
208
+ syncSmoke(repoRoot, canonical.smoke, r);
183
209
 
184
210
  // publish-docs.yml is optional and verbatim (no per-repo line) — only re-sync a
185
211
  // repo that already carries it, unlike biome/tsconfig which every repo must have.
@@ -212,6 +238,7 @@ if (import.meta.main) {
212
238
  buildImageBootstrap: readFileSync(join(dir, "..", "build", "build-image.bootstrap.sh"), "utf8"),
213
239
  gitignoreCore: readFileSync(join(dir, "gitignore.core"), "utf8"),
214
240
  buildImage: readFileSync(join(dir, "build-image.yml"), "utf8"),
241
+ smoke: readFileSync(join(dir, "smoke.yml"), "utf8"),
215
242
  };
216
243
  const report = syncDrift(repoRoot, canonical);
217
244
  if (report.written.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",