@norskvideo/ctl-dev-kit 0.1.84 → 0.1.86

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.
@@ -30,23 +30,61 @@ jobs:
30
30
  drift:
31
31
  runs-on: x64
32
32
  steps:
33
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
34
- # around: the integration suite bind-mounts host dirs into containers, and a
35
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
36
- # innocent job checked out next. Skipping the clean cured that symptom and
37
- # caused another: every gitignored artifact survived between runs, so a test
38
- # could pass on build output an earlier job left behind and go red the first
39
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
40
- # from a root container FIRST, and the checkout can clean properly again.
41
- # Best-effort by construction: no docker, no test-temp, or a failed run all
42
- # fall through without failing the job.
43
- - name: Clear stale root-owned test-temp (pre-checkout)
33
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
34
+ # out of the workspace for every later step in this job, and clears whatever
35
+ # the old repo-local default left behind on this runner. The body carries the
36
+ # why. It fails the job only when legacy litter survives -- which would take
37
+ # the checkout down a step later anyway, but report nothing useful.
38
+ - name: Route test-temp out of the workspace (pre-checkout)
44
39
  run: |
45
40
  set -uo pipefail
41
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
42
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
43
+ # host -- even when the launch passes --container-user (reproduced
44
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
45
+ # inotify constraint that binds macOS dev and nothing here, so every run
46
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
47
+ # cannot remove. That took commentary's checks AND integration red on
48
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
49
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
50
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
51
+ mkdir -p "$base"
52
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
53
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
54
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
55
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
56
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
57
+ done
58
+ # LEGACY: a runner that ran the old default still carries a workspace
59
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
60
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
61
+ # RESTARTING leaked container re-creates the path as root seconds after
62
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
63
+ # by construction, because a killed run labels nothing.
46
64
  tt="$GITHUB_WORKSPACE/test-temp"
47
65
  [ -d "$tt" ] || exit 0
48
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
49
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
66
+ stuck=""
67
+ for c in $(docker ps -aq 2>/dev/null || true); do
68
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
69
+ stuck="$stuck $c"
70
+ fi
71
+ done
72
+ if [ -n "$stuck" ]; then
73
+ echo "reaping containers holding mounts under $tt:$stuck"
74
+ docker rm -f $stuck || true
75
+ fi
76
+ # The DIRECTORY, not just its contents: an empty but root-owned base
77
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
78
+ # container unlink the leaf.
79
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
80
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
81
+ # Fail HERE if it survived. The checkout fails on it either way, but
82
+ # reports only an EACCES rmdir with no clue what was holding the path.
83
+ if [ -e "$tt" ]; then
84
+ echo "::error::$tt survived the pre-checkout clean"
85
+ ls -lan "$tt" || true
86
+ exit 1
87
+ fi
50
88
 
51
89
  - uses: actions/checkout@v5
52
90
  with:
@@ -68,23 +106,61 @@ jobs:
68
106
  quality:
69
107
  runs-on: x64
70
108
  steps:
71
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
72
- # around: the integration suite bind-mounts host dirs into containers, and a
73
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
74
- # innocent job checked out next. Skipping the clean cured that symptom and
75
- # caused another: every gitignored artifact survived between runs, so a test
76
- # could pass on build output an earlier job left behind and go red the first
77
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
78
- # from a root container FIRST, and the checkout can clean properly again.
79
- # Best-effort by construction: no docker, no test-temp, or a failed run all
80
- # fall through without failing the job.
81
- - name: Clear stale root-owned test-temp (pre-checkout)
109
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
110
+ # out of the workspace for every later step in this job, and clears whatever
111
+ # the old repo-local default left behind on this runner. The body carries the
112
+ # why. It fails the job only when legacy litter survives -- which would take
113
+ # the checkout down a step later anyway, but report nothing useful.
114
+ - name: Route test-temp out of the workspace (pre-checkout)
82
115
  run: |
83
116
  set -uo pipefail
117
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
118
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
119
+ # host -- even when the launch passes --container-user (reproduced
120
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
121
+ # inotify constraint that binds macOS dev and nothing here, so every run
122
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
123
+ # cannot remove. That took commentary's checks AND integration red on
124
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
125
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
126
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
127
+ mkdir -p "$base"
128
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
129
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
130
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
131
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
132
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
133
+ done
134
+ # LEGACY: a runner that ran the old default still carries a workspace
135
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
136
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
137
+ # RESTARTING leaked container re-creates the path as root seconds after
138
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
139
+ # by construction, because a killed run labels nothing.
84
140
  tt="$GITHUB_WORKSPACE/test-temp"
85
141
  [ -d "$tt" ] || exit 0
86
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
87
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
142
+ stuck=""
143
+ for c in $(docker ps -aq 2>/dev/null || true); do
144
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
145
+ stuck="$stuck $c"
146
+ fi
147
+ done
148
+ if [ -n "$stuck" ]; then
149
+ echo "reaping containers holding mounts under $tt:$stuck"
150
+ docker rm -f $stuck || true
151
+ fi
152
+ # The DIRECTORY, not just its contents: an empty but root-owned base
153
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
154
+ # container unlink the leaf.
155
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
156
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
157
+ # Fail HERE if it survived. The checkout fails on it either way, but
158
+ # reports only an EACCES rmdir with no clue what was holding the path.
159
+ if [ -e "$tt" ]; then
160
+ echo "::error::$tt survived the pre-checkout clean"
161
+ ls -lan "$tt" || true
162
+ exit 1
163
+ fi
88
164
 
89
165
  - uses: actions/checkout@v5
90
166
  with:
@@ -160,23 +236,61 @@ jobs:
160
236
  if: ${{ !cancelled() && github.event_name == 'push' }}
161
237
  runs-on: x64
162
238
  steps:
163
- # Root-owned leftovers under test-temp/ are what `clean: false` used to route
164
- # around: the integration suite bind-mounts host dirs into containers, and a
165
- # non-root git clean then EACCESes on what they wrote -- taking down whatever
166
- # innocent job checked out next. Skipping the clean cured that symptom and
167
- # caused another: every gitignored artifact survived between runs, so a test
168
- # could pass on build output an earlier job left behind and go red the first
169
- # time it landed on a cold runner (probe, 2026-09-01). Clear the leftovers
170
- # from a root container FIRST, and the checkout can clean properly again.
171
- # Best-effort by construction: no docker, no test-temp, or a failed run all
172
- # fall through without failing the job.
173
- - name: Clear stale root-owned test-temp (pre-checkout)
239
+ # Runs BEFORE checkout, and does two things: points the harness's temp base
240
+ # out of the workspace for every later step in this job, and clears whatever
241
+ # the old repo-local default left behind on this runner. The body carries the
242
+ # why. It fails the job only when legacy litter survives -- which would take
243
+ # the checkout down a step later anyway, but report nothing useful.
244
+ - name: Route test-temp out of the workspace (pre-checkout)
174
245
  run: |
175
246
  set -uo pipefail
247
+ # These runners are docker-OUTSIDE-of-docker, so a bind-mount source the
248
+ # HOST daemon has not seen before is created by dockerd, as ROOT, on the
249
+ # host -- even when the launch passes --container-user (reproduced
250
+ # 2026-09-04). The harness's default base is repo-local for an OrbStack
251
+ # inotify constraint that binds macOS dev and nothing here, so every run
252
+ # seeded the CHECKOUT with root-owned dirs a non-root `git clean -ffdx`
253
+ # cannot remove. That took commentary's checks AND integration red on
254
+ # 2026-09-03. Use the sanctioned override, per-run and outside the
255
+ # checkout -- as 8ff17a8d did in June for one debug workflow, and stopped.
256
+ base="$RUNNER_TEMP/nctt-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
257
+ mkdir -p "$base"
258
+ echo "NORSK_CTL_TEST_TMP=$base" >> "$GITHUB_ENV"
259
+ # Yesterday's bases hold dockerd-created root dirs, so a non-root rm
260
+ # EACCESes. They cannot wedge a checkout, but they do fill the disk.
261
+ for old in $(find "$RUNNER_TEMP" -maxdepth 1 -name 'nctt-*' -mtime +0 -printf '%f\n' 2>/dev/null || true); do
262
+ docker run --rm --user 0:0 -v "$RUNNER_TEMP":/t alpine sh -c 'rm -rf -- "/t/$1"' sh "$old" >/dev/null 2>&1 || true
263
+ done
264
+ # LEGACY: a runner that ran the old default still carries a workspace
265
+ # test-temp, and a checkout with clean:true dies on it. Reap the holders
266
+ # first -- rm cannot unlink a live mountpoint even as root (EBUSY), and a
267
+ # RESTARTING leaked container re-creates the path as root seconds after
268
+ # any clean. Match on mount SOURCE, not a label: the holders are untracked
269
+ # by construction, because a killed run labels nothing.
176
270
  tt="$GITHUB_WORKSPACE/test-temp"
177
271
  [ -d "$tt" ] || exit 0
178
- docker run --rm --user 0:0 -v "$tt":/t alpine sh \
179
- -c 'rm -rf /t/* /t/.[!.]* 2>/dev/null || true' || rm -rf "$tt"/* 2>/dev/null || true
272
+ stuck=""
273
+ for c in $(docker ps -aq 2>/dev/null || true); do
274
+ if docker inspect -f '{{range .Mounts}}{{println .Source}}{{end}}' "$c" 2>/dev/null | grep -q "^$tt/"; then
275
+ stuck="$stuck $c"
276
+ fi
277
+ done
278
+ if [ -n "$stuck" ]; then
279
+ echo "reaping containers holding mounts under $tt:$stuck"
280
+ docker rm -f $stuck || true
281
+ fi
282
+ # The DIRECTORY, not just its contents: an empty but root-owned base
283
+ # still EACCESes a later mkdtemp. Mounting the PARENT is what lets a root
284
+ # container unlink the leaf.
285
+ docker run --rm --user 0:0 -v "$GITHUB_WORKSPACE":/w alpine \
286
+ sh -c 'rm -rf /w/test-temp' || rm -rf "$tt" || true
287
+ # Fail HERE if it survived. The checkout fails on it either way, but
288
+ # reports only an EACCES rmdir with no clue what was holding the path.
289
+ if [ -e "$tt" ]; then
290
+ echo "::error::$tt survived the pre-checkout clean"
291
+ ls -lan "$tt" || true
292
+ exit 1
293
+ fi
180
294
 
181
295
  - uses: actions/checkout@v5
182
296
  with:
@@ -138,3 +138,43 @@ clients that create media nodes (e.g. a WHIP driver) must answer the license
138
138
  creation with `10 ABORTED: License handshake required`. The `license is marked
139
139
  NON-PRODUCTION` warning is expected in CI, not an error. The media engine's
140
140
  `hugepages / rte_eal_init failed` line is **benign noise** — do not chase it.
141
+
142
+ ## The workflow: `integration.yml` is a convention
143
+
144
+ `.github/workflows/integration.yml` is single-sourced in this dev-kit
145
+ (`conventions/integration.yml`) and drift-gated like `smoke.yml`, with the same
146
+ two per-repo lines: the `product:` dashboard key and the matrix `runner:` label
147
+ array. Everything else a repo used to hand-edit into the workflow now lives in
148
+ three optional hooks the workflow calls if they are executable:
149
+
150
+ | Hook | When | What goes there |
151
+ | -------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------- |
152
+ | `scripts/integration/prepull-extra.sh` | before the suite, outside nix | `docker pull` of images the suite launches beyond media + studio (a driver, a sink) |
153
+ | `scripts/integration/prepare.sh` | before `bun run test:integration` | builds the suite needs on disk — a frontend dist the daemon fetches, an image it launches |
154
+ | `scripts/integration/verify.sh` | after the suite | post-suite checks; absent, a repo with `tests/demo.spec.ts` gets `bun run demo -- check` |
155
+
156
+ `bun run test:integration` is the product's claim of what its tier is, and
157
+ ctl's release gate believes it: a `ctl-candidate` dispatch runs this workflow
158
+ and the reported result decides whether `latest` moves.
159
+
160
+ **Converging a repo.** The gate only takes over a copy whose first line is the
161
+ canonical's marker (`# ctl-dev-kit convention: integration`); a copy without it
162
+ is hand-owned and ungated, which is the state every repo started in. To
163
+ converge: move the repo's bespoke steps into the hooks above, replace the
164
+ workflow with the canonical (product and runner filled in), and prove it with
165
+ `gh workflow run integration.yml --ref <branch>` before merging — the tier
166
+ only fires on `main` and on dispatch, so a PR's checks never run it. Once the
167
+ marker is in, `sync-dev-kit` keeps the copy current and `check:drift` names any
168
+ hand edit.
169
+
170
+ **Why the pipe is shaped the way it is.** The suite runs inside a shell
171
+ function piped into `tee` so the log survives as an artifact, and the step's
172
+ first line is `set -euo pipefail`. GitHub's default `run:` shell is `bash -e
173
+ {0}` with no pipefail, so without it the step exits with tee's status and a
174
+ dead suite reads green; a `set -euo pipefail` inside the nested `bash -c` body
175
+ cannot reach the pipeline one shell up. Both turnkeys ran a void tier for weeks
176
+ that way. `tee-pipefail.test.ts` guards every convention workflow for it.
177
+
178
+ The workflow also uploads `test-results/` beside the log, and reaps the
179
+ instances the harness recorded in `$NORSK_LAUNCHED_MANIFEST` after a cancelled
180
+ or failed run, so one killed job cannot poison the next with a held port.