@norskvideo/ctl-dev-kit 0.1.8 → 0.1.10
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.
|
@@ -18,9 +18,16 @@
|
|
|
18
18
|
#
|
|
19
19
|
# Everything else is auto-detected off PRODUCT_DIR, so a product opts in just by
|
|
20
20
|
# having the directory/script — no per-product flag to keep in sync:
|
|
21
|
+
# frontend/dist the frontend workspace's build output
|
|
22
|
+
# dashboards/ the dashboard workspace's build output
|
|
23
|
+
# components/lib the components workspace's build output
|
|
21
24
|
# native/ native addon(s) (probe's signer) -> beside bundle
|
|
22
25
|
# assets/ product-template assets (funke's PNGs)
|
|
23
26
|
# scripts/runtime-images.ts -> emits the norsk-ctl.runtime-images label
|
|
27
|
+
#
|
|
28
|
+
# Only backend/dist is required. A product with no frontend/dashboard/components
|
|
29
|
+
# workspace (reuters) stages those empty and builds the same as one that has all
|
|
30
|
+
# three (funke/probe/commentary).
|
|
24
31
|
set -euo pipefail
|
|
25
32
|
|
|
26
33
|
: "${PRODUCT_DIR:?build-bundle-image.sh: PRODUCT_DIR must be set by the wrapper}"
|
|
@@ -43,11 +50,26 @@ stage="$(mktemp -d)"
|
|
|
43
50
|
trap 'rm -rf "${stage}"' EXIT
|
|
44
51
|
|
|
45
52
|
echo "==> staging artifacts"
|
|
46
|
-
mkdir -p "${stage}/backend" "${stage}/frontend" "${stage}/components
|
|
53
|
+
mkdir -p "${stage}/backend" "${stage}/frontend/dist" "${stage}/components/lib" \
|
|
54
|
+
"${stage}/dashboards" "${stage}/native" "${stage}/assets"
|
|
55
|
+
# The backend bundle is the one non-negotiable payload — every bundle product has
|
|
56
|
+
# one, so a missing dist is a broken build, not an absent workspace.
|
|
47
57
|
cp -R "${PRODUCT_DIR}/backend/dist" "${stage}/backend/dist"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
|
|
59
|
+
# Frontend / dashboards / components are produced by the workspaces of the same
|
|
60
|
+
# name, so a product that has no such workspace never produces them (reuters:
|
|
61
|
+
# backend-only). Staged empty in that case, exactly as native/ and assets/ below
|
|
62
|
+
# — Dockerfile.bundle COPYs all six unconditionally, so the path must exist even
|
|
63
|
+
# when there is nothing to put in it.
|
|
64
|
+
if [ -d "${PRODUCT_DIR}/frontend/dist" ]; then
|
|
65
|
+
cp -R "${PRODUCT_DIR}/frontend/dist/." "${stage}/frontend/dist/"
|
|
66
|
+
fi
|
|
67
|
+
if [ -d "${PRODUCT_DIR}/dashboards" ]; then
|
|
68
|
+
cp -R "${PRODUCT_DIR}/dashboards/." "${stage}/dashboards/"
|
|
69
|
+
fi
|
|
70
|
+
if [ -d "${PRODUCT_DIR}/components/lib" ]; then
|
|
71
|
+
cp -R "${PRODUCT_DIR}/components/lib/." "${stage}/components/lib/"
|
|
72
|
+
fi
|
|
51
73
|
|
|
52
74
|
# Native addon(s): staged beside the bundle. Strip the build host's RUNPATH (nix
|
|
53
75
|
# store / repo-local paths) so the shipped .node carries no build-host paths; its
|
|
@@ -45,5 +45,10 @@
|
|
|
45
45
|
named capability in its manifest's `requires: [...]`; ctl refuses only if it
|
|
46
46
|
lacks that capability. Reach for a capability as rarely as possible — it is the
|
|
47
47
|
escape hatch, not the default.
|
|
48
|
+
- **The shared CI workflow is single-sourced too.** `.github/workflows/upgrade-latest.yml`
|
|
49
|
+
is copied verbatim from `@norskvideo/ctl-dev-kit` (`conventions/upgrade-latest.yml`),
|
|
50
|
+
same as this core block and `flake.nix`; `check:drift` fails CI if a copy
|
|
51
|
+
diverges. Only the `product:` key is per-repo. Never hand-edit the copy — edit
|
|
52
|
+
the dev-kit source and re-sync.
|
|
48
53
|
|
|
49
54
|
<!-- END ctl-shared-conventions v1 -->
|
|
@@ -36,6 +36,24 @@ function firstDiffLine(actual: string, expected: string): string {
|
|
|
36
36
|
return "content differs";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// The one legitimately per-repo line in upgrade-latest.yml: the dashboard key
|
|
40
|
+
// the nightly result is dispatched under. Everything else is verbatim-shared.
|
|
41
|
+
const PRODUCT_LINE = /^(\s*product:\s*).*$/m;
|
|
42
|
+
const PRODUCT_SENTINEL = "__PRODUCT__";
|
|
43
|
+
|
|
44
|
+
function workflowProblem(actual: string, canonical: string): string | undefined {
|
|
45
|
+
// Compare structure with the product value masked so any real key passes, then
|
|
46
|
+
// separately reject a copy that still carries the unfilled sentinel.
|
|
47
|
+
const mask = (s: string) => s.replace(PRODUCT_LINE, `$1${PRODUCT_SENTINEL}`);
|
|
48
|
+
if (mask(actual) !== mask(canonical)) {
|
|
49
|
+
return `.github/workflows/upgrade-latest.yml has drifted from @norskvideo/ctl-dev-kit conventions/upgrade-latest.yml (${firstDiffLine(mask(actual), mask(canonical))}). ${RESYNC}`;
|
|
50
|
+
}
|
|
51
|
+
if (actual.match(/^\s*product:\s*(\S+)/m)?.[1] === PRODUCT_SENTINEL) {
|
|
52
|
+
return `.github/workflows/upgrade-latest.yml still has the ${PRODUCT_SENTINEL} placeholder — set \`product:\` to this repo's dashboard key.`;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
function coreProblem(claude: string, canonicalCore: string): string | undefined {
|
|
40
58
|
if (claude.includes(canonicalCore)) return undefined;
|
|
41
59
|
|
|
@@ -48,7 +66,10 @@ function coreProblem(claude: string, canonicalCore: string): string | undefined
|
|
|
48
66
|
return `CLAUDE.md fenced core has drifted from @norskvideo/ctl-dev-kit conventions/CLAUDE.core.md (${firstDiffLine(region, canonicalCore.trimEnd())}). ${RESYNC}`;
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
export function checkDrift(
|
|
69
|
+
export function checkDrift(
|
|
70
|
+
repoRoot: string,
|
|
71
|
+
canonical: { core: string; flake: string; upgradeLatest: string },
|
|
72
|
+
): DriftReport {
|
|
52
73
|
const problems: string[] = [];
|
|
53
74
|
|
|
54
75
|
const claudePath = join(repoRoot, "CLAUDE.md");
|
|
@@ -73,6 +94,15 @@ export function checkDrift(repoRoot: string, canonical: { core: string; flake: s
|
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
96
|
|
|
97
|
+
// upgrade-latest.yml is OPTIONAL — a product may ship without a nightly bump
|
|
98
|
+
// (e.g. until it has an integration tier). But when present it is the shared,
|
|
99
|
+
// single-sourced workflow and must not diverge.
|
|
100
|
+
const workflowPath = join(repoRoot, ".github", "workflows", "upgrade-latest.yml");
|
|
101
|
+
if (existsSync(workflowPath)) {
|
|
102
|
+
const problem = workflowProblem(readFileSync(workflowPath, "utf8"), canonical.upgradeLatest);
|
|
103
|
+
if (problem) problems.push(problem);
|
|
104
|
+
}
|
|
105
|
+
|
|
76
106
|
return { ok: problems.length === 0, problems };
|
|
77
107
|
}
|
|
78
108
|
|
|
@@ -81,10 +111,11 @@ if (import.meta.main) {
|
|
|
81
111
|
const canonical = {
|
|
82
112
|
core: readFileSync(join(import.meta.dir, "CLAUDE.core.md"), "utf8"),
|
|
83
113
|
flake: readFileSync(join(import.meta.dir, "..", "build", "flake.nix"), "utf8"),
|
|
114
|
+
upgradeLatest: readFileSync(join(import.meta.dir, "upgrade-latest.yml"), "utf8"),
|
|
84
115
|
};
|
|
85
116
|
const report = checkDrift(repoRoot, canonical);
|
|
86
117
|
if (report.ok) {
|
|
87
|
-
console.log("drift-check: CLAUDE.md core + flake.nix match @norskvideo/ctl-dev-kit.");
|
|
118
|
+
console.log("drift-check: CLAUDE.md core + flake.nix + upgrade-latest.yml match @norskvideo/ctl-dev-kit.");
|
|
88
119
|
process.exit(0);
|
|
89
120
|
}
|
|
90
121
|
console.error(
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Keep the product fresh against the Norsk "nightly" channel (RFC 0001 —
|
|
2
|
+
# product version freshness). The media + studio engine versions are pinned in
|
|
3
|
+
# two coupled places that MUST move together: the npm `overrides` in the root
|
|
4
|
+
# package.json (build-time libraries) and manifest.seed.json (the launched
|
|
5
|
+
# container image tags). Both use the identical version string, so a bump is a
|
|
6
|
+
# global string-replace of old->new across every tracked file — which also
|
|
7
|
+
# updates the version-pin guard test in lockstep, no drift. Also rebumps the
|
|
8
|
+
# dev flake's pinned norsk-ctl daemon to the `latest` channel (dev-only; CI
|
|
9
|
+
# downloads ctl directly, so a stale pin never affects CI or customers).
|
|
10
|
+
#
|
|
11
|
+
# Commits the bump straight to main (nightly cron + on demand). Note: a
|
|
12
|
+
# GITHUB_TOKEN push does not retrigger build-image/integration (Actions'
|
|
13
|
+
# recursion guard), so the next human push validates the bump; a sad pony on
|
|
14
|
+
# that run flags a bad nightly to revert. Runs nightly + on demand.
|
|
15
|
+
#
|
|
16
|
+
# Single-sourced in @norskvideo/ctl-dev-kit (conventions/upgrade-latest.yml) and
|
|
17
|
+
# copied verbatim into each product repo; the check:drift gate fails CI if a copy
|
|
18
|
+
# diverges. Only the `product:` value below is per-repo (set it to this repo's
|
|
19
|
+
# dashboard key); everything else must match the dev-kit source. Never hand-edit
|
|
20
|
+
# the copy — edit the dev-kit source and re-sync.
|
|
21
|
+
name: upgrade-latest
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
schedule:
|
|
25
|
+
- cron: "17 4 * * *"
|
|
26
|
+
workflow_dispatch:
|
|
27
|
+
|
|
28
|
+
permissions:
|
|
29
|
+
contents: write
|
|
30
|
+
|
|
31
|
+
jobs:
|
|
32
|
+
bump:
|
|
33
|
+
runs-on: x64
|
|
34
|
+
outputs:
|
|
35
|
+
changed: ${{ steps.bump.outputs.changed }}
|
|
36
|
+
summary: ${{ steps.bump.outputs.summary }}
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v5
|
|
39
|
+
|
|
40
|
+
- name: Resolve the nightly channel and rewrite the coupled pins
|
|
41
|
+
id: bump
|
|
42
|
+
run: |
|
|
43
|
+
set -euo pipefail
|
|
44
|
+
reg="https://registry.npmjs.org"
|
|
45
|
+
changed=0
|
|
46
|
+
|
|
47
|
+
# Accumulate a COMPACT summary: only the components that actually moved
|
|
48
|
+
# get an entry, so a no-op nightly (old == new) posts nothing. Was: the
|
|
49
|
+
# full media + studio-lib + studio-image triple every night for every
|
|
50
|
+
# product, even when nothing changed — pure dashboard noise.
|
|
51
|
+
parts=()
|
|
52
|
+
|
|
53
|
+
# MEDIA: the npm packages and the launched image are built from the SAME
|
|
54
|
+
# commit (their hash suffixes match), so one version string drives the
|
|
55
|
+
# whole family. norsk-sdk is the family reference; read its nightly
|
|
56
|
+
# dist-tag off the registry (curl+jq — the runner has no npm) and rewrite
|
|
57
|
+
# every occurrence (overrides, sub-package deps, manifest image ref,
|
|
58
|
+
# version-pin test) EXCEPT the lockfile, which bun install regenerates.
|
|
59
|
+
old_media="$(jq -r '.latest.media | split(":")[1]' manifest.seed.json)"
|
|
60
|
+
new_media="$(curl -fsSL "$reg/@norskvideo%2Fnorsk-sdk" | jq -r '."dist-tags".nightly')"
|
|
61
|
+
[ -n "$new_media" ] && [ "$new_media" != "null" ] || { echo "could not resolve norsk-sdk nightly"; exit 1; }
|
|
62
|
+
echo "media: $old_media -> $new_media"
|
|
63
|
+
if [ "$old_media" != "$new_media" ]; then
|
|
64
|
+
git grep -lF "$old_media" -- . ':!bun.lock' | xargs -r sed -i "s|$old_media|$new_media|g"
|
|
65
|
+
changed=1
|
|
66
|
+
parts+=("media \`$new_media\`")
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# STUDIO: unlike media, Norsk builds the studio npm packages and the
|
|
70
|
+
# studio Docker image from DIFFERENT commits — their hash suffixes never
|
|
71
|
+
# match — so one version string can't serve both: pinning the launched
|
|
72
|
+
# image to the npm nightly yields a tag that was never pushed (integration
|
|
73
|
+
# then dies on "manifest unknown"). Resolve the two apart: build libraries
|
|
74
|
+
# from npm, the launched image from Docker Hub's newest published tag ON
|
|
75
|
+
# THE SAME version line (so it's pullable AND matches the libs'
|
|
76
|
+
# major.minor.patch). Rewrite the image ref FIRST, then the bare library
|
|
77
|
+
# version — so even when the two happen to coincide, the image ref is
|
|
78
|
+
# already moved and the bare-version rewrite only touches the npm overrides
|
|
79
|
+
# (no dependence on which files hold each — some products have no
|
|
80
|
+
# version-pin test).
|
|
81
|
+
old_studio_lib="$(jq -r '.overrides["@norskvideo/norsk-studio"]' package.json)"
|
|
82
|
+
new_studio_lib="$(curl -fsSL "$reg/@norskvideo%2Fnorsk-studio" | jq -r '."dist-tags".nightly')"
|
|
83
|
+
[ -n "$new_studio_lib" ] && [ "$new_studio_lib" != "null" ] || { echo "could not resolve norsk-studio npm nightly"; exit 1; }
|
|
84
|
+
studio_line="${new_studio_lib%%-*}" # e.g. 1.28.0 — keep the image on this line
|
|
85
|
+
old_studio_img="$(jq -r '.latest.studio | split(":")[1]' manifest.seed.json)"
|
|
86
|
+
new_studio_img="$(curl -fsSL "https://hub.docker.com/v2/repositories/norskvideo/norsk-studio/tags?page_size=100&ordering=last_updated" \
|
|
87
|
+
| jq -r '.results[].name' \
|
|
88
|
+
| grep -E "^${studio_line}-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9a-f]+$" | head -1 || true)"
|
|
89
|
+
[ -n "$new_studio_img" ] || { echo "no published norsk-studio image on the $studio_line line — npm/docker version lines have diverged"; exit 1; }
|
|
90
|
+
echo "studio lib: $old_studio_lib -> $new_studio_lib"
|
|
91
|
+
echo "studio image: $old_studio_img -> $new_studio_img"
|
|
92
|
+
if [ "$old_studio_img" != "$new_studio_img" ]; then
|
|
93
|
+
git grep -lF "norskvideo/norsk-studio:$old_studio_img" -- . ':!bun.lock' \
|
|
94
|
+
| xargs -r sed -i "s|norskvideo/norsk-studio:$old_studio_img|norskvideo/norsk-studio:$new_studio_img|g"
|
|
95
|
+
changed=1
|
|
96
|
+
fi
|
|
97
|
+
if [ "$old_studio_lib" != "$new_studio_lib" ]; then
|
|
98
|
+
git grep -lF "$old_studio_lib" -- . ':!bun.lock' | xargs -r sed -i "s|$old_studio_lib|$new_studio_lib|g"
|
|
99
|
+
changed=1
|
|
100
|
+
fi
|
|
101
|
+
# One studio entry, not two — the launched image tag is the runtime-
|
|
102
|
+
# relevant one; only fall back to the lib tag if the image held but the
|
|
103
|
+
# library advanced (npm nightly moved with no new image on the line yet).
|
|
104
|
+
if [ "$old_studio_img" != "$new_studio_img" ]; then
|
|
105
|
+
parts+=("studio \`$new_studio_img\`")
|
|
106
|
+
elif [ "$old_studio_lib" != "$new_studio_lib" ]; then
|
|
107
|
+
parts+=("studio-lib \`$new_studio_lib\`")
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# --- ctl daemon pin (flake.nix) -----------------------------------
|
|
111
|
+
# The pinned norsk-ctl in the dev flake is dev-only (CI downloads the
|
|
112
|
+
# released binary directly), but keep it fresh too. Bump ctlVersion +
|
|
113
|
+
# the four per-platform hashes to the `latest` channel. Mirrors
|
|
114
|
+
# packages/dev-kit/build/refresh-ctl-pin.sh in the ctl monorepo.
|
|
115
|
+
ctl_base="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl"
|
|
116
|
+
old_ctl="$(grep -oE 'ctlVersion = "[^"]+"' flake.nix | sed -E 's/.*"(.*)"/\1/')"
|
|
117
|
+
new_ctl="$(curl -fsSL "$ctl_base/latest")"
|
|
118
|
+
if [ -n "$new_ctl" ] && [ "$old_ctl" != "$new_ctl" ]; then
|
|
119
|
+
declare -A ctlplat=([x86_64-linux]=linux-x64 [aarch64-linux]=linux-arm64 [aarch64-darwin]=darwin-arm64 [x86_64-darwin]=darwin-x64)
|
|
120
|
+
for sys in "${!ctlplat[@]}"; do
|
|
121
|
+
p="${ctlplat[$sys]}"
|
|
122
|
+
hex="$(curl -fsSL "$ctl_base/$new_ctl/norsk-ctl-$new_ctl-$p.sha256" | awk '{print $1}')"
|
|
123
|
+
[ -n "$hex" ] || { echo "no sha256 sidecar for $p"; exit 1; }
|
|
124
|
+
sri="$(nix hash convert --hash-algo sha256 --to sri "$hex")"
|
|
125
|
+
perl -0pi -e "s{(plat = \"$p\";\\s*hash = \")[^\"]*(\")}{\${1}$sri\${2}}" flake.nix
|
|
126
|
+
done
|
|
127
|
+
perl -0pi -e "s{(ctlVersion = \")[^\"]*(\")}{\${1}$new_ctl\${2}}" flake.nix
|
|
128
|
+
echo "ctl: $old_ctl -> $new_ctl"
|
|
129
|
+
changed=1
|
|
130
|
+
parts+=("norsk-ctl \`$new_ctl\`")
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Join the moved components; empty when nothing changed (the dashboard
|
|
134
|
+
# then shows a clean green "nightly" cell with no detail).
|
|
135
|
+
summary=""
|
|
136
|
+
if [ ${#parts[@]} -gt 0 ]; then
|
|
137
|
+
summary="$(printf '%s, ' "${parts[@]}")"
|
|
138
|
+
summary="${summary%, }"
|
|
139
|
+
fi
|
|
140
|
+
{
|
|
141
|
+
echo "changed=$changed"
|
|
142
|
+
echo "summary=$summary"
|
|
143
|
+
} >> "$GITHUB_OUTPUT"
|
|
144
|
+
|
|
145
|
+
- name: Refresh the lockfile against the new pins
|
|
146
|
+
if: steps.bump.outputs.changed == '1'
|
|
147
|
+
run: nix develop .#build --command bash -c 'set -euo pipefail; bun install'
|
|
148
|
+
|
|
149
|
+
- name: Commit the bump to main
|
|
150
|
+
if: steps.bump.outputs.changed == '1'
|
|
151
|
+
run: |
|
|
152
|
+
set -euo pipefail
|
|
153
|
+
git config user.name "github-actions[bot]"
|
|
154
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
155
|
+
git add -A
|
|
156
|
+
git commit -m "chore: bump Norsk nightly + ctl pins to latest channel
|
|
157
|
+
|
|
158
|
+
${{ steps.bump.outputs.summary }}
|
|
159
|
+
|
|
160
|
+
Coupled Norsk pins (npm overrides + manifest.seed.json image tags +
|
|
161
|
+
version-pin guard test) moved together via a global version-string
|
|
162
|
+
replace; lockfile regenerated. Dev flake norsk-ctl pin (version + 4
|
|
163
|
+
platform hashes) rebumped to the latest channel."
|
|
164
|
+
# main can advance under a long bump (lockfile regen + registry
|
|
165
|
+
# lookups run for minutes); a bare push then loses a non-fast-forward
|
|
166
|
+
# race and reds the nightly. Rebase-and-retry so an unrelated commit
|
|
167
|
+
# landing mid-run does not fail the bump; a genuine content conflict
|
|
168
|
+
# still aborts loudly rather than force-pushing over other work.
|
|
169
|
+
for attempt in 1 2 3 4 5; do
|
|
170
|
+
if git push origin HEAD:main; then exit 0; fi
|
|
171
|
+
echo "push rejected (attempt $attempt) - rebasing onto latest origin/main"
|
|
172
|
+
git fetch origin main
|
|
173
|
+
git rebase origin/main || { git rebase --abort; echo "rebase hit a real conflict; aborting"; exit 1; }
|
|
174
|
+
done
|
|
175
|
+
echo "push still failing after 5 rebase attempts"; exit 1
|
|
176
|
+
|
|
177
|
+
- name: Report "already fresh"
|
|
178
|
+
if: steps.bump.outputs.changed != '1'
|
|
179
|
+
run: echo "Nightly pins already current — nothing to bump."
|
|
180
|
+
|
|
181
|
+
# Report the nightly bump result to the aggregated product CI dashboard
|
|
182
|
+
# (id3as/ci-workflows). always() so a failed bump shows a sad pony; the summary
|
|
183
|
+
# rides along as the cell detail.
|
|
184
|
+
notify:
|
|
185
|
+
needs: bump
|
|
186
|
+
if: always()
|
|
187
|
+
runs-on: x64
|
|
188
|
+
steps:
|
|
189
|
+
- uses: actions/checkout@v5
|
|
190
|
+
- id: meta
|
|
191
|
+
run: |
|
|
192
|
+
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "true" ]; then
|
|
193
|
+
echo "state=failure" >> "$GITHUB_OUTPUT"
|
|
194
|
+
else
|
|
195
|
+
echo "state=success" >> "$GITHUB_OUTPUT"
|
|
196
|
+
fi
|
|
197
|
+
- uses: ./.github/actions/ci-status-dispatch
|
|
198
|
+
with:
|
|
199
|
+
token: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
200
|
+
product: __PRODUCT__
|
|
201
|
+
pipeline: nightly
|
|
202
|
+
status: ${{ steps.meta.outputs.state }}
|
|
203
|
+
detail: ${{ needs.bump.outputs.summary }}
|