@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
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
# Keep this product fresh against the ctl-* npm family: the test harness that
|
|
2
|
-
# drives its smoke and integration tiers, the SDK, the foundation helpers and
|
|
3
|
-
# the product-template schema. Also the @norskvideo/moq-* family, the MoQ
|
|
4
|
-
# player stack the dashboards embed.
|
|
5
|
-
#
|
|
6
|
-
# sync-dev-kit already does this for the conventions package, and upgrade-latest
|
|
7
|
-
# does it for the Norsk nightly line. Nothing did it for the rest, so the pins
|
|
8
|
-
# drifted silently: on 2026-08-30 studio carried a conventions pin bumped that
|
|
9
|
-
# morning beside an SDK three releases behind, and three repos ran three
|
|
10
|
-
# different vintages of the same smoke runner. The caret ranges admit the new
|
|
11
|
-
# versions -- `--frozen-lockfile` is what holds them back, and nothing ever
|
|
12
|
-
# regenerated the lockfile.
|
|
13
|
-
#
|
|
14
|
-
# Rewriting the pins is not enough on its own: a family member can be a
|
|
15
|
-
# transitive dep with no pin to rewrite, and the gate step re-resolves those
|
|
16
|
-
# explicitly. Note the residual gap -- a staleness that is PURELY transitive
|
|
17
|
-
# moves nothing in a package.json, so `changed` stays 0 and this workflow does
|
|
18
|
-
# nothing that morning. It is picked up the next time any pin moves, which for
|
|
19
|
-
# this family is most days.
|
|
20
|
-
#
|
|
21
|
-
# This NEVER pushes to main -- and since ci-fragility proposal B neither do
|
|
22
|
-
# upgrade-latest or sync-dev-kit, which once landed on a green gate. Its gate
|
|
23
|
-
# is drift + lint + typecheck + unit, and a bump to the test harness changes
|
|
24
|
-
# the code that drives smoke and integration -- neither of which that gate
|
|
25
|
-
# runs, because both need Docker and a licence; the PR's own tiers do, and
|
|
26
|
-
# bot-prs merges it only once they pass. A red gate
|
|
27
|
-
# still opens one, as a draft: adopting a breaking change is real engineering,
|
|
28
|
-
# not a copy-paste, and the PR is where that work belongs.
|
|
29
|
-
#
|
|
30
|
-
# Single-sourced in @norskvideo/ctl-dev-kit (conventions/sync-ctl-packages.yml)
|
|
31
|
-
# and copied verbatim into each product repo; the check:drift gate fails CI if a
|
|
32
|
-
# copy diverges. Never hand-edit the copy -- edit the source and re-sync.
|
|
33
|
-
name: sync-ctl-packages
|
|
34
|
-
|
|
35
|
-
on:
|
|
36
|
-
schedule:
|
|
37
|
-
# Clear of sync-dev-kit's 04:37 so the two never race the same lockfile.
|
|
38
|
-
- cron: "52 4 * * *"
|
|
39
|
-
workflow_dispatch:
|
|
40
|
-
|
|
41
|
-
permissions:
|
|
42
|
-
contents: write
|
|
43
|
-
pull-requests: write
|
|
44
|
-
|
|
45
|
-
concurrency:
|
|
46
|
-
group: sync-ctl-packages-${{ github.ref }}
|
|
47
|
-
cancel-in-progress: true
|
|
48
|
-
|
|
49
|
-
jobs:
|
|
50
|
-
sync:
|
|
51
|
-
runs-on: x64
|
|
52
|
-
steps:
|
|
53
|
-
# Runs BEFORE checkout, and does two things: points the harness's temp base
|
|
54
|
-
# out of the workspace for every later step in this job, and clears whatever
|
|
55
|
-
# the old repo-local default left behind on this runner. The body carries the
|
|
56
|
-
# why. It fails the job only when legacy litter survives -- which would take
|
|
57
|
-
# the checkout down a step later anyway, but report nothing useful.
|
|
58
|
-
- name: Route test-temp out of the workspace (pre-checkout)
|
|
59
|
-
run: |
|
|
60
|
-
set -uo pipefail
|
|
61
|
-
# These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
|
|
62
|
-
# HOST daemon has not seen before is created by dockerd, as ROOT, on the
|
|
63
|
-
# host -- even when the launch passes --container-user (reproduced
|
|
64
|
-
# 2026-09-04). The harness's default base is repo-local for an OrbStack
|
|
65
|
-
# inotify constraint that binds macOS dev and nothing here, so every run
|
|
66
|
-
# seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
|
|
67
|
-
# cannot remove. That took commentary's checks AND integration red on
|
|
68
|
-
# 2026-09-03. Use the sanctioned override, per-run and outside the
|
|
69
|
-
# checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
|
|
70
|
-
base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
|
71
|
-
mkdir -p "$base"
|
|
72
|
-
echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
|
|
73
|
-
# Yesterday's bases hold dockerd-created root dirs, so a non-root rm
|
|
74
|
-
# EACCESes. They cannot wedge a checkout, but they do fill the disk.
|
|
75
|
-
for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
|
|
76
|
-
docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
|
|
77
|
-
done
|
|
78
|
-
# LEGACY: a runner that ran the old default still carries a workspace
|
|
79
|
-
# test-temp, and a checkout with clean:true dies on it. Reap the holders
|
|
80
|
-
# first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
|
|
81
|
-
# RESTARTING leaked container re-creates the path as root seconds after
|
|
82
|
-
# any clean. Match on mount SOURCE, not a label: the holders are untracked
|
|
83
|
-
# by construction, because a killed run labels nothing.
|
|
84
|
-
tt="$GITHUB_WORKSPACE/test-temp"
|
|
85
|
-
[ -d "$tt" ] || exit 0
|
|
86
|
-
stuck=""
|
|
87
|
-
for c in $(docker ps -aq 2>/dev/null || true); do
|
|
88
|
-
if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
|
|
89
|
-
stuck="$stuck $c"
|
|
90
|
-
fi
|
|
91
|
-
done
|
|
92
|
-
if [ -n "$stuck" ]; then
|
|
93
|
-
echo "reaping containers holding mounts under $tt:$stuck"
|
|
94
|
-
docker rm -f $stuck || true
|
|
95
|
-
fi
|
|
96
|
-
# The DIRECTORY, not just its contents: an empty but root-owned base
|
|
97
|
-
# still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
|
|
98
|
-
# container unlink the leaf.
|
|
99
|
-
docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
|
|
100
|
-
sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
|
|
101
|
-
# Fail HERE if it survived. The checkout fails on it either way, but
|
|
102
|
-
# reports only an EACCES rmdir with no clue what was holding the path.
|
|
103
|
-
if [ -e "$tt" ]; then
|
|
104
|
-
echo "::error::$tt survived the pre-checkout clean"
|
|
105
|
-
ls -lan "$tt" || true
|
|
106
|
-
exit 1
|
|
107
|
-
fi
|
|
108
|
-
|
|
109
|
-
- uses: actions/checkout@v5
|
|
110
|
-
with:
|
|
111
|
-
token: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
112
|
-
clean: true
|
|
113
|
-
|
|
114
|
-
- name: Resolve the newest published ctl-* and moq-* packages and rewrite the pins
|
|
115
|
-
id: bump
|
|
116
|
-
run: |
|
|
117
|
-
set -euo pipefail
|
|
118
|
-
reg="https://registry.npmjs.org"
|
|
119
|
-
# The conventions package is deliberately absent: sync-dev-kit owns it,
|
|
120
|
-
# bumps it in its own PR, and re-syncs the copied files with it.
|
|
121
|
-
# The moq-* members are the @norskvideo fork of moq-dev/moq that the
|
|
122
|
-
# dashboards play through. They pin each other at one version, so all
|
|
123
|
-
# nine move together, including the ones a product only gets
|
|
124
|
-
# transitively. A minor bump of these (0.1 -> 0.2) is a breaking API
|
|
125
|
-
# change, which is what the red-gate draft PR is for.
|
|
126
|
-
family="ctl-test-harness ctl-sdk ctl-foundation ctl-product-template-schema moq-signals moq-flate moq-pattern moq-net moq-json moq-loc moq-msf moq-hang moq-watch"
|
|
127
|
-
|
|
128
|
-
# Every tracked package.json, not just the root: a repo can pin the
|
|
129
|
-
# same package in a nested workspace (playout pins the harness twice),
|
|
130
|
-
# and a root-only rewrite would split the line across the two.
|
|
131
|
-
mapfile -t manifests < <(git ls-files '*package.json' ':!:**/node_modules/**')
|
|
132
|
-
|
|
133
|
-
moved=""
|
|
134
|
-
for short in $family; do
|
|
135
|
-
pkg="@norskvideo/$short"
|
|
136
|
-
enc="@norskvideo%2F$short"
|
|
137
|
-
# The runner has no npm; read the dist-tag off the registry with
|
|
138
|
-
# curl+jq, as sync-dev-kit and upgrade-latest both do.
|
|
139
|
-
new="$(curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 "$reg/$enc" | jq -r '."dist-tags".latest')"
|
|
140
|
-
[ -n "$new" ] && [ "$new" != "null" ] || { echo "could not resolve $pkg latest"; exit 1; }
|
|
141
|
-
|
|
142
|
-
for m in "${manifests[@]}"; do
|
|
143
|
-
old="$(grep -oE "\"$pkg\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$m" | head -1 | sed -E 's/.*"([^"]*)"$/\1/' || true)"
|
|
144
|
-
# Absent here, or a workspace protocol spec that names no version.
|
|
145
|
-
case "$old" in "" | workspace:*) continue ;; esac
|
|
146
|
-
old_bare="${old#^}"; old_bare="${old_bare#~}"
|
|
147
|
-
[ "$old_bare" != "$new" ] || continue
|
|
148
|
-
# Surgical single-line rewrite so the file keeps its formatting;
|
|
149
|
-
# a jq reserialize would reflow it and trip the formatter.
|
|
150
|
-
sed -i -E "s|(\"$pkg\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1^$new\2|" "$m"
|
|
151
|
-
echo "$m: $pkg $old -> ^$new"
|
|
152
|
-
moved="$moved- \`$pkg\` $old -> \`^$new\` ($m)"$'\n'
|
|
153
|
-
done
|
|
154
|
-
done
|
|
155
|
-
|
|
156
|
-
if [ -z "$moved" ]; then
|
|
157
|
-
echo "every ctl-* and moq-* pin is already on its newest published version."
|
|
158
|
-
echo "changed=0" >> "$GITHUB_OUTPUT"
|
|
159
|
-
else
|
|
160
|
-
echo "changed=1" >> "$GITHUB_OUTPUT"
|
|
161
|
-
{ echo "moved<<PINS"; printf '%s' "$moved"; echo "PINS"; } >> "$GITHUB_OUTPUT"
|
|
162
|
-
fi
|
|
163
|
-
|
|
164
|
-
- name: Refresh the lockfiles against the new pins
|
|
165
|
-
if: steps.bump.outputs.changed == '1'
|
|
166
|
-
# NOT continue-on-error, unlike the gate below. A refresh that dies
|
|
167
|
-
# part-way (the runner OOM-killed it on 2026-10-03) leaves bumped
|
|
168
|
-
# manifests beside the old lockfiles; carrying on then committed exactly
|
|
169
|
-
# that, and every frozen install on the PR failed. Fail here instead,
|
|
170
|
-
# before anything is committed.
|
|
171
|
-
run: |
|
|
172
|
-
nix develop .#build --command bash -c '
|
|
173
|
-
set -euo pipefail
|
|
174
|
-
# Per INSTALL ROOT, not per repo. The rewrite above walks every
|
|
175
|
-
# tracked package.json, and a manifest outside the root workspace
|
|
176
|
-
# list (funke proxy/ui) owns its own lockfile that a root-only
|
|
177
|
-
# install never regenerates -- leaving a bumped pin its own
|
|
178
|
-
# --frozen-lockfile then rejects.
|
|
179
|
-
for lock in $(git ls-files "*bun.lock"); do
|
|
180
|
-
(cd "$(dirname "$lock")" && bun install)
|
|
181
|
-
done
|
|
182
|
-
# A ctl-* package also arrives TRANSITIVELY, and those never move on
|
|
183
|
-
# their own. probe pins no ctl-foundation at all: it comes through
|
|
184
|
-
# ctl-sdk and ctl-product-template-schema, both of which ask for
|
|
185
|
-
# ^0.1.0 -- and `bun install` keeps whatever the lockfile already
|
|
186
|
-
# resolved for a range it still satisfies. So probe sat on
|
|
187
|
-
# ctl-foundation 0.1.14 from 2026-09-02 while 0.1.19 was current,
|
|
188
|
-
# and this gate went red on an Export-named-parseProductRoleName-
|
|
189
|
-
# not-found from @norskvideo+ctl-foundation@0.1.14 -- which reads as
|
|
190
|
-
# the bumped harness breaking the product, when it is a four-week-
|
|
191
|
-
# stale transitive dep that no pin rewrite can reach. (No apostrophe
|
|
192
|
-
# anywhere in this step: the whole body is one bash -c string in
|
|
193
|
-
# single quotes, and conventions/workflow-quoting.test.ts fails on
|
|
194
|
-
# one.)
|
|
195
|
-
#
|
|
196
|
-
# Same family as the rewrite above, and deliberately named again
|
|
197
|
-
# rather than shared: the two steps are separate shells.
|
|
198
|
-
for pkg in ctl-test-harness ctl-sdk ctl-foundation ctl-product-template-schema moq-signals moq-flate moq-pattern moq-net moq-json moq-loc moq-msf moq-hang moq-watch; do
|
|
199
|
-
for lock in $(git ls-files "*bun.lock"); do
|
|
200
|
-
# Not every install root has every member, directly or
|
|
201
|
-
# otherwise; bun says so and this moves on.
|
|
202
|
-
(cd "$(dirname "$lock")" && bun update "@norskvideo/$pkg") || true
|
|
203
|
-
done
|
|
204
|
-
done
|
|
205
|
-
'
|
|
206
|
-
|
|
207
|
-
- name: Run the product gate
|
|
208
|
-
id: gate
|
|
209
|
-
if: steps.bump.outputs.changed == '1'
|
|
210
|
-
continue-on-error: true
|
|
211
|
-
run: |
|
|
212
|
-
nix develop .#build --command bash -c '
|
|
213
|
-
set -euo pipefail
|
|
214
|
-
bun run check:drift
|
|
215
|
-
bun run lint
|
|
216
|
-
bun run typecheck
|
|
217
|
-
bun run test:unit
|
|
218
|
-
'
|
|
219
|
-
|
|
220
|
-
- name: Open (or update) the pin-freshness PR
|
|
221
|
-
if: steps.bump.outputs.changed == '1'
|
|
222
|
-
env:
|
|
223
|
-
API_TOKEN: ${{ secrets.CI_DISPATCH_TOKEN }}
|
|
224
|
-
GATE: ${{ steps.gate.outcome }}
|
|
225
|
-
# Through the env, never interpolated into the script below: see the
|
|
226
|
-
# heredoc comment for what happens when it is.
|
|
227
|
-
MOVED: ${{ steps.bump.outputs.moved }}
|
|
228
|
-
run: |
|
|
229
|
-
set -euo pipefail
|
|
230
|
-
branch="chore/sync-ctl-packages"
|
|
231
|
-
git config user.name "github-actions[bot]"
|
|
232
|
-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
233
|
-
git checkout -B "$branch"
|
|
234
|
-
git add -A
|
|
235
|
-
git commit -m "chore: sync the ctl-* and moq-* pins to their newest published versions"
|
|
236
|
-
# One branch, force-pushed: a daily schedule must refresh the standing
|
|
237
|
-
# PR rather than accumulate a new one every morning.
|
|
238
|
-
git push -f origin "$branch"
|
|
239
|
-
|
|
240
|
-
if [ "$GATE" = "success" ]; then
|
|
241
|
-
verdict="Gate green: check:drift, lint, typecheck and unit tests all pass on the new pins."
|
|
242
|
-
draft=false
|
|
243
|
-
else
|
|
244
|
-
verdict="**Gate red** — the new pins do not fit this product as-is. Adopting them is real engineering, not a merge."
|
|
245
|
-
draft=true
|
|
246
|
-
fi
|
|
247
|
-
|
|
248
|
-
# $MOVED, not the expression that produced it. This heredoc is unquoted
|
|
249
|
-
# so $verdict expands, which means its literal text is scanned for
|
|
250
|
-
# command substitution too -- and the moved-pin list names every package
|
|
251
|
-
# in backticks. Pasted in directly, each name RAN (exit 127, no PR,
|
|
252
|
-
# every morning since this workflow shipped). Bash does not rescan an
|
|
253
|
-
# expansion's result, so through the env the same backticks are inert.
|
|
254
|
-
body="$(cat <<EOF
|
|
255
|
-
Automated by \`sync-ctl-packages\`.
|
|
256
|
-
|
|
257
|
-
## Pins moved
|
|
258
|
-
|
|
259
|
-
$MOVED
|
|
260
|
-
|
|
261
|
-
## What the gate proved
|
|
262
|
-
|
|
263
|
-
$verdict
|
|
264
|
-
|
|
265
|
-
It did **not** run smoke or integration — both need Docker and a licence.
|
|
266
|
-
A test-harness bump changes the code that drives those tiers, so the
|
|
267
|
-
runs raised on this PR are the real evidence. Read them before merging.
|
|
268
|
-
EOF
|
|
269
|
-
)"
|
|
270
|
-
|
|
271
|
-
# gh is not on the self-hosted runner PATH -- it lives only inside
|
|
272
|
-
# `nix develop`, and this step runs outside it -- so create/update the
|
|
273
|
-
# PR straight against the REST API with the same PAT that pushed the
|
|
274
|
-
# branch. This is the identical fix sync-dev-kit already carries and
|
|
275
|
-
# documents; it was applied to one sibling convention and not its twin,
|
|
276
|
-
# so this step failed with exit 127 in every product repo. curl and jq
|
|
277
|
-
# ARE on the host PATH (the pin-resolve step above uses them).
|
|
278
|
-
title="chore: sync the ctl-* and moq-* pins to their newest published versions"
|
|
279
|
-
repo="${{ github.repository }}"; owner="${repo%%/*}"
|
|
280
|
-
api="https://api.github.com/repos/$repo/pulls"
|
|
281
|
-
curl_api=(--fail-with-body -sS -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/vnd.github+json")
|
|
282
|
-
existing="$(curl "${curl_api[@]}" "$api?head=$owner:$branch&state=open" | jq -r '.[0].number // empty')"
|
|
283
|
-
if [ -n "$existing" ]; then
|
|
284
|
-
# draft is not settable by PATCH; the standing PR keeps whichever
|
|
285
|
-
# state it opened in and the refreshed body carries the verdict.
|
|
286
|
-
curl "${curl_api[@]}" -X PATCH "$api/$existing" \
|
|
287
|
-
--data "$(jq -nc --arg t "$title" --arg b "$body" '{title:$t, body:$b}')" >/dev/null
|
|
288
|
-
else
|
|
289
|
-
curl "${curl_api[@]}" -X POST "$api" \
|
|
290
|
-
--data "$(jq -nc --arg t "$title" --arg b "$body" --arg h "$branch" --argjson d "$draft" '{title:$t, body:$b, head:$h, base:"main", draft:$d}')" >/dev/null
|
|
291
|
-
fi
|
|
292
|
-
- name: Reap the containers this job launched
|
|
293
|
-
# Whatever the tests did. A teardown cut short by a test timeout leaves
|
|
294
|
-
# its instance running on the shared Docker daemon (see JOB_REAP_STEP in
|
|
295
|
-
# conventions/workflow-shell.ts); everything this job launched mounts its
|
|
296
|
-
# own test-temp, so that is what goes.
|
|
297
|
-
if: always()
|
|
298
|
-
run: |
|
|
299
|
-
base="${NORSK_CTL_TEST_TMP:-}"
|
|
300
|
-
tt="$GITHUB_WORKSPACE/test-temp"
|
|
301
|
-
s=""
|
|
302
|
-
for c in $(docker ps -aq); do
|
|
303
|
-
src="$(docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null)" || continue
|
|
304
|
-
if { [ -n "$base" ] && grep -q "^$base/" <<<"$src"; } || grep -q "^$tt/" <<<"$src"; then s="$s $c"; fi
|
|
305
|
-
done
|
|
306
|
-
if [ -n "$s" ]; then
|
|
307
|
-
echo "::warning::removing containers this job left running:$s"
|
|
308
|
-
docker rm -f $s
|
|
309
|
-
fi
|