@norskvideo/ctl-dev-kit 0.2.64 → 0.2.66

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.
@@ -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